Release 0.13.0

This commit is contained in:
matze19999 committed 2026-10-06 22:26:27 +02:00
1 parent 7f6eeab39e
commit c0d891b372
49 files changed
+1523 -478

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
#ifndef HYPERDRM_ATOMIC_FILE_H
#define HYPERDRM_ATOMIC_FILE_H
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#ifndef ATOMIC_WRITE
#define ATOMIC_WRITE write
#endif
#ifndef ATOMIC_SYNC
#define ATOMIC_SYNC fsync
#endif
#ifndef ATOMIC_RENAME
#define ATOMIC_RENAME renameat
#endif
typedef enum { SAVE_UNCHANGED, SAVE_DURABLE, SAVE_COMMITTED } SaveResult;
/* Failure before rename preserves the previous file. After rename, a directory
* sync failure cannot truthfully be called a rollback: report committed with
* uncertain durability, so memory and disk never deliberately diverge. */
static inline SaveResult atomic_replace(const char *path, const void *data, size_t length) {
char parent[512], temporary[544];
if (!path || !data || strlen(path) >= sizeof(parent))
return SAVE_UNCHANGED;
strcpy(parent, path);
char *slash = strrchr(parent, '/');
const char *name = slash ? strrchr(path, '/') + 1 : path;
if (!*name || !strcmp(name, ".") || !strcmp(name, ".."))
return SAVE_UNCHANGED;
if (slash) {
if (slash == parent)
slash[1] = 0;
else
*slash = 0;
} else
strcpy(parent, ".");
int dir = open(parent, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir < 0)
return SAVE_UNCHANGED;
struct stat existing;
int found = fstatat(dir, name, &existing, AT_SYMLINK_NOFOLLOW);
if ((found < 0 && errno != ENOENT) || (found == 0 && !S_ISREG(existing.st_mode)) ||
ATOMIC_SYNC(dir) != 0) {
close(dir);
return SAVE_UNCHANGED;
}
if (snprintf(temporary, sizeof(temporary), "%s/.%s.tmp.XXXXXX", parent, name) >=
(int)sizeof(temporary)) {
close(dir);
return SAVE_UNCHANGED;
}
int fd = mkstemp(temporary);
if (fd < 0) {
close(dir);
return SAVE_UNCHANGED;
}
fcntl(fd, F_SETFD, FD_CLOEXEC);
const unsigned char *bytes = data;
size_t written = 0;
bool ok = true;
while (written < length) {
ssize_t n = ATOMIC_WRITE(fd, bytes + written, length - written);
if (n < 0 && errno == EINTR)
continue;
if (n <= 0) {
ok = false;
break;
}
written += (size_t)n;
}
if (ok)
ok = ATOMIC_SYNC(fd) == 0;
if (close(fd) != 0)
ok = false;
const char *tempname = strrchr(temporary, '/') + 1;
if (ok)
ok = ATOMIC_RENAME(dir, tempname, dir, name) == 0;
SaveResult result = SAVE_UNCHANGED;
if (ok) {
result = ATOMIC_SYNC(dir) == 0 ? SAVE_DURABLE : SAVE_COMMITTED;
if (result == SAVE_COMMITTED)
fprintf(stderr, "Preference committed; directory sync failed\n");
} else
unlinkat(dir, tempname, 0);
close(dir);
return result;
}
#endif
+15 -15
View File
@@ -2,6 +2,7 @@
#define HYPERDRM_CALIBRATION_H
#include <math.h>
#include "playback.h"
#include "atomic_file.h"
#define CALIBRATION_PREF \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
"calibration.json"
@@ -52,7 +53,7 @@ static inline CalibrationKind calibration_effective(void) {
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);
void *root = playback_parse_exact(j, text);
if (!root)
return false;
Calibration next = *result;
@@ -102,7 +103,17 @@ static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibrat
return valid;
}
static inline bool calibration_save(const char *path, const Calibration *value) {
char temp[512], data[2048];
if (!value || value->kind < CAL_AUTO || value->kind > CAL_CUSTOM || value->count > 16 ||
(value->count && (value->count < 2 || !calibration_model_valid(value->model))))
return false;
for (unsigned i = 0; i < value->count; i++) {
double x = value->points[i].position;
if (!isfinite(x) || x < 0 || x >= 4 || value->points[i].region >= 16 ||
(i && x <= value->points[i - 1].position))
return false;
}
char 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\":[",
@@ -114,19 +125,8 @@ static inline bool calibration_save(const char *path, const Calibration *value)
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;
return used > 0 && (size_t)used < sizeof(data) &&
atomic_replace(path, data, (size_t)used) != SAVE_UNCHANGED;
}
static inline void calibration_load(PlaybackJson *j, const char *path) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+91 -63
View File
@@ -2,80 +2,115 @@
static int tick(void *unused) {
(void)unused;
uint64_t now = milliseconds();
unsigned long acknowledged = receiver_runtime.output.acknowledgements;
output_tick(&receiver_runtime.output, &playback_json, now);
if (receiver_runtime.output.stage == OUTPUT_FAILED &&
(capture_ctx.handle || receiver_runtime.starting)) {
char error[128];
snprintf(error, sizeof(error), "%s", receiver_runtime.output.error);
release_capture(false);
set_error(error);
retry_update(&retries, boot_enabled() && geteuid() == 0, false, now);
retry_failure(&retries, now);
}
if (receiver_runtime.output.stage == OUTPUT_FAILED && receiver_runtime.clear_pending) {
receiver_runtime.clear_pending = false;
output_close(&receiver_runtime.output);
}
if (receiver_runtime.starting && (piccap_owned || capture_source != SOURCE_HISTOGRAM)) {
receiver_runtime.starting = false;
output_close(&receiver_runtime.output);
}
if (receiver_runtime.output.acknowledgements > acknowledged && capture_ctx.handle) {
capture_ctx.frames += receiver_runtime.output.acknowledgements - acknowledged;
capture_ctx.last_output_at = now;
last_error[0] = 0;
}
if (receiver_runtime.clear_pending && receiver_runtime.output.stage == OUTPUT_READY) {
receiver_runtime.clear_pending = false;
output_stop(&receiver_runtime.output, now, true);
} else if (receiver_runtime.starting && receiver_runtime.output.stage == OUTPUT_READY) {
free(receiver_layout.blends);
receiver_layout.blends = receiver_runtime.output.mapping;
receiver_runtime.output.mapping = NULL;
receiver_layout.count = receiver_runtime.output.count;
receiver_runtime.starting = false;
initialize_capture(NULL, NULL);
if (!capture_ctx.handle)
retry_failure(&retries, now);
}
model_tick(now);
test_tick(&playback_json, now);
layout_refresh_tick(&playback_json, now, capture || capture_source == SOURCE_PICCAP);
layout_refresh_tick(&playback_json, now, capture_ctx.handle || capture_source == SOURCE_PICCAP,
&receiver_layout);
stream_tick(&playback_json, now, handover_enabled && !retries.paused && !retries.blocked);
bool owned = handover_tick();
bridge_tick(now);
bridge_tick(now, &capture_ctx);
if (owned)
return 1;
if (!capture) {
if (!capture_ctx.handle) {
if (receiver_runtime.starting || receiver_runtime.clear_pending ||
receiver_runtime.output.closing)
return 1;
retry_update(&retries, boot_enabled() && geteuid() == 0, false, milliseconds());
if (retry_take(&retries, milliseconds())) {
start(NULL, NULL, NULL);
if (!capture)
if (!capture_ctx.handle && !receiver_runtime.starting)
retry_failure(&retries, milliseconds());
}
return 1;
}
if (revoked) {
if (capture_ctx.revoked) {
set_error("LG revoked capture; recovering after source transition");
capture_recoveries++;
recovery_started = milliseconds();
capture_ctx.recoveries++;
capture_ctx.recovery_started = milliseconds();
release_capture(false);
retry_source_transition(&retries, milliseconds());
return 1;
}
if (settling) {
settling--;
if (capture_ctx.settling) {
capture_ctx.settling--;
return 1;
}
if (!CALIBRATION_ACTIVE && letterbox_mode && capture_preset == 1 &&
milliseconds() >= next_border_probe) {
if (vt_preset(capture, 0) != 0) {
if (!CALIBRATION_ACTIVE && capture_ctx.letterbox_mode && capture_ctx.preset == 1 &&
milliseconds() >= capture_ctx.next_border_probe) {
if (vt_preset(capture_ctx.handle, 0) != 0) {
set_error("Border probe preset failed");
stop_capture();
return 1;
}
capture_preset = 0;
settling = 2;
capture_ctx.preset = 0;
capture_ctx.settling = 2;
return 1;
}
Histogram hist[16] = {0};
int rc = vt_get(capture, hist);
int rc = vt_get(capture_ctx.handle, 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)) {
capture_ctx.color_valid = false;
if (!continuity_frame(&capture_ctx.continuity, false, capture_ctx.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)) {
char *line = region_command(held, receiver_layout.blends, receiver_layout.count,
(unsigned)(1500 - (now - capture_ctx.continuity.missing_at)));
if (!line || (!output_frame(&receiver_runtime.output, 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++;
capture_ctx.sample_at = now;
capture_ctx.sample_sequence++;
unsigned aggregate[3] = {0};
for (int i = 0; i < 16; i++) {
black_percent[i] = 100;
capture_ctx.black_percent[i] = 100;
for (int c = 0; c < 3; c++) {
uint64_t total = 0, black = 0;
for (int b = 0; b < 256; b++) {
@@ -84,67 +119,60 @@ static int tick(void *unused) {
black += hist[i].bins[c][b];
}
unsigned fraction = total ? (unsigned)(100 * black / total) : 0;
if (fraction < black_percent[i])
black_percent[i] = fraction;
if (fraction < capture_ctx.black_percent[i])
capture_ctx.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];
capture_ctx.rgb[i][c] = histogram_mean(hist[i].bins[c]);
aggregate[c] += capture_ctx.rgb[i][c];
}
if (!CALIBRATION_ACTIVE && capture_preset == 0) {
int candidate = letterbox_candidate(black_percent, rgb);
if (!CALIBRATION_ACTIVE && capture_ctx.preset == 0) {
int candidate = letterbox_candidate(capture_ctx.black_percent, capture_ctx.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) {
capture_ctx.pending_letterbox = -1;
else if (candidate != capture_ctx.pending_letterbox) {
capture_ctx.pending_letterbox = candidate;
capture_ctx.candidate_since = now;
} else if ((unsigned)candidate != capture_ctx.letterbox_mode &&
now - capture_ctx.candidate_since >= 2000)
capture_ctx.letterbox_mode = (unsigned)candidate;
if (capture_ctx.letterbox_mode) {
if (vt_preset(capture_ctx.handle, 1) != 0) {
set_error("Letterbox preset failed");
stop_capture();
return 1;
}
capture_preset = 1;
settling = 2;
next_border_probe = now + 2000;
capture_ctx.preset = 1;
capture_ctx.settling = 2;
capture_ctx.next_border_probe = now + 2000;
return 1;
}
}
for (int c = 0; c < 3; c++)
sent_rgb[c] = (unsigned char)(aggregate[c] / 16);
capture_ctx.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);
boost_color(capture_ctx.sent_rgb, boosted_mean);
memcpy(capture_ctx.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);
calibration_colors(capture_ctx.rgb, mapped_rgb);
continuity_frame(&capture_ctx.continuity, true, mapped_rgb, now, stable_rgb);
receiver_layout.interpolation = receiver_layout.count && calibration_effective() != CAL_AVERAGE;
char *line = region_command(stable_rgb, receiver_layout.blends, receiver_layout.count, 1500);
if (!line) {
set_error("Color buffer allocation failed");
stop_capture();
return 1;
}
int failed = send_line(line);
int failed = (!output_frame(&receiver_runtime.output, 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++;
capture_ctx.color_valid = true;
return 1;
}
+5 -1
View File
@@ -135,13 +135,17 @@ static void probe_tick(ReceiverProbe *probe, PlaybackJson *json, uint64_t now) {
probe_finish(probe, "Receiver closed the test connection", now);
return;
}
if (memchr(probe->response + probe->used, 0, (size_t)n)) {
probe_finish(probe, "Receiver sent an embedded NUL", 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);
void *root = playback_parse_exact(json, probe->response);
bool success = playback_true(json, root, "success");
if (root)
json->put(root);
+2 -12
View File
@@ -1,6 +1,7 @@
#ifndef HYPERDRM_HANDOVER_H
#define HYPERDRM_HANDOVER_H
#include "playback.h"
#include "atomic_file.h"
#ifndef CALIBRATION_ACTIVE
#define CALIBRATION_ACTIVE false
#endif
@@ -43,18 +44,7 @@ static inline bool piccap_installed(void) {
}
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;
return atomic_replace(HANDOVER_PREF, data, strlen(data)) != SAVE_UNCHANGED;
}
static void handover_load(void) {
int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+11 -9
View File
@@ -10,16 +10,18 @@ static void layout_refresh_reset(uint64_t due) {
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)
static bool layout_mapping_changed(const ReceiverLayoutContext *layout, const LedBlend *next,
unsigned count) {
if (count != layout->count || !layout->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)
if (next[i].a != layout->blends[i].a || next[i].b != layout->blends[i].b ||
next[i].weight != layout->blends[i].weight)
return true;
return false;
}
static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running) {
static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running,
ReceiverLayoutContext *layout) {
if (!running) {
layout_refresh_reset(now + LAYOUT_REFRESH_MS);
return;
@@ -27,11 +29,11 @@ static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running)
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;
if (layout_mapping_changed(layout, layout_probe.mapping, layout_probe.leds)) {
free(layout->blends);
layout->blends = layout_probe.mapping;
layout_probe.mapping = NULL;
led_count = layout_probe.leds;
layout->count = layout_probe.leds;
layout_updates++;
}
last_layout_check = now;
+38 -1
View File
@@ -14,6 +14,11 @@ typedef struct {
const char *(*string)(const void *);
int (*boolean)(const void *);
int (*put)(void *);
void *(*tokener_new)(void);
void *(*tokener_parse)(void *, const char *, int);
int (*tokener_error)(void *);
size_t (*tokener_end)(void *);
void (*tokener_free)(void *);
} PlaybackJson;
static inline bool playback_json_init(PlaybackJson *j) {
j->lib = dlopen("libjson-c.so.5", RTLD_NOW);
@@ -27,14 +32,46 @@ static inline bool playback_json_init(PlaybackJson *j) {
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");
j->tokener_new = dlsym(j->lib, "json_tokener_new");
j->tokener_parse = dlsym(j->lib, "json_tokener_parse_ex");
j->tokener_error = dlsym(j->lib, "json_tokener_get_error");
j->tokener_end = dlsym(j->lib, "json_tokener_get_parse_end");
j->tokener_free = dlsym(j->lib, "json_tokener_free");
if (!j->parse || !j->get || !j->type || !j->length || !j->at || !j->string || !j->boolean ||
!j->put) {
!j->put || !j->tokener_new || !j->tokener_parse || !j->tokener_error || !j->tokener_end ||
!j->tokener_free) {
dlclose(j->lib);
j->lib = NULL;
return false;
}
return true;
}
/* Require exactly one complete JSON value. json_tokener_parse alone can accept
* an otherwise valid prefix followed by unrelated or truncated content. */
static inline void *playback_parse_exact(PlaybackJson *j, const char *text) {
if (!j || !text || !j->tokener_new || !j->tokener_parse || !j->tokener_error ||
!j->tokener_end || !j->tokener_free || !j->put)
return NULL;
size_t length = strlen(text);
if (length > 2 * 1024 * 1024)
return NULL;
void *tokener = j->tokener_new();
if (!tokener)
return NULL;
void *root = j->tokener_parse(tokener, text, (int)length + 1);
size_t end = j->tokener_end(tokener);
bool valid = j->tokener_error(tokener) == 0;
while (end < length &&
(text[end] == ' ' || text[end] == '\t' || text[end] == '\r' || text[end] == '\n'))
end++;
valid = valid && end == length;
j->tokener_free(tokener);
if (!valid && root) {
j->put(root);
root = NULL;
}
return root;
}
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;
+55 -22
View File
@@ -7,28 +7,61 @@
#include <stdarg.h>
#include "histogram.h"
/* Process-local diagnostic sink. It changes logging only, not capture policy. */
void printLog(const char *context,int level,const char *message_id,
const char *file,const char *function,int line,const char *format,...) {
(void)context;(void)level;(void)message_id;(void)file;
fprintf(stderr,"LG %s:%d: ",function?function:"?",line);
if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);}fputc('\n',stderr);
void printLog(const char *context, int level, const char *message_id, const char *file,
const char *function, int line, const char *format, ...) {
(void)context;
(void)level;
(void)message_id;
(void)file;
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
if (format) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
fputc('\n', stderr);
}
static bool policy(void) {
fprintf(stderr, "Capture revoked by LG policy\n");
return true;
}
static bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; }
int main(void) {
void *lib=dlopen("libvtcapture.so.1",RTLD_NOW);
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;}
void *(*init)(bool(*)(void))=dlsym(lib,"vtCapture_initHistogram");
int (*preset)(void*,int)=dlsym(lib,"vtCapture_setPresetHistogram");
int (*get)(void*,Histogram*)=dlsym(lib,"vtCapture_getHistogram");
int (*finish)(void*)=dlsym(lib,"vtCapture_finalizeHistogram");
if(!init||!preset||!get||!finish)return 2;
void *handle=init(policy); fprintf(stderr,"handle=%p\n",handle);
if(!handle)return 3;
int rc=preset(handle,0);fprintf(stderr,"preset=%d\n",rc);
Histogram hist[16];
for(int frame=0;rc==0 && frame<3;frame++) {
usleep(100000); rc=get(handle,hist); printf("frame=%d rc=%d\n",frame,rc);
if(rc==0)for(int i=0;i<16;i++) {printf("region=%u",hist[i].id);for(int c=0;c<3;c++){uint64_t sum=0;for(int b=0;b<256;b++)sum+=hist[i].bins[c][b];printf(" ch%d_count=%llu mean=%u",c,(unsigned long long)sum,histogram_mean(hist[i].bins[c]));} puts("");}
}
fprintf(stderr,"finalize=%d\n",finish(handle)); return rc?4:0;
void *lib = dlopen("libvtcapture.so.1", RTLD_NOW);
if (!lib) {
fprintf(stderr, "dlopen: %s\n", dlerror());
return 1;
}
void *(*init)(bool (*)(void)) = dlsym(lib, "vtCapture_initHistogram");
int (*preset)(void *, int) = dlsym(lib, "vtCapture_setPresetHistogram");
int (*get)(void *, Histogram *) = dlsym(lib, "vtCapture_getHistogram");
int (*finish)(void *) = dlsym(lib, "vtCapture_finalizeHistogram");
if (!init || !preset || !get || !finish)
return 2;
void *handle = init(policy);
fprintf(stderr, "handle=%p\n", handle);
if (!handle)
return 3;
int rc = preset(handle, 0);
fprintf(stderr, "preset=%d\n", rc);
Histogram hist[16];
for (int frame = 0; rc == 0 && frame < 3; frame++) {
usleep(100000);
rc = get(handle, hist);
printf("frame=%d rc=%d\n", frame, rc);
if (rc == 0)
for (int i = 0; i < 16; i++) {
printf("region=%u", hist[i].id);
for (int c = 0; c < 3; c++) {
uint64_t sum = 0;
for (int b = 0; b < 256; b++)
sum += hist[i].bins[c][b];
printf(" ch%d_count=%llu mean=%u", c, (unsigned long long)sum,
histogram_mean(hist[i].bins[c]));
}
puts("");
}
}
fprintf(stderr, "finalize=%d\n", finish(handle));
return rc ? 4 : 0;
}
+263
View File
@@ -0,0 +1,263 @@
#ifndef HYPERDRM_RECEIVER_OUTPUT_H
#define HYPERDRM_RECEIVER_OUTPUT_H
/* Main output connection. Every socket operation is nonblocking, with bounded
* work per tick. One acknowledged command is in flight; only the newest unsent
* color frame is retained. No Luna message or LG capture handle is owned here. */
typedef enum {
OUTPUT_IDLE,
OUTPUT_CONNECTING,
OUTPUT_LOGIN,
OUTPUT_LAYOUT,
OUTPUT_READY,
OUTPUT_FAILED
} OutputStage;
typedef struct {
int fd;
OutputStage stage;
TargetConfig target;
char *incoming, *request, *latest;
size_t used, sent, length;
uint64_t deadline;
unsigned transaction, awaiting, count;
LedBlend *mapping;
unsigned long acknowledgements;
char error[128];
bool closing;
} ReceiverOutput;
static void output_close(ReceiverOutput *o) {
if (o->fd >= 0)
close(o->fd);
free(o->incoming);
free(o->request);
free(o->latest);
free(o->mapping);
*o = (ReceiverOutput){.fd = -1};
}
static void output_fail(ReceiverOutput *o, const char *error) {
output_close(o);
o->stage = OUTPUT_FAILED;
snprintf(o->error, sizeof(o->error), "%s", error);
}
static bool output_request(ReceiverOutput *o, const char *line, uint64_t now) {
size_t n = strlen(line);
while (n && (line[n - 1] == '\n' || line[n - 1] == ' '))
n--;
if (!n || line[n - 1] != '}' || o->request || o->awaiting)
return false;
o->request = malloc(n + 48);
if (!o->request)
return false;
if (o->transaction >= 2147483647u)
o->transaction = 0;
o->transaction++;
memcpy(o->request, line, n - 1);
snprintf(o->request + n - 1, 48, ",\"tan\":%u}\n", o->transaction);
o->length = strlen(o->request);
o->sent = 0;
o->awaiting = o->transaction;
o->deadline = now + 3000;
return true;
}
static bool output_begin(ReceiverOutput *o, const TargetConfig *target, uint64_t now) {
output_close(o);
o->target = *target;
o->incoming = malloc(2 * 1024 * 1024 + 1);
if (!o->incoming) {
output_fail(o, "Cannot allocate receiver buffer");
return false;
}
o->fd = target_open(target);
if (o->fd < 0) {
output_fail(o, "Cannot open receiver connection");
return false;
}
o->stage = OUTPUT_CONNECTING;
o->deadline = now + 3000;
return true;
}
static bool output_frame(ReceiverOutput *o, const char *line) {
if (o->stage != OUTPUT_READY || o->closing)
return false;
if (strlen(line) > 2 * 1024 * 1024)
return false;
char *next = strdup(line);
if (!next)
return false;
free(o->latest);
o->latest = next;
return true;
}
static void output_stop(ReceiverOutput *o, uint64_t now, bool clear) {
if (!clear || o->stage != OUTPUT_READY) {
output_close(o);
return;
}
free(o->latest);
o->latest = strdup("{\"command\":\"clear\",\"priority\":149}\n");
if (!o->latest) {
output_close(o);
return;
}
o->closing = true;
/* Allow the current transaction and the clear to finish asynchronously. */
if (!o->awaiting)
o->deadline = now + 3000;
}
static bool output_flush(ReceiverOutput *o, size_t *budget) {
if (!o->request || !*budget)
return true;
size_t amount = o->length - o->sent;
if (amount > *budget)
amount = *budget;
ssize_t n = send(o->fd, o->request + o->sent, amount, MSG_NOSIGNAL);
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) {
output_fail(o, "Receiver closed during send");
return false;
}
if (n > 0) {
o->sent += (size_t)n;
*budget -= (size_t)n;
}
if (o->sent == o->length) {
free(o->request);
o->request = NULL;
}
return true;
}
static void output_tick(ReceiverOutput *o, PlaybackJson *json, uint64_t now) {
if (o->stage == OUTPUT_IDLE || o->stage == OUTPUT_FAILED)
return;
if ((o->stage != OUTPUT_READY || o->awaiting || o->closing) && now >= o->deadline) {
if (o->closing)
output_close(o);
else
output_fail(o, "Receiver transaction timed out");
return;
}
if (o->stage == OUTPUT_CONNECTING) {
struct pollfd p = {o->fd, POLLOUT, 0};
int error = 0;
socklen_t size = sizeof(error);
if (poll(&p, 1, 0) <= 0)
return;
if (getsockopt(o->fd, SOL_SOCKET, SO_ERROR, &error, &size) || error) {
output_fail(o, "Receiver connection refused or unreachable");
return;
}
char line[384];
if (o->target.token[0]) {
snprintf(line, sizeof(line),
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
o->target.token);
o->stage = OUTPUT_LOGIN;
} else {
snprintf(line, sizeof(line), "{\"command\":\"serverinfo\"}\n");
o->stage = OUTPUT_LAYOUT;
}
if (!output_request(o, line, now)) {
output_fail(o, "Cannot allocate receiver request");
return;
}
}
if (o->stage == OUTPUT_READY && !o->awaiting && o->latest) {
if (!output_request(o, o->latest, now)) {
output_fail(o, "Cannot allocate color request");
return;
}
free(o->latest);
o->latest = NULL;
}
size_t send_budget = 65536;
if (!output_flush(o, &send_budget))
return;
size_t space = 2 * 1024 * 1024 - o->used;
if (space > 65536)
space = 65536;
if (!space && !memchr(o->incoming, '\n', o->used)) {
output_fail(o, "Receiver response exceeds 2 MiB");
return;
}
ssize_t n = space ? recv(o->fd, o->incoming + o->used, space, 0) : -1;
if (!space)
errno = EAGAIN;
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) {
output_fail(o, "Receiver closed during receive");
return;
}
if (n > 0) {
if (memchr(o->incoming + o->used, 0, (size_t)n)) {
output_fail(o, "Receiver sent an embedded NUL");
return;
}
o->used += (size_t)n;
}
o->incoming[o->used] = 0;
for (unsigned budget = 0; budget < 8; budget++) {
char *end = memchr(o->incoming, '\n', o->used);
if (!end)
break;
*end = 0;
void *root = playback_parse_exact(json, o->incoming);
if (!root || json->type(root) != 4) {
if (root)
json->put(root);
output_fail(o, "Receiver returned malformed JSON");
return;
}
void *tan = playback_field(json, root, "tan");
char *tail = NULL;
unsigned long id = tan && json->type(tan) == 3 ? strtoul(json->string(tan), &tail, 10) : 0;
bool matches = o->awaiting && tail && !*tail && id == o->awaiting;
if (matches) {
if (o->request || !playback_true(json, root, "success")) {
json->put(root);
output_fail(o, o->request ? "Receiver acknowledged an incomplete request"
: o->stage == OUTPUT_LOGIN
? "Receiver authentication failed; check API token"
: o->stage == OUTPUT_LAYOUT ? "Receiver rejected LED layout request"
: "Receiver rejected color request");
return;
}
o->awaiting = 0;
if (o->stage == OUTPUT_LOGIN) {
o->stage = OUTPUT_LAYOUT;
if (!output_request(o, "{\"command\":\"serverinfo\"}\n", now)) {
json->put(root);
output_fail(o, "Cannot allocate layout request");
return;
}
} else if (o->stage == OUTPUT_LAYOUT) {
if (!layout_from_json(o->incoming, &o->mapping, &o->count)) {
json->put(root);
output_fail(o, "Receiver returned an invalid LED layout");
return;
}
o->stage = OUTPUT_READY;
} else {
o->acknowledgements++;
if (o->closing && !o->latest) {
json->put(root);
output_close(o);
return;
}
}
}
json->put(root);
size_t consumed = (size_t)(end - o->incoming) + 1;
memmove(o->incoming, o->incoming + consumed, o->used - consumed);
o->used -= consumed;
o->incoming[o->used] = 0;
}
/* An ACK can free the slot after the first send pass. Use the remaining
* byte budget to send the newest queued frame in this same tick. */
if (o->stage == OUTPUT_READY && !o->awaiting && o->latest) {
if (!output_request(o, o->latest, now)) {
output_fail(o, "Cannot allocate color request");
return;
}
free(o->latest);
o->latest = NULL;
}
output_flush(o, &send_budget);
}
#endif
+5 -1
View File
@@ -181,6 +181,10 @@ static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) {
stream_close(now);
return;
}
if (n > 0 && memchr(stream.data + stream.used, 0, (size_t)n)) {
stream_close(now);
return;
}
if (n > 0)
stream.used += (size_t)n;
stream.data[stream.used] = 0;
@@ -189,7 +193,7 @@ static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) {
if (!end)
break;
*end = 0;
void *root = j->parse(stream.data);
void *root = playback_parse_exact(j, stream.data);
if (stream.stage == STREAM_AUTHENTICATING) {
if (!playback_true(j, root, "success")) {
if (root)
+26
View File
@@ -0,0 +1,26 @@
#ifndef HYPERDRM_RUNTIME_CONTEXT_H
#define HYPERDRM_RUNTIME_CONTEXT_H
#include <signal.h>
#include "histogram.h"
#include "continuity.h"
typedef struct {
void *handle;
char diagnostic[256];
unsigned long frames;
unsigned char rgb[16][3], sent_rgb[3];
unsigned black_percent[16], preset, letterbox_mode, settling;
int pending_letterbox;
uint64_t candidate_since, next_border_probe, sample_at;
unsigned sample_sequence, recoveries;
uint64_t recovery_started, last_recovery_ms, last_output_at;
bool color_valid;
Continuity continuity;
volatile sig_atomic_t revoked;
} CaptureContext;
/* LedBlend is supplied by the layout module (or a portable test fixture). */
typedef struct {
LedBlend *blends;
unsigned count;
bool interpolation;
} ReceiverLayoutContext;
#endif
+26 -144
View File
@@ -29,6 +29,7 @@
#include "spatial_image.h"
#include "diagnostics.h"
#include "continuity.h"
#include "service_context.h"
#define SERVICE "org.webosbrew.histogramhyperion.service"
#define PRIORITY 149
typedef struct {
@@ -52,20 +53,21 @@ 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];
static CaptureContext capture_ctx = {.pending_letterbox = -1};
static ReceiverLayoutContext receiver_layout;
static CalibrationSessionContext calibration_ctx;
/* 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)
if (capture_ctx.handle && level > 3)
return;
if (format) {
va_list args;
va_start(args, format);
vsnprintf(lg_diagnostic, sizeof(lg_diagnostic), format, args);
vsnprintf(capture_ctx.diagnostic, sizeof(capture_ctx.diagnostic), format, args);
va_end(args);
}
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
@@ -77,36 +79,20 @@ void printLog(const char *context, int level, const char *message_id, const char
}
fputc('\n', stderr);
}
static int output = -1;
static unsigned long frames;
static unsigned char rgb[16][3];
static unsigned char sent_rgb[3];
static unsigned black_percent[16];
static unsigned capture_preset;
static unsigned letterbox_mode, settling;
static int pending_letterbox = -1;
static uint64_t candidate_since, next_border_probe;
static bool color_valid;
static Continuity continuity;
static uint64_t sample_at, calibration_until;
static unsigned sample_sequence;
#define CALIBRATION_ACTIVE (calibration_until > milliseconds())
static unsigned led_count;
static LedBlend *led_blends;
static bool interpolation_enabled;
#define CALIBRATION_ACTIVE (calibration_ctx.until > milliseconds())
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 char last_error[256];
static volatile sig_atomic_t revoked, quitting;
static volatile sig_atomic_t quitting;
static int guard_pipe = -1;
static pid_t guard_pid = -1;
#ifndef DEV_MODE
#define DEV_MODE "/var/luna/preferences/vt_devmode"
#endif
/* 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. */
@@ -169,135 +155,30 @@ static void quit_signal(int s) {
quitting = 1;
}
static bool policy(void) {
revoked = 1;
capture_ctx.revoked = 1;
return true;
}
static void set_error(const char *s) {
snprintf(last_error, sizeof(last_error), "%s", s);
fprintf(stderr, "%s\n", s);
}
static int send_line(const char *line) {
size_t left = strlen(line);
const char *p = line;
while (left) {
ssize_t n = send(output, p, left, MSG_NOSIGNAL);
if (n <= 0)
return -1;
p += n;
left -= n;
}
return 0;
}
static bool 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 ReceiverRuntime receiver_runtime = {.output = {.fd = -1}};
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;
free(receiver_layout.blends);
receiver_layout.blends = NULL;
receiver_layout.count = 0;
receiver_layout.interpolation = false;
capture_ctx.color_valid = false;
memset(capture_ctx.rgb, 0, sizeof(capture_ctx.rgb));
memset(capture_ctx.sent_rgb, 0, sizeof(capture_ctx.sent_rgb));
receiver_runtime.starting = false;
receiver_runtime.clear_pending = false;
output_stop(&receiver_runtime.output, milliseconds(), clear_output);
if (capture_ctx.handle) {
vt_finish(capture_ctx.handle);
capture_ctx.handle = NULL;
}
}
static void stop_capture(void) {
@@ -396,6 +277,7 @@ int main(void) {
stream_close(milliseconds());
test_close();
stop_capture();
output_close(&receiver_runtime.output);
if (piccap_owned) {
handover_enabled = false;
retry_pause(&retries);
+35 -41
View File
@@ -20,28 +20,27 @@ static bool start(void *h, void *m, void *ctx) {
retry_reset(&retries);
if (piccap_owned)
return respond(h, m, "{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
if (capture)
if (capture_ctx.handle)
return respond(h, m, "{\"returnValue\":true,\"running\":true}");
frames = 0;
revoked = 0;
if (!playback_json.lib) {
set_error("JSON-C is unavailable; cannot start receiver output");
return start_failure(h, m, "JSON-C is unavailable");
}
capture_ctx.frames = 0;
capture_ctx.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");
capture_ctx.diagnostic[0] = 0;
if (receiver_runtime.starting)
return respond(h, m, "{\"returnValue\":true,\"connecting\":true}");
receiver_runtime.clear_pending = false;
if (!output_begin(&receiver_runtime.output, &target, milliseconds())) {
set_error(receiver_runtime.output.error);
return start_failure(h, m, receiver_runtime.output.error);
}
receiver_runtime.starting = true;
return respond(h, m, "{\"returnValue\":true,\"connecting\":true}");
}
static bool initialize_capture(void *h, void *m) {
if (!enter_developer_mode()) {
stop_capture();
set_error("Developer initialization needs root. Elevate this own service through Homebrew "
@@ -49,54 +48,49 @@ static bool start(void *h, void *m, void *ctx) {
return start_failure(h, m,
"Root access is required for temporary LG developer initialization");
}
capture = vt_init(policy);
capture_ctx.handle = 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) {
if (!capture_ctx.handle) {
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) {
if (vt_preset(capture_ctx.handle, 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;
capture_ctx.preset = capture_ctx.letterbox_mode = capture_ctx.settling = 0;
capture_ctx.pending_letterbox = -1;
capture_ctx.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;
if (capture_ctx.recovery_started) {
capture_ctx.last_recovery_ms = milliseconds() - capture_ctx.recovery_started;
capture_ctx.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;
calibration_ctx.until = 0;
retry_pause(&retries);
bool bridging = bridge_phase != BRIDGE_IDLE;
stream_close(milliseconds());
bridge_phase = BRIDGE_IDLE;
memset(&continuity, 0, sizeof(continuity));
memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.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;
}
if (bridging && receiver_runtime.output.stage != OUTPUT_READY) {
output_begin(&receiver_runtime.output, &target, milliseconds());
receiver_runtime.clear_pending = true;
}
return respond(h, m, "{\"returnValue\":true,\"running\":false}");
}
@@ -122,7 +116,7 @@ static bool autostart(void *h, void *m, void *ctx) {
"the installed boot script and whether another file occupies the hook path.\"}");
if (enabled)
retry_reset(&retries);
retry_update(&retries, enabled, capture != NULL, milliseconds());
retry_update(&retries, enabled, capture_ctx.handle != NULL, milliseconds());
return respond(h, m,
enabled ? "{\"returnValue\":true,\"autostart\":true}"
: "{\"returnValue\":true,\"autostart\":false}");
@@ -172,11 +166,11 @@ static bool settings(void *h, void *m, void *ctx) {
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;
bool active = capture_ctx.handle || receiver_runtime.starting || piccap_owned || retries.due;
stop_capture();
stream_close(milliseconds());
bridge_phase = BRIDGE_IDLE;
memset(&continuity, 0, sizeof(continuity));
memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
test_close();
connection_test.stage = TEST_IDLE;
target = next;
+22 -27
View File
@@ -3,24 +3,22 @@ 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;
calibration_ctx.until = milliseconds() + 110000;
if (capture_ctx.handle && vt_preset(capture_ctx.handle, 0) != 0) {
calibration_ctx.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;
capture_ctx.preset = capture_ctx.letterbox_mode = 0;
capture_ctx.settling = 2;
capture_ctx.pending_letterbox = -1;
} else if (!strcmp(p, "{\"enabled\":false}"))
calibration_until = 0;
calibration_ctx.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\"}");
@@ -28,9 +26,9 @@ static bool report_calibration(void *h, void *m, 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);
strlen(playback_json.string(message)) < sizeof(calibration_ctx.message);
if (valid)
snprintf(calibration_message, sizeof(calibration_message), "%s",
snprintf(calibration_ctx.message, sizeof(calibration_ctx.message), "%s",
playback_json.string(message));
if (root)
playback_json.put(root);
@@ -46,17 +44,14 @@ static bool draft_calibration(void *h, void *m, void *ctx) {
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;
calibration_ctx.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;
calibration_ctx.model_busy = false;
calibration_ctx.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 &&
@@ -70,23 +65,23 @@ static bool model_reply(void *h, void *m, void *ctx) {
return true;
}
static void model_tick(uint64_t now) {
if (model_busy && now >= model_deadline) {
if (calibration_ctx.model_busy && now >= calibration_ctx.model_deadline) {
LSError e;
error_init(&e);
cancel_call(service_handle, model_token, &e);
cancel_call(service_handle, calibration_ctx.model_token, &e);
error_free(&e);
model_busy = false;
model_token = 0;
calibration_ctx.model_busy = false;
calibration_ctx.model_token = 0;
}
if (!tv_model[0] && !model_busy && now >= next_model_probe) {
if (!tv_model[0] && !calibration_ctx.model_busy && now >= calibration_ctx.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);
calibration_ctx.model_busy = call_once(
service_handle, "luna://com.webos.service.tv.systemproperty/getSystemInfo",
"{\"keys\":[\"modelName\"]}", model_reply, NULL, &calibration_ctx.model_token, &e);
error_free(&e);
model_deadline = now + 3000;
next_model_probe = now + 60000;
calibration_ctx.model_deadline = now + 3000;
calibration_ctx.next_model_probe = now + 60000;
}
}
static bool set_calibration(void *h, void *m, void *ctx) {
+17
View File
@@ -0,0 +1,17 @@
#ifndef HYPERDRM_SERVICE_CONTEXT_H
#define HYPERDRM_SERVICE_CONTEXT_H
#include "runtime_context.h"
#include "receiver_output.h"
/* Runtime ownership is separate from the saved calibration configuration. */
typedef struct {
uint64_t until, model_deadline, next_model_probe;
unsigned long model_token;
bool model_busy;
Calibration draft;
char message[256];
} CalibrationSessionContext;
typedef struct {
ReceiverOutput output;
bool starting, clear_pending;
} ReceiverRuntime;
#endif
+56 -50
View File
@@ -16,19 +16,20 @@ static bool status(void *h, void *m, void *ctx) {
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];
for (size_t i = 0; calibration_ctx.message[i] && k + 2 < sizeof(report); i++) {
char c = calibration_ctx.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);
int used = snprintf(
json, sizeof(json),
"{\"receiverConnecting\":%s,\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
"\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",
receiver_runtime.starting ? "true" : "false", 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}",
@@ -40,64 +41,69 @@ static bool status(void *h, void *m, void *ctx) {
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,
"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[",
calibration_ctx.draft.model);
for (unsigned i = 0; i < calibration_ctx.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);
i ? "," : "", calibration_ctx.draft.points[i].position,
calibration_ctx.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",
capture_ctx.sample_sequence,
capture_ctx.sample_at ? (long long)(now - capture_ctx.sample_at) : -1,
capture_ctx.continuity.holding ? "true" : "false",
(capture_ctx.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);
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,
capture_ctx.last_output_at ? (long long)(now - capture_ctx.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_ctx.handle ? "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_ctx.handle || capture_source == SOURCE_PICCAP) ? "true" : "false",
capture_ctx.color_valid ? "true" : "false", capture_ctx.frames, capture_ctx.recoveries,
(unsigned long long)capture_ctx.last_recovery_ms, endpoint, target.host, target.port,
target.receiver, target.token[0] ? "true" : "false",
target_piccap_compatible(&target) ? "true" : "false", PRIORITY, (unsigned)geteuid(),
capture_ctx.sent_rgb[0], capture_ctx.sent_rgb[1], capture_ctx.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, "%s[%u,%u,%u]", i ? "," : "",
capture_ctx.rgb[i][0], capture_ctx.rgb[i][1], capture_ctx.rgb[i][2]);
used += snprintf(json + used, sizeof(json) - used,
"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[", led_count,
led_count ? "true" : "false");
"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",
receiver_layout.count, receiver_layout.count ? "true" : "false");
for (int i = 0; i < 16; i++)
used += snprintf(json + used, sizeof(json) - used, "%s%u", i ? "," : "", black_percent[i]);
used += snprintf(json + used, sizeof(json) - used, "%s%u", i ? "," : "",
capture_ctx.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",
capture_ctx.preset, receiver_layout.interpolation ? "true" : "false",
boot_enabled() ? "true" : "false", retries.due ? "true" : "false",
retries.paused ? "true" : "false", retries.blocked ? "true" : "false",
retries.attempts, piccap_installed() ? "true" : "false",
@@ -106,6 +112,6 @@ static bool status(void *h, void *m, void *ctx) {
protection == PLAYBACK_PROTECTED ? "protected"
: protection == PLAYBACK_CLEAR ? "non-secure decoder"
: "unknown",
capture && letterbox_mode ? "true" : "false");
capture_ctx.handle && capture_ctx.letterbox_mode ? "true" : "false");
return respond(h, m, json);
}
+7 -5
View File
@@ -44,7 +44,7 @@ static bool bridge_send(const unsigned char colors[16][3]) {
free(line);
return ok;
}
static void bridge_tick(uint64_t now) {
static void bridge_tick(uint64_t now, CaptureContext *capture) {
if (retries.paused || retries.blocked) {
if (bridge_phase)
bridge_clear();
@@ -54,15 +54,17 @@ static void bridge_tick(uint64_t now) {
}
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_phase =
capture->continuity.available && now - capture->continuity.valid_at < 2000
? BRIDGE_HOLDING
: BRIDGE_IDLE;
bridge_started = now;
memcpy(bridge_from, continuity.last, sizeof(bridge_from));
memcpy(bridge_from, capture->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);
continuity_seed(&capture->continuity, stream.colors, now);
bridge_send(stream.colors);
}
}
+5 -37
View File
@@ -10,6 +10,7 @@
#include <sys/time.h>
#include <unistd.h>
#include "playback.h"
#include "atomic_file.h"
#define TARGET_PREF \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
typedef struct {
@@ -44,7 +45,7 @@ static bool target_valid(const TargetConfig *t) {
return true;
}
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
void *root = j->parse(text);
void *root = playback_parse_exact(j, text);
if (!root)
return false;
TargetConfig next = *out;
@@ -112,24 +113,13 @@ static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
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;
char data[512];
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;
return n > 0 && (size_t)n < sizeof(data) &&
atomic_replace(path, data, (size_t)n) != SAVE_UNCHANGED;
}
static bool target_load(PlaybackJson *j, const char *path, TargetConfig *t) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
@@ -178,26 +168,4 @@ static int target_open(const TargetConfig *t) {
}
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
+101
View File
@@ -0,0 +1,101 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static bool interrupt_write, short_write, fail_write, fail_sync, fail_rename, fail_commit_sync;
static unsigned directory_syncs;
static ssize_t fixture_write(int fd, const void *data, size_t n) {
if (interrupt_write) {
interrupt_write = false;
errno = EINTR;
return -1;
}
if (fail_write) {
errno = ENOSPC;
return -1;
}
return write(fd, data, short_write && n > 2 ? 2 : n);
}
static int fixture_sync(int fd) {
struct stat info;
assert(!fstat(fd, &info));
if (S_ISDIR(info.st_mode)) {
directory_syncs++;
if (fail_commit_sync && directory_syncs == 2) {
errno = EIO;
return -1;
}
} else if (fail_sync) {
errno = EIO;
return -1;
}
return fsync(fd);
}
static int fixture_rename(int from, const char *a, int to, const char *b) {
if (fail_rename) {
errno = EACCES;
return -1;
}
return renameat(from, a, to, b);
}
#define ATOMIC_WRITE fixture_write
#define ATOMIC_SYNC fixture_sync
#define ATOMIC_RENAME fixture_rename
#include "atomic_file.h"
static void contents(const char *path, const char *expected) {
char data[64] = {0};
int fd = open(path, O_RDONLY);
assert(fd >= 0);
assert(read(fd, data, sizeof(data) - 1) == (ssize_t)strlen(expected));
close(fd);
assert(!strcmp(data, expected));
}
int main(void) {
char directory[] = "/tmp/hyperdrm-save-XXXXXX";
assert(mkdtemp(directory));
char path[128], stale[128], link[128];
snprintf(path, sizeof(path), "%s/settings.json", directory);
snprintf(stale, sizeof(stale), "%s/settings.json.tmp", directory);
snprintf(link, sizeof(link), "%s/link", directory);
int fd = open(stale, O_CREAT | O_WRONLY, 0600);
assert(fd >= 0);
close(fd);
assert(atomic_replace(path, "old", 3) == SAVE_DURABLE);
interrupt_write = short_write = true;
assert(atomic_replace(path, "partial writes handled", 22) == SAVE_DURABLE);
contents(path, "partial writes handled");
short_write = false;
fail_write = true;
assert(atomic_replace(path, "new", 3) == SAVE_UNCHANGED);
fail_write = false;
contents(path, "partial writes handled");
fail_sync = true;
assert(atomic_replace(path, "new", 3) == SAVE_UNCHANGED);
fail_sync = false;
fail_rename = true;
assert(atomic_replace(path, "new", 3) == SAVE_UNCHANGED);
fail_rename = false;
contents(path, "partial writes handled");
directory_syncs = 0;
fail_commit_sync = true;
assert(atomic_replace(path, "committed", 9) == SAVE_COMMITTED);
contents(path, "committed");
fail_commit_sync = false;
assert(!symlink(path, link));
assert(atomic_replace(link, "bad", 3) == SAVE_UNCHANGED);
contents(path, "committed");
struct stat info;
assert(!stat(path, &info) && (info.st_mode & 0777) == 0600);
assert(!access(stale, F_OK));
unlink(link);
unlink(path);
unlink(stale);
assert(!rmdir(directory));
puts("PASS: interrupted/partial writes, write/sync/rename failures, atomic replacement, commit "
"durability, private permissions, symlinks and stale files");
}
+1 -1
View File
@@ -32,7 +32,7 @@ static void fixture(PlaybackJson *json,int kind){
close(server);assert(test_begin(&config,10000));test_tick(json,10010);assert(connection_test.stage==TEST_FAILED);
}
int main(void){
(void)target_save;(void)target_load;(void)target_connect;(void)test_stage_name;
(void)target_save;(void)target_load;(void)target_open;(void)test_stage_name;
PlaybackJson json={0};assert(playback_json_init(&json));
fixture(&json,0);fixture(&json,1);fixture(&json,2);
int peers[2];assert(socketpair(AF_UNIX,SOCK_STREAM,0,peers)==0);
+1 -1
View File
@@ -44,7 +44,7 @@ static void connected_start(void){
media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);assert(capture_source==SOURCE_PICCAP);
}
int main(void){
(void)target_save;(void)target_load;(void)target_connect;(void)handover_save;(void)handover_load;
(void)target_save;(void)target_load;(void)target_open;(void)handover_save;(void)handover_load;
(void)retry_failure;(void)retry_take;(void)retry_update;
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
handover_enabled=true;next_media_probe=UINT64_MAX;
+60
View File
@@ -0,0 +1,60 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <stdint.h>
#include "target.h"
#include "layout.h"
int main(void) {
(void)target_open;
(void)target_save;
(void)target_load;
(void)layout_from_json;
PlaybackJson json = {0};
assert(!playback_parse_exact(&json, "{}"));
assert(playback_json_init(&json));
const char *invalid[] = {"{",
"{\"success\":true",
"{}garbage",
"{} {}",
"[1,",
"{\"success\":tru}",
"{\"a\":\"unterminated}"};
for (unsigned i = 0; i < sizeof(invalid) / sizeof(*invalid); i++)
assert(!playback_parse_exact(&json, invalid[i]));
void *root = playback_parse_exact(&json, " {\"success\":true} \t\r");
assert(root && playback_true(&json, root, "success"));
json.put(root);
const char *wrong[] = {
"[]", "true", "1", "\"success\"", "{\"success\":1}", "{\"success\":\"true\"}"};
for (unsigned i = 0; i < sizeof(wrong) / sizeof(*wrong); i++) {
root = playback_parse_exact(&json, wrong[i]);
assert(!playback_true(&json, root, "success"));
if (root)
json.put(root);
}
TargetConfig original = target, next = original;
assert(!target_parse(
&json, "{\"host\":\"127.0.0.1\",\"receiver\":\"Hyperion\",\"port\":19444}garbage", &next));
assert(!memcmp(&next, &original, sizeof(next)));
Calibration profile = calibration;
assert(!calibration_parse(&json, "{\"profile\":\"average\"}{}", &profile));
assert(!memcmp(&profile, &calibration, sizeof(profile)));
/* Deterministic malformed-input mutations; every successful parse is freed.
* This is regression coverage, not a claim of exhaustive fuzzing. */
const char seed[] = "{\"success\":true,\"tan\":1,\"data\":{\"leds\":[0,255,128]}}";
uint32_t random = 0x12345678;
for (unsigned i = 0; i < 5000; i++) {
char text[sizeof(seed) + 4];
memcpy(text, seed, sizeof(seed));
random = random * 1664525u + 1013904223u;
unsigned position = random % (sizeof(seed) - 1);
text[position] = (char)(1 + (random >> 16) % 126);
if (i % 3 == 0)
text[position] = 0;
root = playback_parse_exact(&json, text);
if (root)
json.put(root);
}
dlclose(json.lib);
puts("PASS: exact JSON framing, truncated/trailing data, unexpected types, unchanged settings "
"on rejection and 5000 deterministic parser mutations");
}
+15 -15
View File
@@ -5,8 +5,8 @@
#include "target.h"
#include "layout.h"
#include "diagnostics.h"
static unsigned led_count;
static LedBlend *led_blends;
#include "runtime_context.h"
static ReceiverLayoutContext receiver_layout;
#include "layout_refresh.h"
static void completed(unsigned count,unsigned generation){
layout_probe.mapping=calloc(count,sizeof(LedBlend));assert(layout_probe.mapping);
@@ -14,22 +14,22 @@ static void completed(unsigned count,unsigned generation){
layout_probe.leds=count;layout_probe.stage=TEST_DONE;layout_probe_generation=generation;
}
int main(void){
(void)target_connect;(void)target_load;(void)target_save;
(void)target_open;(void)target_load;(void)target_save;
PlaybackJson json={0};assert(playback_json_init(&json));
calibration.kind=CAL_G4;next_layout_check=UINT64_MAX;
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true);
assert(led_count==50 && layout_updates==1 && last_layout_check==1000);
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true);
assert(led_count==500 && layout_updates==2 && last_layout_check==2000);
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true);assert(layout_updates==2);
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true,&receiver_layout);
assert(receiver_layout.count==50 && layout_updates==1 && last_layout_check==1000);
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true,&receiver_layout);
assert(receiver_layout.count==500 && layout_updates==2 && last_layout_check==2000);
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true,&receiver_layout);assert(layout_updates==2);
completed(500,calibration_generation);layout_probe.mapping[42].weight^=512;
layout_refresh_tick(&json,4000,true);assert(layout_updates==3);
LedBlend *previous=led_blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
layout_refresh_tick(&json,5000,true);assert(led_blends==previous && led_count==500 && layout_updates==3 && layout_error[0]);
layout_refresh_tick(&json,4000,true,&receiver_layout);assert(layout_updates==3);
LedBlend *previous=receiver_layout.blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
layout_refresh_tick(&json,5000,true,&receiver_layout);assert(receiver_layout.blends==previous && receiver_layout.count==500 && layout_updates==3 && layout_error[0]);
completed(50,calibration_generation);calibration_generation++;
layout_refresh_tick(&json,6000,true);assert(led_count==500 && led_blends==previous);assert(probe_busy(&layout_probe));
layout_refresh_tick(&json,6100,false);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
assert(led_count==500 && led_blends==previous);
layout_refresh_reset(7000);free(led_blends);test_close();
layout_refresh_tick(&json,6000,true,&receiver_layout);assert(receiver_layout.count==500 && receiver_layout.blends==previous);assert(probe_busy(&layout_probe));
layout_refresh_tick(&json,6100,false,&receiver_layout);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
assert(receiver_layout.count==500 && receiver_layout.blends==previous);
layout_refresh_reset(7000);free(receiver_layout.blends);test_close();
puts("PASS: automatic 50/500 LED replacement, same-count position changes, unchanged layout, invalid reply retention, obsolete profile result and cancellation");return 0;
}
+181
View File
@@ -0,0 +1,181 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <stdint.h>
#include <signal.h>
#include <sys/wait.h>
#include <time.h>
#include "target.h"
#include "layout.h"
#include "receiver_output.h"
static unsigned request_tan(const char *request) {
const char *p = strstr(request, "\"tan\":");
assert(p);
return (unsigned)strtoul(p + 6, NULL, 10);
}
static void ack(int fd, unsigned id, bool layout) {
char data[256];
int n = snprintf(data, sizeof(data),
layout ? "{\"success\":true,\"tan\":%u,\"info\":{\"leds\":[{\"hmin\":0,"
"\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n"
: "{\"success\":true,\"tan\":%u}\n",
id);
assert(send(fd, data, 7, MSG_NOSIGNAL) == 7);
usleep(1000);
assert(send(fd, data + 7, n - 7, MSG_NOSIGNAL) == n - 7);
}
static void fixture(PlaybackJson *json, int mode) {
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));
TargetConfig config = target;
config.port = ntohs(address.sin_port);
strcpy(config.token, "fixture");
pid_t child = fork();
assert(child >= 0);
if (!child) {
signal(SIGPIPE, SIG_IGN);
int fd = accept(server, NULL, NULL);
assert(fd >= 0);
FILE *input = fdopen(dup(fd), "r");
char line[1024];
assert(fgets(line, sizeof(line), input));
assert(strstr(line, "authorize"));
ack(fd, request_tan(line), false);
assert(fgets(line, sizeof(line), input) && strstr(line, "serverinfo"));
unsigned id = request_tan(line);
if (mode == 1) {
send(fd, "{\"success\":true", 15, MSG_NOSIGNAL);
goto done;
}
if (mode == 2) {
send(fd, "{} garbage\n", 11, MSG_NOSIGNAL);
goto done;
}
if (mode == 3) {
send(fd, "[]\n", 3, MSG_NOSIGNAL);
goto done;
}
if (mode == 4) {
char *huge = malloc(2 * 1024 * 1024 + 64);
assert(huge);
memset(huge, 'x', 2 * 1024 * 1024 + 64);
size_t sent = 0;
while (sent < 2 * 1024 * 1024 + 64) {
ssize_t n = send(fd, huge + sent, 2 * 1024 * 1024 + 64 - sent, MSG_NOSIGNAL);
if (n <= 0)
break;
sent += (size_t)n;
}
free(huge);
goto done;
}
if (mode == 6) {
const char bytes[] = "{}\0ignored\n";
send(fd, bytes, sizeof(bytes) - 1, MSG_NOSIGNAL);
goto done;
}
if (mode == 5) {
char bad[128];
int n = snprintf(bad, sizeof(bad), "{\"success\":\"true\",\"tan\":%u}\n", id);
send(fd, bad, n, MSG_NOSIGNAL);
goto done;
}
ack(fd, id, true);
assert(fgets(line, sizeof(line), input) && strstr(line, "[1,2,3]"));
id = request_tan(line);
/* Unrelated/stale acknowledgements must not release the in-flight frame. */
ack(fd, id + 99, false);
usleep(50000);
ack(fd, id, false);
assert(fgets(line, sizeof(line), input) && strstr(line, "[7,8,9]"));
ack(fd, request_tan(line), false);
assert(fgets(line, sizeof(line), input) && strstr(line, "clear"));
ack(fd, request_tan(line), false);
done:
usleep(20000);
fclose(input);
close(fd);
close(server);
_exit(0);
}
ReceiverOutput output = {.fd = -1};
assert(output_begin(&output, &config, 1000));
bool first = false, queued = false, stopping = false;
unsigned iterations = 0;
for (; iterations < 5000; iterations++) {
output_tick(&output, json, 1000 + iterations);
if (output.acknowledgements == 1 && queued && !stopping)
assert(output.awaiting && !output.latest);
if (output.stage == OUTPUT_FAILED || (stopping && output.stage == OUTPUT_IDLE))
break;
if (output.stage == OUTPUT_READY && !first) {
first = true;
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[1,2,3]}\n"));
} else if (first && output.awaiting && !queued) {
queued = true;
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[4,5,6]}\n"));
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[7,8,9]}\n"));
}
if (output.acknowledgements == 2 && !stopping) {
stopping = true;
output_stop(&output, 1000 + iterations, true);
}
usleep(1000);
}
assert(iterations < 5000);
if (mode)
assert(output.stage == OUTPUT_FAILED);
else
assert(first && queued && stopping && output.stage == OUTPUT_IDLE);
output_close(&output);
int status;
waitpid(child, &status, 0);
assert(WIFEXITED(status) && !WEXITSTATUS(status));
close(server);
}
int main(void) {
(void)target_open;
(void)target_parse;
(void)target_load;
(void)target_save;
PlaybackJson json = {0};
assert(playback_json_init(&json));
for (int mode = 0; mode <= 6; mode++)
fixture(&json, mode);
ReceiverOutput output = {.fd = -1};
int sockets[2];
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, sockets));
output.fd = sockets[0];
output.stage = OUTPUT_READY;
output.incoming = malloc(2 * 1024 * 1024 + 1);
assert(output.incoming);
assert(!fcntl(sockets[0], F_SETFL, O_NONBLOCK));
char block[4096] = {0};
while (send(sockets[0], block, sizeof(block), MSG_NOSIGNAL) > 0) {
}
assert(errno == EAGAIN || errno == EWOULDBLOCK);
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[0,0,0]}\n"));
output_tick(&output, &json, 1000);
assert(output.stage == OUTPUT_READY && output.request && output.sent == 0);
output_tick(&output, &json, 4000);
assert(output.stage == OUTPUT_FAILED);
close(sockets[1]);
output_close(&output);
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, sockets));
output.fd = sockets[0];
output.stage = OUTPUT_READY;
output.incoming = malloc(2 * 1024 * 1024 + 1);
assert(output.incoming);
close(sockets[1]);
assert(output_frame(&output, "{\"command\":\"clear\"}\n"));
output_tick(&output, &json, 5000);
assert(output.stage == OUTPUT_FAILED);
output_close(&output);
dlclose(json.lib);
puts("PASS: asynchronous login/layout, fragmented and unrelated replies, latest-frame "
"coalescing, clear/stop, truncation, invalid JSON/types, oversized responses, send "
"backpressure, timeout and disconnect");
}
+1 -1
View File
@@ -104,7 +104,7 @@ int main(void) {
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);
int fd = target_open(&copy);
if (fd >= 0)
close(fd);
dlclose(json.lib);
+162
View File
@@ -0,0 +1,162 @@
#define DEV_MODE "/tmp/hyperdrm-lifecycle-marker"
#define main service_entry_point
#include "service.c"
#undef main
#include <assert.h>
static unsigned initializes, finalizes;
static int fake_handle;
static char answer[8192];
static bool fake_reply(void *h, void *m, const char *s, LSError *e) {
(void)h;
(void)m;
(void)e;
snprintf(answer, sizeof(answer), "%s", s);
return true;
}
static const char *fake_payload(void *m) {
return m;
}
static bool fake_error_init(LSError *e) {
memset(e, 0, sizeof(*e));
return true;
}
static void fake_error_free(LSError *e) {
(void)e;
}
static bool fake_call(void *h, const char *u, const char *p, Callback cb, void *ctx,
unsigned long *t, LSError *e) {
(void)h;
(void)u;
(void)p;
(void)cb;
(void)ctx;
(void)t;
(void)e;
return false;
}
static bool fake_cancel(void *h, unsigned long t, LSError *e) {
(void)h;
(void)t;
(void)e;
return true;
}
static void *fake_init(bool (*p)(void)) {
(void)p;
initializes++;
return &fake_handle;
}
static int fake_preset(void *h, int p) {
assert(h == &fake_handle);
(void)p;
return 0;
}
static int fake_get(void *h, Histogram *hist) {
assert(h == &fake_handle);
for (unsigned r = 0; r < 16; r++)
for (unsigned c = 0; c < 3; c++)
hist[r].bins[c][80 + c * 20] = 100;
return 0;
}
static int fake_finish(void *h) {
assert(h == &fake_handle);
finalizes++;
return 0;
}
static unsigned request_id(const char *line) {
const char *p = strstr(line, "\"tan\":");
assert(p);
return strtoul(p + 6, NULL, 10);
}
static void success(int fd, unsigned id, bool layout) {
char data[256];
int n = snprintf(data, sizeof(data),
layout ? "{\"success\":true,\"tan\":%u,\"info\":{\"leds\":[{\"hmin\":0,"
"\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n"
: "{\"success\":true,\"tan\":%u}\n",
id);
assert(send(fd, data, n, MSG_NOSIGNAL) == n);
}
int main(void) {
assert(access(DEV_MODE, F_OK) != 0);
reply = fake_reply;
payload = fake_payload;
error_init = fake_error_init;
error_free = fake_error_free;
call_once = fake_call;
cancel_call = fake_cancel;
vt_init = fake_init;
vt_preset = fake_preset;
vt_get = fake_get;
vt_finish = fake_finish;
assert(playback_json_init(&playback_json));
next_layout_check = UINT64_MAX;
calibration.kind = CAL_G4;
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, 2));
socklen_t size = sizeof(address);
assert(!getsockname(server, (void *)&address, &size));
target.port = ntohs(address.sin_port);
assert(start((void *)1, "{}", NULL) && strstr(answer, "connecting"));
assert(receiver_runtime.starting && !capture_ctx.handle && !initializes);
assert(stop((void *)1, "{}", NULL));
assert(!receiver_runtime.starting && receiver_runtime.output.fd < 0 && !initializes);
int cancelled = accept(server, NULL, NULL);
assert(cancelled >= 0);
close(cancelled);
tick(NULL);
assert(!initializes && retries.paused);
pid_t child = fork();
assert(child >= 0);
if (!child) {
int fd = accept(server, NULL, NULL);
assert(fd >= 0);
FILE *input = fdopen(dup(fd), "r");
char line[2048];
assert(fgets(line, sizeof(line), input) && strstr(line, "serverinfo"));
success(fd, request_id(line), true);
assert(fgets(line, sizeof(line), input) && strstr(line, "color"));
unsigned id = request_id(line);
usleep(200000);
success(fd, id, false);
assert(fgets(line, sizeof(line), input) && strstr(line, "clear"));
success(fd, request_id(line), false);
usleep(20000);
fclose(input);
close(fd);
close(server);
_exit(0);
}
assert(start((void *)1, "{}", NULL));
for (unsigned i = 0; i < 1000 && !receiver_runtime.output.awaiting; i++) {
tick(NULL);
usleep(1000);
}
for (unsigned i = 0; i < 1000 && (!capture_ctx.handle || !receiver_runtime.output.awaiting);
i++) {
tick(NULL);
usleep(1000);
}
assert(capture_ctx.handle && initializes == 1 && receiver_runtime.output.awaiting);
uint64_t before = milliseconds();
assert(status((void *)1, "{}", NULL));
assert(milliseconds() - before < 100 && strstr(answer, "\"running\":true"));
before = milliseconds();
assert(stop((void *)1, "{}", NULL));
assert(milliseconds() - before < 100);
assert(!capture_ctx.handle && finalizes == 1 && retries.paused);
for (unsigned i = 0; i < 1000 && receiver_runtime.output.stage != OUTPUT_IDLE; i++) {
tick(NULL);
usleep(1000);
}
assert(receiver_runtime.output.stage == OUTPUT_IDLE && initializes == 1 && finalizes == 1);
int result;
waitpid(child, &result, 0);
assert(WIFEXITED(result) && !WEXITSTATUS(result));
assert(access(DEV_MODE, F_OK) != 0);
close(server);
dlclose(playback_json.lib);
puts("PASS: service Start/Stop during connect, delayed receiver ACK leaves Status/Stop "
"responsive, fake capture lifecycle, asynchronous clear and no restart after Stop");
}
+17 -16
View File
@@ -14,7 +14,8 @@ static CaptureSource capture_source;
static struct {bool paused,blocked;} retries;
static struct {char receiver[16];} target={"Hyperion"};
static struct {int stage;bool valid;uint64_t updated;unsigned sequence,count;LedBlend *mapping;unsigned char colors[16][3];} stream;
static Continuity continuity;
#include "runtime_context.h"
static CaptureContext capture_ctx;
static bool piccap_connected;
static char *queued;
static bool stream_queue(const char *line){if(queued)return false;queued=strdup(line);return queued!=NULL;}
@@ -23,21 +24,21 @@ static char *spatial_image_command_duration(const unsigned char colors[16][3],un
static void consume(void){free(queued);queued=NULL;}
int main(void){
LedBlend mapping={0,0,0};stream.mapping=&mapping;stream.count=1;stream.stage=4;
for(unsigned r=0;r<16;r++){continuity.last[r][0]=200;stream.colors[r][2]=200;}continuity.available=true;continuity.valid_at=1000;
capture_source=SOURCE_PICCAP_STARTING;bridge_tick(1000);assert(bridge_phase==1 && strstr(queued,"200,0,0"));consume();
capture_source=SOURCE_PICCAP;piccap_connected=true;bridge_tick(1200);assert(bridge_phase==2 && strstr(queued,"clear"));consume();
stream.valid=true;stream.sequence=1;stream.updated=1300;bridge_tick(1300);assert(bridge_phase==2);
stream.sequence=2;stream.updated=1400;bridge_tick(1400);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume();
bridge_tick(1650);assert(strstr(queued,"100,0,100"));consume();
bridge_tick(1900);assert(!bridge_phase && strstr(queued,"clear"));consume();
capture_source=SOURCE_PICCAP_STOPPING;stream.updated=2000;bridge_tick(2000);assert(continuity.fading && continuity.last[0][2]==200 && strstr(queued,"0,0,200"));consume();
capture_source=SOURCE_HISTOGRAM;bridge_tick(2100);assert(!queued);
capture_source=SOURCE_PICCAP_STARTING;continuity.valid_at=3000;bridge_tick(3000);consume();bridge_tick(6000);assert(!bridge_phase && strstr(queued,"clear"));consume();
capture_source=SOURCE_PICCAP_STARTING;bridge_source=SOURCE_HISTOGRAM;continuity.valid_at=7000;bridge_tick(7000);consume();retries.paused=true;bridge_tick(7100);assert(!bridge_phase && strstr(queued,"clear"));consume();
retries.paused=false;bridge_source=SOURCE_HISTOGRAM;continuity.valid_at=8000;capture_source=SOURCE_PICCAP_STARTING;bridge_tick(8000);consume();
capture_source=SOURCE_PICCAP;bridge_tick(8100);consume();stream.sequence++;stream.updated=8300;memset(stream.colors,0,sizeof(stream.colors));bridge_tick(8300);assert(!bridge_phase && !queued);
bridge_source=SOURCE_HISTOGRAM;capture_source=SOURCE_PICCAP_STARTING;continuity.valid_at=1000;bridge_tick(9000);assert(!bridge_phase && !queued);
stream.stage=1;bridge_phase=1;bridge_started=9000;bridge_tick(12000);assert(!queued);retries.paused=true;bridge_tick(12100);assert(!bridge_phase && !queued);
for(unsigned r=0;r<16;r++){capture_ctx.continuity.last[r][0]=200;stream.colors[r][2]=200;}capture_ctx.continuity.available=true;capture_ctx.continuity.valid_at=1000;
capture_source=SOURCE_PICCAP_STARTING;bridge_tick(1000,&capture_ctx);assert(bridge_phase==1 && strstr(queued,"200,0,0"));consume();
capture_source=SOURCE_PICCAP;piccap_connected=true;bridge_tick(1200,&capture_ctx);assert(bridge_phase==2 && strstr(queued,"clear"));consume();
stream.valid=true;stream.sequence=1;stream.updated=1300;bridge_tick(1300,&capture_ctx);assert(bridge_phase==2);
stream.sequence=2;stream.updated=1400;bridge_tick(1400,&capture_ctx);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume();
bridge_tick(1650,&capture_ctx);assert(strstr(queued,"100,0,100"));consume();
bridge_tick(1900,&capture_ctx);assert(!bridge_phase && strstr(queued,"clear"));consume();
capture_source=SOURCE_PICCAP_STOPPING;stream.updated=2000;bridge_tick(2000,&capture_ctx);assert(capture_ctx.continuity.fading && capture_ctx.continuity.last[0][2]==200 && strstr(queued,"0,0,200"));consume();
capture_source=SOURCE_HISTOGRAM;bridge_tick(2100,&capture_ctx);assert(!queued);
capture_source=SOURCE_PICCAP_STARTING;capture_ctx.continuity.valid_at=3000;bridge_tick(3000,&capture_ctx);consume();bridge_tick(6000,&capture_ctx);assert(!bridge_phase && strstr(queued,"clear"));consume();
capture_source=SOURCE_PICCAP_STARTING;bridge_source=SOURCE_HISTOGRAM;capture_ctx.continuity.valid_at=7000;bridge_tick(7000,&capture_ctx);consume();retries.paused=true;bridge_tick(7100,&capture_ctx);assert(!bridge_phase && strstr(queued,"clear"));consume();
retries.paused=false;bridge_source=SOURCE_HISTOGRAM;capture_ctx.continuity.valid_at=8000;capture_source=SOURCE_PICCAP_STARTING;bridge_tick(8000,&capture_ctx);consume();
capture_source=SOURCE_PICCAP;bridge_tick(8100,&capture_ctx);consume();stream.sequence++;stream.updated=8300;memset(stream.colors,0,sizeof(stream.colors));bridge_tick(8300,&capture_ctx);assert(!bridge_phase && !queued);
bridge_source=SOURCE_HISTOGRAM;capture_source=SOURCE_PICCAP_STARTING;capture_ctx.continuity.valid_at=1000;bridge_tick(9000,&capture_ctx);assert(!bridge_phase && !queued);
stream.stage=1;bridge_phase=1;bridge_started=9000;bridge_tick(12000,&capture_ctx);assert(!queued);retries.paused=true;bridge_tick(12100,&capture_ctx);assert(!bridge_phase && !queued);
strcpy(target.receiver,"HyperHDR");char *line=region_command(stream.colors,&mapping,1,500);assert(!strcmp(line,"image"));free(line);assert(!region_command(stream.colors,NULL,0,500));
puts("PASS: both source transitions, fresh sample gate, fade midpoint/end, bounded timeout, manual-stop cleanup");return 0;
}
+10 -3
View File
@@ -3,6 +3,13 @@
#include <sys/stat.h>
#include <string.h>
#include "target.h"
static int fixture_connect(const TargetConfig *target){
int fd=target_open(target);if(fd<0)return -1;
assert(fcntl(fd,F_GETFL,0)&O_NONBLOCK);
struct pollfd event={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
if(poll(&event,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size) || error){close(fd);return -1;}
return fd;
}
int main(void){
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
assert(target_valid(&t));assert(t.piccap_port==19400);assert(target_piccap_local(&t));
@@ -22,7 +29,7 @@ int main(void){
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
int client=target_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
assert(target_connect(&t)<0);
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, bounded TCP connect and refusal");return 0;
int client=fixture_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
assert(fixture_connect(&t)<0);
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, nonblocking socket and refused connection");return 0;
}
+1 -1
View File
@@ -1,4 +1,4 @@
#ifndef HYPERDRM_VERSION_H
#define HYPERDRM_VERSION_H
#define HYPERDRM_VERSION "0.12.0"
#define HYPERDRM_VERSION "0.13.0"
#endif