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
+1504 -459

No files matched your search

+5
View File
@@ -42,3 +42,8 @@ test-spatial_image
test-continuity test-continuity
test-receiver_stream test-receiver_stream
test-source_bridge test-source_bridge
test-atomic_file
test-json_protocol
test-receiver_output
test-service_lifecycle
+4 -4
View File
@@ -1,15 +1,15 @@
{ {
"id": "org.webosbrew.histogramhyperion", "id": "org.webosbrew.histogramhyperion",
"version": "0.12.0", "version": "0.13.0",
"type": "web", "type": "web",
"title": "HyperDRM", "title": "HyperDRM",
"appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR",
"iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png", "iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png",
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM",
"rootRequired": true, "rootRequired": true,
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.12.0_arm.ipk", "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.13.0_arm.ipk",
"ipkHash": { "ipkHash": {
"sha256": "b4fcc609d763b8e1a72d84484a5d454627416dde0ef51ef65ab705173e3c0810" "sha256": "a7ab184db8c948ceb4657f093c2cf10c13df3565ff9ed4d8482dcf2d59896a0a"
}, },
"ipkSize": 1204332 "ipkSize": 1207634
} }
Binary file not shown.
+25 -1
View File
@@ -27,7 +27,7 @@ Only the own service is modified. Optional Auto PicCap can configure its receive
## Installation ## Installation
The current package is [HyperDRM_0.12.0_arm.ipk](HyperDRM_0.12.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. The current package is [HyperDRM_0.13.0_arm.ipk](HyperDRM_0.13.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
Elevate the installed own service through Homebrew Channel: Elevate the installed own service through Homebrew Channel:
@@ -209,3 +209,27 @@ An existing tag or a branch other than main is rejected. Attach the exact verifi
IPK to the matching Gitea release using an authorized session, then compare the IPK to the matching Gitea release using an authorized session, then compare the
downloaded attachment's size and SHA-256. Git access does not authorize web uploads. downloaded attachment's size and SHA-256. Git access does not authorize web uploads.
See [runtime architecture](docs/architecture.md) for module boundaries. See [runtime architecture](docs/architecture.md) for module boundaries.
### Receiver resilience and updates
The main receiver socket stays nonblocking during connect, authentication, layout
loading, color output and acknowledgements. Only one command is in flight and the
newest unsent frame replaces older unsent frames. Timed-out or invalid replies
close the connection and use the existing retry policy. Start returns a Connecting
state; Stop cancels startup and sends a bounded asynchronous clear when needed.
LG capture initialization itself remains dependent on the vendor library.
Receiver, profile and PicCap preferences share an atomic writer with private
permissions, interrupted/partial-write handling, file sync, rename and directory
sync. Failures before rename preserve the previous preference. A directory-sync
failure after rename is reported as committed with uncertain crash durability;
it is not represented as a rollback.
The UI compares its generated version against the running service. Mismatches
block settings and startup actions while keeping Stop and status available. After
installing the matching package, Reload service explicitly restarts only HyperDRM
and briefly interrupts ambient lighting; it never runs automatically.
`python release.py --sanitizers` additionally checks protocol, atomic-file,
receiver-output and simulated service-lifecycle tests with ASan/UBSan. These host
tests do not replace real LG capture and remote-control validation on a TV.
+1 -1
View File
@@ -1 +1 @@
0.12.0 0.13.0
+48 -2
View File
@@ -1,6 +1,26 @@
'use strict'; 'use strict';
var pending = []; var pending = [];
var actionError = ''; var actionError = '';
var versionCompatible = typeof HYPERDRM_APP_VERSION === 'undefined';
function checkServiceVersion(result) {
if (typeof HYPERDRM_APP_VERSION === 'undefined' || result.returnValue === false) return;
versionCompatible = result.version === HYPERDRM_APP_VERSION;
var warning = document.getElementById('versionWarning');
warning.style.display = versionCompatible ? 'none' : 'block';
document.getElementById('reloadService').style.display = versionCompatible ? 'none' : 'inline-block';
warning.textContent = versionCompatible ? '' : 'App ' + HYPERDRM_APP_VERSION + ' / service ' + (result.version || 'unknown') + '. The installed service does not match this app. Finish the update, then select Reload service before changing settings. Reloading briefly stops ambient lighting.';
['start', 'autostart', 'handover', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'saveProfile'].forEach(function(id) {
var button = document.getElementById(id);
if (!versionCompatible) {
button.versionBlocked = true;
button.disabled = true;
} else if (button.versionBlocked) {
button.versionBlocked = false;
button.disabled = id === 'handover' ? !result.piccapInstalled : id === 'testConnection' && result.connectionTest && ['Connecting', 'Sending request', 'Checking authentication', 'Reading LED layout'].indexOf(result.connectionTest.state) >= 0;
}
});
}
function showActionError(message) { function showActionError(message) {
actionError = message || ''; actionError = message || '';
@@ -32,6 +52,13 @@ function showProfile() {
} }
function call(method, done, data) { function call(method, done, data) {
if (!versionCompatible && ['start', 'autostart', 'handover', 'settings', 'testConnection', 'calibration', 'calibrationSession', 'calibrationDraft'].indexOf(method) >= 0 && !(method === 'calibrationSession' && data && data.enabled === false)) {
done({
returnValue: false,
errorText: 'App and service versions do not match. Finish the update first.'
});
return;
}
if (['start', 'stop', 'autostart', 'handover', 'settings', 'testConnection', 'calibration'].indexOf(method) >= 0) clearActionError(); if (['start', 'stop', 'autostart', 'handover', 'settings', 'testConnection', 'calibration'].indexOf(method) >= 0) clearActionError();
callUri('luna://org.webosbrew.histogramhyperion.service/' + method, function(result) { callUri('luna://org.webosbrew.histogramhyperion.service/' + method, function(result) {
if (method !== 'status' && result.returnValue === false) showActionError(result.errorText || 'The action failed.'); if (method !== 'status' && result.returnValue === false) showActionError(result.errorText || 'The action failed.');
@@ -117,10 +144,11 @@ function display(result) {
toggle.textContent = 'Autostart: ' + (result.autostart ? 'On' : 'Off'); toggle.textContent = 'Autostart: ' + (result.autostart ? 'On' : 'Off');
} }
document.getElementById('error').textContent = actionError || result.errorText || result.error || ''; document.getElementById('error').textContent = actionError || result.errorText || result.error || '';
document.getElementById('status').textContent = result.returnValue === false ? 'Service unavailable' : (result.running ? 'Running' : 'Stopped') + ' · ' + (result.frames || 0) + ' acknowledged color updates'; document.getElementById('status').textContent = result.returnValue === false ? 'Service unavailable' : (result.receiverConnecting ? 'Connecting to receiver' : result.running ? 'Running' : 'Stopped') + ' · ' + (result.frames || 0) + ' acknowledged color updates';
if (result.activeSource === 'PicCap') { if (result.activeSource === 'PicCap') {
document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver'); document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver');
document.getElementById('regions').textContent = ''; document.getElementById('regions').textContent = '';
checkServiceVersion(result);
return; return;
} }
if (result.piccapHandover) document.getElementById('status').textContent += ' · HyperDRM · ' + result.playbackProtection; if (result.piccapHandover) document.getElementById('status').textContent += ' · HyperDRM · ' + result.playbackProtection;
@@ -143,7 +171,25 @@ function display(result) {
e.title = 'Histogram ' + index; e.title = 'Histogram ' + index;
box.appendChild(e); box.appendChild(e);
}); });
checkServiceVersion(result);
} }
document.getElementById('reloadService').onclick = function() {
var button = this;
button.disabled = true;
clearActionError();
call('restartService', function(result) {
if (result.returnValue === false) {
button.disabled = false;
showActionError(result.errorText || 'Reinstall the current package, then restart its service.');
return;
}
setTimeout(function() {
button.disabled = false;
call('status', display);
}, 1500);
});
};
document.getElementById('start').onclick = function() { document.getElementById('start').onclick = function() {
call('start', function(r) { call('start', function(r) {
if (r.returnValue === false) display(r); if (r.returnValue === false) display(r);
@@ -347,6 +393,6 @@ document.getElementById('editProfile').onclick = function() {
showProfile(); showProfile();
if (advancedProfile) document.getElementById('customProfileField').focus(); if (advancedProfile) document.getElementById('customProfileField').focus();
}; };
setupRemoteNavigation(['overviewTab', 'receiverTab', 'calibrationTab', 'grantRoot', 'start', 'stop', 'refresh', 'autostart', 'handover', 'calibrationShortcut', 'receiver', 'hostField', 'portField', 'piccapPortField', 'tokenField', 'clearToken', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'profile', 'saveProfile', 'editProfile', 'customProfileField', 'cancelCalibration'].map(function(id) { setupRemoteNavigation(['overviewTab', 'receiverTab', 'calibrationTab', 'reloadService', 'grantRoot', 'start', 'stop', 'refresh', 'autostart', 'handover', 'calibrationShortcut', 'receiver', 'hostField', 'portField', 'piccapPortField', 'tokenField', 'clearToken', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'profile', 'saveProfile', 'editProfile', 'customProfileField', 'cancelCalibration'].map(function(id) {
return document.getElementById(id); return document.getElementById(id);
}), document, window); }), document, window);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "org.webosbrew.histogramhyperion", "id": "org.webosbrew.histogramhyperion",
"version": "0.12.0", "version": "0.13.0",
"vendor": "Local development", "vendor": "Local development",
"type": "web", "type": "web",
"main": "index.html", "main": "index.html",
+2 -1
View File
@@ -21,6 +21,7 @@ nav{border-bottom:1px solid #456185;padding-bottom:10px;margin-bottom:18px}nav b
<h1><img src="logo.png" alt="" width="76" height="76">HyperDRM</h1> <h1><img src="logo.png" alt="" width="76" height="76">HyperDRM</h1>
<nav aria-label="App sections"><button id="overviewTab" aria-pressed="true">Overview</button><button id="receiverTab" aria-pressed="false">Receiver</button><button id="calibrationTab" aria-pressed="false">Calibration</button></nav> <nav aria-label="App sections"><button id="overviewTab" aria-pressed="true">Overview</button><button id="receiverTab" aria-pressed="false">Receiver</button><button id="calibrationTab" aria-pressed="false">Calibration</button></nav>
<p id="status" role="status">Checking service &hellip;</p><p id="error" role="alert"></p> <p id="status" role="status">Checking service &hellip;</p><p id="error" role="alert"></p>
<p id="versionWarning" role="alert" style="display:none"></p><button id="reloadService" style="display:none">Reload service</button>
<section id="overviewPanel" aria-label="Overview"> <section id="overviewPanel" aria-label="Overview">
<button id="grantRoot" class="primary" style="display:none">Enable Root Access</button><p id="rootHint" style="display:none"><small>Homebrew Channel grants root to the HyperDRM service. The service then restarts with its updated permissions.</small></p> <button id="grantRoot" class="primary" style="display:none">Enable Root Access</button><p id="rootHint" style="display:none"><small>Homebrew Channel grants root to the HyperDRM service. The service then restarts with its updated permissions.</small></p>
<div role="group" aria-label="Service controls"><button id="start" class="primary">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button></div> <div role="group" aria-label="Service controls"><button id="start" class="primary">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button></div>
@@ -55,4 +56,4 @@ nav{border-bottom:1px solid #456185;padding-bottom:10px;margin-bottom:18px}nav b
<div id="customEditor" style="display:none"><div id="customProfileField" class="field wide" tabindex="0" role="button" aria-label="Custom profile JSON, press OK to edit"><label>Custom model profile JSON<textarea id="customProfile" tabindex="-1" readonly rows="4" maxlength="2048" disabled spellcheck="false"></textarea></label></div></div> <div id="customEditor" style="display:none"><div id="customProfileField" class="field wide" tabindex="0" role="button" aria-label="Custom profile JSON, press OK to edit"><label>Custom model profile JSON<textarea id="customProfile" tabindex="-1" readonly rows="4" maxlength="2048" disabled spellcheck="false"></textarea></label></div></div>
</fieldset></section> </fieldset></section>
<button id="cancelCalibration" disabled>Cancel calibration</button><video id="calibrationVideo" src="calibration.mp4" muted preload="auto" playsinline></video> <button id="cancelCalibration" disabled>Cancel calibration</button><video id="calibrationVideo" src="calibration.mp4" muted preload="auto" playsinline></video>
<script src="remote.js"></script><script src="calibration.js"></script><script src="navigation.js"></script><script src="app.js"></script><script src="wizard.js"></script></html> <script src="version.js"></script><script src="remote.js"></script><script src="calibration.js"></script><script src="navigation.js"></script><script src="app.js"></script><script src="wizard.js"></script></html>
+2
View File
@@ -0,0 +1,2 @@
'use strict';
var HYPERDRM_APP_VERSION = '0.13.0';
+4
View File
@@ -89,6 +89,10 @@ function wizardSample() {
} }
function wizardStart(validation) { function wizardStart(validation) {
if (typeof versionCompatible !== 'undefined' && !versionCompatible) {
document.getElementById('wizardStatus').textContent = 'Finish the app/service update before calibration.';
return;
}
if (wizard) return; if (wizard) return;
if (typeof showPanel === 'function') showPanel('calibration', false); if (typeof showPanel === 'function') showPanel('calibration', false);
if (targetDirty || profileDirty) { if (targetDirty || profileDirty) {
+25
View File
@@ -12,6 +12,9 @@ Formatting must preserve include order (`SortIncludes: Never`).
| service_calibration.h | Bounded calibration lease, proposals, profiles, model discovery | | service_calibration.h | Bounded calibration lease, proposals, profiles, model discovery |
| service_status.h | Read-only status serialization | | service_status.h | Read-only status serialization |
| capture_loop.h | Scheduler, histogram acquisition, missing-data recovery and output | | capture_loop.h | Scheduler, histogram acquisition, missing-data recovery and output |
| receiver_output.h | Main asynchronous connection, transaction matching and frame coalescing |
| atomic_file.h | Durable preference replacement and explicit commit outcomes |
| service_context.h / runtime_context.h | Capture, receiver/layout and calibration session ownership |
| receiver_stream.h | Independent, nonblocking receiver LED subscription | | receiver_stream.h | Independent, nonblocking receiver LED subscription |
| source_bridge.h | Bounded color continuity during source transitions | | source_bridge.h | Bounded color continuity during source transitions |
| handover.h | Exclusive PicCap/histogram capture ownership | | handover.h | Exclusive PicCap/histogram capture ownership |
@@ -44,3 +47,25 @@ VERSION generates app/package metadata and native/version.h. release.py tests
portable logic locally, builds the ARM service and verifies the exact package. portable logic locally, builds the ARM service and verifies the exact package.
Hardware capture and Magic Remote behavior still require a later TV check; a host Hardware capture and Magic Remote behavior still require a later TV check; a host
test is not evidence of LG hardware behavior. test is not evidence of LG hardware behavior.
The main receiver owns a ReceiverOutput context passed explicitly to begin, tick,
frame, stop and close. Its lifecycle is Idle -> Connecting -> Login (optional) ->
Layout -> Ready, with Failed on transport/protocol errors. Requests use a positive
32-bit `tan`, echoed by the receiver, to avoid acknowledging the wrong command.
Each tick has a 64 KiB send budget and a 64 KiB read budget; buffered input has an
eight-message budget and 2 MiB limit. An ACK can send the newest queued frame in
the same tick without resetting that byte budget. There is one pending command
and one replaceable frame, not an unbounded queue. Transaction deadlines are three
seconds. Shutdown and cancellation release all owned buffers and sockets.
The source bridge receives CaptureContext explicitly; layout refresh receives
ReceiverLayoutContext explicitly. Capture data, continuity, recovery counters and
LG handles are grouped separately from receiver transport and calibration session
lease/model-query/proposal fields. LG/Luna symbols, persisted calibration and
handover ownership still have shared service scope; this refactor reduces globals
rather than claiming complete isolation.
Transaction echo behavior is defined by the official implementations:
[Hyperion 2.0.16 JsonAPI](https://github.com/hyperion-project/hyperion.ng/blob/2.0.16/libsrc/api/JsonAPI.cpp)
and [HyperHDR HyperAPI](https://github.com/awawa-dev/HyperHDR/blob/master/sources/api/HyperAPI.cpp).
This code check does not replace a deployment test against the user's receiver.
+33
View File
@@ -157,3 +157,36 @@ checks the IPK's embedded metadata, executable architecture, size and SHA-256.
Package timestamps are deterministic. No TV installation, application launch or Package timestamps are deterministic. No TV installation, application launch or
service restart was performed for this version while the user was watching a film. service restart was performed for this version while the user was watching a film.
Live LG capture and physical remote validation remain pending for this release. Live LG capture and physical remote validation remain pending for this release.
## Version 0.13.0
The main receiver connection uses nonblocking sockets for connect, login, layout,
color commands, matching transaction acknowledgements and clear. The synchronous
connector and send/receive loops were removed from production. Unsent frames
coalesce and per-tick byte/message budgets and transaction deadlines bound work.
Stop cancels an in-progress connection and clears pending output asynchronously.
LG capture initialization itself still depends on synchronous vendor APIs.
Receiver, calibration and PicCap preferences share a private atomic writer with
unique temporary files, partial/EINTR write handling, file and directory sync,
rename, symlink rejection and explicit post-commit durability outcomes. Failure
before rename preserves the previous file; failure syncing the directory after
commit is logged as uncertain durability rather than incorrectly claiming rollback.
Protocol tests cover fragmented, truncated, malformed, oversized and embedded-NUL
responses, invalid types, stale transaction IDs, send backpressure, deadlines and
disconnects. A deterministic 5000-input parser mutation test supplements the fixed
cases. ASan/UBSan checks include protocol, file replacement, output and a simulated
full service lifecycle. The latter verifies Start/Stop during connect and responsive
Status/Stop while an ACK is delayed, with a fake LG capture implementation.
Capture, receiver/layout and calibration session state have explicit contexts;
bridge and layout-refresh functions receive their contexts directly. Shared LG/Luna
and persisted calibration/handover state remain scoped to the service. Generated
UI and native versions must match; settings/startup actions are blocked otherwise,
while Stop and status remain available. Reload service is an explicit user action,
never an automatic restart.
All host checks and the ARM package verification pass. No TV connection, install,
launch or service restart was performed. Real LG capture, flash-filesystem behavior,
receiver interoperability and physical remote validation remain pending on the TV.
+12
View File
@@ -0,0 +1,12 @@
# HyperDRM 0.13.0
- Keep receiver connect/auth/layout/color/clear operations nonblocking and coalesce unsent frames.
- Match transaction acknowledgements and bound work, buffers and deadlines.
- Share atomic preference persistence with interrupted-write handling and explicit commit outcomes.
- Add malformed-input, backpressure, disconnect, parser-mutation and simulated lifecycle tests.
- Group runtime state in capture, receiver/layout and calibration contexts.
- Detect app/service version mismatches and provide an explicit Reload service action.
Validation: host JavaScript, native and Python tests pass, including ASan/UBSan
checks. The ARM IPK's embedded versions, architecture, size and SHA-256 match its
manifest. This version has not been installed or validated on the TV.
+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 #define HYPERDRM_CALIBRATION_H
#include <math.h> #include <math.h>
#include "playback.h" #include "playback.h"
#include "atomic_file.h"
#define CALIBRATION_PREF \ #define CALIBRATION_PREF \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \ "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
"calibration.json" "calibration.json"
@@ -52,7 +53,7 @@ static inline CalibrationKind calibration_effective(void) {
return calibration_is_g4(tv_model) ? CAL_G4 : CAL_AVERAGE; return calibration_is_g4(tv_model) ? CAL_G4 : CAL_AVERAGE;
} }
static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibration *result) { 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) if (!root)
return false; return false;
Calibration next = *result; Calibration next = *result;
@@ -102,7 +103,17 @@ static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibrat
return valid; return valid;
} }
static inline bool calibration_save(const char *path, const Calibration *value) { 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)); int used = snprintf(data, sizeof(data), "{\"profile\":\"%s\"", calibration_key(value->kind));
if (value->count) { if (value->count) {
used += snprintf(data + used, sizeof(data) - used, ",\"model\":\"%s\",\"points\":[", 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, "]");
} }
used += snprintf(data + used, sizeof(data) - used, "}\n"); used += snprintf(data + used, sizeof(data) - used, "}\n");
if (snprintf(temp, sizeof(temp), "%s.tmp", path) >= (int)sizeof(temp)) return used > 0 && (size_t)used < sizeof(data) &&
return false; atomic_replace(path, data, (size_t)used) != SAVE_UNCHANGED;
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) { static inline void calibration_load(PlaybackJson *j, const char *path) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+91 -63
View File
@@ -2,80 +2,115 @@
static int tick(void *unused) { static int tick(void *unused) {
(void)unused; (void)unused;
uint64_t now = milliseconds(); 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); model_tick(now);
test_tick(&playback_json, 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); stream_tick(&playback_json, now, handover_enabled && !retries.paused && !retries.blocked);
bool owned = handover_tick(); bool owned = handover_tick();
bridge_tick(now); bridge_tick(now, &capture_ctx);
if (owned) if (owned)
return 1; 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()); retry_update(&retries, boot_enabled() && geteuid() == 0, false, milliseconds());
if (retry_take(&retries, milliseconds())) { if (retry_take(&retries, milliseconds())) {
start(NULL, NULL, NULL); start(NULL, NULL, NULL);
if (!capture) if (!capture_ctx.handle && !receiver_runtime.starting)
retry_failure(&retries, milliseconds()); retry_failure(&retries, milliseconds());
} }
return 1; return 1;
} }
if (revoked) { if (capture_ctx.revoked) {
set_error("LG revoked capture; recovering after source transition"); set_error("LG revoked capture; recovering after source transition");
capture_recoveries++; capture_ctx.recoveries++;
recovery_started = milliseconds(); capture_ctx.recovery_started = milliseconds();
release_capture(false); release_capture(false);
retry_source_transition(&retries, milliseconds()); retry_source_transition(&retries, milliseconds());
return 1; return 1;
} }
if (settling) { if (capture_ctx.settling) {
settling--; capture_ctx.settling--;
return 1; return 1;
} }
if (!CALIBRATION_ACTIVE && letterbox_mode && capture_preset == 1 && if (!CALIBRATION_ACTIVE && capture_ctx.letterbox_mode && capture_ctx.preset == 1 &&
milliseconds() >= next_border_probe) { milliseconds() >= capture_ctx.next_border_probe) {
if (vt_preset(capture, 0) != 0) { if (vt_preset(capture_ctx.handle, 0) != 0) {
set_error("Border probe preset failed"); set_error("Border probe preset failed");
stop_capture(); stop_capture();
return 1; return 1;
} }
capture_preset = 0; capture_ctx.preset = 0;
settling = 2; capture_ctx.settling = 2;
return 1; return 1;
} }
Histogram hist[16] = {0}; Histogram hist[16] = {0};
int rc = vt_get(capture, hist); int rc = vt_get(capture_ctx.handle, hist);
bool populated = !rc && histogram_populated(hist); bool populated = !rc && histogram_populated(hist);
if (!populated) { if (!populated) {
unsigned char held[16][3]; unsigned char held[16][3];
color_valid = false; capture_ctx.color_valid = false;
if (!continuity_frame(&continuity, false, rgb, now, held)) { if (!continuity_frame(&capture_ctx.continuity, false, capture_ctx.rgb, now, held)) {
set_error("Histogram data unavailable beyond hold window; recovering capture"); set_error("Histogram data unavailable beyond hold window; recovering capture");
release_capture(false); release_capture(false);
retry_source_transition(&retries, now); retry_source_transition(&retries, now);
return 1; return 1;
} }
char *line = region_command(held, led_blends, led_count, char *line = region_command(held, receiver_layout.blends, receiver_layout.count,
(unsigned)(1500 - (now - continuity.missing_at))); (unsigned)(1500 - (now - capture_ctx.continuity.missing_at)));
if (!line || send_line(line)) { if (!line || (!output_frame(&receiver_runtime.output, line))) {
free(line); free(line);
set_error("Receiver connection lost while holding colors"); set_error("Receiver connection lost while holding colors");
stop_capture(); stop_capture();
return 1; return 1;
} }
free(line); free(line);
if (!receiver_ack()) {
set_error("Receiver did not acknowledge held colors");
stop_capture();
return 1; return 1;
} }
last_output_at = now; capture_ctx.sample_at = now;
return 1; capture_ctx.sample_sequence++;
}
sample_at = now;
sample_sequence++;
unsigned aggregate[3] = {0}; unsigned aggregate[3] = {0};
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
black_percent[i] = 100; capture_ctx.black_percent[i] = 100;
for (int c = 0; c < 3; c++) { for (int c = 0; c < 3; c++) {
uint64_t total = 0, black = 0; uint64_t total = 0, black = 0;
for (int b = 0; b < 256; b++) { for (int b = 0; b < 256; b++) {
@@ -84,67 +119,60 @@ static int tick(void *unused) {
black += hist[i].bins[c][b]; black += hist[i].bins[c][b];
} }
unsigned fraction = total ? (unsigned)(100 * black / total) : 0; unsigned fraction = total ? (unsigned)(100 * black / total) : 0;
if (fraction < black_percent[i]) if (fraction < capture_ctx.black_percent[i])
black_percent[i] = fraction; capture_ctx.black_percent[i] = fraction;
} }
} }
for (int i = 0; i < 16; i++) for (int i = 0; i < 16; i++)
for (int c = 0; c < 3; c++) { for (int c = 0; c < 3; c++) {
rgb[i][c] = histogram_mean(hist[i].bins[c]); capture_ctx.rgb[i][c] = histogram_mean(hist[i].bins[c]);
aggregate[c] += rgb[i][c]; aggregate[c] += capture_ctx.rgb[i][c];
} }
if (!CALIBRATION_ACTIVE && capture_preset == 0) { if (!CALIBRATION_ACTIVE && capture_ctx.preset == 0) {
int candidate = letterbox_candidate(black_percent, rgb); int candidate = letterbox_candidate(capture_ctx.black_percent, capture_ctx.rgb);
uint64_t now = milliseconds(); uint64_t now = milliseconds();
if (candidate < 0) if (candidate < 0)
pending_letterbox = -1; capture_ctx.pending_letterbox = -1;
else if (candidate != pending_letterbox) { else if (candidate != capture_ctx.pending_letterbox) {
pending_letterbox = candidate; capture_ctx.pending_letterbox = candidate;
candidate_since = now; capture_ctx.candidate_since = now;
} else if ((unsigned)candidate != letterbox_mode && now - candidate_since >= 2000) } else if ((unsigned)candidate != capture_ctx.letterbox_mode &&
letterbox_mode = (unsigned)candidate; now - capture_ctx.candidate_since >= 2000)
if (letterbox_mode) { capture_ctx.letterbox_mode = (unsigned)candidate;
if (vt_preset(capture, 1) != 0) { if (capture_ctx.letterbox_mode) {
if (vt_preset(capture_ctx.handle, 1) != 0) {
set_error("Letterbox preset failed"); set_error("Letterbox preset failed");
stop_capture(); stop_capture();
return 1; return 1;
} }
capture_preset = 1; capture_ctx.preset = 1;
settling = 2; capture_ctx.settling = 2;
next_border_probe = now + 2000; capture_ctx.next_border_probe = now + 2000;
return 1; return 1;
} }
} }
for (int c = 0; c < 3; c++) 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]; unsigned char boosted_mean[3];
boost_color(sent_rgb, boosted_mean); boost_color(capture_ctx.sent_rgb, boosted_mean);
memcpy(sent_rgb, boosted_mean, 3); memcpy(capture_ctx.sent_rgb, boosted_mean, 3);
unsigned char mapped_rgb[16][3], stable_rgb[16][3]; unsigned char mapped_rgb[16][3], stable_rgb[16][3];
calibration_colors(rgb, mapped_rgb); calibration_colors(capture_ctx.rgb, mapped_rgb);
continuity_frame(&continuity, true, mapped_rgb, now, stable_rgb); continuity_frame(&capture_ctx.continuity, true, mapped_rgb, now, stable_rgb);
interpolation_enabled = led_count && calibration_effective() != CAL_AVERAGE; receiver_layout.interpolation = receiver_layout.count && calibration_effective() != CAL_AVERAGE;
char *line = region_command(stable_rgb, led_blends, led_count, 1500); char *line = region_command(stable_rgb, receiver_layout.blends, receiver_layout.count, 1500);
if (!line) { if (!line) {
set_error("Color buffer allocation failed"); set_error("Color buffer allocation failed");
stop_capture(); stop_capture();
return 1; return 1;
} }
int failed = send_line(line); int failed = (!output_frame(&receiver_runtime.output, line));
free(line); free(line);
if (failed) { if (failed) {
set_error("Receiver connection lost"); set_error("Receiver connection lost");
stop_capture(); stop_capture();
return 1; return 1;
} }
if (!receiver_ack()) { capture_ctx.color_valid = true;
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; 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); probe_finish(probe, "Receiver closed the test connection", now);
return; 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->used += (size_t)n;
probe->response[probe->used] = 0; probe->response[probe->used] = 0;
char *end = strchr(probe->response, '\n'); char *end = strchr(probe->response, '\n');
if (!end) if (!end)
return; return;
*end = 0; *end = 0;
void *root = json->parse(probe->response); void *root = playback_parse_exact(json, probe->response);
bool success = playback_true(json, root, "success"); bool success = playback_true(json, root, "success");
if (root) if (root)
json->put(root); json->put(root);
+2 -12
View File
@@ -1,6 +1,7 @@
#ifndef HYPERDRM_HANDOVER_H #ifndef HYPERDRM_HANDOVER_H
#define HYPERDRM_HANDOVER_H #define HYPERDRM_HANDOVER_H
#include "playback.h" #include "playback.h"
#include "atomic_file.h"
#ifndef CALIBRATION_ACTIVE #ifndef CALIBRATION_ACTIVE
#define CALIBRATION_ACTIVE false #define CALIBRATION_ACTIVE false
#endif #endif
@@ -43,18 +44,7 @@ static inline bool piccap_installed(void) {
} }
static bool handover_save(bool enabled) { static bool handover_save(bool enabled) {
const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n"; 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); return atomic_replace(HANDOVER_PREF, data, strlen(data)) != SAVE_UNCHANGED;
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 void handover_load(void) { static void handover_load(void) {
int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); 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; layout_probe.stage = TEST_IDLE;
next_layout_check = due; next_layout_check = due;
} }
static bool layout_mapping_changed(const LedBlend *next, unsigned count) { static bool layout_mapping_changed(const ReceiverLayoutContext *layout, const LedBlend *next,
if (count != led_count || !led_blends) unsigned count) {
if (count != layout->count || !layout->blends)
return true; return true;
for (unsigned i = 0; i < count; i++) for (unsigned i = 0; i < count; i++)
if (next[i].a != led_blends[i].a || next[i].b != led_blends[i].b || if (next[i].a != layout->blends[i].a || next[i].b != layout->blends[i].b ||
next[i].weight != led_blends[i].weight) next[i].weight != layout->blends[i].weight)
return true; return true;
return false; 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) { if (!running) {
layout_refresh_reset(now + LAYOUT_REFRESH_MS); layout_refresh_reset(now + LAYOUT_REFRESH_MS);
return; return;
@@ -27,11 +29,11 @@ static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running)
probe_tick(&layout_probe, json, now); probe_tick(&layout_probe, json, now);
if (layout_probe.stage == TEST_DONE) { if (layout_probe.stage == TEST_DONE) {
if (layout_probe_generation == calibration_generation) { if (layout_probe_generation == calibration_generation) {
if (layout_mapping_changed(layout_probe.mapping, layout_probe.leds)) { if (layout_mapping_changed(layout, layout_probe.mapping, layout_probe.leds)) {
free(led_blends); free(layout->blends);
led_blends = layout_probe.mapping; layout->blends = layout_probe.mapping;
layout_probe.mapping = NULL; layout_probe.mapping = NULL;
led_count = layout_probe.leds; layout->count = layout_probe.leds;
layout_updates++; layout_updates++;
} }
last_layout_check = now; last_layout_check = now;
+38 -1
View File
@@ -14,6 +14,11 @@ typedef struct {
const char *(*string)(const void *); const char *(*string)(const void *);
int (*boolean)(const void *); int (*boolean)(const void *);
int (*put)(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; } PlaybackJson;
static inline bool playback_json_init(PlaybackJson *j) { static inline bool playback_json_init(PlaybackJson *j) {
j->lib = dlopen("libjson-c.so.5", RTLD_NOW); 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->string = dlsym(j->lib, "json_object_get_string");
j->boolean = dlsym(j->lib, "json_object_get_boolean"); j->boolean = dlsym(j->lib, "json_object_get_boolean");
j->put = dlsym(j->lib, "json_object_put"); 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 || 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); dlclose(j->lib);
j->lib = NULL; j->lib = NULL;
return false; return false;
} }
return true; 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) { static inline void *playback_field(PlaybackJson *j, void *o, const char *key) {
void *v = NULL; void *v = NULL;
return o && j->type(o) == 4 && j->get(o, key, &v) ? v : NULL; return o && j->type(o) == 4 && j->get(o, key, &v) ? v : NULL;
+56 -23
View File
@@ -7,28 +7,61 @@
#include <stdarg.h> #include <stdarg.h>
#include "histogram.h" #include "histogram.h"
/* Process-local diagnostic sink. It changes logging only, not capture policy. */ /* Process-local diagnostic sink. It changes logging only, not capture policy. */
void printLog(const char *context,int level,const char *message_id, void printLog(const char *context, int level, const char *message_id, const char *file,
const char *file,const char *function,int line,const char *format,...) { const char *function, int line, const char *format, ...) {
(void)context;(void)level;(void)message_id;(void)file; (void)context;
fprintf(stderr,"LG %s:%d: ",function?function:"?",line); (void)level;
if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);}fputc('\n',stderr); (void)message_id;
} (void)file;
static bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; } fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
int main(void) { if (format) {
void *lib=dlopen("libvtcapture.so.1",RTLD_NOW); va_list args;
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;} va_start(args, format);
void *(*init)(bool(*)(void))=dlsym(lib,"vtCapture_initHistogram"); vfprintf(stderr, format, args);
int (*preset)(void*,int)=dlsym(lib,"vtCapture_setPresetHistogram"); va_end(args);
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; fputc('\n', stderr);
}
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;
} }
+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); stream_close(now);
return; return;
} }
if (n > 0 && memchr(stream.data + stream.used, 0, (size_t)n)) {
stream_close(now);
return;
}
if (n > 0) if (n > 0)
stream.used += (size_t)n; stream.used += (size_t)n;
stream.data[stream.used] = 0; stream.data[stream.used] = 0;
@@ -189,7 +193,7 @@ static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) {
if (!end) if (!end)
break; break;
*end = 0; *end = 0;
void *root = j->parse(stream.data); void *root = playback_parse_exact(j, stream.data);
if (stream.stage == STREAM_AUTHENTICATING) { if (stream.stage == STREAM_AUTHENTICATING) {
if (!playback_true(j, root, "success")) { if (!playback_true(j, root, "success")) {
if (root) 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 "spatial_image.h"
#include "diagnostics.h" #include "diagnostics.h"
#include "continuity.h" #include "continuity.h"
#include "service_context.h"
#define SERVICE "org.webosbrew.histogramhyperion.service" #define SERVICE "org.webosbrew.histogramhyperion.service"
#define PRIORITY 149 #define PRIORITY 149
typedef struct { typedef struct {
@@ -52,20 +53,21 @@ static bool (*error_init)(LSError *);
static void (*error_free)(LSError *); static void (*error_free)(LSError *);
static void *(*vt_init)(bool (*)(void)); static void *(*vt_init)(bool (*)(void));
static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *); static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *);
static void *capture; static CaptureContext capture_ctx = {.pending_letterbox = -1};
static char lg_diagnostic[256]; static ReceiverLayoutContext receiver_layout;
static CalibrationSessionContext calibration_ctx;
/* Process-local LG diagnostics; no changes to the loaded LG library. */ /* 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, void printLog(const char *context, int level, const char *message_id, const char *file,
const char *function, int line, const char *format, ...) { const char *function, int line, const char *format, ...) {
(void)context; (void)context;
(void)message_id; (void)message_id;
(void)file; (void)file;
if (capture && level > 3) if (capture_ctx.handle && level > 3)
return; return;
if (format) { if (format) {
va_list args; va_list args;
va_start(args, format); 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); va_end(args);
} }
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line); 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); fputc('\n', stderr);
} }
static int output = -1; #define CALIBRATION_ACTIVE (calibration_ctx.until > milliseconds())
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;
static RetryState retries; static RetryState retries;
static unsigned capture_recoveries;
static uint64_t recovery_started, last_recovery_ms, last_output_at;
static uint64_t milliseconds(void) { static uint64_t milliseconds(void) {
struct timespec t; struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t); clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000; return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
} }
static char last_error[256]; static char last_error[256];
static volatile sig_atomic_t revoked, quitting; static volatile sig_atomic_t quitting;
static int guard_pipe = -1; static int guard_pipe = -1;
static pid_t guard_pid = -1; static pid_t guard_pid = -1;
#ifndef DEV_MODE
#define DEV_MODE "/var/luna/preferences/vt_devmode" #define DEV_MODE "/var/luna/preferences/vt_devmode"
#endif
/* LG's developer gate is enabled only during initialization. A separate /* LG's developer gate is enabled only during initialization. A separate
* guardian removes our own marker even if this process aborts or is killed. * 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. */ * An existing marker is preserved, as are files replaced by another owner. */
@@ -169,135 +155,30 @@ static void quit_signal(int s) {
quitting = 1; quitting = 1;
} }
static bool policy(void) { static bool policy(void) {
revoked = 1; capture_ctx.revoked = 1;
return true; return true;
} }
static void set_error(const char *s) { static void set_error(const char *s) {
snprintf(last_error, sizeof(last_error), "%s", s); snprintf(last_error, sizeof(last_error), "%s", s);
fprintf(stderr, "%s\n", s); fprintf(stderr, "%s\n", s);
} }
static int send_line(const char *line) { static ReceiverRuntime receiver_runtime = {.output = {.fd = -1}};
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 layout_refresh_reset(uint64_t due);
static void release_capture(bool clear_output) { static void release_capture(bool clear_output) {
layout_refresh_reset(milliseconds() + 5000); layout_refresh_reset(milliseconds() + 5000);
free(led_blends); free(receiver_layout.blends);
led_blends = NULL; receiver_layout.blends = NULL;
led_count = 0; receiver_layout.count = 0;
interpolation_enabled = false; receiver_layout.interpolation = false;
color_valid = false; capture_ctx.color_valid = false;
memset(rgb, 0, sizeof(rgb)); memset(capture_ctx.rgb, 0, sizeof(capture_ctx.rgb));
memset(sent_rgb, 0, sizeof(sent_rgb)); memset(capture_ctx.sent_rgb, 0, sizeof(capture_ctx.sent_rgb));
if (output >= 0) { receiver_runtime.starting = false;
if (clear_output && frames) { receiver_runtime.clear_pending = false;
char line[80]; output_stop(&receiver_runtime.output, milliseconds(), clear_output);
snprintf(line, sizeof(line), "{\"command\":\"clear\",\"priority\":%d}\n", PRIORITY); if (capture_ctx.handle) {
send_line(line); vt_finish(capture_ctx.handle);
} capture_ctx.handle = NULL;
close(output);
output = -1;
}
if (capture) {
vt_finish(capture);
capture = NULL;
} }
} }
static void stop_capture(void) { static void stop_capture(void) {
@@ -396,6 +277,7 @@ int main(void) {
stream_close(milliseconds()); stream_close(milliseconds());
test_close(); test_close();
stop_capture(); stop_capture();
output_close(&receiver_runtime.output);
if (piccap_owned) { if (piccap_owned) {
handover_enabled = false; handover_enabled = false;
retry_pause(&retries); retry_pause(&retries);
+35 -41
View File
@@ -20,28 +20,27 @@ static bool start(void *h, void *m, void *ctx) {
retry_reset(&retries); retry_reset(&retries);
if (piccap_owned) if (piccap_owned)
return respond(h, m, "{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}"); return respond(h, m, "{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
if (capture) if (capture_ctx.handle)
return respond(h, m, "{\"returnValue\":true,\"running\":true}"); return respond(h, m, "{\"returnValue\":true,\"running\":true}");
frames = 0; if (!playback_json.lib) {
revoked = 0; 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; last_error[0] = 0;
lg_diagnostic[0] = 0; capture_ctx.diagnostic[0] = 0;
output = target_connect(&target); if (receiver_runtime.starting)
if (output < 0) { return respond(h, m, "{\"returnValue\":true,\"connecting\":true}");
stop_capture(); receiver_runtime.clear_pending = false;
set_error("Cannot connect to configured receiver IP and JSON port"); if (!output_begin(&receiver_runtime.output, &target, milliseconds())) {
return start_failure(h, m, "Configured receiver is unavailable"); set_error(receiver_runtime.output.error);
} return start_failure(h, m, receiver_runtime.output.error);
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");
} }
receiver_runtime.starting = true;
return respond(h, m, "{\"returnValue\":true,\"connecting\":true}");
}
static bool initialize_capture(void *h, void *m) {
if (!enter_developer_mode()) { if (!enter_developer_mode()) {
stop_capture(); stop_capture();
set_error("Developer initialization needs root. Elevate this own service through Homebrew " 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, return start_failure(h, m,
"Root access is required for temporary LG developer initialization"); "Root access is required for temporary LG developer initialization");
} }
capture = vt_init(policy); capture_ctx.handle = vt_init(policy);
if (!leave_developer_mode()) { if (!leave_developer_mode()) {
stop_capture(); stop_capture();
retries.blocked = true; retries.blocked = true;
set_error("Developer marker cleanup failed; manual intervention required"); set_error("Developer marker cleanup failed; manual intervention required");
return start_failure(h, m, "Developer marker cleanup failed"); return start_failure(h, m, "Developer marker cleanup failed");
} }
if (!capture) { if (!capture_ctx.handle) {
stop_capture(); stop_capture();
set_error("LG refused histogram initialization"); set_error("LG refused histogram initialization");
return start_failure( return start_failure(
h, m, h, m,
"LG refused histogram initialization. Another capture source may hold VT_CAPTURE."); "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(); stop_capture();
set_error("LG histogram preset initialization failed"); set_error("LG histogram preset initialization failed");
return start_failure(h, m, "Histogram preset failed"); return start_failure(h, m, "Histogram preset failed");
} }
capture_preset = letterbox_mode = settling = 0; capture_ctx.preset = capture_ctx.letterbox_mode = capture_ctx.settling = 0;
pending_letterbox = -1; capture_ctx.pending_letterbox = -1;
next_border_probe = 0; capture_ctx.next_border_probe = 0;
retries.due = 0; retries.due = 0;
retries.attempts = 0; retries.attempts = 0;
retries.quick_retries = 0; retries.quick_retries = 0;
if (recovery_started) { if (capture_ctx.recovery_started) {
last_recovery_ms = milliseconds() - recovery_started; capture_ctx.last_recovery_ms = milliseconds() - capture_ctx.recovery_started;
recovery_started = 0; capture_ctx.recovery_started = 0;
} }
return respond(h, m, "{\"returnValue\":true,\"running\":true}"); return respond(h, m, "{\"returnValue\":true,\"running\":true}");
} }
static bool stop(void *h, void *m, void *ctx) { static bool stop(void *h, void *m, void *ctx) {
(void)ctx; (void)ctx;
calibration_until = 0; calibration_ctx.until = 0;
retry_pause(&retries); retry_pause(&retries);
bool bridging = bridge_phase != BRIDGE_IDLE; bool bridging = bridge_phase != BRIDGE_IDLE;
stream_close(milliseconds()); stream_close(milliseconds());
bridge_phase = BRIDGE_IDLE; bridge_phase = BRIDGE_IDLE;
memset(&continuity, 0, sizeof(continuity)); memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
stop_capture(); stop_capture();
if (bridging) { if (bridging && receiver_runtime.output.stage != OUTPUT_READY) {
output = target_connect(&target); output_begin(&receiver_runtime.output, &target, milliseconds());
if (output >= 0) { receiver_runtime.clear_pending = true;
if (target_login())
send_line("{\"command\":\"clear\",\"priority\":149}\n");
close(output);
output = -1;
}
} }
return respond(h, m, "{\"returnValue\":true,\"running\":false}"); 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.\"}"); "the installed boot script and whether another file occupies the hook path.\"}");
if (enabled) if (enabled)
retry_reset(&retries); retry_reset(&retries);
retry_update(&retries, enabled, capture != NULL, milliseconds()); retry_update(&retries, enabled, capture_ctx.handle != NULL, milliseconds());
return respond(h, m, return respond(h, m,
enabled ? "{\"returnValue\":true,\"autostart\":true}" enabled ? "{\"returnValue\":true,\"autostart\":true}"
: "{\"returnValue\":true,\"autostart\":false}"); : "{\"returnValue\":true,\"autostart\":false}");
@@ -172,11 +166,11 @@ static bool settings(void *h, void *m, void *ctx) {
if (!target_save(TARGET_PREF, &next)) if (!target_save(TARGET_PREF, &next))
return respond( return respond(
h, m, "{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}"); 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(); stop_capture();
stream_close(milliseconds()); stream_close(milliseconds());
bridge_phase = BRIDGE_IDLE; bridge_phase = BRIDGE_IDLE;
memset(&continuity, 0, sizeof(continuity)); memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
test_close(); test_close();
connection_test.stage = TEST_IDLE; connection_test.stage = TEST_IDLE;
target = next; target = next;
+22 -27
View File
@@ -3,24 +3,22 @@ static bool calibration_session(void *h, void *m, void *ctx) {
(void)ctx; (void)ctx;
const char *p = payload(m); const char *p = payload(m);
if (!strcmp(p, "{\"enabled\":true}")) { if (!strcmp(p, "{\"enabled\":true}")) {
calibration_until = milliseconds() + 110000; calibration_ctx.until = milliseconds() + 110000;
if (capture && vt_preset(capture, 0) != 0) { if (capture_ctx.handle && vt_preset(capture_ctx.handle, 0) != 0) {
calibration_until = 0; calibration_ctx.until = 0;
return respond(h, m, return respond(h, m,
"{\"returnValue\":false,\"errorText\":\"Cannot select full-screen " "{\"returnValue\":false,\"errorText\":\"Cannot select full-screen "
"calibration capture\"}"); "calibration capture\"}");
} }
capture_preset = letterbox_mode = 0; capture_ctx.preset = capture_ctx.letterbox_mode = 0;
settling = 2; capture_ctx.settling = 2;
pending_letterbox = -1; capture_ctx.pending_letterbox = -1;
} else if (!strcmp(p, "{\"enabled\":false}")) } else if (!strcmp(p, "{\"enabled\":false}"))
calibration_until = 0; calibration_ctx.until = 0;
else else
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
return respond(h, m, "{\"returnValue\":true}"); 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) { static bool report_calibration(void *h, void *m, void *ctx) {
if (!playback_json.lib) if (!playback_json.lib)
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}"); 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)), void *root = playback_json.parse(payload(m)),
*message = playback_field(&playback_json, root, "message"); *message = playback_field(&playback_json, root, "message");
bool valid = message && playback_json.type(message) == 6 && 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) if (valid)
snprintf(calibration_message, sizeof(calibration_message), "%s", snprintf(calibration_ctx.message, sizeof(calibration_ctx.message), "%s",
playback_json.string(message)); playback_json.string(message));
if (root) if (root)
playback_json.put(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)) next.kind != CAL_CUSTOM || next.count < 4 || strcmp(next.model, tv_model))
return respond(h, m, return respond(h, m,
"{\"returnValue\":false,\"errorText\":\"Invalid measured model profile\"}"); "{\"returnValue\":false,\"errorText\":\"Invalid measured model profile\"}");
calibration_draft = next; calibration_ctx.draft = next;
return respond(h, m, "{\"returnValue\":true}"); 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) { static bool model_reply(void *h, void *m, void *ctx) {
(void)h; (void)h;
(void)ctx; (void)ctx;
model_busy = false; calibration_ctx.model_busy = false;
model_token = 0; calibration_ctx.model_token = 0;
void *root = playback_json.parse(payload(m)), void *root = playback_json.parse(payload(m)),
*model = playback_field(&playback_json, root, "modelName"); *model = playback_field(&playback_json, root, "modelName");
if (playback_true(&playback_json, root, "returnValue") && model && if (playback_true(&playback_json, root, "returnValue") && model &&
@@ -70,23 +65,23 @@ static bool model_reply(void *h, void *m, void *ctx) {
return true; return true;
} }
static void model_tick(uint64_t now) { 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; LSError e;
error_init(&e); error_init(&e);
cancel_call(service_handle, model_token, &e); cancel_call(service_handle, calibration_ctx.model_token, &e);
error_free(&e); error_free(&e);
model_busy = false; calibration_ctx.model_busy = false;
model_token = 0; 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; LSError e;
error_init(&e); error_init(&e);
model_busy = calibration_ctx.model_busy = call_once(
call_once(service_handle, "luna://com.webos.service.tv.systemproperty/getSystemInfo", service_handle, "luna://com.webos.service.tv.systemproperty/getSystemInfo",
"{\"keys\":[\"modelName\"]}", model_reply, NULL, &model_token, &e); "{\"keys\":[\"modelName\"]}", model_reply, NULL, &calibration_ctx.model_token, &e);
error_free(&e); error_free(&e);
model_deadline = now + 3000; calibration_ctx.model_deadline = now + 3000;
next_model_probe = now + 60000; calibration_ctx.next_model_probe = now + 60000;
} }
} }
static bool set_calibration(void *h, void *m, void *ctx) { 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
+37 -31
View File
@@ -16,19 +16,20 @@ static bool status(void *h, void *m, void *ctx) {
uint64_t now = milliseconds(); uint64_t now = milliseconds();
char report[512]; char report[512];
size_t k = 0; size_t k = 0;
for (size_t i = 0; calibration_message[i] && k + 2 < sizeof(report); i++) { for (size_t i = 0; calibration_ctx.message[i] && k + 2 < sizeof(report); i++) {
char c = calibration_message[i]; char c = calibration_ctx.message[i];
if (c == '"' || c == '\\') if (c == '"' || c == '\\')
report[k++] = '\\'; report[k++] = '\\';
if ((unsigned char)c >= 32) if ((unsigned char)c >= 32)
report[k++] = c; report[k++] = c;
} }
report[k] = 0; report[k] = 0;
int used = snprintf(json, sizeof(json), int used = snprintf(
"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\"," json, sizeof(json),
"{\"receiverConnecting\":%s,\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
"\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[", "\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",
calibration_key(calibration.kind), calibration_key(calibration_effective()), receiver_runtime.starting ? "true" : "false", calibration_key(calibration.kind),
tv_model, calibration.model); calibration_key(calibration_effective()), tv_model, calibration.model);
for (unsigned i = 0; i < calibration.count; i++) for (unsigned i = 0; i < calibration.count; i++)
used += used +=
snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}", 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, layout_error, piccap_fallbacks,
(unsigned long long)(handover_cooldown > now ? handover_cooldown - now : 0)); (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, "\"calibrationMessage\":\"%s\",", report);
used += used += snprintf(json + used, sizeof(json) - used,
snprintf(json + used, sizeof(json) - used, "\"calibrationDraft\":{\"model\":\"%s\",\"points\":[",
"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[", calibration_draft.model); calibration_ctx.draft.model);
for (unsigned i = 0; i < calibration_draft.count; i++) for (unsigned i = 0; i < calibration_ctx.draft.count; i++)
used += snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}", used += snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}",
i ? "," : "", calibration_draft.points[i].position, i ? "," : "", calibration_ctx.draft.points[i].position,
calibration_draft.points[i].region); calibration_ctx.draft.points[i].region);
used += snprintf(json + used, sizeof(json) - used, "]},\"receiverStreamConnected\":%s,", used += snprintf(json + used, sizeof(json) - used, "]},\"receiverStreamConnected\":%s,",
stream.stage == STREAM_SUBSCRIBED ? "true" : "false"); stream.stage == STREAM_SUBSCRIBED ? "true" : "false");
used += snprintf(json + used, sizeof(json) - used, used += snprintf(json + used, sizeof(json) - used,
"\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s," "\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s,"
"\"sourceFade\":%s,\"calibrating\":%s,", "\"sourceFade\":%s,\"calibrating\":%s,",
sample_sequence, sample_at ? (long long)(now - sample_at) : -1, capture_ctx.sample_sequence,
continuity.holding ? "true" : "false", capture_ctx.sample_at ? (long long)(now - capture_ctx.sample_at) : -1,
(continuity.fading || bridge_phase == BRIDGE_FADING) ? "true" : "false", capture_ctx.continuity.holding ? "true" : "false",
(capture_ctx.continuity.fading || bridge_phase == BRIDGE_FADING) ? "true"
: "false",
CALIBRATION_ACTIVE ? "true" : "false"); CALIBRATION_ACTIVE ? "true" : "false");
used += snprintf(json + used, sizeof(json) - used, used +=
snprintf(json + used, sizeof(json) - used,
"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\"," "\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\","
"\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%" "\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%"
"lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",", "lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",
target.piccap_port, test_stage_name(), connection_test.leds, target.piccap_port, test_stage_name(), connection_test.leds,
(unsigned long long)connection_test.elapsed, connection_test.error, (unsigned long long)connection_test.elapsed, connection_test.error,
last_output_at ? (long long)(now - last_output_at) : -1, capture_ctx.last_output_at ? (long long)(now - capture_ctx.last_output_at) : -1,
(unsigned long long)(retries.due > now ? retries.due - now : 0), (unsigned long long)(retries.due > now ? retries.due - now : 0),
capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap" capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap"
: capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap" : capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap"
: piccap_owned ? "PicCap" : piccap_owned ? "PicCap"
: capture ? "Histogram" : capture_ctx.handle ? "Histogram"
: "Stopped", : "Stopped",
piccap_error); piccap_error);
used += snprintf(json + used, sizeof(json) - used, used += snprintf(
json + used, sizeof(json) - used,
"\"version\":\"" HYPERDRM_VERSION "\"version\":\"" HYPERDRM_VERSION
"\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u," "\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,"
"\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u," "\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,"
"\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s," "\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,"
"\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false," "\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,"
"\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[", "\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",
(capture || capture_source == SOURCE_PICCAP) ? "true" : "false", (capture_ctx.handle || capture_source == SOURCE_PICCAP) ? "true" : "false",
color_valid ? "true" : "false", frames, capture_recoveries, capture_ctx.color_valid ? "true" : "false", capture_ctx.frames, capture_ctx.recoveries,
(unsigned long long)last_recovery_ms, endpoint, target.host, target.port, (unsigned long long)capture_ctx.last_recovery_ms, endpoint, target.host, target.port,
target.receiver, target.token[0] ? "true" : "false", target.receiver, target.token[0] ? "true" : "false",
target_piccap_compatible(&target) ? "true" : "false", PRIORITY, target_piccap_compatible(&target) ? "true" : "false", PRIORITY, (unsigned)geteuid(),
(unsigned)geteuid(), sent_rgb[0], sent_rgb[1], sent_rgb[2], safe); capture_ctx.sent_rgb[0], capture_ctx.sent_rgb[1], capture_ctx.sent_rgb[2], safe);
for (int i = 0; i < 16; i++) for (int i = 0; i < 16; i++)
used += snprintf(json + used, sizeof(json) - used, "%s[%u,%u,%u]", i ? "," : "", rgb[i][0], used += snprintf(json + used, sizeof(json) - used, "%s[%u,%u,%u]", i ? "," : "",
rgb[i][1], rgb[i][2]); capture_ctx.rgb[i][0], capture_ctx.rgb[i][1], capture_ctx.rgb[i][2]);
used += snprintf(json + used, sizeof(json) - used, used += snprintf(json + used, sizeof(json) - used,
"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[", led_count, "],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",
led_count ? "true" : "false"); receiver_layout.count, receiver_layout.count ? "true" : "false");
for (int i = 0; i < 16; i++) 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, snprintf(json + used, sizeof(json) - used,
"],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0," "],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0,"
"\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s," "\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,"
"\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u," "\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,"
"\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":" "\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":"
"\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}", "\"%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", boot_enabled() ? "true" : "false", retries.due ? "true" : "false",
retries.paused ? "true" : "false", retries.blocked ? "true" : "false", retries.paused ? "true" : "false", retries.blocked ? "true" : "false",
retries.attempts, piccap_installed() ? "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_PROTECTED ? "protected"
: protection == PLAYBACK_CLEAR ? "non-secure decoder" : protection == PLAYBACK_CLEAR ? "non-secure decoder"
: "unknown", : "unknown",
capture && letterbox_mode ? "true" : "false"); capture_ctx.handle && capture_ctx.letterbox_mode ? "true" : "false");
return respond(h, m, json); return respond(h, m, json);
} }
+6 -4
View File
@@ -44,7 +44,7 @@ static bool bridge_send(const unsigned char colors[16][3]) {
free(line); free(line);
return ok; return ok;
} }
static void bridge_tick(uint64_t now) { static void bridge_tick(uint64_t now, CaptureContext *capture) {
if (retries.paused || retries.blocked) { if (retries.paused || retries.blocked) {
if (bridge_phase) if (bridge_phase)
bridge_clear(); bridge_clear();
@@ -54,15 +54,17 @@ static void bridge_tick(uint64_t now) {
} }
if (capture_source != bridge_source) { if (capture_source != bridge_source) {
if (capture_source == SOURCE_PICCAP_STARTING) { if (capture_source == SOURCE_PICCAP_STARTING) {
bridge_phase = continuity.available && now - continuity.valid_at < 2000 ? BRIDGE_HOLDING bridge_phase =
capture->continuity.available && now - capture->continuity.valid_at < 2000
? BRIDGE_HOLDING
: BRIDGE_IDLE; : BRIDGE_IDLE;
bridge_started = now; 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 || } else if (capture_source == SOURCE_PICCAP_STOPPING ||
(bridge_source == SOURCE_PICCAP && capture_source == SOURCE_HISTOGRAM)) { (bridge_source == SOURCE_PICCAP && capture_source == SOURCE_HISTOGRAM)) {
bridge_phase = BRIDGE_IDLE; bridge_phase = BRIDGE_IDLE;
if (stream.valid && now - stream.updated < 1000) { if (stream.valid && now - stream.updated < 1000) {
continuity_seed(&continuity, stream.colors, now); continuity_seed(&capture->continuity, stream.colors, now);
bridge_send(stream.colors); bridge_send(stream.colors);
} }
} }
+5 -37
View File
@@ -10,6 +10,7 @@
#include <sys/time.h> #include <sys/time.h>
#include <unistd.h> #include <unistd.h>
#include "playback.h" #include "playback.h"
#include "atomic_file.h"
#define TARGET_PREF \ #define TARGET_PREF \
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json" "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
typedef struct { typedef struct {
@@ -44,7 +45,7 @@ static bool target_valid(const TargetConfig *t) {
return true; return true;
} }
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) { 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) if (!root)
return false; return false;
TargetConfig next = *out; 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) { static bool target_save(const char *path, const TargetConfig *t) {
if (!target_valid(t)) if (!target_valid(t))
return false; return false;
char tmp[512], data[512]; char data[512];
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
return false;
int n = snprintf( int n = snprintf(
data, sizeof(data), data, sizeof(data),
"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n", "{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",
t->host, t->port, t->piccap_port, t->receiver, t->token); 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); return n > 0 && (size_t)n < sizeof(data) &&
if (fd < 0) atomic_replace(path, data, (size_t)n) != SAVE_UNCHANGED;
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) { static bool target_load(PlaybackJson *j, const char *path, TargetConfig *t) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
@@ -178,26 +168,4 @@ static int target_open(const TargetConfig *t) {
} }
return fd; 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 #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); close(server);assert(test_begin(&config,10000));test_tick(json,10010);assert(connection_test.stage==TEST_FAILED);
} }
int main(void){ 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)); PlaybackJson json={0};assert(playback_json_init(&json));
fixture(&json,0);fixture(&json,1);fixture(&json,2); fixture(&json,0);fixture(&json,1);fixture(&json,2);
int peers[2];assert(socketpair(AF_UNIX,SOCK_STREAM,0,peers)==0); 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); media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);assert(capture_source==SOURCE_PICCAP);
} }
int main(void){ 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; (void)retry_failure;(void)retry_take;(void)retry_update;
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel; assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
handover_enabled=true;next_media_probe=UINT64_MAX; 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 "target.h"
#include "layout.h" #include "layout.h"
#include "diagnostics.h" #include "diagnostics.h"
static unsigned led_count; #include "runtime_context.h"
static LedBlend *led_blends; static ReceiverLayoutContext receiver_layout;
#include "layout_refresh.h" #include "layout_refresh.h"
static void completed(unsigned count,unsigned generation){ static void completed(unsigned count,unsigned generation){
layout_probe.mapping=calloc(count,sizeof(LedBlend));assert(layout_probe.mapping); 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; layout_probe.leds=count;layout_probe.stage=TEST_DONE;layout_probe_generation=generation;
} }
int main(void){ 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)); PlaybackJson json={0};assert(playback_json_init(&json));
calibration.kind=CAL_G4;next_layout_check=UINT64_MAX; calibration.kind=CAL_G4;next_layout_check=UINT64_MAX;
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true); completed(50,calibration_generation);layout_refresh_tick(&json,1000,true,&receiver_layout);
assert(led_count==50 && layout_updates==1 && last_layout_check==1000); assert(receiver_layout.count==50 && layout_updates==1 && last_layout_check==1000);
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true); completed(500,calibration_generation);layout_refresh_tick(&json,2000,true,&receiver_layout);
assert(led_count==500 && layout_updates==2 && last_layout_check==2000); assert(receiver_layout.count==500 && layout_updates==2 && last_layout_check==2000);
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true);assert(layout_updates==2); completed(500,calibration_generation);layout_refresh_tick(&json,3000,true,&receiver_layout);assert(layout_updates==2);
completed(500,calibration_generation);layout_probe.mapping[42].weight^=512; completed(500,calibration_generation);layout_probe.mapping[42].weight^=512;
layout_refresh_tick(&json,4000,true);assert(layout_updates==3); layout_refresh_tick(&json,4000,true,&receiver_layout);assert(layout_updates==3);
LedBlend *previous=led_blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout"); LedBlend *previous=receiver_layout.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,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++; 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,6000,true,&receiver_layout);assert(receiver_layout.count==500 && receiver_layout.blends==previous);assert(probe_busy(&layout_probe));
layout_refresh_tick(&json,6100,false);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1); layout_refresh_tick(&json,6100,false,&receiver_layout);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
assert(led_count==500 && led_blends==previous); assert(receiver_layout.count==500 && receiver_layout.blends==previous);
layout_refresh_reset(7000);free(led_blends);test_close(); 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; 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_parse(&json, "{}", &copy));
assert(!target_save("/no/such/profile", &copy)); assert(!target_save("/no/such/profile", &copy));
assert(!target_load(&json, "/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) if (fd >= 0)
close(fd); close(fd);
dlclose(json.lib); 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 {bool paused,blocked;} retries;
static struct {char receiver[16];} target={"Hyperion"}; 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 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 bool piccap_connected;
static char *queued; static char *queued;
static bool stream_queue(const char *line){if(queued)return false;queued=strdup(line);return queued!=NULL;} 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;} static void consume(void){free(queued);queued=NULL;}
int main(void){ int main(void){
LedBlend mapping={0,0,0};stream.mapping=&mapping;stream.count=1;stream.stage=4; 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; 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);assert(bridge_phase==1 && strstr(queued,"200,0,0"));consume(); 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);assert(bridge_phase==2 && strstr(queued,"clear"));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);assert(bridge_phase==2); 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);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume(); stream.sequence=2;stream.updated=1400;bridge_tick(1400,&capture_ctx);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume();
bridge_tick(1650);assert(strstr(queued,"100,0,100"));consume(); bridge_tick(1650,&capture_ctx);assert(strstr(queued,"100,0,100"));consume();
bridge_tick(1900);assert(!bridge_phase && strstr(queued,"clear"));consume(); bridge_tick(1900,&capture_ctx);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_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);assert(!queued); capture_source=SOURCE_HISTOGRAM;bridge_tick(2100,&capture_ctx);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;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;continuity.valid_at=7000;bridge_tick(7000);consume();retries.paused=true;bridge_tick(7100);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;continuity.valid_at=8000;capture_source=SOURCE_PICCAP_STARTING;bridge_tick(8000);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);consume();stream.sequence++;stream.updated=8300;memset(stream.colors,0,sizeof(stream.colors));bridge_tick(8300);assert(!bridge_phase && !queued); 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;continuity.valid_at=1000;bridge_tick(9000);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);assert(!queued);retries.paused=true;bridge_tick(12100);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)); 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; 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 <sys/stat.h>
#include <string.h> #include <string.h>
#include "target.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){ int main(void){
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target; 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)); 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); 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)}}; 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); 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); int client=fixture_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); assert(fixture_connect(&t)<0);
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, bounded TCP connect and refusal");return 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 #ifndef HYPERDRM_VERSION_H
#define HYPERDRM_VERSION_H #define HYPERDRM_VERSION_H
#define HYPERDRM_VERSION "0.12.0" #define HYPERDRM_VERSION "0.13.0"
#endif #endif
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "org.webosbrew.histogramhyperion", "id": "org.webosbrew.histogramhyperion",
"version": "0.12.0", "version": "0.13.0",
"vendor": "Local development", "vendor": "Local development",
"app": "org.webosbrew.histogramhyperion", "app": "org.webosbrew.histogramhyperion",
"services": [ "services": [
+8
View File
@@ -53,6 +53,7 @@ def verify_package(version):
for directory, filename in [('applications', 'appinfo.json'), ('packages', 'packageinfo.json')]: for directory, filename in [('applications', 'appinfo.json'), ('packages', 'packageinfo.json')]:
entry = archive.extractfile(f'usr/palm/{directory}/org.webosbrew.histogramhyperion/{filename}') entry = archive.extractfile(f'usr/palm/{directory}/org.webosbrew.histogramhyperion/{filename}')
assert json.load(entry)['version'] == version assert json.load(entry)['version'] == version
assert archive.extractfile('usr/palm/applications/org.webosbrew.histogramhyperion/version.js').read() == (ROOT / 'app/version.js').read_bytes()
binary = archive.extractfile('usr/palm/services/org.webosbrew.histogramhyperion.service/histogram-service').read() binary = archive.extractfile('usr/palm/services/org.webosbrew.histogramhyperion.service/histogram-service').read()
assert binary[:4] == b'\x7fELF' and binary[4] == 1 and binary[18:20] == b'\x28\x00' assert binary[:4] == b'\x7fELF' and binary[4] == 1 and binary[18:20] == b'\x28\x00'
assert version.encode() in binary assert version.encode() in binary
@@ -62,6 +63,7 @@ def verify_package(version):
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--push', action='store_true') parser.add_argument('--push', action='store_true')
parser.add_argument('--sanitizers', action='store_true', help='Run parser, atomic-save, output and service lifecycle tests with ASan/UBSan')
options = parser.parse_args() options = parser.parse_args()
version = synchronize() version = synchronize()
run(sys.executable, '-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py') run(sys.executable, '-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py')
@@ -75,6 +77,12 @@ def main():
name = '/tmp/hyperdrm-' + test.stem name = '/tmp/hyperdrm-' + test.stem
linux('gcc', '-Wall', '-Wextra', '-Werror', '-O2', str(test.relative_to(ROOT)).replace('\\', '/'), '-ldl', '-lm', '-o', name) linux('gcc', '-Wall', '-Wextra', '-Werror', '-O2', str(test.relative_to(ROOT)).replace('\\', '/'), '-ldl', '-lm', '-o', name)
linux(name) linux(name)
if options.sanitizers:
for name in ('json_protocol', 'atomic_file', 'receiver_output', 'service_lifecycle'):
destination = '/tmp/hyperdrm-sanitized-' + name
linux('gcc', '-Wall', '-Wextra', '-Werror', '-O1', '-g', '-fsanitize=address,undefined',
'native/test_' + name + '.c', '-ldl', '-lm', '-o', destination)
linux(destination)
linux('arm-linux-gnueabi-gcc', '-Wall', '-Wextra', '-Werror', '-O2', '-rdynamic', linux('arm-linux-gnueabi-gcc', '-Wall', '-Wextra', '-Werror', '-O2', '-rdynamic',
'native/service.c', '-ldl', '-o', 'service/histogram-service') 'native/service.c', '-ldl', '-o', 'service/histogram-service')
run(sys.executable, 'build_package.py') run(sys.executable, 'build_package.py')
Binary file not shown.
+4
View File
@@ -21,6 +21,10 @@ def synchronize(check=False):
path = ROOT / 'native/version.h' path = ROOT / 'native/version.h'
if not path.exists() or path.read_text(encoding='utf-8') != header: if not path.exists() or path.read_text(encoding='utf-8') != header:
updates[path] = header updates[path] = header
path = ROOT / 'app/version.js'
script = "'use strict';\nvar HYPERDRM_APP_VERSION = '" + version + "';\n"
if not path.exists() or path.read_text(encoding='utf-8') != script:
updates[path] = script
if check and updates: if check and updates:
raise ValueError('Version metadata is stale; run sync_version.py') raise ValueError('Version metadata is stale; run sync_version.py')
for path, content in updates.items(): for path, content in updates.items():
+1 -1
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
const assert=require('assert'),fs=require('fs'),vm=require('vm'); const assert=require('assert'),fs=require('fs'),vm=require('vm');
const ids=['host','port','piccapPort','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics','profile','customEditor','customProfile','saveProfile','profileStatus','calibrate','validateCalibration','cancelCalibration','editProfile','customProfileField','overviewTab','receiverTab','calibrationTab','calibrationShortcut','hostField','portField','piccapPortField','tokenField','grantRoot','rootHint']; const ids=['reloadService','versionWarning','host','port','piccapPort','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics','profile','customEditor','customProfile','saveProfile','profileStatus','calibrate','validateCalibration','cancelCalibration','editProfile','customProfileField','overviewTab','receiverTab','calibrationTab','calibrationShortcut','hostField','portField','piccapPortField','tokenField','grantRoot','rootHint'];
const elements={}; const elements={};
ids.forEach(id=>elements[id]={value:'',textContent:'',disabled:false,checked:false,attributes:{},style:{},addEventListener(){},focus(){},appendChild(){},setAttribute(k,v){this.attributes[k]=v;},getAttribute(k){return this.attributes[k];}}); ids.forEach(id=>elements[id]={value:'',textContent:'',disabled:false,checked:false,attributes:{},style:{},addEventListener(){},focus(){},appendChild(){},setAttribute(k,v){this.attributes[k]=v;},getAttribute(k){return this.attributes[k];}});
const requests=[]; const requests=[];
+6
View File
@@ -34,6 +34,12 @@ class VersionTests(unittest.TestCase):
self.assertEqual(app, {'version': '1.2.3', 'id': 'unchanged'}) self.assertEqual(app, {'version': '1.2.3', 'id': 'unchanged'})
self.assertIn('"1.2.3"', (sync_version.ROOT / 'native/version.h').read_text()) self.assertIn('"1.2.3"', (sync_version.ROOT / 'native/version.h').read_text())
def test_ui_drift_is_rejected(self):
sync_version.synchronize()
(sync_version.ROOT / 'app/version.js').write_text("var HYPERDRM_APP_VERSION = 'old';")
with self.assertRaises(ValueError):
sync_version.synchronize(check=True)
def test_invalid_version_is_rejected(self): def test_invalid_version_is_rejected(self):
(sync_version.ROOT / 'VERSION').write_text('1.2\n') (sync_version.ROOT / 'VERSION').write_text('1.2\n')
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
+21
View File
@@ -0,0 +1,21 @@
'use strict';
const assert=require('assert'),fs=require('fs'),vm=require('vm');
const elements={},requests=[];let serviceVersion='0.12.0';
for(const match of fs.readFileSync('app/index.html','utf8').matchAll(/id="([^"]+)"/g))elements[match[1]]={style:{},value:'',textContent:'',disabled:false,attributes:{},addEventListener(){},focus(){},appendChild(){},getAttribute(k){return this.attributes[k];},setAttribute(k,v){this.attributes[k]=v;}};
class Bridge {
call(uri){requests.push(uri);this.onservicecallback(JSON.stringify(uri.endsWith('/status')?{returnValue:true,version:serviceVersion,piccapInstalled:false,receiverConnecting:true,connectionTest:{state:'Not tested'}}:{returnValue:true}));}
cancel(){}
}
const c={document:{getElementById(id){assert(elements[id],id);return elements[id];},createElement(){return {style:{}};}},window:{},PalmServiceBridge:Bridge,setupRemoteNavigation(){},setInterval(){},setTimeout(){return 1;},clearTimeout(){}};
vm.createContext(c);vm.runInContext(fs.readFileSync('app/version.js','utf8'),c);vm.runInContext(fs.readFileSync('app/app.js','utf8'),c);
assert(!c.versionCompatible && elements.start.disabled && elements.saveTarget.disabled);
assert(elements.versionWarning.textContent.includes('0.12.0'));assert.equal(elements.reloadService.style.display,'inline-block');
elements.reloadService.onclick();assert(requests.some(uri=>uri.endsWith('/restartService')));
const count=requests.length;c.call('start',r=>assert.equal(r.returnValue,false));assert.equal(requests.length,count);
elements.stop.onclick();assert(requests.some(uri=>uri.endsWith('/stop')));
serviceVersion=c.HYPERDRM_APP_VERSION;c.call('status',c.display);
assert(c.versionCompatible && !elements.start.disabled && elements.versionWarning.style.display==='none');
assert.equal(elements.reloadService.style.display,'none');assert(elements.handover.disabled);assert(elements.status.textContent.includes('Connecting'));
serviceVersion=undefined;c.call('status',c.display);assert(!c.versionCompatible && elements.versionWarning.textContent.includes('unknown'));
c.display({returnValue:false,errorText:'Disconnected'});assert(!c.versionCompatible);
console.log('PASS: old/missing service versions block mutations, Stop remains available, matching service restores controls and unavailable PicCap remains disabled');