Initial HyperDRM 0.6.0: webOS ambient light app and native service

This commit is contained in:
matze19999 committed 2026-10-06 13:28:45 +02:00
commit 376798d6ce
34 files changed
+2170

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
#ifndef HISTOGRAM_AUTOSTART_H
#define HISTOGRAM_AUTOSTART_H
#define BOOT_HOOK "/var/lib/webosbrew/init.d/60-histogramhyperion"
static const char boot_script[] = "#!/bin/sh\n# Histogram Hyperion own boot hook v1\n(\n attempt=0\n while [ -x /var/lib/webosbrew/init.d/60-histogramhyperion ] && [ \"$attempt\" -lt 120 ]; do\n result=$(timeout 12 luna-send -n 1 luna://org.webosbrew.histogramhyperion.service/start '{}' 2>&1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 </dev/null &\nexit 0\n";
static bool boot_enabled(void){return access(BOOT_HOOK,X_OK)==0;}
static bool set_boot(bool enabled){
struct stat st;
if(lstat(BOOT_HOOK,&st)==0){
if(!S_ISREG(st.st_mode))return false;
char existing[sizeof(boot_script)];int fd=open(BOOT_HOOK,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false;
ssize_t size=read(fd,existing,sizeof(existing));close(fd);
if(size!=(ssize_t)(sizeof(boot_script)-1) || memcmp(existing,boot_script,(size_t)size)!=0)return false;
if(!enabled)return unlink(BOOT_HOOK)==0;
return chmod(BOOT_HOOK,0755)==0;
}
if(errno!=ENOENT)return false;
if(!enabled)return true;
if(mkdir("/var/lib/webosbrew",0755)!=0 && errno!=EEXIST)return false;
if(mkdir("/var/lib/webosbrew/init.d",0755)!=0 && errno!=EEXIST)return false;
int fd=open(BOOT_HOOK ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0700);if(fd<0)return false;
size_t used=0;bool ok=true;
while(used<sizeof(boot_script)-1){ssize_t n=write(fd,boot_script+used,sizeof(boot_script)-1-used);if(n<=0){ok=false;break;}used+=(size_t)n;}
if(ok)ok=fchmod(fd,0755)==0 && fsync(fd)==0;
if(close(fd)!=0)ok=false;
if(ok)ok=rename(BOOT_HOOK ".tmp",BOOT_HOOK)==0;
if(!ok)unlink(BOOT_HOOK ".tmp");
return ok;
}
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef BLACKBARS_H
#define BLACKBARS_H
/* Calibrated preset-0 edge probes on this G4. Return -1 for an ambiguous
* or dark frame so darkness alone cannot switch the active capture mode. */
static inline int letterbox_candidate(const unsigned black[16],const unsigned char color[16][3]) {
const unsigned interior[]={2,6,10,15};unsigned lit=0;
for(unsigned i=0;i<4;i++){
unsigned r=interior[i],peak=color[r][0];
for(unsigned c=1;c<3;c++)if(color[r][c]>peak)peak=color[r][c];
if(black[r]<20 && peak>35)lit++;
}
if(lit<2)return -1;
unsigned top=(black[3]+black[11]+black[12])/3;
unsigned bottom=(black[1]+black[5]+black[9])/3;
if(top>=55 && bottom>=25)return 1;
if(top<20 && bottom<20)return 0;
return -1;
}
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef COLOR_BOOST_H
#define COLOR_BOOST_H
/* HSV saturation x2 and value x1.33, preserving hue and clamping value. */
static inline void boost_color(const unsigned char in[3],unsigned char out[3]){
unsigned lo=in[0],hi=in[0];
for(unsigned c=1;c<3;c++){if(in[c]<lo)lo=in[c];if(in[c]>hi)hi=in[c];}
unsigned chroma=hi-lo;
unsigned peak=(hi*133+50)/100;if(peak>255)peak=255;
if(!chroma){for(unsigned c=0;c<3;c++)out[c]=(unsigned char)peak;return;}
unsigned expanded=chroma*2;if(expanded>hi)expanded=hi;
for(unsigned c=0;c<3;c++){
unsigned value=hi-expanded+((in[c]-lo)*expanded+chroma/2)/chroma;
out[c]=(unsigned char)((value*peak+hi/2)/hi);
}
}
#endif
+15
View File
@@ -0,0 +1,15 @@
#ifndef LG_HISTOGRAM_H
#define LG_HISTOGRAM_H
#include <stdint.h>
#include <stddef.h>
/* LG ABI: region ID followed by three planar channels, 256 uint16 bins
* per channel. Bin index is the 8-bit intensity (0..255). */
typedef struct { uint32_t id; uint16_t bins[3][256]; } Histogram;
_Static_assert(sizeof(Histogram)==0x604,"LG ABI record size");
_Static_assert(offsetof(Histogram,bins)==4,"LG ABI channel offset");
static inline unsigned char histogram_mean(const uint16_t bins[256]) {
uint64_t count=0,weight=0;
for(unsigned b=0;b<256;b++){count+=bins[b];weight+=(uint64_t)bins[b]*b;}
return count?(unsigned char)((weight+count/2)/count):0;
}
#endif
+6
View File
@@ -0,0 +1,6 @@
#ifndef SPATIAL_INTERPOLATION_H
#define SPATIAL_INTERPOLATION_H
static inline void blend_color(const unsigned char a[3],const unsigned char b[3],unsigned weight,unsigned char out[3]){
for(unsigned c=0;c<3;c++)out[c]=(unsigned char)(((unsigned)a[c]*(65535-weight)+(unsigned)b[c]*weight+32767)/65535);
}
#endif
+59
View File
@@ -0,0 +1,59 @@
#ifndef HYPERION_LAYOUT_H
#define HYPERION_LAYOUT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <math.h>
typedef struct {unsigned char a,b;unsigned weight;} LedBlend;
/* Measured G4 rim samples, independent of LED count and wiring order. */
static const struct {double position;unsigned char region;} rim[]={
{0.2078431373,7},{0.3176470588,3},{0.5490196078,11},{0.7529411765,12},
{0.8392156863,13},{1.4941176471,10},{2.1843137255,9},{2.5490196078,1},
{2.8666666667,5},{3.4,15}
};
static double screen_perimeter(double x,double y){
double distances[]={y,1-x,1-y,x},positions[]={x,1+y,3-x,4-y};unsigned edge=0;
for(unsigned i=1;i<4;i++)if(distances[i]<distances[edge])edge=i;
return positions[edge]>=4?positions[edge]-4:positions[edge];
}
static LedBlend layout_blend(double x,double y){
double t=screen_perimeter(x,y);unsigned n=sizeof(rim)/sizeof(*rim),right=0;
while(right<n && rim[right].position<=t)right++;
right%=n;unsigned left=(right+n-1)%n;
double span=rim[right].position-rim[left].position,distance=t-rim[left].position;
if(span<=0)span+=4;
if(distance<0)distance+=4;
return (LedBlend){rim[left].region,rim[right].region,(unsigned)(distance/span*65535+0.5)};
}
/* Use the TV's JSON-C library; validate every rectangle before replacing the
* layout. The safety limit is independent of the installed LED count. */
static bool layout_from_json(const char *text,LedBlend **result,unsigned *count){
void *lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!lib)return false;
void *(*parse)(const char*)=dlsym(lib,"json_tokener_parse");
int (*get)(const void*,const char*,void**)=dlsym(lib,"json_object_object_get_ex");
int (*type)(const void*)=dlsym(lib,"json_object_get_type");
size_t (*length)(const void*)=dlsym(lib,"json_object_array_length");
void *(*at)(const void*,size_t)=dlsym(lib,"json_object_array_get_idx");
double (*number)(const void*)=dlsym(lib,"json_object_get_double");
int (*put)(void*)=dlsym(lib,"json_object_put");
if(!parse||!get||!type||!length||!at||!number||!put){dlclose(lib);return false;}
void *root=parse(text),*info=NULL,*leds=NULL;bool valid=root && type(root)==4 && get(root,"info",&info) && type(info)==4 && get(info,"leds",&leds) && type(leds)==5;
size_t n=valid?length(leds):0;valid=valid && n>0 && n<=16384;
LedBlend *mapping=valid?calloc(n,sizeof(*mapping)):NULL;valid=valid && mapping;
const char *keys[]={"hmin","hmax","vmin","vmax"};
for(size_t i=0;valid && i<n;i++){
void *led=at(leds,i);double coordinates[4];valid=led && type(led)==4;
for(unsigned c=0;valid && c<4;c++){
void *v=NULL;valid=get(led,keys[c],&v) && (type(v)==2||type(v)==3);
if(valid){coordinates[c]=number(v);valid=isfinite(coordinates[c]) && coordinates[c]>=0 && coordinates[c]<=1;}
}
if(valid)valid=coordinates[0]<=coordinates[1] && coordinates[2]<=coordinates[3];
if(valid)mapping[i]=layout_blend((coordinates[0]+coordinates[1])/2,(coordinates[2]+coordinates[3])/2);
}
if(root)put(root);
dlclose(lib);
if(!valid){free(mapping);return false;}
*result=mapping;*count=(unsigned)n;return true;
}
#endif
+34
View File
@@ -0,0 +1,34 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <stdarg.h>
#include "histogram.h"
/* Process-local diagnostic sink. It changes logging only, not capture policy. */
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)level;(void)message_id;(void)file;
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 bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; }
int main(void) {
void *lib=dlopen("libvtcapture.so.1",RTLD_NOW);
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;}
void *(*init)(bool(*)(void))=dlsym(lib,"vtCapture_initHistogram");
int (*preset)(void*,int)=dlsym(lib,"vtCapture_setPresetHistogram");
int (*get)(void*,Histogram*)=dlsym(lib,"vtCapture_getHistogram");
int (*finish)(void*)=dlsym(lib,"vtCapture_finalizeHistogram");
if(!init||!preset||!get||!finish)return 2;
void *handle=init(policy); fprintf(stderr,"handle=%p\n",handle);
if(!handle)return 3;
int rc=preset(handle,0);fprintf(stderr,"preset=%d\n",rc);
Histogram hist[16];
for(int frame=0;rc==0 && frame<3;frame++) {
usleep(100000); rc=get(handle,hist); printf("frame=%d rc=%d\n",frame,rc);
if(rc==0)for(int i=0;i<16;i++) {printf("region=%u",hist[i].id);for(int c=0;c<3;c++){uint64_t sum=0;for(int b=0;b<256;b++)sum+=hist[i].bins[c][b];printf(" ch%d_count=%llu mean=%u",c,(unsigned long long)sum,histogram_mean(hist[i].bins[c]));} puts("");}
}
fprintf(stderr,"finalize=%d\n",finish(handle)); return rc?4:0;
}
+239
View File
@@ -0,0 +1,239 @@
#define _DEFAULT_SOURCE
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <stdarg.h>
#include <time.h>
#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,&current)==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<capacity){ssize_t n=recv(output,data+used,capacity-used,0);if(n<=0)break;used+=(size_t)n;data[used]=0;char *end=strchr(data,'\n');if(end){*end=0;complete=true;break;}}
}
bool ok=complete && layout_from_json(data,&led_blends,&led_count);free(data);interpolation_enabled=ok;return ok;
}
static void stop_capture(void){
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));
if(output>=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<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.6.0\",\"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<black_percent[i])black_percent[i]=fraction;
}
}
for(int i=0;i<16;i++)for(int c=0;c<3;c++){
rgb[i][c]=histogram_mean(hist[i].bins[c]);aggregate[c]+=rgb[i][c];
}
if(capture_preset==0){
int candidate=letterbox_candidate(black_percent,rgb);uint64_t now=milliseconds();
if(candidate<0)pending_letterbox=-1;
else if(candidate!=pending_letterbox){pending_letterbox=candidate;candidate_since=now;}
else if((unsigned)candidate!=letterbox_mode && now-candidate_since>=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<led_count;i++){
unsigned char interpolated[3],boosted[3];LedBlend blend=led_blends[i];
blend_color(rgb[blend.a],rgb[blend.b],blend.weight,interpolated);boost_color(interpolated,boosted);
written+=(size_t)snprintf(line+written,capacity-written,"%s%u,%u,%u",i?",":"",boosted[0],boosted[1],boosted[2]);
}
snprintf(line+written,capacity-written,"]}\n");
int failed=send_line(line);free(line);
if(failed){set_error("Hyperion 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;}
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;}
int main(void){
signal(SIGTERM,quit_signal);signal(SIGINT,quit_signal);
void *vt=dlopen("libvtcapture.so.1",RTLD_NOW),*ls=dlopen("libluna-service2.so.3",RTLD_NOW),*glib=dlopen("libglib-2.0.so.0",RTLD_NOW);
if(!vt||!ls||!glib){fprintf(stderr,"Missing LG library: %s\n",dlerror());return 2;}
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
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");
bool (*attach)(void*,void*,LSError*)=symbol(ls,"LSGmainAttach");
bool (*unreg)(void*,LSError*)=symbol(ls,"LSUnregister");
void *(*loop_new)(void*,int)=symbol(glib,"g_main_loop_new");
int (*iterate)(void*,int)=symbol(glib,"g_main_context_iteration");
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},{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;}
timer(100,tick,NULL);
while(!quitting){iterate(NULL,0);usleep(10000);}
stop_capture();unreg(handle,&e);error_free(&e);loop_unref(loop);return 0;
}
+15
View File
@@ -0,0 +1,15 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "blackbars.h"
int main(void){
unsigned black[16]={0};unsigned char color[16][3];memset(color,100,sizeof(color));
assert(letterbox_candidate(black,color)==0);
black[3]=black[11]=black[12]=66;black[1]=55;black[5]=black[9]=33;
assert(letterbox_candidate(black,color)==1);
black[1]=black[5]=black[9]=0;assert(letterbox_candidate(black,color)==-1);
for(unsigned i=0;i<16;i++)black[i]=100;
memset(color,0,sizeof(color));assert(letterbox_candidate(black,color)==-1);
memset(black,0,sizeof(black));assert(letterbox_candidate(black,color)==-1);
puts("PASS: full frame, measured letterbox, one edge only, black frame, dark frame");
}
+20
View File
@@ -0,0 +1,20 @@
#include <assert.h>
#include <stdio.h>
#include "color_boost.h"
int main(void){
unsigned char out[3];
for(unsigned v=0;v<256;v++){
unsigned char gray[3]={v,v,v};boost_color(gray,out);
unsigned expected=(v*133+50)/100;if(expected>255)expected=255;
assert(out[0]==expected && out[1]==expected && out[2]==expected);
}
unsigned char cyan[3]={93,224,242};boost_color(cyan,out);
assert(out[0]<93 && out[1]>out[0] && out[1]<out[2] && out[2]==255);
for(unsigned c=0;c<3;c++){
unsigned char primary[3]={0};primary[c]=255;boost_color(primary,out);
for(unsigned j=0;j<3;j++)assert(out[j]==primary[j]);
}
unsigned char warm[3]={171,82,0};boost_color(warm,out);
assert(out[0]==227 && out[1]>82 && out[2]==0);
puts("PASS: neutral grays, stronger cyan, brighter warm color, clamped primaries");
}
+25
View File
@@ -0,0 +1,25 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "histogram.h"
int main(void) {
Histogram h={0};
assert(histogram_mean(h.bins[0])==0);
for(unsigned intensity=0;intensity<256;intensity++) {
memset(&h,0,sizeof(h));h.bins[0][intensity]=65535;
assert(histogram_mean(h.bins[0])==intensity);
}
/* Adjacent halfwords must stay independent, including the final bin. */
memset(&h,0,sizeof(h));h.bins[0][254]=65535;h.bins[0][255]=1;
assert(histogram_mean(h.bins[0])==254);
/* Weighted sum exceeds uint32 for full uint16 counts in every bin. */
for(unsigned b=0;b<256;b++)h.bins[0][b]=65535;
assert(histogram_mean(h.bins[0])==128);
for(unsigned primary=0;primary<3;primary++) {
memset(&h,0,sizeof(h));
for(unsigned c=0;c<3;c++)h.bins[c][c==primary?255:0]=100;
for(unsigned c=0;c<3;c++)assert(histogram_mean(h.bins[c])==(c==primary?255:0));
}
puts("PASS: ABI, all 256 intensities, adjacent bins, overflow, RGB primaries");
return 0;
}
+16
View File
@@ -0,0 +1,16 @@
#include <assert.h>
#include <stdio.h>
#include "interpolation.h"
int main(void){
const unsigned char a[3]={255,0,0},b[3]={0,255,0};unsigned char out[3];
blend_color(a,b,0,out);assert(out[0]==255 && out[1]==0 && out[2]==0);
blend_color(a,b,65535,out);assert(out[0]==0 && out[1]==255 && out[2]==0);
blend_color(a,b,32768,out);assert(out[0]==127 && out[1]==128 && out[2]==0);
unsigned previous=0;
for(unsigned w=0;w<=65535;w++){
blend_color(a,b,w,out);assert(out[1]>=previous && out[2]==0);previous=out[1];
unsigned char gray[3]={173,173,173};blend_color(gray,gray,w,out);
assert(out[0]==173 && out[1]==173 && out[2]==173);
}
puts("PASS: endpoints, midpoint, all weights monotonic, constant gray preserved");
}
+18
View File
@@ -0,0 +1,18 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "layout.h"
int main(void){
unsigned sizes[]={50,500};
for(unsigned k=0;k<2;k++){
unsigned n=sizes[k];size_t capacity=150*n+100;char *text=malloc(capacity);size_t used=snprintf(text,capacity,"{\"info\":{\"leds\":[");
for(unsigned i=0;i<n;i++)used+=snprintf(text+used,capacity-used,"%s{\"hmin\":%.8f,\"hmax\":%.8f,\"vmin\":0,\"vmax\":0.05}",i?",":"",(double)i/n,(double)(i+1)/n);
snprintf(text+used,capacity-used,"]}}");LedBlend *mapping=NULL;unsigned count=0;
assert(layout_from_json(text,&mapping,&count));assert(count==n);
for(unsigned i=0;i<n;i++)assert(mapping[i].a<16 && mapping[i].b<16 && mapping[i].weight<=65535);
assert(mapping[0].a!=mapping[n-1].a);free(mapping);free(text);printf("PASS: %u LEDs, positions and order preserved\n",n);
}
const char *bad[]={"{}","{\"info\":{\"leds\":[]}}","{\"info\":{\"leds\":[42]}}","{\"info\":{\"leds\":[{\"hmin\":2,\"hmax\":1,\"vmin\":0,\"vmax\":1}]}}"};
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){LedBlend *mapping=NULL;unsigned count=0;assert(!layout_from_json(bad[i],&mapping,&count));assert(!mapping && !count);}
puts("PASS: malformed layouts rejected without replacing output");
}