Support configurable Hyperion and HyperHDR receivers with token login

This commit is contained in:
matze19999 committed 2026-10-06 18:05:33 +02:00
1 parent c3a2ac8002
commit 90c4c88942
19 files changed
+264 -34

No files matched your search

+1 -1
View File
@@ -80,7 +80,7 @@ static bool handover_tick(void){
media_deadline=now+3000;
error_free(&e);next_media_probe=now+1000;
}
bool clear=handover_enabled && piccap_installed() && !retries.paused && !retries.blocked &&
bool clear=handover_enabled && target_piccap_local(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
protection==PLAYBACK_CLEAR && clear_samples>=2 && now-protection_at<3000 && now>=handover_cooldown;
if(capture_source==SOURCE_HISTOGRAM && clear){
stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
+35 -14
View File
@@ -24,6 +24,7 @@
#include "layout.h"
#include "autostart.h"
#include "retry.h"
#include "target.h"
#define SERVICE "org.webosbrew.histogramhyperion.service"
#define PRIORITY 149
typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError;
@@ -113,6 +114,18 @@ static bool read_layout(void){
}
bool ok=complete && layout_from_json(data,&led_blends,&led_count);free(data);interpolation_enabled=ok;return ok;
}
static bool target_login(void){
if(!target.token[0])return true;
char request[384],answer[4096];size_t used=0;
snprintf(request,sizeof(request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);
if(send_line(request)!=0)return false;
while(used+1<sizeof(answer)){
ssize_t n=recv(output,answer+used,sizeof(answer)-used-1,0);if(n<=0)return false;
used+=(size_t)n;answer[used]=0;if(strchr(answer,'\n'))break;
}
PlaybackJson json={0};if(!playback_json_init(&json))return false;
void *root=json.parse(answer);bool ok=playback_true(&json,root,"success");if(root)json.put(root);dlclose(json.lib);return ok;
}
static void release_capture(bool clear_output){
free(led_blends);led_blends=NULL;led_count=0;interpolation_enabled=false;
color_valid=false;memset(rgb,0,sizeof(rgb));memset(sent_rgb,0,sizeof(sent_rgb));
@@ -129,17 +142,15 @@ static bool start_failure(void *h,void *m,const char *message){
}
static bool start(void *h,void *m,void *ctx){
(void)ctx;const char *p=m?payload(m):NULL;
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; endpoint is localhost:19444\"}");
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use settings to configure the receiver\"}");
if(m)retry_reset(&retries);
if(piccap_owned)return respond(h,m,"{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}");
frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0;
output=socket(AF_INET,SOCK_STREAM,0);
struct timeval timeout={1,0};
if(output>=0){setsockopt(output,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(output,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));}
struct sockaddr_in a={.sin_family=AF_INET,.sin_port=htons(19444),.sin_addr={htonl(INADDR_LOOPBACK)}};
if(output<0||connect(output,(void*)&a,sizeof(a))!=0){stop_capture();set_error("Hyperion JSON server is unavailable on localhost:19444");return start_failure(h,m,"Cannot connect to local Hyperion");}
if(!read_layout()){stop_capture();set_error("Cannot read a valid LED layout from Hyperion");return start_failure(h,m,"Cannot read Hyperion LED layout");}
output=target_connect(&target);
if(output<0){stop_capture();set_error("Cannot connect to configured receiver IP and JSON port");return start_failure(h,m,"Configured receiver is unavailable");}
if(!target_login()){stop_capture();set_error("Receiver token login failed; check the saved API token");return start_failure(h,m,"Receiver authentication failed");}
if(!read_layout()){stop_capture();set_error("Cannot read receiver LED layout; check JSON port and API authorization");return start_failure(h,m,"Receiver layout unavailable; check API authorization");}
if(!enter_developer_mode()){stop_capture();set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return start_failure(h,m,"Root access is required for temporary LG developer initialization");}
capture=vt_init(policy);
if(!leave_developer_mode()){stop_capture();retries.blocked=true;set_error("Developer marker cleanup failed; manual intervention required");return start_failure(h,m,"Developer marker cleanup failed");}
@@ -172,10 +183,20 @@ static bool handover(void *h,void *m,void *ctx){
handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;next_media_probe=0;
return respond(h,m,"{\"returnValue\":true}");
}
static bool settings(void *h,void *m,void *ctx){
(void)ctx;TargetConfig next=target;
if(!playback_json.lib || !target_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Enter a valid IPv4/IPv6 address, integer port (1-65535), receiver and optional API token\"}");
if(!target_save(TARGET_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}");
bool active=capture || piccap_owned || retries.due;
stop_capture();target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;
if(active && !retries.paused && !retries.blocked)retries.due=milliseconds();
return respond(h,m,"{\"returnValue\":true}");
}
static bool status(void *h,void *m,void *ctx){
(void)ctx;char json[4096],safe[512];size_t n=0;
(void)ctx;char json[4096],safe[512],endpoint[80];size_t n=0;
snprintf(endpoint,sizeof(endpoint),strchr(target.host,':')?"[%s]:%u":"%s:%u",target.host,target.port);
for(size_t i=0;last_error[i] && n+2<sizeof(safe);i++){char c=last_error[i];if(c=='"'||c=='\\')safe[n++]='\\';if((unsigned char)c>=32)safe[n++]=c;}safe[n]=0;
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.7.2\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.8.0\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_local(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]);
used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false");
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]);
@@ -235,11 +256,11 @@ static int tick(void *unused){
}
snprintf(line+written,capacity-written,"]}\n");
int failed=send_line(line);free(line);
if(failed){set_error("Hyperion connection lost");stop_capture();return 1;}
if(failed){set_error("Receiver connection lost");stop_capture();return 1;}
char answer[4096];size_t used=0;bool complete=false;
while(used+1<sizeof(answer)){ssize_t len=recv(output,answer+used,sizeof(answer)-used-1,0);if(len<=0)break;used+=(size_t)len;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
if(!complete){set_error("Hyperion did not acknowledge color data");stop_capture();return 1;}
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Hyperion rejected the color command");stop_capture();return 1;}
if(!complete){set_error("Receiver did not acknowledge color data");stop_capture();return 1;}
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Receiver rejected the color command");stop_capture();return 1;}
color_valid=true;frames++;return 1;
}
static void *symbol(void *lib,const char *name){void *s=dlsym(lib,name);if(!s){fprintf(stderr,"Missing symbol %s\n",name);exit(2);}return s;}
@@ -251,7 +272,7 @@ int main(void){
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
call_once=symbol(ls,"LSCallOneReply");
cancel_call=symbol(ls,"LSCallCancel");
if(playback_json_init(&playback_json))handover_load();
if(playback_json_init(&playback_json)){handover_load();target_load(&playback_json,TARGET_PREF,&target);}
reply=symbol(ls,"LSMessageReply");payload=symbol(ls,"LSMessageGetPayload");error_init=symbol(ls,"LSErrorInit");error_free=symbol(ls,"LSErrorFree");
bool (*reg)(const char*,void**,LSError*)=symbol(ls,"LSRegister");
bool (*category)(void*,const char*,Method*,void*,void*,LSError*)=symbol(ls,"LSRegisterCategory");
@@ -262,7 +283,7 @@ int main(void){
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{NULL,NULL,0}};
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{NULL,NULL,0}};
if(!reg(SERVICE,&handle,&e)||!category(handle,"/",methods,NULL,NULL,&e)||!attach(handle,loop,&e)){fprintf(stderr,"Luna registration failed: %s\n",e.message?e.message:"unknown");error_free(&e);return 3;}
service_handle=handle;
if(!boot_enabled())retry_pause(&retries);
+76
View File
@@ -0,0 +1,76 @@
#ifndef HYPERDRM_TARGET_H
#define HYPERDRM_TARGET_H
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include "playback.h"
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
typedef struct {char host[46],receiver[16],token[257];unsigned port;} TargetConfig;
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444};
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
static bool target_valid(const TargetConfig *t){
unsigned char address[16];
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
for(const unsigned char *p=(const unsigned char*)t->token;*p;p++)
if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false;
return true;
}
static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
void *root=j->parse(text);if(!root)return false;
TargetConfig next=*out;bool valid=j->type(root)==4;
const char *names[]={"host","receiver","token"};char *fields[]={next.host,next.receiver,next.token};
size_t sizes[]={sizeof(next.host),sizeof(next.receiver),sizeof(next.token)};
for(unsigned i=0;valid && i<3;i++){
void *v=playback_field(j,root,names[i]);
if(!v){if(i<2)valid=false;continue;}
if(j->type(v)!=6){valid=false;break;}
const char *s=j->string(v);if(strlen(s)>=sizes[i]){valid=false;break;}
strcpy(fields[i],s);
}
void *port=playback_field(j,root,"port");
if(!port || j->type(port)!=3)valid=false;
else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;}
void *clear=playback_field(j,root,"clearToken");
if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;}
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
}
static bool target_save(const char *path,const TargetConfig *t){
if(!target_valid(t))return false;
char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false;
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->receiver,t->token);
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false;
if(ok)ok=rename(tmp,path)==0;
if(!ok)unlink(tmp);
return ok;
}
static bool target_load(PlaybackJson *j,const char *path,TargetConfig *t){
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false;
char data[1024];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);if(n<=0 || n==(ssize_t)sizeof(data)-1)return false;
data[n]=0;return target_parse(j,data,t);
}
static int target_connect(const TargetConfig *t){
if(!target_valid(t))return -1;
struct sockaddr_storage storage={0};socklen_t length;int family;
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
if(inet_pton(AF_INET,t->host,&v4->sin_addr)==1){family=AF_INET;v4->sin_family=family;v4->sin_port=htons(t->port);length=sizeof(*v4);}
else {family=AF_INET6;v6->sin6_family=family;v6->sin6_port=htons(t->port);length=sizeof(*v6);}
int fd=socket(family,SOCK_STREAM,0);if(fd<0)return -1;
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
if(connect(fd,(void*)&storage,length)!=0){
if(errno!=EINPROGRESS){close(fd);return -1;}
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
}
if(fcntl(fd,F_SETFL,flags)<0){close(fd);return -1;}
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
return fd;
}
#endif
+2
View File
@@ -17,6 +17,8 @@ static const char *payload(void *m){return m;}
static void error_init(LSError *e){(void)e;}
static void error_free(LSError *e){(void)e;}
#define PICCAP_BINARY "/bin/sh"
static int target;
static bool target_piccap_local(const int *t){(void)t;return true;}
#include "handover.h"
static bool mock_call(void *h,const char *uri,const char *data,Callback cb,void *ctx,unsigned long *token,LSError *e){
(void)h;(void)data;(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
+23
View File
@@ -0,0 +1,23 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include "target.h"
int main(void){
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
assert(target_valid(&t));assert(target_piccap_local(&t));
assert(target_parse(&j,"{\"host\":\"10.1.0.91\",\"port\":19445,\"receiver\":\"HyperHDR\",\"token\":\"test-token\"}",&t));
assert(t.port==19445 && !strcmp(t.receiver,"HyperHDR") && !strcmp(t.token,"test-token"));assert(!target_piccap_local(&t));
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"Hyperion\"}",&t));assert(!strcmp(t.token,"test-token"));
const char *bad[]={"{}","[]","{\"host\":\"localhost\",\"port\":19444,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":0,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":65536,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444.5,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":\"19444\",\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"Other\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"token\":\"bad\\\"token\"}"};
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){TargetConfig before=t;assert(!target_parse(&j,bad[i],&t));assert(!memcmp(&t,&before,sizeof(t)));}
const char *file="/tmp/hyperdrm-target-test.json";assert(access(file,F_OK)!=0);assert(access("/tmp/hyperdrm-target-test.json.tmp",F_OK)!=0);
assert(target_save(file,&t));struct stat st;assert(stat(file,&st)==0 && (st.st_mode&0777)==0600);
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
int client=target_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
assert(target_connect(&t)<0);
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, bounded TCP connect and refusal");return 0;
}