Release 0.11.0 with guided calibration, source transitions and root repair

This commit is contained in:
matze19999 committed 2026-10-06 20:29:25 +02:00
1 parent e55d37772a
commit f74a658097
33 files changed
+855 -75

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
#ifndef HYPERDRM_CONTINUITY_H
#define HYPERDRM_CONTINUITY_H
/* Zero population is missing data, while populated bin zero is real black. */
static inline bool histogram_populated(const Histogram hist[16]){
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++){
uint64_t total=0;for(unsigned b=0;b<256;b++)total+=hist[r].bins[c][b];
if(!total)return false;
}
return true;
}
typedef struct {unsigned char last[16][3],from[16][3];uint64_t valid_at,fade_at,missing_at;bool available,fading,holding;} Continuity;
static inline void continuity_seed(Continuity *s,const unsigned char colors[16][3],uint64_t now){
memcpy(s->last,colors,sizeof(s->last));memcpy(s->from,colors,sizeof(s->from));s->valid_at=now;s->available=true;s->fading=true;s->fade_at=0;s->missing_at=0;s->holding=false;
}
static inline void continuity_mix(const unsigned char from[16][3],const unsigned char to[16][3],unsigned elapsed,unsigned char out[16][3]){
if(elapsed>500)elapsed=500;
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)out[r][c]=(from[r][c]*(500-elapsed)+to[r][c]*elapsed+250)/500;
}
static inline bool continuity_frame(Continuity *s,bool valid,const unsigned char input[16][3],uint64_t now,unsigned char out[16][3]){
if(!valid){
if(!s->missing_at)s->missing_at=now;
s->holding=s->available && now-s->missing_at<1500;
if(s->holding)memcpy(out,s->last,sizeof(s->last));
return s->holding;
}
if(s->holding){memcpy(s->from,s->last,sizeof(s->from));s->fading=true;s->fade_at=0;}
s->holding=false;s->missing_at=0;
/* Near-black codec rounding is valid darkness, not a lost measurement. */
bool black=true;for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)if(input[r][c]>3)black=false;
if(black)s->fading=false;
if(s->fading){if(!s->fade_at)s->fade_at=now;continuity_mix(s->from,input,(unsigned)(now-s->fade_at),out);if(now-s->fade_at>=500)s->fading=false;}
else memcpy(out,input,sizeof(s->last));
memcpy(s->last,out,sizeof(s->last));s->available=true;s->valid_at=now;return true;
}
#endif
+5 -2
View File
@@ -1,6 +1,9 @@
#ifndef HYPERDRM_HANDOVER_H
#define HYPERDRM_HANDOVER_H
#include "playback.h"
#ifndef CALIBRATION_ACTIVE
#define CALIBRATION_ACTIVE false
#endif
#ifndef PICCAP_BINARY
#define PICCAP_BINARY "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
#endif
@@ -107,11 +110,11 @@ static bool handover_tick(void){
error_free(&e);next_media_probe=now+1000;
}
if(protection==PLAYBACK_CLEAR && now-protection_at>=3000){protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=0;if(!unknown_since)unknown_since=now;}
bool session=handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked;
bool session=!CALIBRATION_ACTIVE && handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked;
bool clear=session && protection==PLAYBACK_CLEAR && now-protection_at<3000;
bool stable=clear && clear_samples>=3 && now-clear_since>=CLEAR_STABLE_MS && now>=handover_cooldown;
if(capture_source==SOURCE_HISTOGRAM && stable){
piccap_target=target;piccap_error[0]=0;piccap_disconnected_since=now;stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
piccap_target=target;piccap_error[0]=0;piccap_disconnected_since=now;release_capture(false);retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;}
}
bool grace=protection==PLAYBACK_UNKNOWN && unknown_since && now-unknown_since<UNKNOWN_GRACE_MS && capture_source==SOURCE_PICCAP;
+71
View File
@@ -0,0 +1,71 @@
#ifndef HYPERDRM_RECEIVER_STREAM_H
#define HYPERDRM_RECEIVER_STREAM_H
#ifndef RECEIVER_LAYOUT_REVISION
#define RECEIVER_LAYOUT_REVISION 0
#endif
/* Independent raw LED subscription: bounded, nonblocking, before receiver
* color correction. Old result and newer data envelopes are both accepted. */
typedef struct {int fd,stage;uint64_t deadline,retry_at,updated;unsigned sequence,count,revision,layout_revision;LedBlend *mapping;char *data,*request;size_t used,sent,length;unsigned char colors[16][3];bool valid;} ReceiverStream;
static ReceiverStream stream={.fd=-1};
static void stream_close(uint64_t now){if(stream.fd>=0)close(stream.fd);free(stream.mapping);free(stream.data);free(stream.request);stream=(ReceiverStream){.fd=-1,.retry_at=now+5000};}
static bool stream_queue(const char *line){
if(stream.fd<0 || stream.request)return false;
stream.request=strdup(line);if(!stream.request)return false;stream.length=strlen(line);stream.sent=0;return true;
}
static void stream_start(uint64_t now){
stream.fd=target_open(&target);if(stream.fd<0){stream.retry_at=now+5000;return;}
stream.data=malloc(262145);if(!stream.data){stream_close(now);return;}stream.stage=1;stream.deadline=now+3000;stream.revision=calibration_revision();stream.layout_revision=RECEIVER_LAYOUT_REVISION;
}
static bool stream_colors(PlaybackJson *j,void *root,uint64_t now){
void *body=playback_field(j,root,"result");if(!body)body=playback_field(j,root,"data");
void *leds=playback_field(j,body,"leds");
if(!leds || j->type(leds)!=5)return false;
if(j->length(leds)!=(size_t)stream.count*3){stream.revision=0;return false;}
uint64_t sums[16][3]={{0}},weights[16]={0};
for(unsigned i=0;i<stream.count;i++){
unsigned values[3];for(unsigned c=0;c<3;c++){void *v=j->at(leds,i*3+c);if(!v || j->type(v)!=3)return false;char *end;long n=strtol(j->string(v),&end,10);if(*end || n<0 || n>255)return false;values[c]=(unsigned)n;}
LedBlend b=stream.mapping[i];unsigned w[2]={65535-b.weight,b.weight},r[2]={b.a,b.b};
for(unsigned k=0;k<2;k++){weights[r[k]]+=w[k];for(unsigned c=0;c<3;c++)sums[r[k]][c]+=(uint64_t)values[c]*w[k];}
}
unsigned char mean[3]={0};uint64_t total=0,all[3]={0};
for(unsigned r=0;r<16;r++){total+=weights[r];for(unsigned c=0;c<3;c++)all[c]+=sums[r][c];}
if(!total)return false;
for(unsigned c=0;c<3;c++)mean[c]=(unsigned char)(all[c]/total);
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)stream.colors[r][c]=weights[r]?(unsigned char)(sums[r][c]/weights[r]):mean[c];
stream.valid=true;stream.updated=now;stream.sequence++;return true;
}
static void stream_tick(PlaybackJson *j,uint64_t now,bool enabled){
if(!enabled){if(stream.fd>=0)stream_close(now);return;}
if(stream.fd>=0 && (stream.revision!=calibration_revision() || stream.layout_revision!=RECEIVER_LAYOUT_REVISION)){stream_close(now);stream.retry_at=now;}
if(stream.fd<0){if(now>=stream.retry_at)stream_start(now);return;}
if(stream.stage<4 && now>=stream.deadline){stream_close(now);return;}
if(stream.stage==1){
struct pollfd p={stream.fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);if(poll(&p,1,0)<=0)return;
if(getsockopt(stream.fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){stream_close(now);return;}
char line[384];if(target.token[0]){snprintf(line,sizeof(line),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);stream.stage=2;}
else {snprintf(line,sizeof(line),"{\"command\":\"serverinfo\"}\n");stream.stage=3;}
if(!stream_queue(line)){stream_close(now);return;}
}
if(stream.request){
ssize_t n=send(stream.fd,stream.request+stream.sent,stream.length-stream.sent,MSG_NOSIGNAL);
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
if(n<=0){stream_close(now);return;}stream.sent+=(size_t)n;
if(stream.sent==stream.length){free(stream.request);stream.request=NULL;}
}
if(stream.used==262144){stream_close(now);return;}
size_t available=262144-stream.used;if(available>65536)available=65536;
ssize_t n=recv(stream.fd,stream.data+stream.used,available,0);
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
if(n<=0){stream_close(now);return;}stream.used+=(size_t)n;stream.data[stream.used]=0;
for(unsigned messages=0;messages<8;messages++){
char *end=strchr(stream.data,'\n');if(!end)break;*end=0;void *root=j->parse(stream.data);
if(stream.stage==2){if(!playback_true(j,root,"success")){if(root)j->put(root);stream_close(now);return;}stream.stage=3;stream_queue("{\"command\":\"serverinfo\"}\n");stream.deadline=now+3000;}
else if(stream.stage==3){
LedBlend *mapping=NULL;unsigned count=0;if(!layout_from_json(stream.data,&mapping,&count)){if(root)j->put(root);stream_close(now);return;}
stream.mapping=mapping;stream.count=count;stream.stage=4;stream_queue("{\"command\":\"ledcolors\",\"subcommand\":\"ledstream-start\",\"interval\":100}\n");
}else if(playback_is(j,root,"command","ledcolors-ledstream-update"))stream_colors(j,root,now);
if(root)j->put(root);
size_t consumed=(size_t)(end-stream.data)+1;memmove(stream.data,stream.data+consumed,stream.used-consumed);stream.used-=consumed;stream.data[stream.used]=0;
}
}
#endif
+87 -27
View File
@@ -27,6 +27,7 @@
#include "target.h"
#include "spatial_image.h"
#include "diagnostics.h"
#include "continuity.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;
@@ -60,6 +61,10 @@ static unsigned letterbox_mode,settling;
static int pending_letterbox=-1;
static uint64_t candidate_since,next_border_probe;
static bool color_valid;
static Continuity continuity;
static uint64_t sample_at,calibration_until;
static unsigned sample_sequence;
#define CALIBRATION_ACTIVE (calibration_until>milliseconds())
static unsigned led_count;
static LedBlend *led_blends;
static bool interpolation_enabled;
@@ -108,6 +113,12 @@ static int send_line(const char *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 receiver_ack(void){
char answer[4096];size_t used=0;bool complete=false;
while(used+1<sizeof(answer)){ssize_t n=recv(output,answer+used,sizeof(answer)-used-1,0);if(n<=0)break;used+=(size_t)n;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
PlaybackJson json={0};if(!complete || !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 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;
@@ -141,6 +152,34 @@ static void stop_capture(void){release_capture(true);}
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;}
#include "handover.h"
#include "layout_refresh.h"
#define RECEIVER_LAYOUT_REVISION layout_updates
#include "receiver_stream.h"
#include "source_bridge.h"
static bool calibration_session(void *h,void *m,void *ctx){
(void)ctx;const char *p=payload(m);
if(!strcmp(p,"{\"enabled\":true}")){
calibration_until=milliseconds()+110000;
if(capture && vt_preset(capture,0)!=0){calibration_until=0;return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot select full-screen calibration capture\"}");}
capture_preset=letterbox_mode=0;settling=2;pending_letterbox=-1;
}else if(!strcmp(p,"{\"enabled\":false}"))calibration_until=0;
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
return respond(h,m,"{\"returnValue\":true}");
}
static Calibration calibration_draft;
static char calibration_message[256];
static bool report_calibration(void *h,void *m,void *ctx){
if(!playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}");
(void)ctx;void *root=playback_json.parse(payload(m)),*message=playback_field(&playback_json,root,"message");
bool valid=message && playback_json.type(message)==6 && strlen(playback_json.string(message))<sizeof(calibration_message);
if(valid)snprintf(calibration_message,sizeof(calibration_message),"%s",playback_json.string(message));
if(root)playback_json.put(root);
return respond(h,m,valid?"{\"returnValue\":true}":"{\"returnValue\":false,\"errorText\":\"Invalid calibration result message\"}");
}
static bool draft_calibration(void *h,void *m,void *ctx){
(void)ctx;Calibration next={0};
if(!playback_json.lib || !calibration_parse(&playback_json,payload(m),&next) || next.kind!=CAL_CUSTOM || next.count<4 || strcmp(next.model,tv_model))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Invalid measured model profile\"}");
calibration_draft=next;return respond(h,m,"{\"returnValue\":true}");
}
static bool model_busy;
static unsigned long model_token;
static uint64_t model_deadline,next_model_probe;
@@ -192,17 +231,31 @@ static bool start(void *h,void *m,void *ctx){
if(recovery_started){last_recovery_ms=milliseconds()-recovery_started;recovery_started=0;}
return respond(h,m,"{\"returnValue\":true,\"running\":true}");
}
static bool stop(void *h,void *m,void *ctx){(void)ctx;retry_pause(&retries);stop_capture();return respond(h,m,"{\"returnValue\":true,\"running\":false}");}
static bool stop(void *h,void *m,void *ctx){
(void)ctx;calibration_until=0;retry_pause(&retries);
bool bridging=bridge_phase!=0;stream_close(milliseconds());bridge_phase=0;
memset(&continuity,0,sizeof(continuity));stop_capture();
if(bridging){
output=target_connect(&target);
if(output>=0){if(target_login())send_line("{\"command\":\"clear\",\"priority\":149}\n");close(output);output=-1;}
}
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.\"}");
if(geteuid()!=0)return respond(h,m,"{\"returnValue\":false,\"rootRequired\":true,\"errorText\":\"Root access is required to change autostart. Select Enable Root Access in Overview.\"}");
if(!set_boot(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot update the HyperDRM boot hook. Check the installed boot script and whether another file occupies the hook path.\"}");
if(enabled)retry_reset(&retries);
retry_update(&retries,enabled,capture!=NULL,milliseconds());
return respond(h,m,enabled?"{\"returnValue\":true,\"autostart\":true}":"{\"returnValue\":true,\"autostart\":false}");
}
static bool restart_service(void *h,void *m,void *ctx){
(void)ctx;if(strcmp(payload(m),"{}"))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Restart expects an empty object\"}");
bool ok=respond(h,m,"{\"returnValue\":true,\"restarting\":true}");quitting=1;return ok;
}
static bool handover(void *h,void *m,void *ctx){
(void)ctx;const char *p=payload(m);bool enabled;
if(strcmp(p,"{\"enabled\":true}")==0)enabled=true;
@@ -219,7 +272,7 @@ static bool settings(void *h,void *m,void *ctx){
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();test_close();connection_test.stage=TEST_IDLE;target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=0;
stop_capture();stream_close(milliseconds());bridge_phase=0;memset(&continuity,0,sizeof(continuity));test_close();connection_test.stage=TEST_IDLE;target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=0;
if(active && !retries.paused && !retries.blocked)retries.due=milliseconds();
return respond(h,m,"{\"returnValue\":true}");
}
@@ -230,15 +283,21 @@ static bool test_connection(void *h,void *m,void *ctx){
return respond(h,m,"{\"returnValue\":true,\"testing\":true}");
}
static bool status(void *h,void *m,void *ctx){
(void)ctx;char json[4096],safe[512],endpoint[80];size_t n=0;
(void)ctx;char json[8192],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;
uint64_t now=milliseconds();
char report[512];size_t k=0;for(size_t i=0;calibration_message[i] && k+2<sizeof(report);i++){char c=calibration_message[i];if(c=='"'||c=='\\')report[k++]='\\';if((unsigned char)c>=32)report[k++]=c;}report[k]=0;
int used=snprintf(json,sizeof(json),"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\",\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",calibration_key(calibration.kind),calibration_key(calibration_effective()),tv_model,calibration.model);
for(unsigned i=0;i<calibration.count;i++)used+=snprintf(json+used,sizeof(json)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",calibration.points[i].position,calibration.points[i].region);
used+=snprintf(json+used,sizeof(json)-used,"],\"layoutUpdates\":%u,\"lastLayoutCheckAgeMs\":%lld,\"layoutError\":\"%s\",\"piccapFallbacks\":%u,\"piccapCooldownMs\":%llu,",layout_updates,last_layout_check?(long long)(now-last_layout_check):-1,layout_error,piccap_fallbacks,(unsigned long long)(handover_cooldown>now?handover_cooldown-now:0));
used+=snprintf(json+used,sizeof(json)-used,"\"calibrationMessage\":\"%s\",",report);
used+=snprintf(json+used,sizeof(json)-used,"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[",calibration_draft.model);
for(unsigned i=0;i<calibration_draft.count;i++)used+=snprintf(json+used,sizeof(json)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",calibration_draft.points[i].position,calibration_draft.points[i].region);
used+=snprintf(json+used,sizeof(json)-used,"]},\"receiverStreamConnected\":%s,",stream.stage==4?"true":"false");
used+=snprintf(json+used,sizeof(json)-used,"\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s,\"sourceFade\":%s,\"calibrating\":%s,",sample_sequence,sample_at?(long long)(now-sample_at):-1,continuity.holding?"true":"false",(continuity.fading || bridge_phase==3)?"true":"false",CALIBRATION_ACTIVE?"true":"false");
used+=snprintf(json+used,sizeof(json)-used,"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\",\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",target.piccap_port,test_stage_name(),connection_test.leds,(unsigned long long)connection_test.elapsed,connection_test.error,last_output_at?(long long)(now-last_output_at):-1,(unsigned long long)(retries.due>now?retries.due-now:0),capture_source==SOURCE_PICCAP_STARTING?"Starting PicCap":capture_source==SOURCE_PICCAP_STOPPING?"Stopping PicCap":piccap_owned?"PicCap":capture?"Histogram":"Stopped",piccap_error);
used+=snprintf(json+used,sizeof(json)-used,"\"version\":\"0.10.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_compatible(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
used+=snprintf(json+used,sizeof(json)-used,"\"version\":\"0.11.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_compatible(&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]);
@@ -248,7 +307,9 @@ static int tick(void *unused){
(void)unused;
uint64_t now=milliseconds();
model_tick(now);test_tick(&playback_json,now);layout_refresh_tick(&playback_json,now,capture || capture_source==SOURCE_PICCAP);
if(handover_tick())return 1;
stream_tick(&playback_json,now,handover_enabled && !retries.paused && !retries.blocked);
bool owned=handover_tick();bridge_tick(now);
if(owned)return 1;
if(!capture){
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
if(retry_take(&retries,milliseconds())){
@@ -259,12 +320,22 @@ static int tick(void *unused){
}
if(revoked){set_error("LG revoked capture; recovering after source transition");capture_recoveries++;recovery_started=milliseconds();release_capture(false);retry_source_transition(&retries,milliseconds());return 1;}
if(settling){settling--;return 1;}
if(letterbox_mode && capture_preset==1 && milliseconds()>=next_border_probe){
if(!CALIBRATION_ACTIVE && 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;}
bool populated=!rc && histogram_populated(hist);
if(!populated){
unsigned char held[16][3];
color_valid=false;
if(!continuity_frame(&continuity,false,rgb,now,held)){set_error("Histogram data unavailable beyond hold window; recovering capture");release_capture(false);retry_source_transition(&retries,now);return 1;}
char *line=region_command(held,led_blends,led_count,(unsigned)(1500-(now-continuity.missing_at)));
if(!line || send_line(line)){free(line);set_error("Receiver connection lost while holding colors");stop_capture();return 1;}free(line);
if(!receiver_ack()){set_error("Receiver did not acknowledge held colors");stop_capture();return 1;}
last_output_at=now;return 1;
}
sample_at=now;sample_sequence++;
unsigned aggregate[3]={0};
for(int i=0;i<16;i++){
black_percent[i]=100;
@@ -278,7 +349,7 @@ static int tick(void *unused){
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){
if(!CALIBRATION_ACTIVE && 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;}
@@ -290,26 +361,15 @@ static int tick(void *unused){
}
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);
unsigned char mapped_rgb[16][3];calibration_colors(rgb,mapped_rgb);
unsigned char mapped_rgb[16][3],stable_rgb[16][3];calibration_colors(rgb,mapped_rgb);
continuity_frame(&continuity,true,mapped_rgb,now,stable_rgb);
interpolation_enabled=led_count && calibration_effective()!=CAL_AVERAGE;
bool image_output=strcmp(target.receiver,"HyperHDR")==0;
size_t capacity=256+(size_t)led_count*12;char *line=image_output?spatial_image_command(mapped_rgb,PRIORITY):malloc(capacity);
char *line=region_command(stable_rgb,led_blends,led_count,1500);
if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;}
if(!image_output){
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(mapped_rgb[blend.a],mapped_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("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("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;}
if(!receiver_ack()){set_error("Receiver did not acknowledge color data");stop_capture();return 1;}
last_error[0]=0;
color_valid=true;last_output_at=milliseconds();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;}
@@ -332,13 +392,13 @@ 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},{"settings",settings,0},{"testConnection",test_connection,0},{"calibration",set_calibration,0},{NULL,NULL,0}};
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"restartService",restart_service,0},{"handover",handover,0},{"settings",settings,0},{"testConnection",test_connection,0},{"calibration",set_calibration,0},{"calibrationSession",calibration_session,0},{"calibrationDraft",draft_calibration,0},{"calibrationResult",report_calibration,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);
timer(100,tick,NULL);
while(!quitting){iterate(NULL,0);usleep(10000);}
test_close();stop_capture();
stream_close(milliseconds());test_close();stop_capture();
if(piccap_owned){
handover_enabled=false;retry_pause(&retries);
uint64_t deadline=milliseconds()+4000;
+47
View File
@@ -0,0 +1,47 @@
#ifndef HYPERDRM_SOURCE_BRIDGE_H
#define HYPERDRM_SOURCE_BRIDGE_H
static unsigned bridge_phase,bridge_sequence;
static uint64_t bridge_started,bridge_clear_at,bridge_fade_at,bridge_last_sent;
static unsigned char bridge_from[16][3],bridge_to[16][3];
static CaptureSource bridge_source;
static char *region_command(const unsigned char colors[16][3],const LedBlend *mapping,unsigned count,unsigned duration){
if(!count || !mapping)return NULL;
if(!strcmp(target.receiver,"HyperHDR"))return spatial_image_command_duration(colors,PRIORITY,duration);
size_t capacity=256+(size_t)count*12;char *line=malloc(capacity);if(!line)return NULL;
size_t n=(size_t)snprintf(line,capacity,"{\"command\":\"color\",\"priority\":%d,\"duration\":%u,\"origin\":\"HyperDRM\",\"color\":[",PRIORITY,duration);
for(unsigned i=0;i<count;i++){unsigned char value[3];LedBlend b=mapping[i];blend_color(colors[b.a],colors[b.b],b.weight,value);n+=(size_t)snprintf(line+n,capacity-n,"%s%u,%u,%u",i?",":"",value[0],value[1],value[2]);}
snprintf(line+n,capacity-n,"]}\n");return line;
}
static bool bridge_clear(void){return stream.stage==4 && stream_queue("{\"command\":\"clear\",\"priority\":149}\n");}
static bool bridge_send(const unsigned char colors[16][3]){
if(stream.stage!=4)return false;
char *line=region_command(colors,stream.mapping,stream.count,1500);if(!line)return false;
bool ok=stream_queue(line);free(line);return ok;
}
static void bridge_tick(uint64_t now){
if(retries.paused || retries.blocked){if(bridge_phase)bridge_clear();bridge_phase=0;bridge_source=capture_source;return;}
if(capture_source!=bridge_source){
if(capture_source==SOURCE_PICCAP_STARTING){
bridge_phase=continuity.available && now-continuity.valid_at<2000?1:0;bridge_started=now;memcpy(bridge_from,continuity.last,sizeof(bridge_from));
}else if(capture_source==SOURCE_PICCAP_STOPPING || (bridge_source==SOURCE_PICCAP && capture_source==SOURCE_HISTOGRAM)){
bridge_phase=0;
if(stream.valid && now-stream.updated<1000){continuity_seed(&continuity,stream.colors,now);bridge_send(stream.colors);}
}
bridge_source=capture_source;
}
if(bridge_phase && now-bridge_started>=3000){if(bridge_clear())bridge_phase=0;return;}
if(bridge_phase==1 && capture_source==SOURCE_PICCAP && piccap_connected && stream.stage==4){
if(bridge_clear()){bridge_sequence=stream.sequence;bridge_clear_at=now;bridge_phase=2;}
}else if(bridge_phase==1 && now-bridge_last_sent>=100 && stream.stage==4){
if(bridge_send(bridge_from))bridge_last_sent=now;
}else if(bridge_phase==2 && stream.valid && stream.sequence>bridge_sequence && stream.updated>=bridge_clear_at+150){
memcpy(bridge_to,stream.colors,sizeof(bridge_to));bridge_fade_at=now;bridge_phase=3;
bool black=true;for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)if(bridge_to[r][c]>3)black=false;
if(black)bridge_phase=0;
}
if(bridge_phase==3 && now-bridge_last_sent>=100){
if(now-bridge_fade_at>=500){if(bridge_clear())bridge_phase=0;}
else {unsigned char colors[16][3];continuity_mix(bridge_from,bridge_to,(unsigned)(now-bridge_fade_at),colors);if(bridge_send(colors))bridge_last_sent=now;}
}
}
#endif
+3 -2
View File
@@ -9,7 +9,7 @@ enum {SPATIAL_WIDTH=160,SPATIAL_HEIGHT=90,SPATIAL_PIXELS=SPATIAL_WIDTH*SPATIAL_H
/* Synthetic color plane from histogram samples, never captured video pixels.
* HyperHDR 22 treats JSON color as a solid color; image input retains space.
* The receiver samples this plane using its configured LED rectangles. */
static char *spatial_image_command(const unsigned char regions[16][3],unsigned priority){
static char *spatial_image_command_duration(const unsigned char regions[16][3],unsigned priority,unsigned duration){
static LedBlend blends[SPATIAL_PIXELS];static unsigned generation;
if(generation!=calibration_generation){
for(unsigned y=0;y<SPATIAL_HEIGHT;y++)for(unsigned x=0;x<SPATIAL_WIDTH;x++)
@@ -18,7 +18,7 @@ static char *spatial_image_command(const unsigned char regions[16][3],unsigned p
}
const char alphabet[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t capacity=SPATIAL_PIXELS*4+256;char *line=malloc(capacity);if(!line)return NULL;
size_t used=snprintf(line,capacity,"{\"command\":\"image\",\"priority\":%u,\"duration\":1500,\"origin\":\"HyperDRM\",\"imagewidth\":%u,\"imageheight\":%u,\"format\":\"rgb\",\"name\":\"Histogram colors\",\"imagedata\":\"",priority,SPATIAL_WIDTH,SPATIAL_HEIGHT);
size_t used=snprintf(line,capacity,"{\"command\":\"image\",\"priority\":%u,\"duration\":%u,\"origin\":\"HyperDRM\",\"imagewidth\":%u,\"imageheight\":%u,\"format\":\"rgb\",\"name\":\"Histogram colors\",\"imagedata\":\"",priority,duration,SPATIAL_WIDTH,SPATIAL_HEIGHT);
for(unsigned i=0;i<SPATIAL_PIXELS;i++){
unsigned char rgb[3],color[3];LedBlend b=blends[i];blend_color(regions[b.a],regions[b.b],b.weight,rgb);boost_color(rgb,color);
line[used++]=alphabet[color[0]>>2];line[used++]=alphabet[((color[0]&3)<<4)|(color[1]>>4)];
@@ -26,4 +26,5 @@ static char *spatial_image_command(const unsigned char regions[16][3],unsigned p
}
line[used++]='"';line[used++]='}';line[used++]='\n';line[used]=0;return line;
}
static inline char *spatial_image_command(const unsigned char regions[16][3],unsigned priority){return spatial_image_command_duration(regions,priority,1500);}
#endif
+27
View File
@@ -0,0 +1,27 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include "histogram.h"
#include "continuity.h"
int main(void){
Histogram hist[16]={0};assert(!histogram_populated(hist));
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)hist[r].bins[c][0]=100;
assert(histogram_populated(hist));hist[4].bins[2][0]=0;assert(!histogram_populated(hist));
Continuity s={0};unsigned char red[16][3]={{0}},blue[16][3]={{0}},black[16][3]={{0}},out[16][3];
for(unsigned r=0;r<16;r++){red[r][0]=200;blue[r][2]=200;}
assert(!continuity_frame(&s,false,red,1000,out));
assert(continuity_frame(&s,true,red,1100,out));assert(!memcmp(out,red,sizeof(out)));
assert(continuity_frame(&s,false,black,1200,out));assert(!memcmp(out,red,sizeof(out)));
assert(continuity_frame(&s,false,black,2699,out));assert(!continuity_frame(&s,false,black,2700,out));
assert(continuity_frame(&s,true,blue,2800,out));
continuity_seed(&s,red,3000);continuity_frame(&s,true,blue,3100,out);assert(out[0][0]==200 && out[0][2]==0);
continuity_frame(&s,true,blue,3350,out);assert(out[0][0]==100 && out[0][2]==100);
continuity_frame(&s,true,blue,3600,out);assert(out[0][0]==0 && out[0][2]==200 && !s.fading);
continuity_seed(&s,red,4000);continuity_frame(&s,true,black,4100,out);assert(!memcmp(out,black,sizeof(out)) && !s.fading);
continuity_frame(&s,false,red,4200,out);assert(!memcmp(out,black,sizeof(out)));
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)black[r][c]=2;
continuity_seed(&s,red,4300);continuity_frame(&s,true,black,4400,out);assert(out[0][0]==2 && out[0][1]==2 && !s.fading);
puts("PASS: empty data versus populated black, bounded hold, recovery fade, immediate real black");return 0;
}
+1 -1
View File
@@ -12,7 +12,7 @@ static uint64_t clock_ms=10000;
static unsigned stopped,called,canceled;
static char last_uri[128],last_data[256];
static uint64_t milliseconds(void){return clock_ms;}
static void stop_capture(void){stopped++;}
static void release_capture(bool clear){(void)clear;stopped++;}
static const char *payload(void *m){return m;}
static void error_init(LSError *e){(void)e;}
static void error_free(LSError *e){(void)e;}
+39
View File
@@ -0,0 +1,39 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <stdint.h>
#include <sys/wait.h>
#include "target.h"
#include "layout.h"
#include "receiver_stream.h"
static void fixture(PlaybackJson *json,bool modern,bool login){
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);
struct sockaddr_in address={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
assert(!bind(server,(void*)&address,sizeof(address)) && !listen(server,1));socklen_t size=sizeof(address);assert(!getsockname(server,(void*)&address,&size));
target.port=ntohs(address.sin_port);snprintf(target.token,sizeof(target.token),"%s",login?"fixture-token":"");
pid_t child=fork();assert(child>=0);
if(!child){
int fd=accept(server,NULL,NULL);if(fd<0)_exit(1);FILE *input=fdopen(dup(fd),"r");char request[512];
if(login){if(!fgets(request,sizeof(request),input) || !strstr(request,"authorize"))_exit(2);write(fd,"{\"success\":true}\n",17);}
if(!fgets(request,sizeof(request),input) || !strstr(request,"serverinfo"))_exit(3);
const char *layout="{\"success\":true,\"info\":{\"leds\":[{\"hmin\":0,\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n";write(fd,layout,strlen(layout));
if(!fgets(request,sizeof(request),input) || !strstr(request,"ledstream-start"))_exit(4);
const char *reply=modern?"{\"success\":true,\"command\":\"ledcolors-ledstream-update\",\"data\":{\"leds\":[20,30,40]}}\n":"{\"success\":true,\"command\":\"ledcolors-ledstream-update\",\"result\":{\"leds\":[20,30,40]}}\n";
write(fd,reply,13);usleep(20000);write(fd,reply+13,strlen(reply)-13);usleep(500000);close(fd);fclose(input);close(server);_exit(0);
}
stream_close(0);stream.retry_at=0;
for(unsigned i=0;i<500 && !stream.valid;i++){stream_tick(json,1000+i*5,true);usleep(1000);}
assert(stream.valid && stream.sequence==1 && stream.count==1);
for(unsigned r=0;r<16;r++){assert(stream.colors[r][0]==20 && stream.colors[r][1]==30 && stream.colors[r][2]==40);}
void *bad=json->parse("{\"result\":{\"leds\":[0,0,999]}}");assert(!stream_colors(json,bad,2000));json->put(bad);assert(stream.colors[0][2]==40);
bad=json->parse("{\"data\":{\"leds\":[0,0,0]}}");assert(stream_colors(json,bad,2000));json->put(bad);assert(stream.colors[0][2]==0);
bad=json->parse("{\"data\":{\"leds\":[1,2,3,4,5,6]}}");assert(!stream_colors(json,bad,2000) && !stream.revision);json->put(bad);
stream_tick(json,2001,false);assert(stream.fd<0 && !stream.valid);int status;waitpid(child,&status,0);assert(WIFEXITED(status) && !WEXITSTATUS(status));close(server);
}
int main(void){
PlaybackJson json={0};assert(playback_json_init(&json));fixture(&json,false,false);fixture(&json,true,true);
stream_start(1000);stream_tick(&json,5000,true);assert(stream.fd<0);stream_close(0);
TargetConfig copy=target;assert(target_valid(&copy));assert(target_piccap_compatible(&copy));assert(!target_piccap_local(&copy));assert(target_piccap_host(&copy));
assert(!target_parse(&json,"{}",&copy));assert(!target_save("/no/such/profile",&copy));assert(!target_load(&json,"/no/such/profile",&copy));
int fd=target_connect(&copy);if(fd>=0)close(fd);
dlclose(json.lib);puts("PASS: fragmented raw LED subscription, token login, old/new envelopes, invalid samples, real black, timeout and cleanup");return 0;
}
+43
View File
@@ -0,0 +1,43 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "histogram.h"
#include "continuity.h"
#include "interpolation.h"
#define PRIORITY 149
typedef struct {unsigned char a,b;unsigned weight;} LedBlend;
typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource;
static CaptureSource capture_source;
static struct {bool paused,blocked;} retries;
static struct {char receiver[16];} target={"Hyperion"};
static struct {int stage;bool valid;uint64_t updated;unsigned sequence,count;LedBlend *mapping;unsigned char colors[16][3];} stream;
static Continuity continuity;
static bool piccap_connected;
static char *queued;
static bool stream_queue(const char *line){if(queued)return false;queued=strdup(line);return queued!=NULL;}
static char *spatial_image_command_duration(const unsigned char colors[16][3],unsigned priority,unsigned duration){(void)colors;(void)priority;(void)duration;return strdup("image");}
#include "source_bridge.h"
static void consume(void){free(queued);queued=NULL;}
int main(void){
LedBlend mapping={0,0,0};stream.mapping=&mapping;stream.count=1;stream.stage=4;
for(unsigned r=0;r<16;r++){continuity.last[r][0]=200;stream.colors[r][2]=200;}continuity.available=true;continuity.valid_at=1000;
capture_source=SOURCE_PICCAP_STARTING;bridge_tick(1000);assert(bridge_phase==1 && strstr(queued,"200,0,0"));consume();
capture_source=SOURCE_PICCAP;piccap_connected=true;bridge_tick(1200);assert(bridge_phase==2 && strstr(queued,"clear"));consume();
stream.valid=true;stream.sequence=1;stream.updated=1300;bridge_tick(1300);assert(bridge_phase==2);
stream.sequence=2;stream.updated=1400;bridge_tick(1400);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume();
bridge_tick(1650);assert(strstr(queued,"100,0,100"));consume();
bridge_tick(1900);assert(!bridge_phase && strstr(queued,"clear"));consume();
capture_source=SOURCE_PICCAP_STOPPING;stream.updated=2000;bridge_tick(2000);assert(continuity.fading && continuity.last[0][2]==200 && strstr(queued,"0,0,200"));consume();
capture_source=SOURCE_HISTOGRAM;bridge_tick(2100);assert(!queued);
capture_source=SOURCE_PICCAP_STARTING;continuity.valid_at=3000;bridge_tick(3000);consume();bridge_tick(6000);assert(!bridge_phase && strstr(queued,"clear"));consume();
capture_source=SOURCE_PICCAP_STARTING;bridge_source=SOURCE_HISTOGRAM;continuity.valid_at=7000;bridge_tick(7000);consume();retries.paused=true;bridge_tick(7100);assert(!bridge_phase && strstr(queued,"clear"));consume();
retries.paused=false;bridge_source=SOURCE_HISTOGRAM;continuity.valid_at=8000;capture_source=SOURCE_PICCAP_STARTING;bridge_tick(8000);consume();
capture_source=SOURCE_PICCAP;bridge_tick(8100);consume();stream.sequence++;stream.updated=8300;memset(stream.colors,0,sizeof(stream.colors));bridge_tick(8300);assert(!bridge_phase && !queued);
bridge_source=SOURCE_HISTOGRAM;capture_source=SOURCE_PICCAP_STARTING;continuity.valid_at=1000;bridge_tick(9000);assert(!bridge_phase && !queued);
stream.stage=1;bridge_phase=1;bridge_started=9000;bridge_tick(12000);assert(!queued);retries.paused=true;bridge_tick(12100);assert(!bridge_phase && !queued);
strcpy(target.receiver,"HyperHDR");char *line=region_command(stream.colors,&mapping,1,500);assert(!strcmp(line,"image"));free(line);assert(!region_command(stream.colors,NULL,0,500));
puts("PASS: both source transitions, fresh sample gate, fade midpoint/end, bounded timeout, manual-stop cleanup");return 0;
}