Private
Public Access
Release 0.11.0 with guided calibration, source transitions and root repair
This commit is contained in:
1 parent
e55d37772a
commit
f74a658097
33 files changed
+855
-75
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;}
|
||||
|
||||
@@ -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(©));assert(target_piccap_compatible(©));assert(!target_piccap_local(©));assert(target_piccap_host(©));
|
||||
assert(!target_parse(&json,"{}",©));assert(!target_save("/no/such/profile",©));assert(!target_load(&json,"/no/such/profile",©));
|
||||
int fd=target_connect(©);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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user