Private
Public Access
Release 0.13.0
This commit is contained in:
1 parent
7f6eeab39e
commit
c0d891b372
49 files changed
+1523
-478
No files matched your search
@@ -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
|
||||||
@@ -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.
@@ -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.
|
||||||
+48
-2
@@ -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
@@ -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
@@ -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 …</p><p id="error" role="alert"></p>
|
<p id="status" role="status">Checking service …</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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
'use strict';
|
||||||
|
var HYPERDRM_APP_VERSION = '0.13.0';
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
last_output_at = now;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
sample_at = now;
|
capture_ctx.sample_at = now;
|
||||||
sample_sequence++;
|
capture_ctx.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;
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
+55
-22
@@ -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;
|
||||||
|
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
||||||
|
if (format) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vfprintf(stderr, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
fputc('\n', stderr);
|
||||||
|
}
|
||||||
|
static bool policy(void) {
|
||||||
|
fprintf(stderr, "Capture revoked by LG policy\n");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
static bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; }
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
void *lib=dlopen("libvtcapture.so.1",RTLD_NOW);
|
void *lib = dlopen("libvtcapture.so.1", RTLD_NOW);
|
||||||
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;}
|
if (!lib) {
|
||||||
void *(*init)(bool(*)(void))=dlsym(lib,"vtCapture_initHistogram");
|
fprintf(stderr, "dlopen: %s\n", dlerror());
|
||||||
int (*preset)(void*,int)=dlsym(lib,"vtCapture_setPresetHistogram");
|
return 1;
|
||||||
int (*get)(void*,Histogram*)=dlsym(lib,"vtCapture_getHistogram");
|
}
|
||||||
int (*finish)(void*)=dlsym(lib,"vtCapture_finalizeHistogram");
|
void *(*init)(bool (*)(void)) = dlsym(lib, "vtCapture_initHistogram");
|
||||||
if(!init||!preset||!get||!finish)return 2;
|
int (*preset)(void *, int) = dlsym(lib, "vtCapture_setPresetHistogram");
|
||||||
void *handle=init(policy); fprintf(stderr,"handle=%p\n",handle);
|
int (*get)(void *, Histogram *) = dlsym(lib, "vtCapture_getHistogram");
|
||||||
if(!handle)return 3;
|
int (*finish)(void *) = dlsym(lib, "vtCapture_finalizeHistogram");
|
||||||
int rc=preset(handle,0);fprintf(stderr,"preset=%d\n",rc);
|
if (!init || !preset || !get || !finish)
|
||||||
Histogram hist[16];
|
return 2;
|
||||||
for(int frame=0;rc==0 && frame<3;frame++) {
|
void *handle = init(policy);
|
||||||
usleep(100000); rc=get(handle,hist); printf("frame=%d rc=%d\n",frame,rc);
|
fprintf(stderr, "handle=%p\n", handle);
|
||||||
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("");}
|
if (!handle)
|
||||||
}
|
return 3;
|
||||||
fprintf(stderr,"finalize=%d\n",finish(handle)); return rc?4:0;
|
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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#ifndef HYPERDRM_SERVICE_CONTEXT_H
|
||||||
|
#define HYPERDRM_SERVICE_CONTEXT_H
|
||||||
|
#include "runtime_context.h"
|
||||||
|
#include "receiver_output.h"
|
||||||
|
/* Runtime ownership is separate from the saved calibration configuration. */
|
||||||
|
typedef struct {
|
||||||
|
uint64_t until, model_deadline, next_model_probe;
|
||||||
|
unsigned long model_token;
|
||||||
|
bool model_busy;
|
||||||
|
Calibration draft;
|
||||||
|
char message[256];
|
||||||
|
} CalibrationSessionContext;
|
||||||
|
typedef struct {
|
||||||
|
ReceiverOutput output;
|
||||||
|
bool starting, clear_pending;
|
||||||
|
} ReceiverRuntime;
|
||||||
|
#endif
|
||||||
+56
-50
@@ -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),
|
||||||
"\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",
|
"{\"receiverConnecting\":%s,\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
|
||||||
calibration_key(calibration.kind), calibration_key(calibration_effective()),
|
"\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",
|
||||||
tv_model, calibration.model);
|
receiver_runtime.starting ? "true" : "false", calibration_key(calibration.kind),
|
||||||
|
calibration_key(calibration_effective()), tv_model, calibration.model);
|
||||||
for (unsigned i = 0; i < calibration.count; i++)
|
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 +=
|
||||||
"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\","
|
snprintf(json + used, sizeof(json) - used,
|
||||||
"\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%"
|
"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\","
|
||||||
"lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",
|
"\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%"
|
||||||
target.piccap_port, test_stage_name(), connection_test.leds,
|
"lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",
|
||||||
(unsigned long long)connection_test.elapsed, connection_test.error,
|
target.piccap_port, test_stage_name(), connection_test.leds,
|
||||||
last_output_at ? (long long)(now - last_output_at) : -1,
|
(unsigned long long)connection_test.elapsed, connection_test.error,
|
||||||
(unsigned long long)(retries.due > now ? retries.due - now : 0),
|
capture_ctx.last_output_at ? (long long)(now - capture_ctx.last_output_at) : -1,
|
||||||
capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap"
|
(unsigned long long)(retries.due > now ? retries.due - now : 0),
|
||||||
: capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap"
|
capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap"
|
||||||
: piccap_owned ? "PicCap"
|
: capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap"
|
||||||
: capture ? "Histogram"
|
: piccap_owned ? "PicCap"
|
||||||
: "Stopped",
|
: capture_ctx.handle ? "Histogram"
|
||||||
piccap_error);
|
: "Stopped",
|
||||||
used += snprintf(json + used, sizeof(json) - used,
|
piccap_error);
|
||||||
"\"version\":\"" HYPERDRM_VERSION
|
used += snprintf(
|
||||||
"\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,"
|
json + used, sizeof(json) - used,
|
||||||
"\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,"
|
"\"version\":\"" HYPERDRM_VERSION
|
||||||
"\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,"
|
"\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,"
|
||||||
"\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,"
|
"\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,"
|
||||||
"\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",
|
"\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,"
|
||||||
(capture || capture_source == SOURCE_PICCAP) ? "true" : "false",
|
"\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,"
|
||||||
color_valid ? "true" : "false", frames, capture_recoveries,
|
"\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",
|
||||||
(unsigned long long)last_recovery_ms, endpoint, target.host, target.port,
|
(capture_ctx.handle || capture_source == SOURCE_PICCAP) ? "true" : "false",
|
||||||
target.receiver, target.token[0] ? "true" : "false",
|
capture_ctx.color_valid ? "true" : "false", capture_ctx.frames, capture_ctx.recoveries,
|
||||||
target_piccap_compatible(&target) ? "true" : "false", PRIORITY,
|
(unsigned long long)capture_ctx.last_recovery_ms, endpoint, target.host, target.port,
|
||||||
(unsigned)geteuid(), sent_rgb[0], sent_rgb[1], sent_rgb[2], safe);
|
target.receiver, target.token[0] ? "true" : "false",
|
||||||
|
target_piccap_compatible(&target) ? "true" : "false", PRIORITY, (unsigned)geteuid(),
|
||||||
|
capture_ctx.sent_rgb[0], capture_ctx.sent_rgb[1], capture_ctx.sent_rgb[2], safe);
|
||||||
for (int i = 0; i < 16; i++)
|
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);
|
||||||
}
|
}
|
||||||
@@ -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 =
|
||||||
: BRIDGE_IDLE;
|
capture->continuity.available && now - capture->continuity.valid_at < 2000
|
||||||
|
? BRIDGE_HOLDING
|
||||||
|
: 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
@@ -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
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ int main(void) {
|
|||||||
assert(!target_parse(&json, "{}", ©));
|
assert(!target_parse(&json, "{}", ©));
|
||||||
assert(!target_save("/no/such/profile", ©));
|
assert(!target_save("/no/such/profile", ©));
|
||||||
assert(!target_load(&json, "/no/such/profile", ©));
|
assert(!target_load(&json, "/no/such/profile", ©));
|
||||||
int fd = target_connect(©);
|
int fd = target_open(©);
|
||||||
if (fd >= 0)
|
if (fd >= 0)
|
||||||
close(fd);
|
close(fd);
|
||||||
dlclose(json.lib);
|
dlclose(json.lib);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,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": [
|
||||||
|
|||||||
@@ -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.
@@ -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,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=[];
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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');
|
||||||
Reference in new issue
Block a user