Release 0.12.0

This commit is contained in:
matze19999 committed 2026-10-06 22:01:38 +02:00
1 parent f74a658097
commit 7f6eeab39e
48 files changed
+3617 -1266

No files matched your search

+56 -24
View File
@@ -4,34 +4,66 @@
#define BOOT_DIRECTORY "/var/lib/webosbrew/init.d"
#endif
#ifndef BOOT_SCRIPT
#define BOOT_SCRIPT "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/boot.sh"
#define BOOT_SCRIPT \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/boot.sh"
#endif
#define BOOT_HOOK BOOT_DIRECTORY "/60-histogramhyperion"
/* Exact pre-0.6.1 hook; only this known own file may be migrated. */
static const char legacy_boot_script[] = "#!/bin/sh\n# Histogram Hyperion own boot hook v1\n(\n attempt=0\n while [ -x /var/lib/webosbrew/init.d/60-histogramhyperion ] && [ \"$attempt\" -lt 120 ]; do\n result=$(timeout 12 luna-send -n 1 luna://org.webosbrew.histogramhyperion.service/start '{}' 2>&1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 </dev/null &\nexit 0\n";
static const char legacy_boot_script[] =
"#!/bin/sh\n# Histogram Hyperion own boot hook v1\n(\n attempt=0\n while [ -x "
"/var/lib/webosbrew/init.d/60-histogramhyperion ] && [ \"$attempt\" -lt 120 ]; do\n "
"result=$(timeout 12 luna-send -n 1 luna://org.webosbrew.histogramhyperion.service/start '{}' "
"2>&1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n "
"attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start "
"cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 </dev/null "
"&\nexit 0\n";
/* 0: absent, 1: own legacy copy, 2: own symlink, -1: foreign/error. */
static int boot_hook_kind(void){
struct stat st;if(lstat(BOOT_HOOK,&st)!=0)return errno==ENOENT?0:-1;
if(S_ISLNK(st.st_mode)){
char target[sizeof(BOOT_SCRIPT)+1];ssize_t n=readlink(BOOT_HOOK,target,sizeof(target));
return n==(ssize_t)(sizeof(BOOT_SCRIPT)-1) && memcmp(target,BOOT_SCRIPT,(size_t)n)==0?2:-1;
}
if(!S_ISREG(st.st_mode))return -1;
char data[sizeof(legacy_boot_script)];int fd=open(BOOT_HOOK,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return -1;
ssize_t n=read(fd,data,sizeof(data));close(fd);
return n==(ssize_t)(sizeof(legacy_boot_script)-1) && memcmp(data,legacy_boot_script,(size_t)n)==0?1:-1;
static int boot_hook_kind(void) {
struct stat st;
if (lstat(BOOT_HOOK, &st) != 0)
return errno == ENOENT ? 0 : -1;
if (S_ISLNK(st.st_mode)) {
char target[sizeof(BOOT_SCRIPT) + 1];
ssize_t n = readlink(BOOT_HOOK, target, sizeof(target));
return n == (ssize_t)(sizeof(BOOT_SCRIPT) - 1) &&
memcmp(target, BOOT_SCRIPT, (size_t)n) == 0
? 2
: -1;
}
if (!S_ISREG(st.st_mode))
return -1;
char data[sizeof(legacy_boot_script)];
int fd = open(BOOT_HOOK, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0)
return -1;
ssize_t n = read(fd, data, sizeof(data));
close(fd);
return n == (ssize_t)(sizeof(legacy_boot_script) - 1) &&
memcmp(data, legacy_boot_script, (size_t)n) == 0
? 1
: -1;
}
static bool boot_enabled(void){return boot_hook_kind()>0 && access(BOOT_HOOK,X_OK)==0;}
static bool set_boot(bool enabled){
int kind=boot_hook_kind();if(kind<0)return false;
if(!enabled)return kind==0 || unlink(BOOT_HOOK)==0;
if(access(BOOT_SCRIPT,X_OK)!=0)return false;
if(kind==2)return true;
if(mkdir(BOOT_DIRECTORY,0755)!=0 && errno!=EEXIST)return false;
/* Never overwrite another process's temporary file or a foreign hook. */
if(symlink(BOOT_SCRIPT,BOOT_HOOK ".tmp")!=0)return false;
bool ok=rename(BOOT_HOOK ".tmp",BOOT_HOOK)==0;
if(!ok)unlink(BOOT_HOOK ".tmp");
return ok;
static bool boot_enabled(void) {
return boot_hook_kind() > 0 && access(BOOT_HOOK, X_OK) == 0;
}
static bool set_boot(bool enabled) {
int kind = boot_hook_kind();
if (kind < 0)
return false;
if (!enabled)
return kind == 0 || unlink(BOOT_HOOK) == 0;
if (access(BOOT_SCRIPT, X_OK) != 0)
return false;
if (kind == 2)
return true;
if (mkdir(BOOT_DIRECTORY, 0755) != 0 && errno != EEXIST)
return false;
/* Never overwrite another process's temporary file or a foreign hook. */
if (symlink(BOOT_SCRIPT, BOOT_HOOK ".tmp") != 0)
return false;
bool ok = rename(BOOT_HOOK ".tmp", BOOT_HOOK) == 0;
if (!ok)
unlink(BOOT_HOOK ".tmp");
return ok;
}
#endif
+20 -13
View File
@@ -2,18 +2,25 @@
#define BLACKBARS_H
/* Calibrated preset-0 edge probes on this G4. Return -1 for an ambiguous
* or dark frame so darkness alone cannot switch the active capture mode. */
static inline int letterbox_candidate(const unsigned black[16],const unsigned char color[16][3]) {
const unsigned interior[]={2,6,10,15};unsigned lit=0;
for(unsigned i=0;i<4;i++){
unsigned r=interior[i],peak=color[r][0];
for(unsigned c=1;c<3;c++)if(color[r][c]>peak)peak=color[r][c];
if(black[r]<20 && peak>35)lit++;
}
if(lit<2)return -1;
unsigned top=(black[3]+black[11]+black[12])/3;
unsigned bottom=(black[1]+black[5]+black[9])/3;
if(top>=55 && bottom>=25)return 1;
if(top<20 && bottom<20)return 0;
return -1;
static inline int letterbox_candidate(const unsigned black[16], const unsigned char color[16][3]) {
const unsigned interior[] = {2, 6, 10, 15};
unsigned lit = 0;
for (unsigned i = 0; i < 4; i++) {
unsigned r = interior[i], peak = color[r][0];
for (unsigned c = 1; c < 3; c++)
if (color[r][c] > peak)
peak = color[r][c];
if (black[r] < 20 && peak > 35)
lit++;
}
if (lit < 2)
return -1;
unsigned top = (black[3] + black[11] + black[12]) / 3;
unsigned bottom = (black[1] + black[5] + black[9]) / 3;
if (top >= 55 && bottom >= 25)
return 1;
if (top < 20 && bottom < 20)
return 0;
return -1;
}
#endif
+140 -68
View File
@@ -2,83 +2,155 @@
#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;
#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 unsigned calibration_generation = 1;
static inline unsigned calibration_revision(void) {
return calibration_generation;
}
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;
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 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 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_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 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_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;
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
+150
View File
@@ -0,0 +1,150 @@
/* Capture scheduler and histogram processing. */
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);
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())) {
start(NULL, NULL, NULL);
if (!capture)
retry_failure(&retries, milliseconds());
}
return 1;
}
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 (!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);
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;
for (int c = 0; c < 3; c++) {
uint64_t total = 0, black = 0;
for (int b = 0; b < 256; b++) {
total += hist[i].bins[c][b];
if (b <= 12)
black += hist[i].bins[c][b];
}
unsigned fraction = total ? (unsigned)(100 * black / total) : 0;
if (fraction < black_percent[i])
black_percent[i] = fraction;
}
}
for (int i = 0; i < 16; i++)
for (int c = 0; c < 3; c++) {
rgb[i][c] = histogram_mean(hist[i].bins[c]);
aggregate[c] += rgb[i][c];
}
if (!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;
} else if ((unsigned)candidate != letterbox_mode && now - candidate_since >= 2000)
letterbox_mode = (unsigned)candidate;
if (letterbox_mode) {
if (vt_preset(capture, 1) != 0) {
set_error("Letterbox preset failed");
stop_capture();
return 1;
}
capture_preset = 1;
settling = 2;
next_border_probe = now + 2000;
return 1;
}
}
for (int c = 0; c < 3; c++)
sent_rgb[c] = (unsigned char)(aggregate[c] / 16);
unsigned char boosted_mean[3];
boost_color(sent_rgb, boosted_mean);
memcpy(sent_rgb, boosted_mean, 3);
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;
char *line = region_command(stable_rgb, led_blends, led_count, 1500);
if (!line) {
set_error("Color buffer allocation failed");
stop_capture();
return 1;
}
int failed = send_line(line);
free(line);
if (failed) {
set_error("Receiver connection lost");
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;
}
+3 -2
View File
@@ -1,7 +1,8 @@
#ifndef COLOR_BOOST_H
#define COLOR_BOOST_H
/* Preserve the original saturation and brightness. */
static inline void boost_color(const unsigned char in[3],unsigned char out[3]){
for(unsigned c=0;c<3;c++)out[c]=in[c];
static inline void boost_color(const unsigned char in[3], unsigned char out[3]) {
for (unsigned c = 0; c < 3; c++)
out[c] = in[c];
}
#endif
+68 -27
View File
@@ -1,35 +1,76 @@
#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;
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;
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 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;
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
+173 -66
View File
@@ -2,77 +2,184 @@
#define HYPERDRM_DIAGNOSTICS_H
/* 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;
typedef enum {
TEST_IDLE,
TEST_CONNECT,
TEST_SEND,
TEST_LOGIN,
TEST_LAYOUT,
TEST_DONE,
TEST_FAILED
} TestStage;
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;
LedBlend *mapping;
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;
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[probe->stage];
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[probe->stage];
}
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 bool probe_busy(const ReceiverProbe *probe) {
return probe->stage >= TEST_CONNECT && probe->stage <= TEST_LAYOUT;
}
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 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 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 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 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){probe_finish(probe,"Receiver connection refused or unreachable",now);return;}
probe_request(probe,probe->target.token[0]!=0,now);return;
}
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){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-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){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(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 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 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 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) {
probe_finish(probe, "Receiver connection refused or unreachable", now);
return;
}
probe_request(probe, probe->target.token[0] != 0, now);
return;
}
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) {
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 - 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) {
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(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);
}
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
+255 -105
View File
@@ -5,22 +5,32 @@
#define CALIBRATION_ACTIVE false
#endif
#ifndef PICCAP_BINARY
#define PICCAP_BINARY "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
#define PICCAP_BINARY \
"/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
#endif
#define HANDOVER_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/handover.json"
#define HANDOVER_PREF \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
"handover.json"
static PlaybackJson playback_json;
static bool handover_enabled,piccap_owned,piccap_busy,media_busy,piccap_connected;
static bool handover_enabled, piccap_owned, piccap_busy, media_busy, piccap_connected;
static PlaybackProtection protection;
static uint64_t protection_at,next_media_probe,next_piccap_probe,handover_cooldown,piccap_action_at;
typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource;
static uint64_t protection_at, next_media_probe, next_piccap_probe, handover_cooldown,
piccap_action_at;
typedef enum {
SOURCE_HISTOGRAM,
SOURCE_PICCAP_STARTING,
SOURCE_PICCAP,
SOURCE_PICCAP_STOPPING
} CaptureSource;
static CaptureSource capture_source;
static void *service_handle;
static bool (*call_once)(void*,const char*,const char*,Callback,void*,unsigned long*,LSError*);
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,piccap_fallbacks;
static uint64_t clear_since,unknown_since,piccap_disconnected_since;
static bool (*call_once)(void *, const char *, const char *, Callback, void *, unsigned long *,
LSError *);
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, 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
@@ -28,105 +38,245 @@ static uint64_t clear_since,unknown_since,piccap_disconnected_since;
static TargetConfig piccap_target;
static char piccap_error[128];
static bool piccap_request(int command);
static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;}
static bool handover_save(bool enabled){
const char *data=enabled?"{\"piccapHandover\":true}\n":"{\"piccapHandover\":false}\n";
int fd=open(HANDOVER_PREF ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
ssize_t n=write(fd,data,strlen(data));bool ok=n==(ssize_t)strlen(data) && fsync(fd)==0;
if(close(fd)!=0)ok=false;
if(ok)ok=rename(HANDOVER_PREF ".tmp",HANDOVER_PREF)==0;
if(!ok)unlink(HANDOVER_PREF ".tmp");
return ok;
static inline bool piccap_installed(void) {
return access(PICCAP_BINARY, X_OK) == 0;
}
static void handover_load(void){
int fd=open(HANDOVER_PREF,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return;
char text[128];ssize_t n=read(fd,text,sizeof(text)-1);close(fd);
if(n>0){text[n]=0;void *root=playback_json.parse(text);handover_enabled=playback_true(&playback_json,root,"piccapHandover");if(root)playback_json.put(root);}
static bool handover_save(bool enabled) {
const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n";
int fd = open(HANDOVER_PREF ".tmp", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0)
return false;
ssize_t n = write(fd, data, strlen(data));
bool ok = n == (ssize_t)strlen(data) && fsync(fd) == 0;
if (close(fd) != 0)
ok = false;
if (ok)
ok = rename(HANDOVER_PREF ".tmp", HANDOVER_PREF) == 0;
if (!ok)
unlink(HANDOVER_PREF ".tmp");
return ok;
}
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)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;
static void handover_load(void) {
int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0)
return;
char text[128];
ssize_t n = read(fd, text, sizeof(text) - 1);
close(fd);
if (n > 0) {
text[n] = 0;
void *root = playback_json.parse(text);
handover_enabled = playback_true(&playback_json, root, "piccapHandover");
if (root)
playback_json.put(root);
}
}
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before VT reuse. */
static bool piccap_reply(void *h,void *m,void *ctx){
(void)h;int command=(int)(intptr_t)ctx;piccap_busy=false;piccap_token=0;
void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue");
if(command==0){
void *running=playback_field(&playback_json,root,"isRunning");
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(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_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;}
}else if(command==3 && capture_source==SOURCE_PICCAP_STARTING){
void *port=playback_field(&playback_json,root,"port"),*socket=playback_field(&playback_json,root,"unix-socket");
bool same=ok && playback_is(&playback_json,root,"address",target_piccap_host(&piccap_target)) && port && playback_json.type(port)==3 && strtoul(playback_json.string(port),NULL,10)==piccap_target.piccap_port && socket && playback_json.type(socket)==1 && !playback_json.boolean(socket);
if(ok){if(!piccap_request(same?1:4)){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap endpoint request failed");}}
else {capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"Cannot read PicCap settings");}
}else if(command==4){
if(!ok){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap rejected receiver settings");}
/* setSettings restarts PicCap itself; status confirms ownership. */
}else if(command==1 && !ok){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=milliseconds()+10000;}
if(command>=3 && capture_source==SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
if(root)playback_json.put(root);
next_piccap_probe=milliseconds()+500;
if(command==2)piccap_request(0);
return true;
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)
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;
}
static bool piccap_request(int command){
if(piccap_busy)return false;
const char *uri=command==3?"luna://org.webosbrew.piccap.service/getSettings":command==4?"luna://org.webosbrew.piccap.service/setSettings":command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
LSError e;error_init(&e);
char data[160];snprintf(data,sizeof(data),"{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",target_piccap_host(&piccap_target),piccap_target.piccap_port);
piccap_busy=call_once(service_handle,uri,command==4?data:"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
piccap_deadline=milliseconds()+3000;
error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy;
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before
* VT reuse. */
static bool piccap_reply(void *h, void *m, void *ctx) {
(void)h;
int command = (int)(intptr_t)ctx;
piccap_busy = false;
piccap_token = 0;
void *root = playback_json.parse(payload(m));
bool ok = playback_true(&playback_json, root, "returnValue");
if (command == 0) {
void *running = playback_field(&playback_json, root, "isRunning");
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 (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_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;
}
} else if (command == 3 && capture_source == SOURCE_PICCAP_STARTING) {
void *port = playback_field(&playback_json, root, "port"),
*socket = playback_field(&playback_json, root, "unix-socket");
bool same =
ok &&
playback_is(&playback_json, root, "address", target_piccap_host(&piccap_target)) &&
port && playback_json.type(port) == 3 &&
strtoul(playback_json.string(port), NULL, 10) == piccap_target.piccap_port && socket &&
playback_json.type(socket) == 1 && !playback_json.boolean(socket);
if (ok) {
if (!piccap_request(same ? 1 : 4)) {
capture_source = SOURCE_PICCAP_STOPPING;
snprintf(piccap_error, sizeof(piccap_error), "PicCap endpoint request failed");
}
} else {
capture_source = SOURCE_PICCAP_STOPPING;
snprintf(piccap_error, sizeof(piccap_error), "Cannot read PicCap settings");
}
} else if (command == 4) {
if (!ok) {
capture_source = SOURCE_PICCAP_STOPPING;
snprintf(piccap_error, sizeof(piccap_error), "PicCap rejected receiver settings");
}
/* setSettings restarts PicCap itself; status confirms ownership. */
} else if (command == 1 && !ok) {
capture_source = SOURCE_PICCAP_STOPPING;
handover_cooldown = milliseconds() + 10000;
}
if (command >= 3 && capture_source == SOURCE_PICCAP_STOPPING)
handover_cooldown = milliseconds() + 10000;
if (root)
playback_json.put(root);
next_piccap_probe = milliseconds() + 500;
if (command == 2)
piccap_request(0);
return true;
}
static bool piccap_request(int command) {
if (piccap_busy)
return false;
const char *uri = command == 3 ? "luna://org.webosbrew.piccap.service/getSettings"
: command == 4 ? "luna://org.webosbrew.piccap.service/setSettings"
: command == 1 ? "luna://org.webosbrew.piccap.service/start"
: command == 2 ? "luna://org.webosbrew.piccap.service/stop"
: "luna://org.webosbrew.piccap.service/status";
LSError e;
error_init(&e);
char data[160];
snprintf(data, sizeof(data), "{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",
target_piccap_host(&piccap_target), piccap_target.piccap_port);
piccap_busy = call_once(service_handle, uri, command == 4 ? data : "{}", piccap_reply,
(void *)(intptr_t)command, &piccap_token, &e);
piccap_deadline = milliseconds() + 3000;
error_free(&e);
if (command)
piccap_action_at = milliseconds();
return piccap_busy;
}
/* 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;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);
media_busy=call_once(service_handle,"luna://com.webos.media/getActivePipelines","{}",media_reply,NULL,&media_token,&e);
media_deadline=now+3000;
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=!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;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;
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;
}
return piccap_owned;
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;
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);
media_busy = call_once(service_handle, "luna://com.webos.media/getActivePipelines", "{}",
media_reply, NULL, &media_token, &e);
media_deadline = now + 3000;
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 = !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;
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;
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;
}
return piccap_owned;
}
#endif
+12 -6
View File
@@ -4,12 +4,18 @@
#include <stddef.h>
/* LG ABI: region ID followed by three planar channels, 256 uint16 bins
* per channel. Bin index is the 8-bit intensity (0..255). */
typedef struct { uint32_t id; uint16_t bins[3][256]; } Histogram;
_Static_assert(sizeof(Histogram)==0x604,"LG ABI record size");
_Static_assert(offsetof(Histogram,bins)==4,"LG ABI channel offset");
typedef struct {
uint32_t id;
uint16_t bins[3][256];
} Histogram;
_Static_assert(sizeof(Histogram) == 0x604, "LG ABI record size");
_Static_assert(offsetof(Histogram, bins) == 4, "LG ABI channel offset");
static inline unsigned char histogram_mean(const uint16_t bins[256]) {
uint64_t count=0,weight=0;
for(unsigned b=0;b<256;b++){count+=bins[b];weight+=(uint64_t)bins[b]*b;}
return count?(unsigned char)((weight+count/2)/count):0;
uint64_t count = 0, weight = 0;
for (unsigned b = 0; b < 256; b++) {
count += bins[b];
weight += (uint64_t)bins[b] * b;
}
return count ? (unsigned char)((weight + count / 2) / count) : 0;
}
#endif
+6 -2
View File
@@ -1,6 +1,10 @@
#ifndef SPATIAL_INTERPOLATION_H
#define SPATIAL_INTERPOLATION_H
static inline void blend_color(const unsigned char a[3],const unsigned char b[3],unsigned weight,unsigned char out[3]){
for(unsigned c=0;c<3;c++)out[c]=(unsigned char)(((unsigned)a[c]*(65535-weight)+(unsigned)b[c]*weight+32767)/65535);
static inline void blend_color(const unsigned char a[3], const unsigned char b[3], unsigned weight,
unsigned char out[3]) {
for (unsigned c = 0; c < 3; c++)
out[c] =
(unsigned char)(((unsigned)a[c] * (65535 - weight) + (unsigned)b[c] * weight + 32767) /
65535);
}
#endif
+79 -41
View File
@@ -5,54 +5,92 @@
#include <stdlib.h>
#include <dlfcn.h>
#include <math.h>
typedef struct {unsigned char a,b;unsigned weight;} LedBlend;
typedef struct {
unsigned char a, b;
unsigned weight;
} LedBlend;
#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 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){
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;
if(span<=0)span+=4;
if(distance<0)distance+=4;
return (LedBlend){rim[left].region,rim[right].region,(unsigned)(distance/span*65535+0.5)};
static LedBlend layout_blend(double x, double y) {
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;
if (span <= 0)
span += 4;
if (distance < 0)
distance += 4;
return (LedBlend){rim[left].region, rim[right].region,
(unsigned)(distance / span * 65535 + 0.5)};
}
/* Use the TV's JSON-C library; validate every rectangle before replacing the
* layout. The safety limit is independent of the installed LED count. */
static bool layout_from_json(const char *text,LedBlend **result,unsigned *count){
void *lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!lib)return false;
void *(*parse)(const char*)=dlsym(lib,"json_tokener_parse");
int (*get)(const void*,const char*,void**)=dlsym(lib,"json_object_object_get_ex");
int (*type)(const void*)=dlsym(lib,"json_object_get_type");
size_t (*length)(const void*)=dlsym(lib,"json_object_array_length");
void *(*at)(const void*,size_t)=dlsym(lib,"json_object_array_get_idx");
double (*number)(const void*)=dlsym(lib,"json_object_get_double");
int (*put)(void*)=dlsym(lib,"json_object_put");
if(!parse||!get||!type||!length||!at||!number||!put){dlclose(lib);return false;}
void *root=parse(text),*info=NULL,*leds=NULL;bool valid=root && type(root)==4 && get(root,"info",&info) && type(info)==4 && get(info,"leds",&leds) && type(leds)==5;
size_t n=valid?length(leds):0;valid=valid && n>0 && n<=16384;
LedBlend *mapping=valid?calloc(n,sizeof(*mapping)):NULL;valid=valid && mapping;
const char *keys[]={"hmin","hmax","vmin","vmax"};
for(size_t i=0;valid && i<n;i++){
void *led=at(leds,i);double coordinates[4];valid=led && type(led)==4;
for(unsigned c=0;valid && c<4;c++){
void *v=NULL;valid=get(led,keys[c],&v) && (type(v)==2||type(v)==3);
if(valid){coordinates[c]=number(v);valid=isfinite(coordinates[c]) && coordinates[c]>=0 && coordinates[c]<=1;}
}
if(valid)valid=coordinates[0]<=coordinates[1] && coordinates[2]<=coordinates[3];
if(valid)mapping[i]=layout_blend((coordinates[0]+coordinates[1])/2,(coordinates[2]+coordinates[3])/2);
}
if(root)put(root);
dlclose(lib);
if(!valid){free(mapping);return false;}
*result=mapping;*count=(unsigned)n;return true;
static bool layout_from_json(const char *text, LedBlend **result, unsigned *count) {
void *lib = dlopen("libjson-c.so.5", RTLD_NOW);
if (!lib)
return false;
void *(*parse)(const char *) = dlsym(lib, "json_tokener_parse");
int (*get)(const void *, const char *, void **) = dlsym(lib, "json_object_object_get_ex");
int (*type)(const void *) = dlsym(lib, "json_object_get_type");
size_t (*length)(const void *) = dlsym(lib, "json_object_array_length");
void *(*at)(const void *, size_t) = dlsym(lib, "json_object_array_get_idx");
double (*number)(const void *) = dlsym(lib, "json_object_get_double");
int (*put)(void *) = dlsym(lib, "json_object_put");
if (!parse || !get || !type || !length || !at || !number || !put) {
dlclose(lib);
return false;
}
void *root = parse(text), *info = NULL, *leds = NULL;
bool valid = root && type(root) == 4 && get(root, "info", &info) && type(info) == 4 &&
get(info, "leds", &leds) && type(leds) == 5;
size_t n = valid ? length(leds) : 0;
valid = valid && n > 0 && n <= 16384;
LedBlend *mapping = valid ? calloc(n, sizeof(*mapping)) : NULL;
valid = valid && mapping;
const char *keys[] = {"hmin", "hmax", "vmin", "vmax"};
for (size_t i = 0; valid && i < n; i++) {
void *led = at(leds, i);
double coordinates[4];
valid = led && type(led) == 4;
for (unsigned c = 0; valid && c < 4; c++) {
void *v = NULL;
valid = get(led, keys[c], &v) && (type(v) == 2 || type(v) == 3);
if (valid) {
coordinates[c] = number(v);
valid = isfinite(coordinates[c]) && coordinates[c] >= 0 && coordinates[c] <= 1;
}
}
if (valid)
valid = coordinates[0] <= coordinates[1] && coordinates[2] <= coordinates[3];
if (valid)
mapping[i] = layout_blend((coordinates[0] + coordinates[1]) / 2,
(coordinates[2] + coordinates[3]) / 2);
}
if (root)
put(root);
dlclose(lib);
if (!valid) {
free(mapping);
return false;
}
*result = mapping;
*count = (unsigned)n;
return true;
}
#endif
+45 -26
View File
@@ -1,34 +1,53 @@
#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 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 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 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;
}
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
+79 -33
View File
@@ -5,43 +5,89 @@
#include <string.h>
#include <dlfcn.h>
typedef struct {
void *lib;void *(*parse)(const char*);int (*get)(const void*,const char*,void**);
int (*type)(const void*);size_t (*length)(const void*);void *(*at)(const void*,size_t);
const char *(*string)(const void*);int (*boolean)(const void*);int (*put)(void*);
void *lib;
void *(*parse)(const char *);
int (*get)(const void *, const char *, void **);
int (*type)(const void *);
size_t (*length)(const void *);
void *(*at)(const void *, size_t);
const char *(*string)(const void *);
int (*boolean)(const void *);
int (*put)(void *);
} PlaybackJson;
static inline bool playback_json_init(PlaybackJson *j){
j->lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!j->lib)return false;
j->parse=dlsym(j->lib,"json_tokener_parse");j->get=dlsym(j->lib,"json_object_object_get_ex");
j->type=dlsym(j->lib,"json_object_get_type");j->length=dlsym(j->lib,"json_object_array_length");
j->at=dlsym(j->lib,"json_object_array_get_idx");j->string=dlsym(j->lib,"json_object_get_string");
j->boolean=dlsym(j->lib,"json_object_get_boolean");j->put=dlsym(j->lib,"json_object_put");
if(!j->parse||!j->get||!j->type||!j->length||!j->at||!j->string||!j->boolean||!j->put){dlclose(j->lib);j->lib=NULL;return false;}return true;
static inline bool playback_json_init(PlaybackJson *j) {
j->lib = dlopen("libjson-c.so.5", RTLD_NOW);
if (!j->lib)
return false;
j->parse = dlsym(j->lib, "json_tokener_parse");
j->get = dlsym(j->lib, "json_object_object_get_ex");
j->type = dlsym(j->lib, "json_object_get_type");
j->length = dlsym(j->lib, "json_object_array_length");
j->at = dlsym(j->lib, "json_object_array_get_idx");
j->string = dlsym(j->lib, "json_object_get_string");
j->boolean = dlsym(j->lib, "json_object_get_boolean");
j->put = dlsym(j->lib, "json_object_put");
if (!j->parse || !j->get || !j->type || !j->length || !j->at || !j->string || !j->boolean ||
!j->put) {
dlclose(j->lib);
j->lib = NULL;
return false;
}
return true;
}
static inline void *playback_field(PlaybackJson *j,void *o,const char *key){void *v=NULL;return o && j->type(o)==4 && j->get(o,key,&v)?v:NULL;}
static inline bool playback_true(PlaybackJson *j,void *o,const char *key){void *v=playback_field(j,o,key);return v && j->type(v)==1 && j->boolean(v);}
static inline bool playback_is(PlaybackJson *j,void *o,const char *key,const char *value){void *v=playback_field(j,o,key);return v && j->type(v)==6 && strcmp(j->string(v),value)==0;}
typedef enum {PLAYBACK_UNKNOWN,PLAYBACK_CLEAR,PLAYBACK_PROTECTED} PlaybackProtection;
static inline void *playback_field(PlaybackJson *j, void *o, const char *key) {
void *v = NULL;
return o && j->type(o) == 4 && j->get(o, key, &v) ? v : NULL;
}
static inline bool playback_true(PlaybackJson *j, void *o, const char *key) {
void *v = playback_field(j, o, key);
return v && j->type(v) == 1 && j->boolean(v);
}
static inline bool playback_is(PlaybackJson *j, void *o, const char *key, const char *value) {
void *v = playback_field(j, o, key);
return v && j->type(v) == 6 && strcmp(j->string(v), value) == 0;
}
typedef enum { PLAYBACK_UNKNOWN, PLAYBACK_CLEAR, PLAYBACK_PROTECTED } PlaybackProtection;
/* G4 resource-state classification, not black-frame or app-name guessing.
* Any active Secure Video Path wins. Only a foreground media decoder without
* a secure resource is eligible; HDMI, idle and malformed replies are unknown. */
static inline PlaybackProtection playback_classify(PlaybackJson *j,const char *text){
void *root=j->parse(text);if(!root)return PLAYBACK_UNKNOWN;
bool clear=false,invalid=j->type(root)!=5,secure=false;
size_t n=invalid?0:j->length(root);
for(size_t i=0;i<n;i++){
void *p=j->at(root,i);if(!p || j->type(p)!=4){invalid=true;continue;}
void *resources=playback_field(j,p,"resource");if(!resources)resources=playback_field(j,p,"resources");
if(!resources || j->type(resources)!=5){invalid=true;continue;}
bool decoder=false;
for(size_t k=0;k<j->length(resources);k++){
void *resource=j->at(resources,k);void *name=playback_field(j,resource,"resource");
if(!name || j->type(name)!=6){invalid=true;continue;}
const char *s=j->string(name);
if(strcmp(s,"SVP_CPB")==0)secure=true;
if(strcmp(s,"VDEC")==0)decoder=true;
}
if(decoder && playback_true(j,p,"is_foreground") && playback_is(j,p,"type","media"))clear=true;
}
j->put(root);return secure?PLAYBACK_PROTECTED:clear&&!invalid?PLAYBACK_CLEAR:PLAYBACK_UNKNOWN;
static inline PlaybackProtection playback_classify(PlaybackJson *j, const char *text) {
void *root = j->parse(text);
if (!root)
return PLAYBACK_UNKNOWN;
bool clear = false, invalid = j->type(root) != 5, secure = false;
size_t n = invalid ? 0 : j->length(root);
for (size_t i = 0; i < n; i++) {
void *p = j->at(root, i);
if (!p || j->type(p) != 4) {
invalid = true;
continue;
}
void *resources = playback_field(j, p, "resource");
if (!resources)
resources = playback_field(j, p, "resources");
if (!resources || j->type(resources) != 5) {
invalid = true;
continue;
}
bool decoder = false;
for (size_t k = 0; k < j->length(resources); k++) {
void *resource = j->at(resources, k);
void *name = playback_field(j, resource, "resource");
if (!name || j->type(name) != 6) {
invalid = true;
continue;
}
const char *s = j->string(name);
if (strcmp(s, "SVP_CPB") == 0)
secure = true;
if (strcmp(s, "VDEC") == 0)
decoder = true;
}
if (decoder && playback_true(j, p, "is_foreground") && playback_is(j, p, "type", "media"))
clear = true;
}
j->put(root);
return secure ? PLAYBACK_PROTECTED : clear && !invalid ? PLAYBACK_CLEAR : PLAYBACK_UNKNOWN;
}
#endif
+215 -59
View File
@@ -5,67 +5,223 @@
#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;
#include "runtime_states.h"
typedef struct {
int fd;
StreamStage 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 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_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 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_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 = STREAM_CONNECTING;
stream.deadline = now + 3000;
stream.revision = calibration_revision();
stream.layout_revision = RECEIVER_LAYOUT_REVISION;
}
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;
}
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 < STREAM_SUBSCRIBED && now >= stream.deadline) {
stream_close(now);
return;
}
if (stream.stage == STREAM_CONNECTING) {
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 = STREAM_AUTHENTICATING;
} else {
snprintf(line, sizeof(line), "{\"command\":\"serverinfo\"}\n");
stream.stage = STREAM_LAYOUT;
}
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;
}
}
/* Drain complete buffered replies even when recv would return EAGAIN.
* The per-tick budget applies independently of incoming network traffic. */
if (stream.used == 262144 && !strchr(stream.data, '\n')) {
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) {
stream_close(now);
return;
}
if (n == 0 && available) {
stream_close(now);
return;
}
if (n > 0)
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 == STREAM_AUTHENTICATING) {
if (!playback_true(j, root, "success")) {
if (root)
j->put(root);
stream_close(now);
return;
}
stream.stage = STREAM_LAYOUT;
stream_queue("{\"command\":\"serverinfo\"}\n");
stream.deadline = now + 3000;
} else if (stream.stage == STREAM_LAYOUT) {
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 = STREAM_SUBSCRIBED;
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
+42 -18
View File
@@ -5,26 +5,50 @@
#include <limits.h>
#define RETRY_DELAY_MS 5000
/* Continuous autostart retries and bounded manual source recovery. */
typedef struct {uint64_t due;unsigned attempts,quick_retries;bool automatic,paused,blocked;} RetryState;
static void retry_reset(RetryState *state){*state=(RetryState){0};}
static void retry_pause(RetryState *state){retry_reset(state);state->paused=true;}
static void retry_update(RetryState *state,bool enabled,bool running,uint64_t now){
bool previous=state->automatic;
state->automatic=enabled && !state->paused && !state->blocked;
if(previous && !state->automatic){state->due=0;state->attempts=0;}
if(state->automatic && !running && !state->due)state->due=now+RETRY_DELAY_MS;
typedef struct {
uint64_t due;
unsigned attempts, quick_retries;
bool automatic, paused, blocked;
} RetryState;
static void retry_reset(RetryState *state) {
*state = (RetryState){0};
}
static bool retry_take(RetryState *state,uint64_t now){
if(!state->due || now<state->due || state->paused || state->blocked)return false;
state->due=0;if(state->attempts<UINT_MAX)state->attempts++;return true;
static void retry_pause(RetryState *state) {
retry_reset(state);
state->paused = true;
}
static void retry_failure(RetryState *state,uint64_t now){
if(!state->paused && !state->blocked && (state->automatic || state->attempts<6)){
if(state->quick_retries){state->quick_retries--;state->due=now+400;}
else state->due=now+RETRY_DELAY_MS;
}
static void retry_update(RetryState *state, bool enabled, bool running, uint64_t now) {
bool previous = state->automatic;
state->automatic = enabled && !state->paused && !state->blocked;
if (previous && !state->automatic) {
state->due = 0;
state->attempts = 0;
}
if (state->automatic && !running && !state->due)
state->due = now + RETRY_DELAY_MS;
}
static inline void retry_source_transition(RetryState *state,uint64_t now){
if(!state->paused && !state->blocked){state->attempts=0;state->quick_retries=2;state->due=now+200;}
static bool retry_take(RetryState *state, uint64_t now) {
if (!state->due || now < state->due || state->paused || state->blocked)
return false;
state->due = 0;
if (state->attempts < UINT_MAX)
state->attempts++;
return true;
}
static void retry_failure(RetryState *state, uint64_t now) {
if (!state->paused && !state->blocked && (state->automatic || state->attempts < 6)) {
if (state->quick_retries) {
state->quick_retries--;
state->due = now + 400;
} else
state->due = now + RETRY_DELAY_MS;
}
}
static inline void retry_source_transition(RetryState *state, uint64_t now) {
if (!state->paused && !state->blocked) {
state->attempts = 0;
state->quick_retries = 2;
state->due = now + 200;
}
}
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef HYPERDRM_RUNTIME_STATES_H
#define HYPERDRM_RUNTIME_STATES_H
/* Connection and source transitions are independent state machines. */
typedef enum {
STREAM_DISCONNECTED,
STREAM_CONNECTING,
STREAM_AUTHENTICATING,
STREAM_LAYOUT,
STREAM_SUBSCRIBED
} StreamStage;
typedef enum { BRIDGE_IDLE, BRIDGE_HOLDING, BRIDGE_WAITING_FRAME, BRIDGE_FADING } BridgePhase;
#endif
+344 -339
View File
@@ -17,6 +17,7 @@
#include <sys/time.h>
#include <stdarg.h>
#include <time.h>
#include "version.h"
#include "histogram.h"
#include "blackbars.h"
#include "color_boost.h"
@@ -30,379 +31,383 @@
#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;
typedef bool (*Callback)(void*,void*,void*);
typedef struct {const char *name;Callback function;int flags;} Method;
static bool (*reply)(void*,void*,const char*,LSError*);
static const char *(*payload)(void*);
static bool (*error_init)(LSError*);
static void (*error_free)(LSError*);
static void *(*vt_init)(bool(*)(void));
static int (*vt_preset)(void*,int),(*vt_get)(void*,Histogram*),(*vt_finish)(void*);
typedef struct {
int code;
char *message;
const char *file;
int line;
const char *func;
void *padding;
unsigned long magic;
} LSError;
typedef bool (*Callback)(void *, void *, void *);
typedef struct {
const char *name;
Callback function;
int flags;
} Method;
static bool (*reply)(void *, void *, const char *, LSError *);
static const char *(*payload)(void *);
static bool (*error_init)(LSError *);
static void (*error_free)(LSError *);
static void *(*vt_init)(bool (*)(void));
static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *);
static void *capture;
static char lg_diagnostic[256];
/* Process-local LG diagnostics; no changes to the loaded LG library. */
void printLog(const char *context,int level,const char *message_id,
const char *file,const char *function,int line,const char *format,...) {
(void)context;(void)message_id;(void)file;
if(capture && level>3)return;
if(format){va_list args;va_start(args,format);vsnprintf(lg_diagnostic,sizeof(lg_diagnostic),format,args);va_end(args);}
fprintf(stderr,"LG %s:%d: ",function?function:"?",line);
if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);}
fputc('\n',stderr);
void printLog(const char *context, int level, const char *message_id, const char *file,
const char *function, int line, const char *format, ...) {
(void)context;
(void)message_id;
(void)file;
if (capture && level > 3)
return;
if (format) {
va_list args;
va_start(args, format);
vsnprintf(lg_diagnostic, sizeof(lg_diagnostic), format, args);
va_end(args);
}
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
if (format) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
fputc('\n', stderr);
}
static int output=-1;
static int output = -1;
static unsigned long frames;
static unsigned char rgb[16][3];
static unsigned char sent_rgb[3];
static unsigned black_percent[16];
static unsigned capture_preset;
static unsigned letterbox_mode,settling;
static int pending_letterbox=-1;
static uint64_t candidate_since,next_border_probe;
static 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 uint64_t sample_at, calibration_until;
static unsigned sample_sequence;
#define CALIBRATION_ACTIVE (calibration_until>milliseconds())
#define CALIBRATION_ACTIVE (calibration_until > milliseconds())
static unsigned led_count;
static LedBlend *led_blends;
static bool interpolation_enabled;
static RetryState retries;
static unsigned capture_recoveries;
static uint64_t recovery_started,last_recovery_ms,last_output_at;
static uint64_t milliseconds(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000+t.tv_nsec/1000000;}
static uint64_t recovery_started, last_recovery_ms, last_output_at;
static uint64_t milliseconds(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
}
static char last_error[256];
static volatile sig_atomic_t revoked,quitting;
static int guard_pipe=-1;
static pid_t guard_pid=-1;
static volatile sig_atomic_t revoked, quitting;
static int guard_pipe = -1;
static pid_t guard_pid = -1;
#define DEV_MODE "/var/luna/preferences/vt_devmode"
/* LG's developer gate is enabled only during initialization. A separate
* guardian removes our own marker even if this process aborts or is killed.
* An existing marker is preserved, as are files replaced by another owner. */
static bool enter_developer_mode(void){
int marker=open(DEV_MODE,O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC,0600);
if(marker<0)return errno==EEXIST;
struct stat owned;
if(fstat(marker,&owned)!=0){close(marker);unlink(DEV_MODE);return false;}
close(marker);int p[2];
if(pipe(p)!=0){unlink(DEV_MODE);return false;}
pid_t child=fork();
if(child<0){close(p[0]);close(p[1]);unlink(DEV_MODE);return false;}
if(child==0){
close(p[1]);struct pollfd fd={p[0],POLLIN|POLLHUP,0};
/* Bounded cleanup also covers an indefinitely blocked initialization. */
poll(&fd,1,10000);close(p[0]);struct stat current;
int rc=0;
if(lstat(DEV_MODE,&current)==0 && current.st_dev==owned.st_dev && current.st_ino==owned.st_ino)rc=unlink(DEV_MODE);
_exit(rc==0?0:1);
}
close(p[0]);guard_pipe=p[1];guard_pid=child;return true;
static bool enter_developer_mode(void) {
int marker = open(DEV_MODE, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
if (marker < 0)
return errno == EEXIST;
struct stat owned;
if (fstat(marker, &owned) != 0) {
close(marker);
unlink(DEV_MODE);
return false;
}
close(marker);
int p[2];
if (pipe(p) != 0) {
unlink(DEV_MODE);
return false;
}
pid_t child = fork();
if (child < 0) {
close(p[0]);
close(p[1]);
unlink(DEV_MODE);
return false;
}
if (child == 0) {
close(p[1]);
struct pollfd fd = {p[0], POLLIN | POLLHUP, 0};
/* Bounded cleanup also covers an indefinitely blocked initialization. */
poll(&fd, 1, 10000);
close(p[0]);
struct stat current;
int rc = 0;
if (lstat(DEV_MODE, &current) == 0 && current.st_dev == owned.st_dev &&
current.st_ino == owned.st_ino)
rc = unlink(DEV_MODE);
_exit(rc == 0 ? 0 : 1);
}
close(p[0]);
guard_pipe = p[1];
guard_pid = child;
return true;
}
static bool leave_developer_mode(void){
if(guard_pipe<0)return true;
close(guard_pipe);guard_pipe=-1;int status;
while(waitpid(guard_pid,&status,0)<0){if(errno!=EINTR)return false;}
guard_pid=-1;return WIFEXITED(status)&&WEXITSTATUS(status)==0;
static bool leave_developer_mode(void) {
if (guard_pipe < 0)
return true;
close(guard_pipe);
guard_pipe = -1;
int status;
while (waitpid(guard_pid, &status, 0) < 0) {
if (errno != EINTR)
return false;
}
guard_pid = -1;
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static void quit_signal(int s){(void)s;quitting=1;}
static bool policy(void){revoked=1;return true;}
static void set_error(const char *s){snprintf(last_error,sizeof(last_error),"%s",s);fprintf(stderr,"%s\n",s);}
static int send_line(const char *line){
size_t left=strlen(line);const char *p=line;
while(left){ssize_t n=send(output,p,left,MSG_NOSIGNAL);if(n<=0)return -1;p+=n;left-=n;}
return 0;
static void quit_signal(int s) {
(void)s;
quitting = 1;
}
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 policy(void) {
revoked = 1;
return true;
}
static bool read_layout(void){
const size_t capacity=2*1024*1024;char *data=malloc(capacity+1);if(!data)return false;
bool complete=false;size_t used=0;
if(send_line("{\"command\":\"serverinfo\"}\n")==0){
while(used<capacity){ssize_t n=recv(output,data+used,capacity-used,0);if(n<=0)break;used+=(size_t)n;data[used]=0;char *end=strchr(data,'\n');if(end){*end=0;complete=true;break;}}
}
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 void set_error(const char *s) {
snprintf(last_error, sizeof(last_error), "%s", s);
fprintf(stderr, "%s\n", s);
}
static bool target_login(void){
if(!target.token[0])return true;
char request[384],answer[4096];size_t used=0;
snprintf(request,sizeof(request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);
if(send_line(request)!=0)return false;
while(used+1<sizeof(answer)){
ssize_t n=recv(output,answer+used,sizeof(answer)-used-1,0);if(n<=0)return false;
used+=(size_t)n;answer[used]=0;if(strchr(answer,'\n'))break;
}
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 int send_line(const char *line) {
size_t left = strlen(line);
const char *p = line;
while (left) {
ssize_t n = send(output, p, left, MSG_NOSIGNAL);
if (n <= 0)
return -1;
p += n;
left -= n;
}
return 0;
}
static bool 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;
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;
}
}
}
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;
char request[384], answer[4096];
size_t used = 0;
snprintf(request, sizeof(request),
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
target.token);
if (send_line(request) != 0)
return false;
while (used + 1 < sizeof(answer)) {
ssize_t n = recv(output, answer + used, sizeof(answer) - used - 1, 0);
if (n <= 0)
return false;
used += (size_t)n;
answer[used] = 0;
if (strchr(answer, '\n'))
break;
}
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;}
if(capture){vt_finish(capture);capture=NULL;}
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;
}
if (capture) {
vt_finish(capture);
capture = NULL;
}
}
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;
}
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}");
#include "service_api.h"
#include "capture_loop.h"
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;
}
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;
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");
return respond(h,m,json);
}
static bool start(void *h,void *m,void *ctx){
(void)ctx;const char *p=m?payload(m):NULL;
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use settings to configure the receiver\"}");
if(m)retry_reset(&retries);
if(piccap_owned)return respond(h,m,"{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}");
frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0;
output=target_connect(&target);
if(output<0){stop_capture();set_error("Cannot connect to configured receiver IP and JSON port");return start_failure(h,m,"Configured receiver is unavailable");}
if(!target_login()){stop_capture();set_error("Receiver token login failed; check the saved API token");return start_failure(h,m,"Receiver authentication failed");}
if(!read_layout()){stop_capture();set_error("Cannot read receiver LED layout; check JSON port and API authorization");return start_failure(h,m,"Receiver layout unavailable; check API authorization");}
if(!enter_developer_mode()){stop_capture();set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return start_failure(h,m,"Root access is required for temporary LG developer initialization");}
capture=vt_init(policy);
if(!leave_developer_mode()){stop_capture();retries.blocked=true;set_error("Developer marker cleanup failed; manual intervention required");return start_failure(h,m,"Developer marker cleanup failed");}
if(!capture){stop_capture();set_error("LG refused histogram initialization");return start_failure(h,m,"LG refused histogram initialization. Another capture source may hold VT_CAPTURE.");}
if(vt_preset(capture,0)!=0){stop_capture();set_error("LG histogram preset initialization failed");return start_failure(h,m,"Histogram preset failed");}
capture_preset=letterbox_mode=settling=0;pending_letterbox=-1;next_border_probe=0;
retries.due=0;retries.attempts=0;retries.quick_retries=0;
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;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)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;
else if(strcmp(p,"{\"enabled\":false}")==0)enabled=false;
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
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;clear_since=unknown_since=0;next_media_probe=0;
return respond(h,m,"{\"returnValue\":true}");
}
static bool settings(void *h,void *m,void *ctx){
(void)ctx;TargetConfig next=target;
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();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}");
}
static bool test_connection(void *h,void *m,void *ctx){
(void)ctx;
if(strcmp(payload(m),"{}") || !playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Test requires an empty object and JSON-C\"}");
if(!test_begin(&target,milliseconds()))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"A connection test is already running\"}");
return respond(h,m,"{\"returnValue\":true,\"testing\":true}");
}
static bool status(void *h,void *m,void *ctx){
(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.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]);
snprintf(json+used,sizeof(json)-used,"],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0,\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}",capture_preset,interpolation_enabled?"true":"false",boot_enabled()?"true":"false",retries.due?"true":"false",retries.paused?"true":"false",retries.blocked?"true":"false",retries.attempts,piccap_installed()?"true":"false",handover_enabled?"true":"false",piccap_connected?"true":"false",piccap_owned?"PicCap":"HyperDRM",protection==PLAYBACK_PROTECTED?"protected":protection==PLAYBACK_CLEAR?"non-secure decoder":"unknown",capture&&letterbox_mode?"true":"false");return respond(h,m,json);
}
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);
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())){
start(NULL,NULL,NULL);
if(!capture)retry_failure(&retries,milliseconds());
}
return 1;
}
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(!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);
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;
for(int c=0;c<3;c++){
uint64_t total=0,black=0;
for(int b=0;b<256;b++){total+=hist[i].bins[c][b];if(b<=12)black+=hist[i].bins[c][b];}
unsigned fraction=total?(unsigned)(100*black/total):0;
if(fraction<black_percent[i])black_percent[i]=fraction;
}
}
for(int i=0;i<16;i++)for(int c=0;c<3;c++){
rgb[i][c]=histogram_mean(hist[i].bins[c]);aggregate[c]+=rgb[i][c];
}
if(!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;}
else if((unsigned)candidate!=letterbox_mode && now-candidate_since>=2000)letterbox_mode=(unsigned)candidate;
if(letterbox_mode){
if(vt_preset(capture,1)!=0){set_error("Letterbox preset failed");stop_capture();return 1;}
capture_preset=1;settling=2;next_border_probe=now+2000;return 1;
}
}
for(int c=0;c<3;c++)sent_rgb[c]=(unsigned char)(aggregate[c]/16);
unsigned char boosted_mean[3];boost_color(sent_rgb,boosted_mean);memcpy(sent_rgb,boosted_mean,3);
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;
char *line=region_command(stable_rgb,led_blends,led_count,1500);
if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;}
int failed=send_line(line);free(line);
if(failed){set_error("Receiver connection lost");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;}
int main(void){
signal(SIGTERM,quit_signal);signal(SIGINT,quit_signal);
if(boot_enabled() && !set_boot(true))fprintf(stderr,"Own legacy boot hook migration failed\n");
void *vt=dlopen("libvtcapture.so.1",RTLD_NOW),*ls=dlopen("libluna-service2.so.3",RTLD_NOW),*glib=dlopen("libglib-2.0.so.0",RTLD_NOW);
if(!vt||!ls||!glib){fprintf(stderr,"Missing LG library: %s\n",dlerror());return 2;}
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
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);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");
bool (*attach)(void*,void*,LSError*)=symbol(ls,"LSGmainAttach");
bool (*unreg)(void*,LSError*)=symbol(ls,"LSUnregister");
void *(*loop_new)(void*,int)=symbol(glib,"g_main_loop_new");
int (*iterate)(void*,int)=symbol(glib,"g_main_context_iteration");
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"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);}
stream_close(milliseconds());test_close();stop_capture();
if(piccap_owned){
handover_enabled=false;retry_pause(&retries);
uint64_t deadline=milliseconds()+4000;
while(piccap_owned && milliseconds()<deadline){handover_tick();iterate(NULL,0);usleep(10000);}
}
unreg(handle,&e);error_free(&e);loop_unref(loop);return 0;
int main(void) {
signal(SIGTERM, quit_signal);
signal(SIGINT, quit_signal);
if (boot_enabled() && !set_boot(true))
fprintf(stderr, "Own legacy boot hook migration failed\n");
void *vt = dlopen("libvtcapture.so.1", RTLD_NOW),
*ls = dlopen("libluna-service2.so.3", RTLD_NOW),
*glib = dlopen("libglib-2.0.so.0", RTLD_NOW);
if (!vt || !ls || !glib) {
fprintf(stderr, "Missing LG library: %s\n", dlerror());
return 2;
}
vt_init = symbol(vt, "vtCapture_initHistogram");
vt_preset = symbol(vt, "vtCapture_setPresetHistogram");
vt_get = symbol(vt, "vtCapture_getHistogram");
vt_finish = symbol(vt, "vtCapture_finalizeHistogram");
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);
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");
bool (*attach)(void *, void *, LSError *) = symbol(ls, "LSGmainAttach");
bool (*unreg)(void *, LSError *) = symbol(ls, "LSUnregister");
void *(*loop_new)(void *, int) = symbol(glib, "g_main_loop_new");
int (*iterate)(void *, int) = symbol(glib, "g_main_context_iteration");
unsigned (*timer)(unsigned, int (*)(void *), void *) = symbol(glib, "g_timeout_add");
void (*loop_unref)(void *) = symbol(glib, "g_main_loop_unref");
void *handle = NULL, *loop = loop_new(NULL, 0);
LSError e;
error_init(&e);
Method methods[] = {{"start", start, 0},
{"stop", stop, 0},
{"status", status, 0},
{"autostart", autostart, 0},
{"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);
}
stream_close(milliseconds());
test_close();
stop_capture();
if (piccap_owned) {
handover_enabled = false;
retry_pause(&retries);
uint64_t deadline = milliseconds() + 4000;
while (piccap_owned && milliseconds() < deadline) {
handover_tick();
iterate(NULL, 0);
usleep(10000);
}
}
unreg(handle, &e);
error_free(&e);
loop_unref(loop);
return 0;
}
+202
View File
@@ -0,0 +1,202 @@
/* Luna API handlers. Included after the shared runtime context. */
#include "service_calibration.h"
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");
return respond(h, m, json);
}
static bool start(void *h, void *m, void *ctx) {
(void)ctx;
const char *p = m ? payload(m) : NULL;
if (p && strcmp(p, "{}") != 0)
return respond(h, m,
"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use "
"settings to configure the receiver\"}");
if (m)
retry_reset(&retries);
if (piccap_owned)
return respond(h, m, "{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
if (capture)
return respond(h, m, "{\"returnValue\":true,\"running\":true}");
frames = 0;
revoked = 0;
last_error[0] = 0;
lg_diagnostic[0] = 0;
output = target_connect(&target);
if (output < 0) {
stop_capture();
set_error("Cannot connect to configured receiver IP and JSON port");
return start_failure(h, m, "Configured receiver is unavailable");
}
if (!target_login()) {
stop_capture();
set_error("Receiver token login failed; check the saved API token");
return start_failure(h, m, "Receiver authentication failed");
}
if (!read_layout()) {
stop_capture();
set_error("Cannot read receiver LED layout; check JSON port and API authorization");
return start_failure(h, m, "Receiver layout unavailable; check API authorization");
}
if (!enter_developer_mode()) {
stop_capture();
set_error("Developer initialization needs root. Elevate this own service through Homebrew "
"Channel.");
return start_failure(h, m,
"Root access is required for temporary LG developer initialization");
}
capture = vt_init(policy);
if (!leave_developer_mode()) {
stop_capture();
retries.blocked = true;
set_error("Developer marker cleanup failed; manual intervention required");
return start_failure(h, m, "Developer marker cleanup failed");
}
if (!capture) {
stop_capture();
set_error("LG refused histogram initialization");
return start_failure(
h, m,
"LG refused histogram initialization. Another capture source may hold VT_CAPTURE.");
}
if (vt_preset(capture, 0) != 0) {
stop_capture();
set_error("LG histogram preset initialization failed");
return start_failure(h, m, "Histogram preset failed");
}
capture_preset = letterbox_mode = settling = 0;
pending_letterbox = -1;
next_border_probe = 0;
retries.due = 0;
retries.attempts = 0;
retries.quick_retries = 0;
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;
calibration_until = 0;
retry_pause(&retries);
bool bridging = bridge_phase != BRIDGE_IDLE;
stream_close(milliseconds());
bridge_phase = BRIDGE_IDLE;
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)
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;
else if (strcmp(p, "{\"enabled\":false}") == 0)
enabled = false;
else
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
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;
clear_since = unknown_since = 0;
next_media_probe = 0;
return respond(h, m, "{\"returnValue\":true}");
}
static bool settings(void *h, void *m, void *ctx) {
(void)ctx;
TargetConfig next = target;
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();
stream_close(milliseconds());
bridge_phase = BRIDGE_IDLE;
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}");
}
static bool test_connection(void *h, void *m, void *ctx) {
(void)ctx;
if (strcmp(payload(m), "{}") || !playback_json.lib)
return respond(
h, m,
"{\"returnValue\":false,\"errorText\":\"Test requires an empty object and JSON-C\"}");
if (!test_begin(&target, milliseconds()))
return respond(
h, m, "{\"returnValue\":false,\"errorText\":\"A connection test is already running\"}");
return respond(h, m, "{\"returnValue\":true,\"testing\":true}");
}
#include "service_status.h"
+107
View File
@@ -0,0 +1,107 @@
/* Calibration session, proposals, saved profiles and model discovery. */
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;
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}");
}
+111
View File
@@ -0,0 +1,111 @@
/* Read-only runtime status serialization. */
static bool status(void *h, void *m, void *ctx) {
(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 == STREAM_SUBSCRIBED ? "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 == BRIDGE_FADING) ? "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\":\"" HYPERDRM_VERSION
"\",\"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]);
snprintf(json + used, sizeof(json) - used,
"],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0,"
"\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,"
"\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,"
"\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":"
"\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}",
capture_preset, interpolation_enabled ? "true" : "false",
boot_enabled() ? "true" : "false", retries.due ? "true" : "false",
retries.paused ? "true" : "false", retries.blocked ? "true" : "false",
retries.attempts, piccap_installed() ? "true" : "false",
handover_enabled ? "true" : "false", piccap_connected ? "true" : "false",
piccap_owned ? "PicCap" : "HyperDRM",
protection == PLAYBACK_PROTECTED ? "protected"
: protection == PLAYBACK_CLEAR ? "non-secure decoder"
: "unknown",
capture && letterbox_mode ? "true" : "false");
return respond(h, m, json);
}
+105 -40
View File
@@ -1,47 +1,112 @@
#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];
#include "runtime_states.h"
static BridgePhase bridge_phase;
static unsigned 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 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 bool bridge_clear(void) {
return stream.stage == STREAM_SUBSCRIBED &&
stream_queue("{\"command\":\"clear\",\"priority\":149}\n");
}
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;}
}
static bool bridge_send(const unsigned char colors[16][3]) {
if (stream.stage != STREAM_SUBSCRIBED)
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 = BRIDGE_IDLE;
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 ? BRIDGE_HOLDING
: BRIDGE_IDLE;
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 = BRIDGE_IDLE;
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 = BRIDGE_IDLE;
return;
}
if (bridge_phase == BRIDGE_HOLDING && capture_source == SOURCE_PICCAP && piccap_connected &&
stream.stage == STREAM_SUBSCRIBED) {
if (bridge_clear()) {
bridge_sequence = stream.sequence;
bridge_clear_at = now;
bridge_phase = BRIDGE_WAITING_FRAME;
}
} else if (bridge_phase == BRIDGE_HOLDING && now - bridge_last_sent >= 100 &&
stream.stage == STREAM_SUBSCRIBED) {
if (bridge_send(bridge_from))
bridge_last_sent = now;
} else if (bridge_phase == BRIDGE_WAITING_FRAME && 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 = BRIDGE_FADING;
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 = BRIDGE_IDLE;
}
if (bridge_phase == BRIDGE_FADING && now - bridge_last_sent >= 100) {
if (now - bridge_fade_at >= 500) {
if (bridge_clear())
bridge_phase = BRIDGE_IDLE;
} 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
+40 -18
View File
@@ -5,26 +5,48 @@
#include "layout.h"
#include "interpolation.h"
#include "color_boost.h"
enum {SPATIAL_WIDTH=160,SPATIAL_HEIGHT=90,SPATIAL_PIXELS=SPATIAL_WIDTH*SPATIAL_HEIGHT};
enum { SPATIAL_WIDTH = 160, SPATIAL_HEIGHT = 90, SPATIAL_PIXELS = SPATIAL_WIDTH * SPATIAL_HEIGHT };
/* 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_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++)
blends[y*SPATIAL_WIDTH+x]=layout_blend((x+0.5)/SPATIAL_WIDTH,(y+0.5)/SPATIAL_HEIGHT);
generation=calibration_generation;
}
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\":%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)];
line[used++]=alphabet[((color[1]&15)<<2)|(color[2]>>6)];line[used++]=alphabet[color[2]&63];
}
line[used++]='"';line[used++]='}';line[used++]='\n';line[used]=0;return line;
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++)
blends[y * SPATIAL_WIDTH + x] =
layout_blend((x + 0.5) / SPATIAL_WIDTH, (y + 0.5) / SPATIAL_HEIGHT);
generation = calibration_generation;
}
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\":%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)];
line[used++] = alphabet[((color[1] & 15) << 2) | (color[2] >> 6)];
line[used++] = alphabet[color[2] & 63];
}
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);
}
static inline char *spatial_image_command(const unsigned char regions[16][3],unsigned priority){return spatial_image_command_duration(regions,priority,1500);}
#endif
+184 -70
View File
@@ -10,80 +10,194 @@
#include <sys/time.h>
#include <unistd.h>
#include "playback.h"
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
typedef struct {char host[46],receiver[16],token[257];unsigned port,piccap_port;} TargetConfig;
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444,.piccap_port=19400};
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
static inline bool target_piccap_compatible(const TargetConfig *t){unsigned char address[4];return !strcmp(t->host,"::1") || inet_pton(AF_INET,t->host,address)==1;}
static inline const char *target_piccap_host(const TargetConfig *t){return !strcmp(t->host,"::1")?"127.0.0.1":t->host;}
static bool target_valid(const TargetConfig *t){
unsigned char address[16];
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
if(!t->piccap_port || t->piccap_port>65535)return false;
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
for(const unsigned char *p=(const unsigned char*)t->token;*p;p++)
if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false;
return true;
#define TARGET_PREF \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
typedef struct {
char host[46], receiver[16], token[257];
unsigned port, piccap_port;
} TargetConfig;
static TargetConfig target = {
.host = "127.0.0.1", .receiver = "Hyperion", .port = 19444, .piccap_port = 19400};
static inline bool target_piccap_local(const TargetConfig *t) {
return t->port == 19444 && (!strcmp(t->host, "127.0.0.1") || !strcmp(t->host, "::1"));
}
static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
void *root=j->parse(text);if(!root)return false;
TargetConfig next=*out;bool valid=j->type(root)==4;
const char *names[]={"host","receiver","token"};char *fields[]={next.host,next.receiver,next.token};
size_t sizes[]={sizeof(next.host),sizeof(next.receiver),sizeof(next.token)};
for(unsigned i=0;valid && i<3;i++){
void *v=playback_field(j,root,names[i]);
if(!v){if(i<2)valid=false;continue;}
if(j->type(v)!=6){valid=false;break;}
const char *s=j->string(v);if(strlen(s)>=sizes[i]){valid=false;break;}
strcpy(fields[i],s);
}
void *port=playback_field(j,root,"port");
if(!port || j->type(port)!=3)valid=false;
else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;}
void *flat=NULL;
if(j->type(root)==4 && j->get(root,"piccapPort",&flat)){
if(j->type(flat)!=3)valid=false;
else {char *end=NULL;errno=0;long n=strtol(j->string(flat),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.piccap_port=(unsigned)n;}
}
void *clear=playback_field(j,root,"clearToken");
if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;}
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
static inline bool target_piccap_compatible(const TargetConfig *t) {
unsigned char address[4];
return !strcmp(t->host, "::1") || inet_pton(AF_INET, t->host, address) == 1;
}
static bool target_save(const char *path,const TargetConfig *t){
if(!target_valid(t))return false;
char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false;
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->piccap_port,t->receiver,t->token);
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false;
if(ok)ok=rename(tmp,path)==0;
if(!ok)unlink(tmp);
return ok;
static inline const char *target_piccap_host(const TargetConfig *t) {
return !strcmp(t->host, "::1") ? "127.0.0.1" : t->host;
}
static bool target_load(PlaybackJson *j,const char *path,TargetConfig *t){
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false;
char data[1024];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);if(n<=0 || n==(ssize_t)sizeof(data)-1)return false;
data[n]=0;return target_parse(j,data,t);
static bool target_valid(const TargetConfig *t) {
unsigned char address[16];
if (inet_pton(AF_INET, t->host, address) != 1 && inet_pton(AF_INET6, t->host, address) != 1)
return false;
if (!t->piccap_port || t->piccap_port > 65535)
return false;
if (!t->port || t->port > 65535 ||
(strcmp(t->receiver, "Hyperion") && strcmp(t->receiver, "HyperHDR")))
return false;
for (const unsigned char *p = (const unsigned char *)t->token; *p; p++)
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
*p == '-' || *p == '_' || *p == '.'))
return false;
return true;
}
static int target_open(const TargetConfig *t){
if(!target_valid(t))return -1;
struct sockaddr_storage storage={0};socklen_t length;int family;
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
if(inet_pton(AF_INET,t->host,&v4->sin_addr)==1){family=AF_INET;v4->sin_family=family;v4->sin_port=htons(t->port);length=sizeof(*v4);}
else {family=AF_INET6;v6->sin6_family=family;v6->sin6_port=htons(t->port);length=sizeof(*v6);}
int fd=socket(family,SOCK_STREAM,0);if(fd<0)return -1;
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
if(connect(fd,(void*)&storage,length)!=0){
if(errno!=EINPROGRESS){close(fd);return -1;}
}
return fd;
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
void *root = j->parse(text);
if (!root)
return false;
TargetConfig next = *out;
bool valid = j->type(root) == 4;
const char *names[] = {"host", "receiver", "token"};
char *fields[] = {next.host, next.receiver, next.token};
size_t sizes[] = {sizeof(next.host), sizeof(next.receiver), sizeof(next.token)};
for (unsigned i = 0; valid && i < 3; i++) {
void *v = playback_field(j, root, names[i]);
if (!v) {
if (i < 2)
valid = false;
continue;
}
if (j->type(v) != 6) {
valid = false;
break;
}
const char *s = j->string(v);
if (strlen(s) >= sizes[i]) {
valid = false;
break;
}
strcpy(fields[i], s);
}
void *port = playback_field(j, root, "port");
if (!port || j->type(port) != 3)
valid = false;
else {
char *end = NULL;
errno = 0;
long n = strtol(j->string(port), &end, 10);
if (errno || !end || *end || n < 1 || n > 65535)
valid = false;
else
next.port = (unsigned)n;
}
void *flat = NULL;
if (j->type(root) == 4 && j->get(root, "piccapPort", &flat)) {
if (j->type(flat) != 3)
valid = false;
else {
char *end = NULL;
errno = 0;
long n = strtol(j->string(flat), &end, 10);
if (errno || !end || *end || n < 1 || n > 65535)
valid = false;
else
next.piccap_port = (unsigned)n;
}
}
void *clear = playback_field(j, root, "clearToken");
if (clear) {
if (j->type(clear) != 1)
valid = false;
else if (j->boolean(clear))
next.token[0] = 0;
}
valid = valid && target_valid(&next);
j->put(root);
if (valid)
*out = next;
return valid;
}
static int target_connect(const TargetConfig *t){
int fd=target_open(t);if(fd<0)return -1;
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
int flags=fcntl(fd,F_GETFL,0);
if(flags<0 || fcntl(fd,F_SETFL,flags & ~O_NONBLOCK)<0){close(fd);return -1;}
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
return fd;
static bool target_save(const char *path, const TargetConfig *t) {
if (!target_valid(t))
return false;
char tmp[512], data[512];
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
return false;
int n = snprintf(
data, sizeof(data),
"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",
t->host, t->port, t->piccap_port, t->receiver, t->token);
int fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0)
return false;
bool ok = write(fd, data, n) == n && fsync(fd) == 0;
if (close(fd) != 0)
ok = false;
if (ok)
ok = rename(tmp, path) == 0;
if (!ok)
unlink(tmp);
return ok;
}
static bool target_load(PlaybackJson *j, const char *path, TargetConfig *t) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0)
return false;
char data[1024];
ssize_t n = read(fd, data, sizeof(data) - 1);
close(fd);
if (n <= 0 || n == (ssize_t)sizeof(data) - 1)
return false;
data[n] = 0;
return target_parse(j, data, t);
}
static int target_open(const TargetConfig *t) {
if (!target_valid(t))
return -1;
struct sockaddr_storage storage = {0};
socklen_t length;
int family;
struct sockaddr_in *v4 = (void *)&storage;
struct sockaddr_in6 *v6 = (void *)&storage;
if (inet_pton(AF_INET, t->host, &v4->sin_addr) == 1) {
family = AF_INET;
v4->sin_family = family;
v4->sin_port = htons(t->port);
length = sizeof(*v4);
} else {
family = AF_INET6;
v6->sin6_family = family;
v6->sin6_port = htons(t->port);
length = sizeof(*v6);
}
int fd = socket(family, SOCK_STREAM, 0);
if (fd < 0)
return -1;
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
close(fd);
return -1;
}
if (connect(fd, (void *)&storage, length) != 0) {
if (errno != EINPROGRESS) {
close(fd);
return -1;
}
}
return fd;
}
static int target_connect(const TargetConfig *t) {
int fd = target_open(t);
if (fd < 0)
return -1;
struct pollfd p = {.fd = fd, .events = POLLOUT};
int error = 0;
socklen_t size = sizeof(error);
if (poll(&p, 1, 1000) <= 0 || getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 ||
error) {
close(fd);
return -1;
}
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
close(fd);
return -1;
}
struct timeval timeout = {1, 0};
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
return fd;
}
#endif
+105 -30
View File
@@ -5,35 +5,110 @@
#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);
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);
}
/* More than the eight-message budget, with no further socket writes. */
const char *buffered =
"{\"command\":\"ledcolors-ledstream-update\",\"data\":{\"leds\":[50,60,70]}}\n";
stream.used = 0;
for (unsigned i = 0; i < 17; i++) {
memcpy(stream.data + stream.used, buffered, strlen(buffered));
stream.used += strlen(buffered);
}
stream.data[stream.used] = 0;
for (unsigned i = 0; i < 3; i++)
stream_tick(json, 1900 + i, true);
assert(stream.sequence == 18 && stream.used == 0 && stream.colors[0][0] == 50);
void *bad = json->parse("{\"result\":{\"leds\":[0,0,999]}}");
assert(!stream_colors(json, bad, 2000));
json->put(bad);
assert(stream.colors[0][2] == 70);
bad = json->parse("{\"data\":{\"leds\":[0,0,0]}}");
assert(stream_colors(json, bad, 2000));
json->put(bad);
assert(stream.colors[0][2] == 0);
bad = json->parse("{\"data\":{\"leds\":[1,2,3,4,5,6]}}");
assert(!stream_colors(json, bad, 2000) && !stream.revision);
json->put(bad);
stream_tick(json, 2001, false);
assert(stream.fd < 0 && !stream.valid);
int status;
waitpid(child, &status, 0);
assert(WIFEXITED(status) && !WEXITSTATUS(status));
close(server);
}
int main(void){
PlaybackJson json={0};assert(playback_json_init(&json));fixture(&json,false,false);fixture(&json,true,true);
stream_start(1000);stream_tick(&json,5000,true);assert(stream.fd<0);stream_close(0);
TargetConfig copy=target;assert(target_valid(&copy));assert(target_piccap_compatible(&copy));assert(!target_piccap_local(&copy));assert(target_piccap_host(&copy));
assert(!target_parse(&json,"{}",&copy));assert(!target_save("/no/such/profile",&copy));assert(!target_load(&json,"/no/such/profile",&copy));
int fd=target_connect(&copy);if(fd>=0)close(fd);
dlclose(json.lib);puts("PASS: fragmented raw LED subscription, token login, old/new envelopes, invalid samples, real black, timeout and cleanup");return 0;
int main(void) {
PlaybackJson json = {0};
assert(playback_json_init(&json));
fixture(&json, false, false);
fixture(&json, true, true);
stream_start(1000);
stream_tick(&json, 5000, true);
assert(stream.fd < 0);
stream_close(0);
TargetConfig copy = target;
assert(target_valid(&copy));
assert(target_piccap_compatible(&copy));
assert(!target_piccap_local(&copy));
assert(target_piccap_host(&copy));
assert(!target_parse(&json, "{}", &copy));
assert(!target_save("/no/such/profile", &copy));
assert(!target_load(&json, "/no/such/profile", &copy));
int fd = target_connect(&copy);
if (fd >= 0)
close(fd);
dlclose(json.lib);
puts("PASS: fragmented raw LED subscription, token login, old/new envelopes, invalid samples, "
"real black, timeout and cleanup");
return 0;
}
+4
View File
@@ -0,0 +1,4 @@
#ifndef HYPERDRM_VERSION_H
#define HYPERDRM_VERSION_H
#define HYPERDRM_VERSION "0.12.0"
#endif