#define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "histogram.h" #include "blackbars.h" #include "color_boost.h" #include "interpolation.h" #include "layout.h" #include "autostart.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; typedef bool (*Callback)(void*,void*,void*); typedef struct {const char *name;Callback function;int flags;} Method; static bool (*reply)(void*,void*,const char*,LSError*); static const char *(*payload)(void*); static bool (*error_init)(LSError*); static void (*error_free)(LSError*); static void *(*vt_init)(bool(*)(void)); static int (*vt_preset)(void*,int),(*vt_get)(void*,Histogram*),(*vt_finish)(void*); static void *capture; static char lg_diagnostic[256]; /* Process-local LG diagnostics; no changes to the loaded LG library. */ void printLog(const char *context,int level,const char *message_id, const char *file,const char *function,int line,const char *format,...) { (void)context;(void)message_id;(void)file; if(capture && level>3)return; if(format){va_list args;va_start(args,format);vsnprintf(lg_diagnostic,sizeof(lg_diagnostic),format,args);va_end(args);} fprintf(stderr,"LG %s:%d: ",function?function:"?",line); if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);} fputc('\n',stderr); } static int output=-1; static unsigned long frames; static unsigned char rgb[16][3]; static unsigned char sent_rgb[3]; static unsigned black_percent[16]; static unsigned capture_preset; static unsigned letterbox_mode,settling; static int pending_letterbox=-1; static uint64_t candidate_since,next_border_probe; static bool color_valid; static unsigned led_count; static LedBlend *led_blends; static bool interpolation_enabled; static unsigned recovery_attempts; static uint64_t retry_at; static uint64_t milliseconds(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000+t.tv_nsec/1000000;} static char last_error[256]; static volatile sig_atomic_t revoked,quitting; static int guard_pipe=-1; static pid_t guard_pid=-1; #define DEV_MODE "/var/luna/preferences/vt_devmode" /* LG's developer gate is enabled only during initialization. A separate * guardian removes our own marker even if this process aborts or is killed. * An existing marker is preserved, as are files replaced by another owner. */ static bool enter_developer_mode(void){ int marker=open(DEV_MODE,O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC,0600); if(marker<0)return errno==EEXIST; struct stat owned; if(fstat(marker,&owned)!=0){close(marker);unlink(DEV_MODE);return false;} close(marker);int p[2]; if(pipe(p)!=0){unlink(DEV_MODE);return false;} pid_t child=fork(); if(child<0){close(p[0]);close(p[1]);unlink(DEV_MODE);return false;} if(child==0){ close(p[1]);struct pollfd fd={p[0],POLLIN|POLLHUP,0}; /* Bounded cleanup also covers an indefinitely blocked initialization. */ poll(&fd,1,10000);close(p[0]);struct stat current; int rc=0; if(lstat(DEV_MODE,¤t)==0 && current.st_dev==owned.st_dev && current.st_ino==owned.st_ino)rc=unlink(DEV_MODE); _exit(rc==0?0:1); } close(p[0]);guard_pipe=p[1];guard_pid=child;return true; } static bool leave_developer_mode(void){ if(guard_pipe<0)return true; close(guard_pipe);guard_pipe=-1;int status; while(waitpid(guard_pid,&status,0)<0){if(errno!=EINTR)return false;} guard_pid=-1;return WIFEXITED(status)&&WEXITSTATUS(status)==0; } static void quit_signal(int s){(void)s;quitting=1;} static bool policy(void){revoked=1;return true;} static void set_error(const char *s){snprintf(last_error,sizeof(last_error),"%s",s);fprintf(stderr,"%s\n",s);} static int send_line(const char *line){ size_t left=strlen(line);const char *p=line; while(left){ssize_t n=send(output,p,left,MSG_NOSIGNAL);if(n<=0)return -1;p+=n;left-=n;} return 0; } static bool read_layout(void){ const size_t capacity=2*1024*1024;char *data=malloc(capacity+1);if(!data)return false; bool complete=false;size_t used=0; if(send_line("{\"command\":\"serverinfo\"}\n")==0){ while(used=0){if(frames){char line[80];snprintf(line,sizeof(line),"{\"command\":\"clear\",\"priority\":%d}\n",PRIORITY);send_line(line);}close(output);output=-1;} if(capture){vt_finish(capture);capture=NULL;} } static bool respond(void *h,void *m,const char *s){if(!h||!m)return true;LSError e;error_init(&e);bool ok=reply(h,m,s,&e);error_free(&e);return ok;} 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(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}"); if(m){retry_at=0;recovery_attempts=0;} frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0; if(!enter_developer_mode()){set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Root access is required for temporary LG developer initialization\"}");} capture=vt_init(policy); if(!leave_developer_mode()){stop_capture();set_error("Could not restore LG developer marker; check vt_devmode before retrying");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Developer marker cleanup failed\"}");} if(!capture){set_error("LG could not acquire the histogram capture resource. Check for another active capture source; no colors are being sent.");return respond(h,m,"{\"returnValue\":false,\"errorCode\":1,\"errorText\":\"LG refused histogram initialization. Another capture source may hold VT_CAPTURE. No colors are being sent.\"}");} if(vt_preset(capture,0)!=0){stop_capture();set_error("LG histogram preset initialization failed");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Histogram preset failed\"}");} capture_preset=letterbox_mode=settling=0;pending_letterbox=-1;next_border_probe=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 respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot connect to local Hyperion\"}");} if(!read_layout()){stop_capture();set_error("Cannot read a valid LED layout from Hyperion");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot read Hyperion LED layout\"}");} return respond(h,m,"{\"returnValue\":true,\"running\":true}"); } static bool stop(void *h,void *m,void *ctx){(void)ctx;retry_at=0;recovery_attempts=0;stop_capture();return respond(h,m,"{\"returnValue\":true,\"running\":false}");} static bool autostart(void *h,void *m,void *ctx){ (void)ctx;const char *p=m?payload(m):NULL;bool enabled; if(p && strcmp(p,"{\"enabled\":true}")==0)enabled=true; else if(p && strcmp(p,"{\"enabled\":false}")==0)enabled=false; else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); if(geteuid()!=0 || !set_boot(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Autostart could not be saved. Check own service root elevation and boot hook.\"}"); return respond(h,m,enabled?"{\"returnValue\":true,\"autostart\":true}":"{\"returnValue\":true,\"autostart\":false}"); } static bool status(void *h,void *m,void *ctx){ (void)ctx;char json[4096],safe[512];size_t n=0; for(size_t i=0;last_error[i] && n+2=32)safe[n++]=c;}safe[n]=0; int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.3\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",capture?"true":"false",color_valid?"true":"false",frames,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]); snprintf(json+used,sizeof(json)-used,"],\"preset\":%u,\"saturationGain\":2.0,\"brightnessGain\":1.33,\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,\"letterboxDetected\":%s}",capture_preset,interpolation_enabled?"true":"false",boot_enabled()?"true":"false",capture&&letterbox_mode?"true":"false");return respond(h,m,json); } static int tick(void *unused){ (void)unused; if(!capture){ if(retry_at && milliseconds()>=retry_at){ retry_at=0;recovery_attempts++;start(NULL,NULL,NULL); if(!capture && recovery_attempts<6)retry_at=milliseconds()+5000; } return 1; } if(revoked){set_error("LG revoked capture; retrying initialization after source transition");stop_capture();recovery_attempts=0;retry_at=milliseconds()+5000;return 1;} if(settling){settling--;return 1;} if(letterbox_mode && capture_preset==1 && milliseconds()>=next_border_probe){ if(vt_preset(capture,0)!=0){set_error("Border probe preset failed");stop_capture();return 1;} capture_preset=0;settling=2;return 1; } Histogram hist[16]={0};int rc=vt_get(capture,hist); if(rc){set_error("LG histogram read failed; streaming stopped");stop_capture();return 1;} unsigned aggregate[3]={0}; for(int i=0;i<16;i++){ black_percent[i]=100; for(int c=0;c<3;c++){ uint64_t total=0,black=0; for(int b=0;b<256;b++){total+=hist[i].bins[c][b];if(b<=12)black+=hist[i].bins[c][b];} unsigned fraction=total?(unsigned)(100*black/total):0; if(fraction=2000)letterbox_mode=(unsigned)candidate; if(letterbox_mode){ if(vt_preset(capture,1)!=0){set_error("Letterbox preset failed");stop_capture();return 1;} capture_preset=1;settling=2;next_border_probe=now+2000;return 1; } } for(int c=0;c<3;c++)sent_rgb[c]=(unsigned char)(aggregate[c]/16); unsigned char boosted_mean[3];boost_color(sent_rgb,boosted_mean);memcpy(sent_rgb,boosted_mean,3); size_t capacity=256+(size_t)led_count*12;char *line=malloc(capacity); if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;} size_t written=(size_t)snprintf(line,capacity,"{\"command\":\"color\",\"priority\":%d,\"duration\":1500,\"origin\":\"HyperDRM\",\"color\":[",PRIORITY); for(unsigned i=0;i