Private
Public Access
Add resilient handover, live LED layout refresh and TV calibration profiles
This commit is contained in:
1 parent
4ab4fa034b
commit
e55d37772a
26 files changed
+543
-134
No files matched your search
@@ -0,0 +1,84 @@
|
||||
#ifndef HYPERDRM_CALIBRATION_H
|
||||
#define HYPERDRM_CALIBRATION_H
|
||||
#include <math.h>
|
||||
#include "playback.h"
|
||||
#define CALIBRATION_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/calibration.json"
|
||||
typedef struct {double position;unsigned char region;} RimPoint;
|
||||
typedef enum {CAL_AUTO,CAL_G4,CAL_AVERAGE,CAL_CUSTOM} CalibrationKind;
|
||||
typedef struct {CalibrationKind kind;char model[64];unsigned count;RimPoint points[16];} Calibration;
|
||||
static Calibration calibration;
|
||||
static char tv_model[64];
|
||||
static unsigned calibration_generation=1;
|
||||
static inline unsigned calibration_revision(void){return calibration_generation;}
|
||||
static const RimPoint g4_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 inline const char *calibration_key(CalibrationKind kind){
|
||||
const char *keys[]={"auto","g4","average","custom"};return keys[kind];
|
||||
}
|
||||
static inline bool calibration_model_valid(const char *model){
|
||||
if(!model[0] || strlen(model)>63)return false;
|
||||
for(const unsigned char *p=(const unsigned char*)model;*p;p++)
|
||||
if(!((*p>='A'&&*p<='Z')||(*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')||*p=='.'||*p=='-'||*p=='_'||*p==' '))return false;
|
||||
return true;
|
||||
}
|
||||
/* LG retail model identifiers: OLED + two-digit panel size + G4 series. */
|
||||
static inline bool calibration_is_g4(const char *model){
|
||||
return strlen(model)>=8 && !strncmp(model,"OLED",4) && model[4]>='0' && model[4]<='9' && model[5]>='0' && model[5]<='9' && model[6]=='G' && model[7]=='4';
|
||||
}
|
||||
static inline CalibrationKind calibration_effective(void){
|
||||
if(calibration.kind!=CAL_AUTO)return calibration.kind;
|
||||
if(calibration.count>=2 && tv_model[0] && !strcmp(calibration.model,tv_model))return CAL_CUSTOM;
|
||||
return calibration_is_g4(tv_model)?CAL_G4:CAL_AVERAGE;
|
||||
}
|
||||
static inline bool calibration_parse(PlaybackJson *j,const char *text,Calibration *result){
|
||||
void *root=j->parse(text);if(!root)return false;
|
||||
Calibration next=*result;bool valid=false;
|
||||
for(unsigned k=0;k<4;k++)if(playback_is(j,root,"profile",calibration_key(k))){next.kind=k;valid=true;break;}
|
||||
void *points=NULL;
|
||||
if(j->type(root)==4 && j->get(root,"points",&points)){
|
||||
void *model=playback_field(j,root,"model");
|
||||
valid=valid && points && j->type(points)==5 && model && j->type(model)==6 && calibration_model_valid(j->string(model));
|
||||
size_t count=valid?j->length(points):0;valid=valid && count>=2 && count<=16;
|
||||
if(valid){strcpy(next.model,j->string(model));next.count=count;memset(next.points,0,sizeof(next.points));}
|
||||
for(size_t i=0;valid && i<count;i++){
|
||||
void *point=j->at(points,i),*position=playback_field(j,point,"position"),*region=playback_field(j,point,"region");
|
||||
valid=position && (j->type(position)==2 || j->type(position)==3) && region && j->type(region)==3;
|
||||
if(valid){char *end;double x=strtod(j->string(position),&end);valid=end && !*end && isfinite(x) && x>=0 && x<4;
|
||||
long r=strtol(j->string(region),&end,10);valid=valid && end && !*end && r>=0 && r<16 && (!i || x>next.points[i-1].position);
|
||||
if(valid){next.points[i].position=x;next.points[i].region=r;}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(next.kind==CAL_CUSTOM && next.count<2)valid=false;
|
||||
j->put(root);if(valid)*result=next;return valid;
|
||||
}
|
||||
static inline bool calibration_save(const char *path,const Calibration *value){
|
||||
char temp[512],data[2048];int used=snprintf(data,sizeof(data),"{\"profile\":\"%s\"",calibration_key(value->kind));
|
||||
if(value->count){
|
||||
used+=snprintf(data+used,sizeof(data)-used,",\"model\":\"%s\",\"points\":[",value->model);
|
||||
for(unsigned i=0;i<value->count;i++)used+=snprintf(data+used,sizeof(data)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",value->points[i].position,value->points[i].region);
|
||||
used+=snprintf(data+used,sizeof(data)-used,"]");
|
||||
}
|
||||
used+=snprintf(data+used,sizeof(data)-used,"}\n");
|
||||
if(snprintf(temp,sizeof(temp),"%s.tmp",path)>=(int)sizeof(temp))return false;
|
||||
int fd=open(temp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
||||
bool ok=write(fd,data,used)==used && fsync(fd)==0;if(close(fd)!=0)ok=false;
|
||||
if(ok)ok=rename(temp,path)==0;
|
||||
if(!ok)unlink(temp);
|
||||
return ok;
|
||||
}
|
||||
static inline void calibration_load(PlaybackJson *j,const char *path){
|
||||
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return;
|
||||
char data[2048];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);
|
||||
if(n<=0 || n==(ssize_t)sizeof(data)-1)return;
|
||||
data[n]=0;calibration_parse(j,data,&calibration);
|
||||
}
|
||||
static inline void calibration_colors(const unsigned char input[16][3],unsigned char output[16][3]){
|
||||
memcpy(output,input,48);if(calibration_effective()!=CAL_AVERAGE)return;
|
||||
unsigned sum[3]={0};for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)sum[c]+=input[i][c];
|
||||
for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)output[i][c]=sum[c]/16;
|
||||
}
|
||||
#endif
|
||||
+55
-48
@@ -3,69 +3,76 @@
|
||||
/* An independent nonblocking connection. Tests never send colors or acquire
|
||||
* capture. Work per timer iteration is bounded to keep output responsive. */
|
||||
typedef enum {TEST_IDLE,TEST_CONNECT,TEST_SEND,TEST_LOGIN,TEST_LAYOUT,TEST_DONE,TEST_FAILED} TestStage;
|
||||
static struct {
|
||||
typedef struct {
|
||||
int fd;TestStage stage,after_send;TargetConfig target;
|
||||
uint64_t started,deadline,elapsed;unsigned leds;
|
||||
char request[384],error[128];size_t sent,used;char *response;
|
||||
} connection_test={.fd=-1};
|
||||
static const char *test_stage_name(void){
|
||||
LedBlend *mapping;
|
||||
} ReceiverProbe;
|
||||
static ReceiverProbe connection_test={.fd=-1};
|
||||
static const char *probe_stage_name(const ReceiverProbe *probe){
|
||||
static const char *names[]={"Not tested","Connecting","Sending request","Checking authentication","Reading LED layout","Passed","Failed"};
|
||||
return names[connection_test.stage];
|
||||
return names[probe->stage];
|
||||
}
|
||||
static bool test_busy(void){return connection_test.stage>=TEST_CONNECT && connection_test.stage<=TEST_LAYOUT;}
|
||||
static void test_close(void){if(connection_test.fd>=0)close(connection_test.fd);connection_test.fd=-1;free(connection_test.response);connection_test.response=NULL;memset(connection_test.request,0,sizeof(connection_test.request));memset(connection_test.target.token,0,sizeof(connection_test.target.token));}
|
||||
static void test_finish(const char *error,uint64_t now){
|
||||
test_close();connection_test.elapsed=now-connection_test.started;
|
||||
connection_test.stage=error?TEST_FAILED:TEST_DONE;
|
||||
snprintf(connection_test.error,sizeof(connection_test.error),"%s",error?error:"");
|
||||
static bool probe_busy(const ReceiverProbe *probe){return probe->stage>=TEST_CONNECT && probe->stage<=TEST_LAYOUT;}
|
||||
static void probe_close(ReceiverProbe *probe){if(probe->fd>=0)close(probe->fd);probe->fd=-1;free(probe->mapping);probe->mapping=NULL;free(probe->response);probe->response=NULL;memset(probe->request,0,sizeof(probe->request));memset(probe->target.token,0,sizeof(probe->target.token));}
|
||||
static void probe_finish(ReceiverProbe *probe,const char *error,uint64_t now){
|
||||
probe_close(probe);probe->elapsed=now-probe->started;
|
||||
probe->stage=error?TEST_FAILED:TEST_DONE;
|
||||
snprintf(probe->error,sizeof(probe->error),"%s",error?error:"");
|
||||
}
|
||||
static void test_request(bool login,uint64_t now){
|
||||
if(login)snprintf(connection_test.request,sizeof(connection_test.request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",connection_test.target.token);
|
||||
else snprintf(connection_test.request,sizeof(connection_test.request),"{\"command\":\"serverinfo\"}\n");
|
||||
connection_test.sent=connection_test.used=0;connection_test.after_send=login?TEST_LOGIN:TEST_LAYOUT;
|
||||
connection_test.stage=TEST_SEND;connection_test.deadline=now+3000;
|
||||
static void probe_request(ReceiverProbe *probe,bool login,uint64_t now){
|
||||
if(login)snprintf(probe->request,sizeof(probe->request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",probe->target.token);
|
||||
else snprintf(probe->request,sizeof(probe->request),"{\"command\":\"serverinfo\"}\n");
|
||||
probe->sent=probe->used=0;probe->after_send=login?TEST_LOGIN:TEST_LAYOUT;
|
||||
probe->stage=TEST_SEND;probe->deadline=now+3000;
|
||||
}
|
||||
static bool test_begin(const TargetConfig *target,uint64_t now){
|
||||
if(test_busy())return false;
|
||||
test_close();memset(&connection_test,0,sizeof(connection_test));connection_test.fd=-1;
|
||||
connection_test.target=*target;connection_test.started=now;
|
||||
connection_test.response=malloc(2*1024*1024+1);
|
||||
if(!connection_test.response){test_finish("Cannot allocate connection test buffer",now);return true;}
|
||||
connection_test.fd=target_open(target);
|
||||
if(connection_test.fd<0){test_finish("Cannot open receiver connection",now);return true;}
|
||||
connection_test.stage=TEST_CONNECT;connection_test.deadline=now+3000;return true;
|
||||
static bool probe_begin(ReceiverProbe *probe,const TargetConfig *target,uint64_t now){
|
||||
if(probe_busy(probe))return false;
|
||||
probe_close(probe);memset(probe,0,sizeof(*probe));probe->fd=-1;
|
||||
probe->target=*target;probe->started=now;
|
||||
probe->response=malloc(2*1024*1024+1);
|
||||
if(!probe->response){probe_finish(probe,"Cannot allocate connection test buffer",now);return true;}
|
||||
probe->fd=target_open(target);
|
||||
if(probe->fd<0){probe_finish(probe,"Cannot open receiver connection",now);return true;}
|
||||
probe->stage=TEST_CONNECT;probe->deadline=now+3000;return true;
|
||||
}
|
||||
static void test_tick(PlaybackJson *json,uint64_t now){
|
||||
if(!test_busy())return;
|
||||
if(now>=connection_test.deadline){test_finish("Receiver timed out",now);return;}
|
||||
int fd=connection_test.fd;
|
||||
if(connection_test.stage==TEST_CONNECT){
|
||||
static void probe_tick(ReceiverProbe *probe,PlaybackJson *json,uint64_t now){
|
||||
if(!probe_busy(probe))return;
|
||||
if(now>=probe->deadline){probe_finish(probe,"Receiver timed out",now);return;}
|
||||
int fd=probe->fd;
|
||||
if(probe->stage==TEST_CONNECT){
|
||||
struct pollfd p={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&p,1,0)<=0)return;
|
||||
if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){test_finish("Receiver connection refused or unreachable",now);return;}
|
||||
test_request(connection_test.target.token[0]!=0,now);return;
|
||||
if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){probe_finish(probe,"Receiver connection refused or unreachable",now);return;}
|
||||
probe_request(probe,probe->target.token[0]!=0,now);return;
|
||||
}
|
||||
if(connection_test.stage==TEST_SEND){
|
||||
size_t length=strlen(connection_test.request);
|
||||
ssize_t n=send(fd,connection_test.request+connection_test.sent,length-connection_test.sent,MSG_NOSIGNAL);
|
||||
if(probe->stage==TEST_SEND){
|
||||
size_t length=strlen(probe->request);
|
||||
ssize_t n=send(fd,probe->request+probe->sent,length-probe->sent,MSG_NOSIGNAL);
|
||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
||||
if(n<=0){test_finish("Receiver closed the test connection",now);return;}
|
||||
connection_test.sent+=(size_t)n;
|
||||
if(connection_test.sent==length){connection_test.stage=connection_test.after_send;connection_test.deadline=now+3000;memset(connection_test.request,0,sizeof(connection_test.request));}
|
||||
if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;}
|
||||
probe->sent+=(size_t)n;
|
||||
if(probe->sent==length){probe->stage=probe->after_send;probe->deadline=now+3000;memset(probe->request,0,sizeof(probe->request));}
|
||||
return;
|
||||
}
|
||||
size_t available=2*1024*1024-connection_test.used;if(available>65536)available=65536;
|
||||
if(!available){test_finish("Receiver response exceeds 2 MiB",now);return;}
|
||||
ssize_t n=recv(fd,connection_test.response+connection_test.used,available,0);
|
||||
size_t available=2*1024*1024-probe->used;if(available>65536)available=65536;
|
||||
if(!available){probe_finish(probe,"Receiver response exceeds 2 MiB",now);return;}
|
||||
ssize_t n=recv(fd,probe->response+probe->used,available,0);
|
||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
||||
if(n<=0){test_finish("Receiver closed the test connection",now);return;}
|
||||
connection_test.used+=(size_t)n;connection_test.response[connection_test.used]=0;
|
||||
char *end=strchr(connection_test.response,'\n');if(!end)return;*end=0;
|
||||
void *root=json->parse(connection_test.response);bool success=playback_true(json,root,"success");if(root)json->put(root);
|
||||
if(!success){test_finish(connection_test.stage==TEST_LOGIN?"Receiver authentication failed":"Receiver rejected serverinfo; check authorization",now);return;}
|
||||
if(connection_test.stage==TEST_LOGIN){test_request(false,now);return;}
|
||||
if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;}
|
||||
probe->used+=(size_t)n;probe->response[probe->used]=0;
|
||||
char *end=strchr(probe->response,'\n');if(!end)return;*end=0;
|
||||
void *root=json->parse(probe->response);bool success=playback_true(json,root,"success");if(root)json->put(root);
|
||||
if(!success){probe_finish(probe,probe->stage==TEST_LOGIN?"Receiver authentication failed":"Receiver rejected serverinfo; check authorization",now);return;}
|
||||
if(probe->stage==TEST_LOGIN){probe_request(probe,false,now);return;}
|
||||
LedBlend *mapping=NULL;unsigned count=0;
|
||||
if(!layout_from_json(connection_test.response,&mapping,&count)){test_finish("Receiver returned an invalid or empty LED layout",now);return;}
|
||||
free(mapping);connection_test.leds=count;test_finish(NULL,now);
|
||||
if(!layout_from_json(probe->response,&mapping,&count)){probe_finish(probe,"Receiver returned an invalid or empty LED layout",now);return;}
|
||||
probe->leds=count;probe_finish(probe,NULL,now);probe->mapping=mapping;
|
||||
}
|
||||
static inline const char *test_stage_name(void){return probe_stage_name(&connection_test);}
|
||||
static inline bool test_busy(void){return probe_busy(&connection_test);}
|
||||
static inline void test_close(void){probe_close(&connection_test);}
|
||||
static inline bool test_begin(const TargetConfig *target,uint64_t now){return probe_begin(&connection_test,target,now);}
|
||||
static inline void test_tick(PlaybackJson *json,uint64_t now){probe_tick(&connection_test,json,now);}
|
||||
#endif
|
||||
+32
-11
@@ -16,7 +16,12 @@ static bool (*call_once)(void*,const char*,const char*,Callback,void*,unsigned l
|
||||
static bool (*cancel_call)(void*,unsigned long,LSError*);
|
||||
static unsigned long piccap_token,media_token;
|
||||
static uint64_t piccap_deadline,media_deadline;
|
||||
static unsigned clear_samples;
|
||||
static unsigned clear_samples,piccap_fallbacks;
|
||||
static uint64_t clear_since,unknown_since,piccap_disconnected_since;
|
||||
#define CLEAR_STABLE_MS 2000
|
||||
#define UNKNOWN_GRACE_MS 1500
|
||||
#define PICCAP_LOSS_MS 5000
|
||||
#define PICCAP_FAILURE_COOLDOWN_MS 30000
|
||||
static TargetConfig piccap_target;
|
||||
static char piccap_error[128];
|
||||
static bool piccap_request(int command);
|
||||
@@ -38,7 +43,11 @@ static void handover_load(void){
|
||||
static bool media_reply(void *h,void *m,void *ctx){
|
||||
(void)h;(void)ctx;media_busy=false;media_token=0;
|
||||
protection=playback_classify(&playback_json,payload(m));protection_at=milliseconds();
|
||||
if(protection==PLAYBACK_CLEAR){if(clear_samples<2)clear_samples++;}else clear_samples=0;
|
||||
if(protection==PLAYBACK_CLEAR){
|
||||
if(!clear_samples)clear_since=protection_at;
|
||||
if(clear_samples<3)clear_samples++;
|
||||
unknown_since=0;
|
||||
}else {clear_samples=0;clear_since=0;if(protection==PLAYBACK_UNKNOWN && !unknown_since)unknown_since=protection_at;else if(protection==PLAYBACK_PROTECTED)unknown_since=0;}
|
||||
return true;
|
||||
}
|
||||
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before VT reuse. */
|
||||
@@ -50,10 +59,15 @@ static bool piccap_reply(void *h,void *m,void *ctx){
|
||||
if(ok && running && playback_json.type(running)==1){
|
||||
bool active=playback_json.boolean(running);
|
||||
piccap_connected=playback_true(&playback_json,root,"connected");
|
||||
if(active){if(capture_source==SOURCE_PICCAP_STARTING)capture_source=SOURCE_PICCAP;}
|
||||
if(active){
|
||||
if(capture_source==SOURCE_PICCAP_STARTING)capture_source=SOURCE_PICCAP;
|
||||
if(piccap_connected)piccap_disconnected_since=0;
|
||||
else if(!piccap_disconnected_since)piccap_disconnected_since=milliseconds();
|
||||
}
|
||||
else if(capture_source!=SOURCE_PICCAP_STARTING || milliseconds()-piccap_action_at>=2000){
|
||||
if(capture_source!=SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
|
||||
piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;
|
||||
piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;piccap_disconnected_since=0;
|
||||
if(handover_cooldown<milliseconds()+3000)handover_cooldown=milliseconds()+3000;
|
||||
if(!retries.paused)retries.due=milliseconds();
|
||||
}
|
||||
}else if(!piccap_installed()){piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;}
|
||||
@@ -84,7 +98,7 @@ static bool piccap_request(int command){
|
||||
/* Return true while PicCap owns capture or a stop confirmation is pending. */
|
||||
static bool handover_tick(void){
|
||||
uint64_t now=milliseconds();
|
||||
if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;}
|
||||
if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=0;if(!unknown_since)unknown_since=now;}
|
||||
if(piccap_busy && now>=piccap_deadline){snprintf(piccap_error,sizeof(piccap_error),"PicCap request timed out");LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
if(handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && !media_busy && now>=next_media_probe){
|
||||
LSError e;error_init(&e);
|
||||
@@ -92,13 +106,20 @@ static bool handover_tick(void){
|
||||
media_deadline=now+3000;
|
||||
error_free(&e);next_media_probe=now+1000;
|
||||
}
|
||||
bool clear=handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
|
||||
protection==PLAYBACK_CLEAR && clear_samples>=2 && now-protection_at<3000 && now>=handover_cooldown;
|
||||
if(capture_source==SOURCE_HISTOGRAM && clear){
|
||||
piccap_target=target;piccap_error[0]=0;stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
|
||||
if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+10000;}
|
||||
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 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;
|
||||
if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;}
|
||||
}
|
||||
if(piccap_owned && (!clear || strcmp(piccap_target.host,target.host) || piccap_target.piccap_port!=target.piccap_port) && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
bool grace=protection==PLAYBACK_UNKNOWN && unknown_since && now-unknown_since<UNKNOWN_GRACE_MS && capture_source==SOURCE_PICCAP;
|
||||
bool lost=piccap_owned && capture_source==SOURCE_PICCAP && piccap_disconnected_since && now-piccap_disconnected_since>=PICCAP_LOSS_MS;
|
||||
if(lost){
|
||||
snprintf(piccap_error,sizeof(piccap_error),"PicCap receiver disconnected; histogram fallback");piccap_fallbacks++;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;
|
||||
}
|
||||
if(piccap_owned && (!session || (!clear && !grace) || lost || strcmp(piccap_target.host,target.host) || piccap_target.piccap_port!=target.piccap_port) && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
if(piccap_owned && !piccap_busy && now>=next_piccap_probe){
|
||||
if(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0);
|
||||
next_piccap_probe=now+1000;
|
||||
|
||||
+6
-7
@@ -6,19 +6,18 @@
|
||||
#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}
|
||||
};
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include "calibration.h"
|
||||
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;
|
||||
const RimPoint *rim=calibration_effective()==CAL_CUSTOM?calibration.points:g4_rim;
|
||||
double t=screen_perimeter(x,y);unsigned n=calibration_effective()==CAL_CUSTOM?calibration.count:sizeof(g4_rim)/sizeof(*g4_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;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef HYPERDRM_LAYOUT_REFRESH_H
|
||||
#define HYPERDRM_LAYOUT_REFRESH_H
|
||||
#define LAYOUT_REFRESH_MS 5000
|
||||
static ReceiverProbe layout_probe={.fd=-1};
|
||||
static uint64_t next_layout_check,last_layout_check;
|
||||
static unsigned layout_updates,layout_probe_generation;
|
||||
static char layout_error[128];
|
||||
static void layout_refresh_reset(uint64_t due){
|
||||
probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;next_layout_check=due;
|
||||
}
|
||||
static bool layout_mapping_changed(const LedBlend *next,unsigned count){
|
||||
if(count!=led_count || !led_blends)return true;
|
||||
for(unsigned i=0;i<count;i++)if(next[i].a!=led_blends[i].a || next[i].b!=led_blends[i].b || next[i].weight!=led_blends[i].weight)return true;
|
||||
return false;
|
||||
}
|
||||
static void layout_refresh_tick(PlaybackJson *json,uint64_t now,bool running){
|
||||
if(!running){layout_refresh_reset(now+LAYOUT_REFRESH_MS);return;}
|
||||
probe_tick(&layout_probe,json,now);
|
||||
if(layout_probe.stage==TEST_DONE){
|
||||
if(layout_probe_generation==calibration_generation){
|
||||
if(layout_mapping_changed(layout_probe.mapping,layout_probe.leds)){
|
||||
free(led_blends);led_blends=layout_probe.mapping;layout_probe.mapping=NULL;led_count=layout_probe.leds;layout_updates++;
|
||||
}
|
||||
last_layout_check=now;layout_error[0]=0;
|
||||
}else next_layout_check=now;
|
||||
probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;
|
||||
}else if(layout_probe.stage==TEST_FAILED){
|
||||
snprintf(layout_error,sizeof(layout_error),"%s",layout_probe.error);layout_probe.stage=TEST_IDLE;
|
||||
}
|
||||
if(!probe_busy(&layout_probe) && now>=next_layout_check){
|
||||
layout_probe_generation=calibration_generation;probe_begin(&layout_probe,&target,now);next_layout_check=now+LAYOUT_REFRESH_MS;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+45
-10
@@ -114,7 +114,8 @@ static bool read_layout(void){
|
||||
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;
|
||||
LedBlend *mapping=NULL;unsigned count=0;bool ok=complete && layout_from_json(data,&mapping,&count);
|
||||
if(ok){free(led_blends);led_blends=mapping;led_count=count;}free(data);interpolation_enabled=ok && calibration_effective()!=CAL_AVERAGE;return ok;
|
||||
}
|
||||
static bool target_login(void){
|
||||
if(!target.token[0])return true;
|
||||
@@ -128,7 +129,9 @@ static bool target_login(void){
|
||||
PlaybackJson json={0};if(!playback_json_init(&json))return false;
|
||||
void *root=json.parse(answer);bool ok=playback_true(&json,root,"success");if(root)json.put(root);dlclose(json.lib);return ok;
|
||||
}
|
||||
static void layout_refresh_reset(uint64_t due);
|
||||
static void release_capture(bool clear_output){
|
||||
layout_refresh_reset(milliseconds()+5000);
|
||||
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(clear_output && frames){char line[80];snprintf(line,sizeof(line),"{\"command\":\"clear\",\"priority\":%d}\n",PRIORITY);send_line(line);}close(output);output=-1;}
|
||||
@@ -137,6 +140,32 @@ static void release_capture(bool clear_output){
|
||||
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"
|
||||
static bool model_busy;
|
||||
static unsigned long model_token;
|
||||
static uint64_t model_deadline,next_model_probe;
|
||||
static bool model_reply(void *h,void *m,void *ctx){
|
||||
(void)h;(void)ctx;model_busy=false;model_token=0;
|
||||
void *root=playback_json.parse(payload(m)),*model=playback_field(&playback_json,root,"modelName");
|
||||
if(playback_true(&playback_json,root,"returnValue") && model && playback_json.type(model)==6 && calibration_model_valid(playback_json.string(model))){
|
||||
snprintf(tv_model,sizeof(tv_model),"%s",playback_json.string(model));calibration_generation++;layout_refresh_reset(milliseconds());
|
||||
}
|
||||
if(root)playback_json.put(root);
|
||||
return true;
|
||||
}
|
||||
static void model_tick(uint64_t now){
|
||||
if(model_busy && now>=model_deadline){LSError e;error_init(&e);cancel_call(service_handle,model_token,&e);error_free(&e);model_busy=false;model_token=0;}
|
||||
if(!tv_model[0] && !model_busy && now>=next_model_probe){
|
||||
LSError e;error_init(&e);model_busy=call_once(service_handle,"luna://com.webos.service.tv.systemproperty/getSystemInfo","{\"keys\":[\"modelName\"]}",model_reply,NULL,&model_token,&e);error_free(&e);model_deadline=now+3000;next_model_probe=now+60000;
|
||||
}
|
||||
}
|
||||
static bool set_calibration(void *h,void *m,void *ctx){
|
||||
(void)ctx;Calibration next=calibration;
|
||||
if(!playback_json.lib || !calibration_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Select a valid profile; custom profiles need a model and 2-16 sorted rim points (position 0-4, region 0-15)\"}");
|
||||
if(!calibration_save(CALIBRATION_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot save calibration profile\"}");
|
||||
calibration=next;calibration_generation++;layout_refresh_reset(milliseconds());
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static bool start_failure(void *h,void *m,const char *message){
|
||||
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
|
||||
char json[512];snprintf(json,sizeof(json),"{\"returnValue\":false,\"errorText\":\"%s\",\"retryScheduled\":%s,\"retryBlocked\":%s}",message,retries.due?"true":"false",retries.blocked?"true":"false");
|
||||
@@ -182,7 +211,7 @@ static bool handover(void *h,void *m,void *ctx){
|
||||
if(!playback_json.lib || geteuid()!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Root service and JSON-C are required\"}");
|
||||
if(enabled && !piccap_installed())return respond(h,m,"{\"returnValue\":false,\"errorText\":\"PicCap is not installed\"}");
|
||||
if(!handover_save(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save PicCap handover preference\"}");
|
||||
handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;next_media_probe=0;
|
||||
handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=0;next_media_probe=0;
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static bool settings(void *h,void *m,void *ctx){
|
||||
@@ -190,7 +219,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;
|
||||
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;
|
||||
if(active && !retries.paused && !retries.blocked)retries.due=milliseconds();
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
@@ -205,8 +234,11 @@ static bool status(void *h,void *m,void *ctx){
|
||||
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();
|
||||
int used=snprintf(json,sizeof(json),"{\"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.9.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);
|
||||
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,"\"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);
|
||||
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]);
|
||||
@@ -214,7 +246,8 @@ static bool status(void *h,void *m,void *ctx){
|
||||
}
|
||||
static int tick(void *unused){
|
||||
(void)unused;
|
||||
test_tick(&playback_json,milliseconds());
|
||||
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;
|
||||
if(!capture){
|
||||
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
|
||||
@@ -257,14 +290,16 @@ 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);
|
||||
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(rgb,PRIORITY):malloc(capacity);
|
||||
size_t capacity=256+(size_t)led_count*12;char *line=image_output?spatial_image_command(mapped_rgb,PRIORITY):malloc(capacity);
|
||||
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(rgb[blend.a],rgb[blend.b],blend.weight,interpolated);boost_color(interpolated,boosted);
|
||||
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");
|
||||
@@ -286,7 +321,7 @@ int main(void){
|
||||
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
|
||||
call_once=symbol(ls,"LSCallOneReply");
|
||||
cancel_call=symbol(ls,"LSCallCancel");
|
||||
if(playback_json_init(&playback_json)){handover_load();target_load(&playback_json,TARGET_PREF,&target);}
|
||||
if(playback_json_init(&playback_json)){handover_load();target_load(&playback_json,TARGET_PREF,&target);calibration_load(&playback_json,CALIBRATION_PREF);}
|
||||
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");
|
||||
@@ -297,7 +332,7 @@ int main(void){
|
||||
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
|
||||
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
|
||||
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
|
||||
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{"testConnection",test_connection,0},{NULL,NULL,0}};
|
||||
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}};
|
||||
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);
|
||||
|
||||
@@ -10,11 +10,11 @@ enum {SPATIAL_WIDTH=160,SPATIAL_HEIGHT=90,SPATIAL_PIXELS=SPATIAL_WIDTH*SPATIAL_H
|
||||
* 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 LedBlend blends[SPATIAL_PIXELS];static bool initialized;
|
||||
if(!initialized){
|
||||
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++)
|
||||
blends[y*SPATIAL_WIDTH+x]=layout_blend((x+0.5)/SPATIAL_WIDTH,(y+0.5)/SPATIAL_HEIGHT);
|
||||
initialized=true;
|
||||
generation=calibration_generation;
|
||||
}
|
||||
const char alphabet[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t capacity=SPATIAL_PIXELS*4+256;char *line=malloc(capacity);if(!line)return NULL;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include "layout.h"
|
||||
int main(void){
|
||||
(void)layout_from_json;
|
||||
PlaybackJson json={0};assert(playback_json_init(&json));
|
||||
assert(calibration_effective()==CAL_AVERAGE);
|
||||
strcpy(tv_model,"OLED65G42LW.DEUQLJP");assert(calibration_effective()==CAL_G4);
|
||||
assert(!calibration_is_g4("OLED65G52LW") && !calibration_is_g4("LG G4"));
|
||||
Calibration next=calibration;
|
||||
const char *custom="{\"profile\":\"custom\",\"model\":\"OLED65C42LA\",\"points\":[{\"position\":0,\"region\":2},{\"position\":2,\"region\":10}]}";
|
||||
assert(calibration_parse(&json,custom,&next));calibration=next;calibration_generation++;
|
||||
LedBlend b=layout_blend(.5,0);assert(b.a==2 && b.b==10 && b.weight>=16383 && b.weight<=16385);
|
||||
const char *bad[]={"{}","{\"profile\":\"custom\",\"points\":null}","{\"profile\":\"other\"}","{\"profile\":\"custom\",\"model\":\"bad\\\"name\",\"points\":[]}","{\"profile\":\"custom\",\"model\":\"TV\",\"points\":[{\"position\":2,\"region\":1},{\"position\":1,\"region\":2}]}","{\"profile\":\"custom\",\"model\":\"TV\",\"points\":[{\"position\":0,\"region\":16},{\"position\":4,\"region\":2}]}"};
|
||||
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){Calibration before=next;assert(!calibration_parse(&json,bad[i],&next));assert(!memcmp(&before,&next,sizeof(next)));}
|
||||
const char *path="/tmp/hyperdrm-calibration-test.json";assert(access(path,F_OK)!=0);assert(calibration_save(path,&next));
|
||||
calibration=(Calibration){0};calibration_load(&json,path);assert(calibration.count==2 && !strcmp(calibration.model,"OLED65C42LA"));unlink(path);
|
||||
assert(calibration_parse(&json,"{\"profile\":\"auto\"}",&calibration));assert(calibration_effective()==CAL_G4);
|
||||
strcpy(tv_model,"OLED65C42LA");assert(calibration_effective()==CAL_CUSTOM);
|
||||
strcpy(tv_model,"UNKNOWN-TV");assert(calibration_effective()==CAL_AVERAGE);
|
||||
unsigned char input[16][3],output[16][3];for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)input[i][c]=i*10+c;
|
||||
calibration_colors(input,output);for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)assert(output[i][c]==75+c);
|
||||
calibration.kind=CAL_G4;calibration_colors(input,output);assert(!memcmp(input,output,48));
|
||||
puts("PASS: G4 detection, custom model profile, rim mapping, invalid profile rejection, persistence and honest average fallback");return 0;
|
||||
}
|
||||
+51
-37
@@ -20,50 +20,64 @@ static void error_free(LSError *e){(void)e;}
|
||||
#include "target.h"
|
||||
#include "handover.h"
|
||||
static bool mock_call(void *h,const char *uri,const char *data,Callback cb,void *ctx,unsigned long *token,LSError *e){
|
||||
(void)h;snprintf(last_data,sizeof(last_data),"%s",data);(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
|
||||
(void)h;(void)cb;(void)ctx;(void)e;*token=++called;
|
||||
snprintf(last_uri,sizeof(last_uri),"%s",uri);snprintf(last_data,sizeof(last_data),"%s",data);return true;
|
||||
}
|
||||
static bool mock_cancel(void *h,unsigned long token,LSError *e){(void)h;(void)token;(void)e;canceled++;return true;}
|
||||
static void configured(void){
|
||||
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
|
||||
assert(strstr(last_uri,"/start"));
|
||||
static void media(PlaybackProtection state){
|
||||
const char *json=state==PLAYBACK_CLEAR?"[{\"type\":\"media\",\"is_foreground\":true,\"resource\":[{\"resource\":\"VDEC\"}]}]":state==PLAYBACK_PROTECTED?"[{\"resource\":[{\"resource\":\"SVP_CPB\"}]}]":"[]";
|
||||
media_reply(NULL,(void*)json,NULL);
|
||||
}
|
||||
static void answer(int cmd,bool running){
|
||||
char json[128];snprintf(json,sizeof(json),"{\"returnValue\":true,\"isRunning\":%s,\"connected\":true}",running?"true":"false");
|
||||
piccap_reply(NULL,json,(void*)(intptr_t)cmd);
|
||||
static void answer(int command,bool running,bool connected){
|
||||
char json[128];snprintf(json,sizeof(json),"{\"returnValue\":true,\"isRunning\":%s,\"connected\":%s}",running?"true":"false",connected?"true":"false");
|
||||
piccap_reply(NULL,json,(void*)(intptr_t)command);
|
||||
}
|
||||
static void confirm_stop(void){answer(2,true,false);assert(piccap_owned);answer(0,false,false);assert(!piccap_owned && capture_source==SOURCE_HISTOGRAM);}
|
||||
static void stable_clear(void){
|
||||
media(PLAYBACK_UNKNOWN);media(PLAYBACK_CLEAR);assert(!handover_tick());clock_ms+=1000;
|
||||
media(PLAYBACK_CLEAR);assert(!handover_tick());clock_ms+=1000;
|
||||
media(PLAYBACK_CLEAR);assert(handover_tick());assert(strstr(last_uri,"/getSettings"));
|
||||
}
|
||||
static void connected_start(void){
|
||||
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
|
||||
assert(strstr(last_uri,"/start"));answer(1,false,false);clock_ms+=600;
|
||||
media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);assert(capture_source==SOURCE_PICCAP);
|
||||
}
|
||||
int main(void){
|
||||
(void)target_save;(void)target_load;(void)target_connect;
|
||||
(void)handover_save;(void)handover_load;
|
||||
(void)target_save;(void)target_load;(void)target_connect;(void)handover_save;(void)handover_load;
|
||||
(void)retry_failure;(void)retry_take;(void)retry_update;
|
||||
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
|
||||
handover_enabled=true;protection=PLAYBACK_PROTECTED;protection_at=clock_ms;next_media_probe=UINT64_MAX;
|
||||
assert(!handover_tick());assert(called==0 && stopped==0);
|
||||
protection=PLAYBACK_CLEAR;clear_samples=1;assert(!handover_tick());assert(called==0);
|
||||
clear_samples=2;assert(handover_tick());assert(stopped==1 && strstr(last_uri,"/getSettings"));
|
||||
configured();answer(1,false);clock_ms+=600;assert(handover_tick());assert(strstr(last_uri,"/status"));answer(0,true);
|
||||
assert(capture_source==SOURCE_PICCAP && piccap_connected);
|
||||
protection=PLAYBACK_PROTECTED;assert(handover_tick());assert(strstr(last_uri,"/stop"));
|
||||
answer(2,true);assert(strstr(last_uri,"/status"));assert(piccap_owned);answer(0,false);
|
||||
assert(!piccap_owned && capture_source==SOURCE_HISTOGRAM && retries.due==clock_ms);
|
||||
protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;assert(handover_tick());configured();answer(1,true);
|
||||
retry_pause(&retries);assert(handover_tick());assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
|
||||
assert(!piccap_owned && retries.due==0);assert(!handover_tick());
|
||||
retry_reset(&retries);assert(handover_tick());configured();answer(1,true);handover_enabled=false;
|
||||
assert(handover_tick());answer(2,false);answer(0,false);assert(!piccap_owned);
|
||||
handover_enabled=true;protection_at=clock_ms;assert(handover_tick());
|
||||
clock_ms+=3100;assert(handover_tick());assert(canceled==1 && capture_source==SOURCE_PICCAP_STOPPING);
|
||||
assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
|
||||
assert(!piccap_owned);assert(!handover_tick());
|
||||
retries.paused=false;handover_cooldown=0;protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;
|
||||
handover_enabled=true;next_media_probe=UINT64_MAX;
|
||||
media(PLAYBACK_PROTECTED);assert(!handover_tick() && called==0);
|
||||
stable_clear();assert(stopped==1);connected_start();
|
||||
media(PLAYBACK_UNKNOWN);assert(handover_tick() && capture_source==SOURCE_PICCAP);
|
||||
clock_ms+=1000;assert(handover_tick() && capture_source==SOURCE_PICCAP);answer(0,true,true);
|
||||
media(PLAYBACK_CLEAR);assert(handover_tick() && capture_source==SOURCE_PICCAP);
|
||||
media(PLAYBACK_UNKNOWN);clock_ms+=100;media(PLAYBACK_PROTECTED);
|
||||
assert(handover_tick() && capture_source==SOURCE_PICCAP_STOPPING && strstr(last_uri,"/stop"));confirm_stop();
|
||||
assert(retries.due==clock_ms);clock_ms+=3001;handover_cooldown=0;stable_clear();connected_start();
|
||||
media(PLAYBACK_UNKNOWN);clock_ms+=1600;handover_tick();assert(capture_source==SOURCE_PICCAP_STOPPING);confirm_stop();
|
||||
clock_ms+=3001;handover_cooldown=0;stable_clear();connected_start();
|
||||
answer(0,true,false);clock_ms+=1000;media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);
|
||||
assert(!piccap_disconnected_since && piccap_fallbacks==0);
|
||||
answer(0,true,false);clock_ms+=2500;media(PLAYBACK_CLEAR);handover_tick();answer(0,true,false);
|
||||
clock_ms+=2501;media(PLAYBACK_CLEAR);handover_tick();
|
||||
assert(capture_source==SOURCE_PICCAP_STOPPING && piccap_fallbacks==1 && strstr(piccap_error,"disconnected"));confirm_stop();
|
||||
assert(retries.due==clock_ms);clock_ms+=2000;media(PLAYBACK_CLEAR);assert(!handover_tick());
|
||||
clock_ms=handover_cooldown+1;media(PLAYBACK_CLEAR);assert(handover_tick());connected_start();
|
||||
retry_pause(&retries);handover_tick();assert(capture_source==SOURCE_PICCAP_STOPPING);confirm_stop();assert(!retries.due);
|
||||
retry_reset(&retries);clock_ms+=3001;handover_cooldown=0;stable_clear();
|
||||
strcpy(target.host,"10.1.0.100");target.piccap_port=19446;
|
||||
assert(handover_tick());assert(strstr(last_uri,"/getSettings"));
|
||||
handover_tick();assert(capture_source==SOURCE_PICCAP_STOPPING);answer(3,true,false);handover_tick();confirm_stop();
|
||||
clock_ms+=3001;handover_cooldown=0;stable_clear();
|
||||
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
|
||||
assert(strstr(last_uri,"/setSettings"));assert(!strcmp(last_data,"{\"address\":\"10.1.0.100\",\"port\":19446,\"unix-socket\":false}"));
|
||||
answer(4,true);clock_ms+=600;handover_tick();answer(0,true);assert(capture_source==SOURCE_PICCAP);
|
||||
target.piccap_port=19400;assert(handover_tick());assert(capture_source==SOURCE_PICCAP_STOPPING);answer(2,false);answer(0,false);
|
||||
handover_cooldown=0;protection_at=clock_ms;assert(handover_tick());
|
||||
piccap_reply(NULL,"{\"returnValue\":false}",(void*)(intptr_t)3);
|
||||
assert(capture_source==SOURCE_PICCAP_STOPPING && piccap_error[0]);
|
||||
handover_tick();answer(2,false);answer(0,false);assert(!piccap_owned);
|
||||
puts("PicCap handover tests passed: source ownership, remote endpoint, target change and configuration failure");return 0;
|
||||
assert(strstr(last_uri,"/setSettings") && !strcmp(last_data,"{\"address\":\"10.1.0.100\",\"port\":19446,\"unix-socket\":false}"));
|
||||
answer(4,true,false);clock_ms+=600;media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);
|
||||
handover_enabled=false;handover_tick();confirm_stop();handover_enabled=true;handover_cooldown=0;stable_clear();
|
||||
clock_ms+=3100;handover_tick();assert(canceled==1 && capture_source==SOURCE_PICCAP_STOPPING);confirm_stop();
|
||||
handover_cooldown=0;clock_ms+=3001;stable_clear();
|
||||
piccap_reply(NULL,"{\"returnValue\":false}",(void*)(intptr_t)3);assert(capture_source==SOURCE_PICCAP_STOPPING && piccap_error[0]);
|
||||
clock_ms+=600;handover_tick();confirm_stop();
|
||||
strcpy(target.host,"2001:db8::1");handover_cooldown=0;media(PLAYBACK_CLEAR);clock_ms+=2000;media(PLAYBACK_CLEAR);assert(!handover_tick());
|
||||
puts("PASS: stable clear gate, bounded pause grace, immediate DRM fallback, disconnect/reconnect and cooldown, exclusive capture, custom targets and failures");return 0;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "target.h"
|
||||
#include "layout.h"
|
||||
#include "diagnostics.h"
|
||||
static unsigned led_count;
|
||||
static LedBlend *led_blends;
|
||||
#include "layout_refresh.h"
|
||||
static void completed(unsigned count,unsigned generation){
|
||||
layout_probe.mapping=calloc(count,sizeof(LedBlend));assert(layout_probe.mapping);
|
||||
for(unsigned i=0;i<count;i++)layout_probe.mapping[i]=layout_blend((i+.5)/count,0);
|
||||
layout_probe.leds=count;layout_probe.stage=TEST_DONE;layout_probe_generation=generation;
|
||||
}
|
||||
int main(void){
|
||||
(void)target_connect;(void)target_load;(void)target_save;
|
||||
PlaybackJson json={0};assert(playback_json_init(&json));
|
||||
calibration.kind=CAL_G4;next_layout_check=UINT64_MAX;
|
||||
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true);
|
||||
assert(led_count==50 && layout_updates==1 && last_layout_check==1000);
|
||||
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true);
|
||||
assert(led_count==500 && layout_updates==2 && last_layout_check==2000);
|
||||
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true);assert(layout_updates==2);
|
||||
completed(500,calibration_generation);layout_probe.mapping[42].weight^=512;
|
||||
layout_refresh_tick(&json,4000,true);assert(layout_updates==3);
|
||||
LedBlend *previous=led_blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
|
||||
layout_refresh_tick(&json,5000,true);assert(led_blends==previous && led_count==500 && layout_updates==3 && layout_error[0]);
|
||||
completed(50,calibration_generation);calibration_generation++;
|
||||
layout_refresh_tick(&json,6000,true);assert(led_count==500 && led_blends==previous);assert(probe_busy(&layout_probe));
|
||||
layout_refresh_tick(&json,6100,false);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
|
||||
assert(led_count==500 && led_blends==previous);
|
||||
layout_refresh_reset(7000);free(led_blends);test_close();
|
||||
puts("PASS: automatic 50/500 LED replacement, same-count position changes, unchanged layout, invalid reply retention, obsolete profile result and cancellation");return 0;
|
||||
}
|
||||
@@ -13,5 +13,12 @@ int main(void){
|
||||
command=spatial_image_command(regions,149);encoded=strstr(command,"\"imagedata\":\"")+13;
|
||||
assert(memcmp(encoded,encoded+(SPATIAL_WIDTH-1)*4,4));
|
||||
assert(memcmp(encoded,encoded+(SPATIAL_PIXELS-SPATIAL_WIDTH)*4,4));free(command);
|
||||
calibration=(Calibration){.kind=CAL_CUSTOM,.count=2,.points={{0,2},{2,10}}};calibration_generation++;
|
||||
command=spatial_image_command(regions,149);encoded=strstr(command,"\"imagedata\":\"")+13;
|
||||
const char alphabet[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
unsigned char expected[3],actual[3];LedBlend custom=layout_blend(.5/SPATIAL_WIDTH,.5/SPATIAL_HEIGHT);
|
||||
blend_color(regions[custom.a],regions[custom.b],custom.weight,expected);
|
||||
unsigned v[4];for(unsigned i=0;i<4;i++)v[i]=(unsigned)(strchr(alphabet,encoded[i])-alphabet);
|
||||
actual[0]=(v[0]<<2)|(v[1]>>4);actual[1]=(v[1]<<4)|(v[2]>>2);actual[2]=(v[2]<<6)|v[3];assert(!memcmp(expected,actual,3));free(command);
|
||||
puts("PASS: image command, exact neutral RGB base64, spatial edge differences and cached geometry");return 0;
|
||||
}
|
||||
Reference in new issue
Block a user