Private
Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef03258e22 | ||
|
|
2f6efd9864 | ||
|
|
c3040680a0 | ||
|
|
c0d891b372 |
No files matched your search
@@ -42,3 +42,8 @@ test-spatial_image
|
||||
test-continuity
|
||||
test-receiver_stream
|
||||
test-source_bridge
|
||||
|
||||
test-atomic_file
|
||||
test-json_protocol
|
||||
test-receiver_output
|
||||
test-service_lifecycle
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"id": "org.webosbrew.histogramhyperion",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.3",
|
||||
"type": "web",
|
||||
"title": "HyperDRM",
|
||||
"appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR",
|
||||
"iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png",
|
||||
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM",
|
||||
"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.3_arm.ipk",
|
||||
"ipkHash": {
|
||||
"sha256": "b4fcc609d763b8e1a72d84484a5d454627416dde0ef51ef65ab705173e3c0810"
|
||||
"sha256": "acb0978100796e8765e586a91183599ef4eb783631d8b68205d716621790e232"
|
||||
},
|
||||
"ipkSize": 1204332
|
||||
"ipkSize": 1208120
|
||||
}
|
||||
Binary file not shown.
@@ -27,7 +27,7 @@ Only the own service is modified. Optional Auto PicCap can configure its receive
|
||||
|
||||
## 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.3_arm.ipk](HyperDRM_0.13.3_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||
|
||||
Elevate the installed own service through Homebrew Channel:
|
||||
|
||||
@@ -209,3 +209,31 @@ 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
|
||||
downloaded attachment's size and SHA-256. Git access does not authorize web uploads.
|
||||
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.
|
||||
|
||||
Histogram near-black handling suppresses colors with a maximum RGB channel of 16 or less, gradually restores brightness between 16 and 32, and preserves brighter colors. This applies only to histogram output; PicCap, receiver calibration and raw calibration samples are unchanged.
|
||||
|
||||
Thin letterbox detection compares both edges against lit interior histogram samples and requires a consistently dark upper edge for at least two seconds. Histogram probes cannot determine the exact bar height or guarantee detection on every scene.
|
||||
+48
-2
@@ -1,6 +1,26 @@
|
||||
'use strict';
|
||||
var pending = [];
|
||||
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) {
|
||||
actionError = message || '';
|
||||
@@ -32,6 +52,13 @@ function showProfile() {
|
||||
}
|
||||
|
||||
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();
|
||||
callUri('luna://org.webosbrew.histogramhyperion.service/' + method, function(result) {
|
||||
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');
|
||||
}
|
||||
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') {
|
||||
document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver');
|
||||
document.getElementById('regions').textContent = '';
|
||||
checkServiceVersion(result);
|
||||
return;
|
||||
}
|
||||
if (result.piccapHandover) document.getElementById('status').textContent += ' · HyperDRM · ' + result.playbackProtection;
|
||||
@@ -143,7 +171,25 @@ function display(result) {
|
||||
e.title = 'Histogram ' + index;
|
||||
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() {
|
||||
call('start', function(r) {
|
||||
if (r.returnValue === false) display(r);
|
||||
@@ -347,6 +393,6 @@ document.getElementById('editProfile').onclick = function() {
|
||||
showProfile();
|
||||
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);
|
||||
}), document, window);
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "org.webosbrew.histogramhyperion",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.3",
|
||||
"vendor": "Local development",
|
||||
"type": "web",
|
||||
"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>
|
||||
<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="versionWarning" role="alert" style="display:none"></p><button id="reloadService" style="display:none">Reload service</button>
|
||||
<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>
|
||||
<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>
|
||||
</fieldset></section>
|
||||
<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.3';
|
||||
@@ -89,6 +89,10 @@ function wizardSample() {
|
||||
}
|
||||
|
||||
function wizardStart(validation) {
|
||||
if (typeof versionCompatible !== 'undefined' && !versionCompatible) {
|
||||
document.getElementById('wizardStatus').textContent = 'Finish the app/service update before calibration.';
|
||||
return;
|
||||
}
|
||||
if (wizard) return;
|
||||
if (typeof showPanel === 'function') showPanel('calibration', false);
|
||||
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_status.h | Read-only status serialization |
|
||||
| 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 |
|
||||
| source_bridge.h | Bounded color continuity during source transitions |
|
||||
| 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.
|
||||
Hardware capture and Magic Remote behavior still require a later TV check; a host
|
||||
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
|
||||
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.
|
||||
|
||||
## 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,5 @@
|
||||
# HyperDRM 0.13.1
|
||||
|
||||
Suppress near-black histogram residuals that can appear green on SK6812 LEDs. Colors whose maximum channel is at most 12 become black; values between 12 and 24 fade in proportionally; brighter colors retain their original values. Raw histogram diagnostics and calibration samples remain available unchanged. PicCap frames are not filtered.
|
||||
|
||||
Validation: regression tests cover black, the measured green residual, transition values and unchanged brighter colors. The release workflow runs the existing native and UI suites and verifies the packaged version and checksum.
|
||||
@@ -0,0 +1,3 @@
|
||||
# HyperDRM 0.13.2
|
||||
|
||||
Raise the histogram black cutoff to 16 and extend the proportional transition to 32. Live measurements showed raw [13,14,7] causing residual [1,1,0] to [2,2,1] at the bottom-right LEDs with the previous cutoff. Regression coverage now includes this sample. Brighter colors, PicCap and saved settings remain unchanged; very dark detail is suppressed more strongly.
|
||||
@@ -0,0 +1,3 @@
|
||||
# HyperDRM 0.13.3
|
||||
|
||||
Detect thinner horizontal black bars using edge-versus-interior contrast, distributed upper-edge support and lower-edge support. Preserve the existing dark-scene rejection, wide-bar rule and two-second stability gate. Regression tests include live G4 measurements, one-sided and isolated dark edges, dim frames and insufficient contrast. This is a histogram heuristic; it does not measure exact bar heights.
|
||||
@@ -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
|
||||
+25
-2
@@ -4,14 +4,17 @@
|
||||
* or dark frame so darkness alone cannot switch the active capture mode. */
|
||||
static inline int letterbox_candidate(const unsigned black[16], const unsigned char color[16][3]) {
|
||||
const unsigned interior[] = {2, 6, 10, 15};
|
||||
unsigned lit = 0;
|
||||
unsigned lit = 0, interior_black = 0, interior_peak = 0;
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
unsigned r = interior[i], peak = color[r][0];
|
||||
for (unsigned c = 1; c < 3; c++)
|
||||
if (color[r][c] > peak)
|
||||
peak = color[r][c];
|
||||
if (black[r] < 20 && peak > 35)
|
||||
if (black[r] < 20 && peak > 35) {
|
||||
lit++;
|
||||
interior_black += black[r];
|
||||
interior_peak += peak;
|
||||
}
|
||||
}
|
||||
if (lit < 2)
|
||||
return -1;
|
||||
@@ -19,6 +22,26 @@ static inline int letterbox_candidate(const unsigned black[16], const unsigned c
|
||||
unsigned bottom = (black[1] + black[5] + black[9]) / 3;
|
||||
if (top >= 55 && bottom >= 25)
|
||||
return 1;
|
||||
/* Thin bars occupy only part of an edge probe. Require a broad upper
|
||||
* dark edge, lower-edge support and contrast against lit interior probes.
|
||||
* This remains a histogram heuristic, not a pixel-accurate crop. */
|
||||
const unsigned top_regions[] = {3, 11, 12};
|
||||
const unsigned bottom_regions[] = {1, 5, 9};
|
||||
unsigned top_support = 0, bottom_support = 0, top_peak = 0;
|
||||
for (unsigned i = 0; i < 3; i++) {
|
||||
unsigned r = top_regions[i], peak = color[r][0];
|
||||
for (unsigned c = 1; c < 3; c++)
|
||||
if (color[r][c] > peak)
|
||||
peak = color[r][c];
|
||||
top_peak += peak;
|
||||
top_support += black[r] >= 12;
|
||||
bottom_support += black[bottom_regions[i]] >= 8;
|
||||
}
|
||||
unsigned baseline = interior_black / lit;
|
||||
if (top_support == 3 && bottom_support >= 2 && top >= 12 && bottom >= 10 &&
|
||||
top >= baseline + 8 && bottom >= baseline + 8 &&
|
||||
(unsigned long)top_peak * lit * 100 <= (unsigned long)interior_peak * 3 * 60)
|
||||
return 1;
|
||||
if (top < 20 && bottom < 20)
|
||||
return 0;
|
||||
return -1;
|
||||
|
||||
+15
-15
@@ -2,6 +2,7 @@
|
||||
#define HYPERDRM_CALIBRATION_H
|
||||
#include <math.h>
|
||||
#include "playback.h"
|
||||
#include "atomic_file.h"
|
||||
#define CALIBRATION_PREF \
|
||||
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
|
||||
"calibration.json"
|
||||
@@ -52,7 +53,7 @@ static inline CalibrationKind calibration_effective(void) {
|
||||
return calibration_is_g4(tv_model) ? CAL_G4 : CAL_AVERAGE;
|
||||
}
|
||||
static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibration *result) {
|
||||
void *root = j->parse(text);
|
||||
void *root = playback_parse_exact(j, text);
|
||||
if (!root)
|
||||
return false;
|
||||
Calibration next = *result;
|
||||
@@ -102,7 +103,17 @@ static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibrat
|
||||
return valid;
|
||||
}
|
||||
static inline bool calibration_save(const char *path, const Calibration *value) {
|
||||
char temp[512], data[2048];
|
||||
if (!value || value->kind < CAL_AUTO || value->kind > CAL_CUSTOM || value->count > 16 ||
|
||||
(value->count && (value->count < 2 || !calibration_model_valid(value->model))))
|
||||
return false;
|
||||
for (unsigned i = 0; i < value->count; i++) {
|
||||
double x = value->points[i].position;
|
||||
if (!isfinite(x) || x < 0 || x >= 4 || value->points[i].region >= 16 ||
|
||||
(i && x <= value->points[i - 1].position))
|
||||
return false;
|
||||
}
|
||||
|
||||
char data[2048];
|
||||
int used = snprintf(data, sizeof(data), "{\"profile\":\"%s\"", calibration_key(value->kind));
|
||||
if (value->count) {
|
||||
used += snprintf(data + used, sizeof(data) - used, ",\"model\":\"%s\",\"points\":[",
|
||||
@@ -114,19 +125,8 @@ static inline bool calibration_save(const char *path, const Calibration *value)
|
||||
used += snprintf(data + used, sizeof(data) - used, "]");
|
||||
}
|
||||
used += snprintf(data + used, sizeof(data) - used, "}\n");
|
||||
if (snprintf(temp, sizeof(temp), "%s.tmp", path) >= (int)sizeof(temp))
|
||||
return false;
|
||||
int fd = open(temp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
bool ok = write(fd, data, used) == used && fsync(fd) == 0;
|
||||
if (close(fd) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = rename(temp, path) == 0;
|
||||
if (!ok)
|
||||
unlink(temp);
|
||||
return ok;
|
||||
return used > 0 && (size_t)used < sizeof(data) &&
|
||||
atomic_replace(path, data, (size_t)used) != SAVE_UNCHANGED;
|
||||
}
|
||||
static inline void calibration_load(PlaybackJson *j, const char *path) {
|
||||
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||
|
||||
+94
-63
@@ -2,80 +2,115 @@
|
||||
static int tick(void *unused) {
|
||||
(void)unused;
|
||||
uint64_t now = milliseconds();
|
||||
unsigned long acknowledged = receiver_runtime.output.acknowledgements;
|
||||
output_tick(&receiver_runtime.output, &playback_json, now);
|
||||
if (receiver_runtime.output.stage == OUTPUT_FAILED &&
|
||||
(capture_ctx.handle || receiver_runtime.starting)) {
|
||||
char error[128];
|
||||
snprintf(error, sizeof(error), "%s", receiver_runtime.output.error);
|
||||
release_capture(false);
|
||||
set_error(error);
|
||||
retry_update(&retries, boot_enabled() && geteuid() == 0, false, now);
|
||||
retry_failure(&retries, now);
|
||||
}
|
||||
if (receiver_runtime.output.stage == OUTPUT_FAILED && receiver_runtime.clear_pending) {
|
||||
receiver_runtime.clear_pending = false;
|
||||
output_close(&receiver_runtime.output);
|
||||
}
|
||||
if (receiver_runtime.starting && (piccap_owned || capture_source != SOURCE_HISTOGRAM)) {
|
||||
receiver_runtime.starting = false;
|
||||
output_close(&receiver_runtime.output);
|
||||
}
|
||||
if (receiver_runtime.output.acknowledgements > acknowledged && capture_ctx.handle) {
|
||||
capture_ctx.frames += receiver_runtime.output.acknowledgements - acknowledged;
|
||||
capture_ctx.last_output_at = now;
|
||||
last_error[0] = 0;
|
||||
}
|
||||
if (receiver_runtime.clear_pending && receiver_runtime.output.stage == OUTPUT_READY) {
|
||||
receiver_runtime.clear_pending = false;
|
||||
output_stop(&receiver_runtime.output, now, true);
|
||||
} else if (receiver_runtime.starting && receiver_runtime.output.stage == OUTPUT_READY) {
|
||||
free(receiver_layout.blends);
|
||||
receiver_layout.blends = receiver_runtime.output.mapping;
|
||||
receiver_runtime.output.mapping = NULL;
|
||||
receiver_layout.count = receiver_runtime.output.count;
|
||||
receiver_runtime.starting = false;
|
||||
initialize_capture(NULL, NULL);
|
||||
if (!capture_ctx.handle)
|
||||
retry_failure(&retries, now);
|
||||
}
|
||||
model_tick(now);
|
||||
test_tick(&playback_json, now);
|
||||
layout_refresh_tick(&playback_json, now, capture || capture_source == SOURCE_PICCAP);
|
||||
layout_refresh_tick(&playback_json, now, capture_ctx.handle || capture_source == SOURCE_PICCAP,
|
||||
&receiver_layout);
|
||||
stream_tick(&playback_json, now, handover_enabled && !retries.paused && !retries.blocked);
|
||||
bool owned = handover_tick();
|
||||
bridge_tick(now);
|
||||
bridge_tick(now, &capture_ctx);
|
||||
if (owned)
|
||||
return 1;
|
||||
if (!capture) {
|
||||
if (!capture_ctx.handle) {
|
||||
if (receiver_runtime.starting || receiver_runtime.clear_pending ||
|
||||
receiver_runtime.output.closing)
|
||||
return 1;
|
||||
retry_update(&retries, boot_enabled() && geteuid() == 0, false, milliseconds());
|
||||
if (retry_take(&retries, milliseconds())) {
|
||||
start(NULL, NULL, NULL);
|
||||
if (!capture)
|
||||
if (!capture_ctx.handle && !receiver_runtime.starting)
|
||||
retry_failure(&retries, milliseconds());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (revoked) {
|
||||
if (capture_ctx.revoked) {
|
||||
set_error("LG revoked capture; recovering after source transition");
|
||||
capture_recoveries++;
|
||||
recovery_started = milliseconds();
|
||||
capture_ctx.recoveries++;
|
||||
capture_ctx.recovery_started = milliseconds();
|
||||
release_capture(false);
|
||||
retry_source_transition(&retries, milliseconds());
|
||||
return 1;
|
||||
}
|
||||
if (settling) {
|
||||
settling--;
|
||||
if (capture_ctx.settling) {
|
||||
capture_ctx.settling--;
|
||||
return 1;
|
||||
}
|
||||
if (!CALIBRATION_ACTIVE && letterbox_mode && capture_preset == 1 &&
|
||||
milliseconds() >= next_border_probe) {
|
||||
if (vt_preset(capture, 0) != 0) {
|
||||
if (!CALIBRATION_ACTIVE && capture_ctx.letterbox_mode && capture_ctx.preset == 1 &&
|
||||
milliseconds() >= capture_ctx.next_border_probe) {
|
||||
if (vt_preset(capture_ctx.handle, 0) != 0) {
|
||||
set_error("Border probe preset failed");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
capture_preset = 0;
|
||||
settling = 2;
|
||||
capture_ctx.preset = 0;
|
||||
capture_ctx.settling = 2;
|
||||
return 1;
|
||||
}
|
||||
Histogram hist[16] = {0};
|
||||
int rc = vt_get(capture, hist);
|
||||
int rc = vt_get(capture_ctx.handle, hist);
|
||||
bool populated = !rc && histogram_populated(hist);
|
||||
if (!populated) {
|
||||
unsigned char held[16][3];
|
||||
color_valid = false;
|
||||
if (!continuity_frame(&continuity, false, rgb, now, held)) {
|
||||
capture_ctx.color_valid = false;
|
||||
if (!continuity_frame(&capture_ctx.continuity, false, capture_ctx.rgb, now, held)) {
|
||||
set_error("Histogram data unavailable beyond hold window; recovering capture");
|
||||
release_capture(false);
|
||||
retry_source_transition(&retries, now);
|
||||
return 1;
|
||||
}
|
||||
char *line = region_command(held, led_blends, led_count,
|
||||
(unsigned)(1500 - (now - continuity.missing_at)));
|
||||
if (!line || send_line(line)) {
|
||||
char *line = region_command(held, receiver_layout.blends, receiver_layout.count,
|
||||
(unsigned)(1500 - (now - capture_ctx.continuity.missing_at)));
|
||||
if (!line || (!output_frame(&receiver_runtime.output, line))) {
|
||||
free(line);
|
||||
set_error("Receiver connection lost while holding colors");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
free(line);
|
||||
if (!receiver_ack()) {
|
||||
set_error("Receiver did not acknowledge held colors");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
last_output_at = now;
|
||||
return 1;
|
||||
}
|
||||
sample_at = now;
|
||||
sample_sequence++;
|
||||
capture_ctx.sample_at = now;
|
||||
capture_ctx.sample_sequence++;
|
||||
unsigned aggregate[3] = {0};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
black_percent[i] = 100;
|
||||
capture_ctx.black_percent[i] = 100;
|
||||
for (int c = 0; c < 3; c++) {
|
||||
uint64_t total = 0, black = 0;
|
||||
for (int b = 0; b < 256; b++) {
|
||||
@@ -84,67 +119,63 @@ static int tick(void *unused) {
|
||||
black += hist[i].bins[c][b];
|
||||
}
|
||||
unsigned fraction = total ? (unsigned)(100 * black / total) : 0;
|
||||
if (fraction < black_percent[i])
|
||||
black_percent[i] = fraction;
|
||||
if (fraction < capture_ctx.black_percent[i])
|
||||
capture_ctx.black_percent[i] = fraction;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 16; i++)
|
||||
for (int c = 0; c < 3; c++) {
|
||||
rgb[i][c] = histogram_mean(hist[i].bins[c]);
|
||||
aggregate[c] += rgb[i][c];
|
||||
capture_ctx.rgb[i][c] = histogram_mean(hist[i].bins[c]);
|
||||
aggregate[c] += capture_ctx.rgb[i][c];
|
||||
}
|
||||
if (!CALIBRATION_ACTIVE && capture_preset == 0) {
|
||||
int candidate = letterbox_candidate(black_percent, rgb);
|
||||
if (!CALIBRATION_ACTIVE && capture_ctx.preset == 0) {
|
||||
int candidate = letterbox_candidate(capture_ctx.black_percent, capture_ctx.rgb);
|
||||
uint64_t now = milliseconds();
|
||||
if (candidate < 0)
|
||||
pending_letterbox = -1;
|
||||
else if (candidate != pending_letterbox) {
|
||||
pending_letterbox = candidate;
|
||||
candidate_since = now;
|
||||
} else if ((unsigned)candidate != letterbox_mode && now - candidate_since >= 2000)
|
||||
letterbox_mode = (unsigned)candidate;
|
||||
if (letterbox_mode) {
|
||||
if (vt_preset(capture, 1) != 0) {
|
||||
capture_ctx.pending_letterbox = -1;
|
||||
else if (candidate != capture_ctx.pending_letterbox) {
|
||||
capture_ctx.pending_letterbox = candidate;
|
||||
capture_ctx.candidate_since = now;
|
||||
} else if ((unsigned)candidate != capture_ctx.letterbox_mode &&
|
||||
now - capture_ctx.candidate_since >= 2000)
|
||||
capture_ctx.letterbox_mode = (unsigned)candidate;
|
||||
if (capture_ctx.letterbox_mode) {
|
||||
if (vt_preset(capture_ctx.handle, 1) != 0) {
|
||||
set_error("Letterbox preset failed");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
capture_preset = 1;
|
||||
settling = 2;
|
||||
next_border_probe = now + 2000;
|
||||
capture_ctx.preset = 1;
|
||||
capture_ctx.settling = 2;
|
||||
capture_ctx.next_border_probe = now + 2000;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (int c = 0; c < 3; c++)
|
||||
sent_rgb[c] = (unsigned char)(aggregate[c] / 16);
|
||||
capture_ctx.sent_rgb[c] = (unsigned char)(aggregate[c] / 16);
|
||||
dark_color_filter(capture_ctx.sent_rgb);
|
||||
unsigned char boosted_mean[3];
|
||||
boost_color(sent_rgb, boosted_mean);
|
||||
memcpy(sent_rgb, boosted_mean, 3);
|
||||
boost_color(capture_ctx.sent_rgb, boosted_mean);
|
||||
memcpy(capture_ctx.sent_rgb, boosted_mean, 3);
|
||||
unsigned char mapped_rgb[16][3], stable_rgb[16][3];
|
||||
calibration_colors(rgb, mapped_rgb);
|
||||
continuity_frame(&continuity, true, mapped_rgb, now, stable_rgb);
|
||||
interpolation_enabled = led_count && calibration_effective() != CAL_AVERAGE;
|
||||
char *line = region_command(stable_rgb, led_blends, led_count, 1500);
|
||||
calibration_colors(capture_ctx.rgb, mapped_rgb);
|
||||
for (unsigned i = 0; i < 16; i++)
|
||||
dark_color_filter(mapped_rgb[i]);
|
||||
continuity_frame(&capture_ctx.continuity, true, mapped_rgb, now, stable_rgb);
|
||||
receiver_layout.interpolation = receiver_layout.count && calibration_effective() != CAL_AVERAGE;
|
||||
char *line = region_command(stable_rgb, receiver_layout.blends, receiver_layout.count, 1500);
|
||||
if (!line) {
|
||||
set_error("Color buffer allocation failed");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
int failed = send_line(line);
|
||||
int failed = (!output_frame(&receiver_runtime.output, line));
|
||||
free(line);
|
||||
if (failed) {
|
||||
set_error("Receiver connection lost");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
if (!receiver_ack()) {
|
||||
set_error("Receiver did not acknowledge color data");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
last_error[0] = 0;
|
||||
color_valid = true;
|
||||
last_output_at = milliseconds();
|
||||
frames++;
|
||||
capture_ctx.color_valid = true;
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef HYPERDRM_DARK_COLOR_H
|
||||
#define HYPERDRM_DARK_COLOR_H
|
||||
/* Suppress near-black histogram residuals without changing channel ratios.
|
||||
* Apply only to fresh histogram colors, before continuity and interpolation. */
|
||||
static inline void dark_color_filter(unsigned char color[3]) {
|
||||
unsigned peak = color[0];
|
||||
for (unsigned c = 1; c < 3; c++)
|
||||
if (color[c] > peak)
|
||||
peak = color[c];
|
||||
if (peak >= 32)
|
||||
return;
|
||||
unsigned gain = peak > 16 ? peak - 16 : 0;
|
||||
for (unsigned c = 0; c < 3; c++)
|
||||
color[c] = (unsigned char)((color[c] * gain + 8) / 16);
|
||||
}
|
||||
#endif
|
||||
@@ -135,13 +135,17 @@ static void probe_tick(ReceiverProbe *probe, PlaybackJson *json, uint64_t now) {
|
||||
probe_finish(probe, "Receiver closed the test connection", now);
|
||||
return;
|
||||
}
|
||||
if (memchr(probe->response + probe->used, 0, (size_t)n)) {
|
||||
probe_finish(probe, "Receiver sent an embedded NUL", now);
|
||||
return;
|
||||
}
|
||||
probe->used += (size_t)n;
|
||||
probe->response[probe->used] = 0;
|
||||
char *end = strchr(probe->response, '\n');
|
||||
if (!end)
|
||||
return;
|
||||
*end = 0;
|
||||
void *root = json->parse(probe->response);
|
||||
void *root = playback_parse_exact(json, probe->response);
|
||||
bool success = playback_true(json, root, "success");
|
||||
if (root)
|
||||
json->put(root);
|
||||
|
||||
+2
-12
@@ -1,6 +1,7 @@
|
||||
#ifndef HYPERDRM_HANDOVER_H
|
||||
#define HYPERDRM_HANDOVER_H
|
||||
#include "playback.h"
|
||||
#include "atomic_file.h"
|
||||
#ifndef CALIBRATION_ACTIVE
|
||||
#define CALIBRATION_ACTIVE false
|
||||
#endif
|
||||
@@ -43,18 +44,7 @@ static inline bool piccap_installed(void) {
|
||||
}
|
||||
static bool handover_save(bool enabled) {
|
||||
const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n";
|
||||
int fd = open(HANDOVER_PREF ".tmp", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
ssize_t n = write(fd, data, strlen(data));
|
||||
bool ok = n == (ssize_t)strlen(data) && fsync(fd) == 0;
|
||||
if (close(fd) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = rename(HANDOVER_PREF ".tmp", HANDOVER_PREF) == 0;
|
||||
if (!ok)
|
||||
unlink(HANDOVER_PREF ".tmp");
|
||||
return ok;
|
||||
return atomic_replace(HANDOVER_PREF, data, strlen(data)) != SAVE_UNCHANGED;
|
||||
}
|
||||
static void handover_load(void) {
|
||||
int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||
|
||||
+11
-9
@@ -10,16 +10,18 @@ static void layout_refresh_reset(uint64_t due) {
|
||||
layout_probe.stage = TEST_IDLE;
|
||||
next_layout_check = due;
|
||||
}
|
||||
static bool layout_mapping_changed(const LedBlend *next, unsigned count) {
|
||||
if (count != led_count || !led_blends)
|
||||
static bool layout_mapping_changed(const ReceiverLayoutContext *layout, const LedBlend *next,
|
||||
unsigned count) {
|
||||
if (count != layout->count || !layout->blends)
|
||||
return true;
|
||||
for (unsigned i = 0; i < count; i++)
|
||||
if (next[i].a != led_blends[i].a || next[i].b != led_blends[i].b ||
|
||||
next[i].weight != led_blends[i].weight)
|
||||
if (next[i].a != layout->blends[i].a || next[i].b != layout->blends[i].b ||
|
||||
next[i].weight != layout->blends[i].weight)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running) {
|
||||
static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running,
|
||||
ReceiverLayoutContext *layout) {
|
||||
if (!running) {
|
||||
layout_refresh_reset(now + LAYOUT_REFRESH_MS);
|
||||
return;
|
||||
@@ -27,11 +29,11 @@ static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running)
|
||||
probe_tick(&layout_probe, json, now);
|
||||
if (layout_probe.stage == TEST_DONE) {
|
||||
if (layout_probe_generation == calibration_generation) {
|
||||
if (layout_mapping_changed(layout_probe.mapping, layout_probe.leds)) {
|
||||
free(led_blends);
|
||||
led_blends = layout_probe.mapping;
|
||||
if (layout_mapping_changed(layout, layout_probe.mapping, layout_probe.leds)) {
|
||||
free(layout->blends);
|
||||
layout->blends = layout_probe.mapping;
|
||||
layout_probe.mapping = NULL;
|
||||
led_count = layout_probe.leds;
|
||||
layout->count = layout_probe.leds;
|
||||
layout_updates++;
|
||||
}
|
||||
last_layout_check = now;
|
||||
|
||||
+38
-1
@@ -14,6 +14,11 @@ typedef struct {
|
||||
const char *(*string)(const void *);
|
||||
int (*boolean)(const void *);
|
||||
int (*put)(void *);
|
||||
void *(*tokener_new)(void);
|
||||
void *(*tokener_parse)(void *, const char *, int);
|
||||
int (*tokener_error)(void *);
|
||||
size_t (*tokener_end)(void *);
|
||||
void (*tokener_free)(void *);
|
||||
} PlaybackJson;
|
||||
static inline bool playback_json_init(PlaybackJson *j) {
|
||||
j->lib = dlopen("libjson-c.so.5", RTLD_NOW);
|
||||
@@ -27,14 +32,46 @@ static inline bool playback_json_init(PlaybackJson *j) {
|
||||
j->string = dlsym(j->lib, "json_object_get_string");
|
||||
j->boolean = dlsym(j->lib, "json_object_get_boolean");
|
||||
j->put = dlsym(j->lib, "json_object_put");
|
||||
j->tokener_new = dlsym(j->lib, "json_tokener_new");
|
||||
j->tokener_parse = dlsym(j->lib, "json_tokener_parse_ex");
|
||||
j->tokener_error = dlsym(j->lib, "json_tokener_get_error");
|
||||
j->tokener_end = dlsym(j->lib, "json_tokener_get_parse_end");
|
||||
j->tokener_free = dlsym(j->lib, "json_tokener_free");
|
||||
if (!j->parse || !j->get || !j->type || !j->length || !j->at || !j->string || !j->boolean ||
|
||||
!j->put) {
|
||||
!j->put || !j->tokener_new || !j->tokener_parse || !j->tokener_error || !j->tokener_end ||
|
||||
!j->tokener_free) {
|
||||
dlclose(j->lib);
|
||||
j->lib = NULL;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/* Require exactly one complete JSON value. json_tokener_parse alone can accept
|
||||
* an otherwise valid prefix followed by unrelated or truncated content. */
|
||||
static inline void *playback_parse_exact(PlaybackJson *j, const char *text) {
|
||||
if (!j || !text || !j->tokener_new || !j->tokener_parse || !j->tokener_error ||
|
||||
!j->tokener_end || !j->tokener_free || !j->put)
|
||||
return NULL;
|
||||
size_t length = strlen(text);
|
||||
if (length > 2 * 1024 * 1024)
|
||||
return NULL;
|
||||
void *tokener = j->tokener_new();
|
||||
if (!tokener)
|
||||
return NULL;
|
||||
void *root = j->tokener_parse(tokener, text, (int)length + 1);
|
||||
size_t end = j->tokener_end(tokener);
|
||||
bool valid = j->tokener_error(tokener) == 0;
|
||||
while (end < length &&
|
||||
(text[end] == ' ' || text[end] == '\t' || text[end] == '\r' || text[end] == '\n'))
|
||||
end++;
|
||||
valid = valid && end == length;
|
||||
j->tokener_free(tokener);
|
||||
if (!valid && root) {
|
||||
j->put(root);
|
||||
root = NULL;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
static inline void *playback_field(PlaybackJson *j, void *o, const char *key) {
|
||||
void *v = NULL;
|
||||
return o && j->type(o) == 4 && j->get(o, key, &v) ? v : NULL;
|
||||
|
||||
+56
-23
@@ -7,28 +7,61 @@
|
||||
#include <stdarg.h>
|
||||
#include "histogram.h"
|
||||
/* Process-local diagnostic sink. It changes logging only, not capture policy. */
|
||||
void printLog(const char *context,int level,const char *message_id,
|
||||
const char *file,const char *function,int line,const char *format,...) {
|
||||
(void)context;(void)level;(void)message_id;(void)file;
|
||||
fprintf(stderr,"LG %s:%d: ",function?function:"?",line);
|
||||
if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);}fputc('\n',stderr);
|
||||
}
|
||||
static bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; }
|
||||
int main(void) {
|
||||
void *lib=dlopen("libvtcapture.so.1",RTLD_NOW);
|
||||
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;}
|
||||
void *(*init)(bool(*)(void))=dlsym(lib,"vtCapture_initHistogram");
|
||||
int (*preset)(void*,int)=dlsym(lib,"vtCapture_setPresetHistogram");
|
||||
int (*get)(void*,Histogram*)=dlsym(lib,"vtCapture_getHistogram");
|
||||
int (*finish)(void*)=dlsym(lib,"vtCapture_finalizeHistogram");
|
||||
if(!init||!preset||!get||!finish)return 2;
|
||||
void *handle=init(policy); fprintf(stderr,"handle=%p\n",handle);
|
||||
if(!handle)return 3;
|
||||
int rc=preset(handle,0);fprintf(stderr,"preset=%d\n",rc);
|
||||
Histogram hist[16];
|
||||
for(int frame=0;rc==0 && frame<3;frame++) {
|
||||
usleep(100000); rc=get(handle,hist); printf("frame=%d rc=%d\n",frame,rc);
|
||||
if(rc==0)for(int i=0;i<16;i++) {printf("region=%u",hist[i].id);for(int c=0;c<3;c++){uint64_t sum=0;for(int b=0;b<256;b++)sum+=hist[i].bins[c][b];printf(" ch%d_count=%llu mean=%u",c,(unsigned long long)sum,histogram_mean(hist[i].bins[c]));} puts("");}
|
||||
void printLog(const char *context, int level, const char *message_id, const char *file,
|
||||
const char *function, int line, const char *format, ...) {
|
||||
(void)context;
|
||||
(void)level;
|
||||
(void)message_id;
|
||||
(void)file;
|
||||
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
||||
if (format) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(stderr, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
fprintf(stderr,"finalize=%d\n",finish(handle)); return rc?4:0;
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
static bool policy(void) {
|
||||
fprintf(stderr, "Capture revoked by LG policy\n");
|
||||
return true;
|
||||
}
|
||||
int main(void) {
|
||||
void *lib = dlopen("libvtcapture.so.1", RTLD_NOW);
|
||||
if (!lib) {
|
||||
fprintf(stderr, "dlopen: %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
void *(*init)(bool (*)(void)) = dlsym(lib, "vtCapture_initHistogram");
|
||||
int (*preset)(void *, int) = dlsym(lib, "vtCapture_setPresetHistogram");
|
||||
int (*get)(void *, Histogram *) = dlsym(lib, "vtCapture_getHistogram");
|
||||
int (*finish)(void *) = dlsym(lib, "vtCapture_finalizeHistogram");
|
||||
if (!init || !preset || !get || !finish)
|
||||
return 2;
|
||||
void *handle = init(policy);
|
||||
fprintf(stderr, "handle=%p\n", handle);
|
||||
if (!handle)
|
||||
return 3;
|
||||
int rc = preset(handle, 0);
|
||||
fprintf(stderr, "preset=%d\n", rc);
|
||||
Histogram hist[16];
|
||||
for (int frame = 0; rc == 0 && frame < 3; frame++) {
|
||||
usleep(100000);
|
||||
rc = get(handle, hist);
|
||||
printf("frame=%d rc=%d\n", frame, rc);
|
||||
if (rc == 0)
|
||||
for (int i = 0; i < 16; i++) {
|
||||
printf("region=%u", hist[i].id);
|
||||
for (int c = 0; c < 3; c++) {
|
||||
uint64_t sum = 0;
|
||||
for (int b = 0; b < 256; b++)
|
||||
sum += hist[i].bins[c][b];
|
||||
printf(" ch%d_count=%llu mean=%u", c, (unsigned long long)sum,
|
||||
histogram_mean(hist[i].bins[c]));
|
||||
}
|
||||
puts("");
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "finalize=%d\n", finish(handle));
|
||||
return rc ? 4 : 0;
|
||||
}
|
||||
@@ -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);
|
||||
return;
|
||||
}
|
||||
if (n > 0 && memchr(stream.data + stream.used, 0, (size_t)n)) {
|
||||
stream_close(now);
|
||||
return;
|
||||
}
|
||||
if (n > 0)
|
||||
stream.used += (size_t)n;
|
||||
stream.data[stream.used] = 0;
|
||||
@@ -189,7 +193,7 @@ static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) {
|
||||
if (!end)
|
||||
break;
|
||||
*end = 0;
|
||||
void *root = j->parse(stream.data);
|
||||
void *root = playback_parse_exact(j, stream.data);
|
||||
if (stream.stage == STREAM_AUTHENTICATING) {
|
||||
if (!playback_true(j, root, "success")) {
|
||||
if (root)
|
||||
|
||||
@@ -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
|
||||
+27
-144
@@ -29,6 +29,7 @@
|
||||
#include "spatial_image.h"
|
||||
#include "diagnostics.h"
|
||||
#include "continuity.h"
|
||||
#include "service_context.h"
|
||||
#define SERVICE "org.webosbrew.histogramhyperion.service"
|
||||
#define PRIORITY 149
|
||||
typedef struct {
|
||||
@@ -52,20 +53,21 @@ static bool (*error_init)(LSError *);
|
||||
static void (*error_free)(LSError *);
|
||||
static void *(*vt_init)(bool (*)(void));
|
||||
static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *);
|
||||
static void *capture;
|
||||
static char lg_diagnostic[256];
|
||||
static CaptureContext capture_ctx = {.pending_letterbox = -1};
|
||||
static ReceiverLayoutContext receiver_layout;
|
||||
static CalibrationSessionContext calibration_ctx;
|
||||
/* Process-local LG diagnostics; no changes to the loaded LG library. */
|
||||
void printLog(const char *context, int level, const char *message_id, const char *file,
|
||||
const char *function, int line, const char *format, ...) {
|
||||
(void)context;
|
||||
(void)message_id;
|
||||
(void)file;
|
||||
if (capture && level > 3)
|
||||
if (capture_ctx.handle && level > 3)
|
||||
return;
|
||||
if (format) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(lg_diagnostic, sizeof(lg_diagnostic), format, args);
|
||||
vsnprintf(capture_ctx.diagnostic, sizeof(capture_ctx.diagnostic), format, args);
|
||||
va_end(args);
|
||||
}
|
||||
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
||||
@@ -77,36 +79,20 @@ void printLog(const char *context, int level, const char *message_id, const char
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
static int output = -1;
|
||||
static unsigned long frames;
|
||||
static unsigned char rgb[16][3];
|
||||
static unsigned char sent_rgb[3];
|
||||
static unsigned black_percent[16];
|
||||
static unsigned capture_preset;
|
||||
static unsigned letterbox_mode, settling;
|
||||
static int pending_letterbox = -1;
|
||||
static uint64_t candidate_since, next_border_probe;
|
||||
static bool color_valid;
|
||||
static Continuity continuity;
|
||||
static uint64_t sample_at, calibration_until;
|
||||
static unsigned sample_sequence;
|
||||
#define CALIBRATION_ACTIVE (calibration_until > milliseconds())
|
||||
static unsigned led_count;
|
||||
static LedBlend *led_blends;
|
||||
static bool interpolation_enabled;
|
||||
#define CALIBRATION_ACTIVE (calibration_ctx.until > milliseconds())
|
||||
static RetryState retries;
|
||||
static unsigned capture_recoveries;
|
||||
static uint64_t recovery_started, last_recovery_ms, last_output_at;
|
||||
static uint64_t milliseconds(void) {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||
}
|
||||
static char last_error[256];
|
||||
static volatile sig_atomic_t revoked, quitting;
|
||||
static volatile sig_atomic_t quitting;
|
||||
static int guard_pipe = -1;
|
||||
static pid_t guard_pid = -1;
|
||||
#ifndef DEV_MODE
|
||||
#define DEV_MODE "/var/luna/preferences/vt_devmode"
|
||||
#endif
|
||||
/* LG's developer gate is enabled only during initialization. A separate
|
||||
* guardian removes our own marker even if this process aborts or is killed.
|
||||
* An existing marker is preserved, as are files replaced by another owner. */
|
||||
@@ -169,135 +155,30 @@ static void quit_signal(int s) {
|
||||
quitting = 1;
|
||||
}
|
||||
static bool policy(void) {
|
||||
revoked = 1;
|
||||
capture_ctx.revoked = 1;
|
||||
return true;
|
||||
}
|
||||
static void set_error(const char *s) {
|
||||
snprintf(last_error, sizeof(last_error), "%s", s);
|
||||
fprintf(stderr, "%s\n", s);
|
||||
}
|
||||
static int send_line(const char *line) {
|
||||
size_t left = strlen(line);
|
||||
const char *p = line;
|
||||
while (left) {
|
||||
ssize_t n = send(output, p, left, MSG_NOSIGNAL);
|
||||
if (n <= 0)
|
||||
return -1;
|
||||
p += n;
|
||||
left -= n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static bool receiver_ack(void) {
|
||||
char answer[4096];
|
||||
size_t used = 0;
|
||||
bool complete = false;
|
||||
while (used + 1 < sizeof(answer)) {
|
||||
ssize_t n = recv(output, answer + used, sizeof(answer) - used - 1, 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
used += (size_t)n;
|
||||
answer[used] = 0;
|
||||
if (strchr(answer, '\n')) {
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
PlaybackJson json = {0};
|
||||
if (!complete || !playback_json_init(&json))
|
||||
return false;
|
||||
void *root = json.parse(answer);
|
||||
bool ok = playback_true(&json, root, "success");
|
||||
if (root)
|
||||
json.put(root);
|
||||
dlclose(json.lib);
|
||||
return ok;
|
||||
}
|
||||
static bool read_layout(void) {
|
||||
const size_t capacity = 2 * 1024 * 1024;
|
||||
char *data = malloc(capacity + 1);
|
||||
if (!data)
|
||||
return false;
|
||||
bool complete = false;
|
||||
size_t used = 0;
|
||||
if (send_line("{\"command\":\"serverinfo\"}\n") == 0) {
|
||||
while (used < capacity) {
|
||||
ssize_t n = recv(output, data + used, capacity - used, 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
used += (size_t)n;
|
||||
data[used] = 0;
|
||||
char *end = strchr(data, '\n');
|
||||
if (end) {
|
||||
*end = 0;
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LedBlend *mapping = NULL;
|
||||
unsigned count = 0;
|
||||
bool ok = complete && layout_from_json(data, &mapping, &count);
|
||||
if (ok) {
|
||||
free(led_blends);
|
||||
led_blends = mapping;
|
||||
led_count = count;
|
||||
}
|
||||
free(data);
|
||||
interpolation_enabled = ok && calibration_effective() != CAL_AVERAGE;
|
||||
return ok;
|
||||
}
|
||||
static bool target_login(void) {
|
||||
if (!target.token[0])
|
||||
return true;
|
||||
char request[384], answer[4096];
|
||||
size_t used = 0;
|
||||
snprintf(request, sizeof(request),
|
||||
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
|
||||
target.token);
|
||||
if (send_line(request) != 0)
|
||||
return false;
|
||||
while (used + 1 < sizeof(answer)) {
|
||||
ssize_t n = recv(output, answer + used, sizeof(answer) - used - 1, 0);
|
||||
if (n <= 0)
|
||||
return false;
|
||||
used += (size_t)n;
|
||||
answer[used] = 0;
|
||||
if (strchr(answer, '\n'))
|
||||
break;
|
||||
}
|
||||
PlaybackJson json = {0};
|
||||
if (!playback_json_init(&json))
|
||||
return false;
|
||||
void *root = json.parse(answer);
|
||||
bool ok = playback_true(&json, root, "success");
|
||||
if (root)
|
||||
json.put(root);
|
||||
dlclose(json.lib);
|
||||
return ok;
|
||||
}
|
||||
static ReceiverRuntime receiver_runtime = {.output = {.fd = -1}};
|
||||
static void layout_refresh_reset(uint64_t due);
|
||||
static void release_capture(bool clear_output) {
|
||||
layout_refresh_reset(milliseconds() + 5000);
|
||||
free(led_blends);
|
||||
led_blends = NULL;
|
||||
led_count = 0;
|
||||
interpolation_enabled = false;
|
||||
color_valid = false;
|
||||
memset(rgb, 0, sizeof(rgb));
|
||||
memset(sent_rgb, 0, sizeof(sent_rgb));
|
||||
if (output >= 0) {
|
||||
if (clear_output && frames) {
|
||||
char line[80];
|
||||
snprintf(line, sizeof(line), "{\"command\":\"clear\",\"priority\":%d}\n", PRIORITY);
|
||||
send_line(line);
|
||||
}
|
||||
close(output);
|
||||
output = -1;
|
||||
}
|
||||
if (capture) {
|
||||
vt_finish(capture);
|
||||
capture = NULL;
|
||||
free(receiver_layout.blends);
|
||||
receiver_layout.blends = NULL;
|
||||
receiver_layout.count = 0;
|
||||
receiver_layout.interpolation = false;
|
||||
capture_ctx.color_valid = false;
|
||||
memset(capture_ctx.rgb, 0, sizeof(capture_ctx.rgb));
|
||||
memset(capture_ctx.sent_rgb, 0, sizeof(capture_ctx.sent_rgb));
|
||||
receiver_runtime.starting = false;
|
||||
receiver_runtime.clear_pending = false;
|
||||
output_stop(&receiver_runtime.output, milliseconds(), clear_output);
|
||||
if (capture_ctx.handle) {
|
||||
vt_finish(capture_ctx.handle);
|
||||
capture_ctx.handle = NULL;
|
||||
}
|
||||
}
|
||||
static void stop_capture(void) {
|
||||
@@ -318,6 +199,7 @@ static bool respond(void *h, void *m, const char *s) {
|
||||
#include "receiver_stream.h"
|
||||
#include "source_bridge.h"
|
||||
#include "service_api.h"
|
||||
#include "dark_color.h"
|
||||
#include "capture_loop.h"
|
||||
static void *symbol(void *lib, const char *name) {
|
||||
void *s = dlsym(lib, name);
|
||||
@@ -396,6 +278,7 @@ int main(void) {
|
||||
stream_close(milliseconds());
|
||||
test_close();
|
||||
stop_capture();
|
||||
output_close(&receiver_runtime.output);
|
||||
if (piccap_owned) {
|
||||
handover_enabled = false;
|
||||
retry_pause(&retries);
|
||||
|
||||
+35
-41
@@ -20,28 +20,27 @@ static bool start(void *h, void *m, void *ctx) {
|
||||
retry_reset(&retries);
|
||||
if (piccap_owned)
|
||||
return respond(h, m, "{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
|
||||
if (capture)
|
||||
if (capture_ctx.handle)
|
||||
return respond(h, m, "{\"returnValue\":true,\"running\":true}");
|
||||
frames = 0;
|
||||
revoked = 0;
|
||||
if (!playback_json.lib) {
|
||||
set_error("JSON-C is unavailable; cannot start receiver output");
|
||||
return start_failure(h, m, "JSON-C is unavailable");
|
||||
}
|
||||
capture_ctx.frames = 0;
|
||||
capture_ctx.revoked = 0;
|
||||
last_error[0] = 0;
|
||||
lg_diagnostic[0] = 0;
|
||||
output = target_connect(&target);
|
||||
if (output < 0) {
|
||||
stop_capture();
|
||||
set_error("Cannot connect to configured receiver IP and JSON port");
|
||||
return start_failure(h, m, "Configured receiver is unavailable");
|
||||
}
|
||||
if (!target_login()) {
|
||||
stop_capture();
|
||||
set_error("Receiver token login failed; check the saved API token");
|
||||
return start_failure(h, m, "Receiver authentication failed");
|
||||
}
|
||||
if (!read_layout()) {
|
||||
stop_capture();
|
||||
set_error("Cannot read receiver LED layout; check JSON port and API authorization");
|
||||
return start_failure(h, m, "Receiver layout unavailable; check API authorization");
|
||||
capture_ctx.diagnostic[0] = 0;
|
||||
if (receiver_runtime.starting)
|
||||
return respond(h, m, "{\"returnValue\":true,\"connecting\":true}");
|
||||
receiver_runtime.clear_pending = false;
|
||||
if (!output_begin(&receiver_runtime.output, &target, milliseconds())) {
|
||||
set_error(receiver_runtime.output.error);
|
||||
return start_failure(h, m, receiver_runtime.output.error);
|
||||
}
|
||||
receiver_runtime.starting = true;
|
||||
return respond(h, m, "{\"returnValue\":true,\"connecting\":true}");
|
||||
}
|
||||
static bool initialize_capture(void *h, void *m) {
|
||||
if (!enter_developer_mode()) {
|
||||
stop_capture();
|
||||
set_error("Developer initialization needs root. Elevate this own service through Homebrew "
|
||||
@@ -49,54 +48,49 @@ static bool start(void *h, void *m, void *ctx) {
|
||||
return start_failure(h, m,
|
||||
"Root access is required for temporary LG developer initialization");
|
||||
}
|
||||
capture = vt_init(policy);
|
||||
capture_ctx.handle = vt_init(policy);
|
||||
if (!leave_developer_mode()) {
|
||||
stop_capture();
|
||||
retries.blocked = true;
|
||||
set_error("Developer marker cleanup failed; manual intervention required");
|
||||
return start_failure(h, m, "Developer marker cleanup failed");
|
||||
}
|
||||
if (!capture) {
|
||||
if (!capture_ctx.handle) {
|
||||
stop_capture();
|
||||
set_error("LG refused histogram initialization");
|
||||
return start_failure(
|
||||
h, m,
|
||||
"LG refused histogram initialization. Another capture source may hold VT_CAPTURE.");
|
||||
}
|
||||
if (vt_preset(capture, 0) != 0) {
|
||||
if (vt_preset(capture_ctx.handle, 0) != 0) {
|
||||
stop_capture();
|
||||
set_error("LG histogram preset initialization failed");
|
||||
return start_failure(h, m, "Histogram preset failed");
|
||||
}
|
||||
capture_preset = letterbox_mode = settling = 0;
|
||||
pending_letterbox = -1;
|
||||
next_border_probe = 0;
|
||||
capture_ctx.preset = capture_ctx.letterbox_mode = capture_ctx.settling = 0;
|
||||
capture_ctx.pending_letterbox = -1;
|
||||
capture_ctx.next_border_probe = 0;
|
||||
retries.due = 0;
|
||||
retries.attempts = 0;
|
||||
retries.quick_retries = 0;
|
||||
if (recovery_started) {
|
||||
last_recovery_ms = milliseconds() - recovery_started;
|
||||
recovery_started = 0;
|
||||
if (capture_ctx.recovery_started) {
|
||||
capture_ctx.last_recovery_ms = milliseconds() - capture_ctx.recovery_started;
|
||||
capture_ctx.recovery_started = 0;
|
||||
}
|
||||
return respond(h, m, "{\"returnValue\":true,\"running\":true}");
|
||||
}
|
||||
static bool stop(void *h, void *m, void *ctx) {
|
||||
(void)ctx;
|
||||
calibration_until = 0;
|
||||
calibration_ctx.until = 0;
|
||||
retry_pause(&retries);
|
||||
bool bridging = bridge_phase != BRIDGE_IDLE;
|
||||
stream_close(milliseconds());
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
memset(&continuity, 0, sizeof(continuity));
|
||||
memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
|
||||
stop_capture();
|
||||
if (bridging) {
|
||||
output = target_connect(&target);
|
||||
if (output >= 0) {
|
||||
if (target_login())
|
||||
send_line("{\"command\":\"clear\",\"priority\":149}\n");
|
||||
close(output);
|
||||
output = -1;
|
||||
}
|
||||
if (bridging && receiver_runtime.output.stage != OUTPUT_READY) {
|
||||
output_begin(&receiver_runtime.output, &target, milliseconds());
|
||||
receiver_runtime.clear_pending = true;
|
||||
}
|
||||
return respond(h, m, "{\"returnValue\":true,\"running\":false}");
|
||||
}
|
||||
@@ -122,7 +116,7 @@ static bool autostart(void *h, void *m, void *ctx) {
|
||||
"the installed boot script and whether another file occupies the hook path.\"}");
|
||||
if (enabled)
|
||||
retry_reset(&retries);
|
||||
retry_update(&retries, enabled, capture != NULL, milliseconds());
|
||||
retry_update(&retries, enabled, capture_ctx.handle != NULL, milliseconds());
|
||||
return respond(h, m,
|
||||
enabled ? "{\"returnValue\":true,\"autostart\":true}"
|
||||
: "{\"returnValue\":true,\"autostart\":false}");
|
||||
@@ -172,11 +166,11 @@ static bool settings(void *h, void *m, void *ctx) {
|
||||
if (!target_save(TARGET_PREF, &next))
|
||||
return respond(
|
||||
h, m, "{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}");
|
||||
bool active = capture || piccap_owned || retries.due;
|
||||
bool active = capture_ctx.handle || receiver_runtime.starting || piccap_owned || retries.due;
|
||||
stop_capture();
|
||||
stream_close(milliseconds());
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
memset(&continuity, 0, sizeof(continuity));
|
||||
memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
|
||||
test_close();
|
||||
connection_test.stage = TEST_IDLE;
|
||||
target = next;
|
||||
|
||||
@@ -3,24 +3,22 @@ static bool calibration_session(void *h, void *m, void *ctx) {
|
||||
(void)ctx;
|
||||
const char *p = payload(m);
|
||||
if (!strcmp(p, "{\"enabled\":true}")) {
|
||||
calibration_until = milliseconds() + 110000;
|
||||
if (capture && vt_preset(capture, 0) != 0) {
|
||||
calibration_until = 0;
|
||||
calibration_ctx.until = milliseconds() + 110000;
|
||||
if (capture_ctx.handle && vt_preset(capture_ctx.handle, 0) != 0) {
|
||||
calibration_ctx.until = 0;
|
||||
return respond(h, m,
|
||||
"{\"returnValue\":false,\"errorText\":\"Cannot select full-screen "
|
||||
"calibration capture\"}");
|
||||
}
|
||||
capture_preset = letterbox_mode = 0;
|
||||
settling = 2;
|
||||
pending_letterbox = -1;
|
||||
capture_ctx.preset = capture_ctx.letterbox_mode = 0;
|
||||
capture_ctx.settling = 2;
|
||||
capture_ctx.pending_letterbox = -1;
|
||||
} else if (!strcmp(p, "{\"enabled\":false}"))
|
||||
calibration_until = 0;
|
||||
calibration_ctx.until = 0;
|
||||
else
|
||||
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||
return respond(h, m, "{\"returnValue\":true}");
|
||||
}
|
||||
static Calibration calibration_draft;
|
||||
static char calibration_message[256];
|
||||
static bool report_calibration(void *h, void *m, void *ctx) {
|
||||
if (!playback_json.lib)
|
||||
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}");
|
||||
@@ -28,9 +26,9 @@ static bool report_calibration(void *h, void *m, void *ctx) {
|
||||
void *root = playback_json.parse(payload(m)),
|
||||
*message = playback_field(&playback_json, root, "message");
|
||||
bool valid = message && playback_json.type(message) == 6 &&
|
||||
strlen(playback_json.string(message)) < sizeof(calibration_message);
|
||||
strlen(playback_json.string(message)) < sizeof(calibration_ctx.message);
|
||||
if (valid)
|
||||
snprintf(calibration_message, sizeof(calibration_message), "%s",
|
||||
snprintf(calibration_ctx.message, sizeof(calibration_ctx.message), "%s",
|
||||
playback_json.string(message));
|
||||
if (root)
|
||||
playback_json.put(root);
|
||||
@@ -46,17 +44,14 @@ static bool draft_calibration(void *h, void *m, void *ctx) {
|
||||
next.kind != CAL_CUSTOM || next.count < 4 || strcmp(next.model, tv_model))
|
||||
return respond(h, m,
|
||||
"{\"returnValue\":false,\"errorText\":\"Invalid measured model profile\"}");
|
||||
calibration_draft = next;
|
||||
calibration_ctx.draft = next;
|
||||
return respond(h, m, "{\"returnValue\":true}");
|
||||
}
|
||||
static bool model_busy;
|
||||
static unsigned long model_token;
|
||||
static uint64_t model_deadline, next_model_probe;
|
||||
static bool model_reply(void *h, void *m, void *ctx) {
|
||||
(void)h;
|
||||
(void)ctx;
|
||||
model_busy = false;
|
||||
model_token = 0;
|
||||
calibration_ctx.model_busy = false;
|
||||
calibration_ctx.model_token = 0;
|
||||
void *root = playback_json.parse(payload(m)),
|
||||
*model = playback_field(&playback_json, root, "modelName");
|
||||
if (playback_true(&playback_json, root, "returnValue") && model &&
|
||||
@@ -70,23 +65,23 @@ static bool model_reply(void *h, void *m, void *ctx) {
|
||||
return true;
|
||||
}
|
||||
static void model_tick(uint64_t now) {
|
||||
if (model_busy && now >= model_deadline) {
|
||||
if (calibration_ctx.model_busy && now >= calibration_ctx.model_deadline) {
|
||||
LSError e;
|
||||
error_init(&e);
|
||||
cancel_call(service_handle, model_token, &e);
|
||||
cancel_call(service_handle, calibration_ctx.model_token, &e);
|
||||
error_free(&e);
|
||||
model_busy = false;
|
||||
model_token = 0;
|
||||
calibration_ctx.model_busy = false;
|
||||
calibration_ctx.model_token = 0;
|
||||
}
|
||||
if (!tv_model[0] && !model_busy && now >= next_model_probe) {
|
||||
if (!tv_model[0] && !calibration_ctx.model_busy && now >= calibration_ctx.next_model_probe) {
|
||||
LSError e;
|
||||
error_init(&e);
|
||||
model_busy =
|
||||
call_once(service_handle, "luna://com.webos.service.tv.systemproperty/getSystemInfo",
|
||||
"{\"keys\":[\"modelName\"]}", model_reply, NULL, &model_token, &e);
|
||||
calibration_ctx.model_busy = call_once(
|
||||
service_handle, "luna://com.webos.service.tv.systemproperty/getSystemInfo",
|
||||
"{\"keys\":[\"modelName\"]}", model_reply, NULL, &calibration_ctx.model_token, &e);
|
||||
error_free(&e);
|
||||
model_deadline = now + 3000;
|
||||
next_model_probe = now + 60000;
|
||||
calibration_ctx.model_deadline = now + 3000;
|
||||
calibration_ctx.next_model_probe = now + 60000;
|
||||
}
|
||||
}
|
||||
static bool set_calibration(void *h, void *m, void *ctx) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef HYPERDRM_SERVICE_CONTEXT_H
|
||||
#define HYPERDRM_SERVICE_CONTEXT_H
|
||||
#include "runtime_context.h"
|
||||
#include "receiver_output.h"
|
||||
/* Runtime ownership is separate from the saved calibration configuration. */
|
||||
typedef struct {
|
||||
uint64_t until, model_deadline, next_model_probe;
|
||||
unsigned long model_token;
|
||||
bool model_busy;
|
||||
Calibration draft;
|
||||
char message[256];
|
||||
} CalibrationSessionContext;
|
||||
typedef struct {
|
||||
ReceiverOutput output;
|
||||
bool starting, clear_pending;
|
||||
} ReceiverRuntime;
|
||||
#endif
|
||||
+37
-31
@@ -16,19 +16,20 @@ static bool status(void *h, void *m, void *ctx) {
|
||||
uint64_t now = milliseconds();
|
||||
char report[512];
|
||||
size_t k = 0;
|
||||
for (size_t i = 0; calibration_message[i] && k + 2 < sizeof(report); i++) {
|
||||
char c = calibration_message[i];
|
||||
for (size_t i = 0; calibration_ctx.message[i] && k + 2 < sizeof(report); i++) {
|
||||
char c = calibration_ctx.message[i];
|
||||
if (c == '"' || c == '\\')
|
||||
report[k++] = '\\';
|
||||
if ((unsigned char)c >= 32)
|
||||
report[k++] = c;
|
||||
}
|
||||
report[k] = 0;
|
||||
int used = snprintf(json, sizeof(json),
|
||||
"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
|
||||
int used = snprintf(
|
||||
json, sizeof(json),
|
||||
"{\"receiverConnecting\":%s,\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
|
||||
"\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",
|
||||
calibration_key(calibration.kind), calibration_key(calibration_effective()),
|
||||
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++)
|
||||
used +=
|
||||
snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}",
|
||||
@@ -40,64 +41,69 @@ static bool status(void *h, void *m, void *ctx) {
|
||||
layout_error, piccap_fallbacks,
|
||||
(unsigned long long)(handover_cooldown > now ? handover_cooldown - now : 0));
|
||||
used += snprintf(json + used, sizeof(json) - used, "\"calibrationMessage\":\"%s\",", report);
|
||||
used +=
|
||||
snprintf(json + used, sizeof(json) - used,
|
||||
"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[", calibration_draft.model);
|
||||
for (unsigned i = 0; i < calibration_draft.count; i++)
|
||||
used += snprintf(json + used, sizeof(json) - used,
|
||||
"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[",
|
||||
calibration_ctx.draft.model);
|
||||
for (unsigned i = 0; i < calibration_ctx.draft.count; i++)
|
||||
used += snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}",
|
||||
i ? "," : "", calibration_draft.points[i].position,
|
||||
calibration_draft.points[i].region);
|
||||
i ? "," : "", calibration_ctx.draft.points[i].position,
|
||||
calibration_ctx.draft.points[i].region);
|
||||
used += snprintf(json + used, sizeof(json) - used, "]},\"receiverStreamConnected\":%s,",
|
||||
stream.stage == STREAM_SUBSCRIBED ? "true" : "false");
|
||||
used += snprintf(json + used, sizeof(json) - used,
|
||||
"\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s,"
|
||||
"\"sourceFade\":%s,\"calibrating\":%s,",
|
||||
sample_sequence, sample_at ? (long long)(now - sample_at) : -1,
|
||||
continuity.holding ? "true" : "false",
|
||||
(continuity.fading || bridge_phase == BRIDGE_FADING) ? "true" : "false",
|
||||
capture_ctx.sample_sequence,
|
||||
capture_ctx.sample_at ? (long long)(now - capture_ctx.sample_at) : -1,
|
||||
capture_ctx.continuity.holding ? "true" : "false",
|
||||
(capture_ctx.continuity.fading || bridge_phase == BRIDGE_FADING) ? "true"
|
||||
: "false",
|
||||
CALIBRATION_ACTIVE ? "true" : "false");
|
||||
used += snprintf(json + used, sizeof(json) - used,
|
||||
used +=
|
||||
snprintf(json + used, sizeof(json) - used,
|
||||
"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\","
|
||||
"\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%"
|
||||
"lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",
|
||||
target.piccap_port, test_stage_name(), connection_test.leds,
|
||||
(unsigned long long)connection_test.elapsed, connection_test.error,
|
||||
last_output_at ? (long long)(now - last_output_at) : -1,
|
||||
capture_ctx.last_output_at ? (long long)(now - capture_ctx.last_output_at) : -1,
|
||||
(unsigned long long)(retries.due > now ? retries.due - now : 0),
|
||||
capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap"
|
||||
: capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap"
|
||||
: piccap_owned ? "PicCap"
|
||||
: capture ? "Histogram"
|
||||
: capture_ctx.handle ? "Histogram"
|
||||
: "Stopped",
|
||||
piccap_error);
|
||||
used += snprintf(json + used, sizeof(json) - used,
|
||||
used += snprintf(
|
||||
json + used, sizeof(json) - used,
|
||||
"\"version\":\"" HYPERDRM_VERSION
|
||||
"\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,"
|
||||
"\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,"
|
||||
"\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,"
|
||||
"\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,"
|
||||
"\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",
|
||||
(capture || capture_source == SOURCE_PICCAP) ? "true" : "false",
|
||||
color_valid ? "true" : "false", frames, capture_recoveries,
|
||||
(unsigned long long)last_recovery_ms, endpoint, target.host, target.port,
|
||||
(capture_ctx.handle || capture_source == SOURCE_PICCAP) ? "true" : "false",
|
||||
capture_ctx.color_valid ? "true" : "false", capture_ctx.frames, capture_ctx.recoveries,
|
||||
(unsigned long long)capture_ctx.last_recovery_ms, endpoint, target.host, target.port,
|
||||
target.receiver, target.token[0] ? "true" : "false",
|
||||
target_piccap_compatible(&target) ? "true" : "false", PRIORITY,
|
||||
(unsigned)geteuid(), sent_rgb[0], sent_rgb[1], sent_rgb[2], safe);
|
||||
target_piccap_compatible(&target) ? "true" : "false", PRIORITY, (unsigned)geteuid(),
|
||||
capture_ctx.sent_rgb[0], capture_ctx.sent_rgb[1], capture_ctx.sent_rgb[2], safe);
|
||||
for (int i = 0; i < 16; i++)
|
||||
used += snprintf(json + used, sizeof(json) - used, "%s[%u,%u,%u]", i ? "," : "", rgb[i][0],
|
||||
rgb[i][1], rgb[i][2]);
|
||||
used += snprintf(json + used, sizeof(json) - used, "%s[%u,%u,%u]", i ? "," : "",
|
||||
capture_ctx.rgb[i][0], capture_ctx.rgb[i][1], capture_ctx.rgb[i][2]);
|
||||
used += snprintf(json + used, sizeof(json) - used,
|
||||
"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[", led_count,
|
||||
led_count ? "true" : "false");
|
||||
"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",
|
||||
receiver_layout.count, receiver_layout.count ? "true" : "false");
|
||||
for (int i = 0; i < 16; i++)
|
||||
used += snprintf(json + used, sizeof(json) - used, "%s%u", i ? "," : "", black_percent[i]);
|
||||
used += snprintf(json + used, sizeof(json) - used, "%s%u", i ? "," : "",
|
||||
capture_ctx.black_percent[i]);
|
||||
snprintf(json + used, sizeof(json) - used,
|
||||
"],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0,"
|
||||
"\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,"
|
||||
"\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,"
|
||||
"\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":"
|
||||
"\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}",
|
||||
capture_preset, interpolation_enabled ? "true" : "false",
|
||||
capture_ctx.preset, receiver_layout.interpolation ? "true" : "false",
|
||||
boot_enabled() ? "true" : "false", retries.due ? "true" : "false",
|
||||
retries.paused ? "true" : "false", retries.blocked ? "true" : "false",
|
||||
retries.attempts, piccap_installed() ? "true" : "false",
|
||||
@@ -106,6 +112,6 @@ static bool status(void *h, void *m, void *ctx) {
|
||||
protection == PLAYBACK_PROTECTED ? "protected"
|
||||
: protection == PLAYBACK_CLEAR ? "non-secure decoder"
|
||||
: "unknown",
|
||||
capture && letterbox_mode ? "true" : "false");
|
||||
capture_ctx.handle && capture_ctx.letterbox_mode ? "true" : "false");
|
||||
return respond(h, m, json);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ static bool bridge_send(const unsigned char colors[16][3]) {
|
||||
free(line);
|
||||
return ok;
|
||||
}
|
||||
static void bridge_tick(uint64_t now) {
|
||||
static void bridge_tick(uint64_t now, CaptureContext *capture) {
|
||||
if (retries.paused || retries.blocked) {
|
||||
if (bridge_phase)
|
||||
bridge_clear();
|
||||
@@ -54,15 +54,17 @@ static void bridge_tick(uint64_t now) {
|
||||
}
|
||||
if (capture_source != bridge_source) {
|
||||
if (capture_source == SOURCE_PICCAP_STARTING) {
|
||||
bridge_phase = continuity.available && now - continuity.valid_at < 2000 ? BRIDGE_HOLDING
|
||||
bridge_phase =
|
||||
capture->continuity.available && now - capture->continuity.valid_at < 2000
|
||||
? BRIDGE_HOLDING
|
||||
: BRIDGE_IDLE;
|
||||
bridge_started = now;
|
||||
memcpy(bridge_from, continuity.last, sizeof(bridge_from));
|
||||
memcpy(bridge_from, capture->continuity.last, sizeof(bridge_from));
|
||||
} else if (capture_source == SOURCE_PICCAP_STOPPING ||
|
||||
(bridge_source == SOURCE_PICCAP && capture_source == SOURCE_HISTOGRAM)) {
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
if (stream.valid && now - stream.updated < 1000) {
|
||||
continuity_seed(&continuity, stream.colors, now);
|
||||
continuity_seed(&capture->continuity, stream.colors, now);
|
||||
bridge_send(stream.colors);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-37
@@ -10,6 +10,7 @@
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
#include "playback.h"
|
||||
#include "atomic_file.h"
|
||||
#define TARGET_PREF \
|
||||
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
||||
typedef struct {
|
||||
@@ -44,7 +45,7 @@ static bool target_valid(const TargetConfig *t) {
|
||||
return true;
|
||||
}
|
||||
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
|
||||
void *root = j->parse(text);
|
||||
void *root = playback_parse_exact(j, text);
|
||||
if (!root)
|
||||
return false;
|
||||
TargetConfig next = *out;
|
||||
@@ -112,24 +113,13 @@ static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
|
||||
static bool target_save(const char *path, const TargetConfig *t) {
|
||||
if (!target_valid(t))
|
||||
return false;
|
||||
char tmp[512], data[512];
|
||||
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
||||
return false;
|
||||
char data[512];
|
||||
int n = snprintf(
|
||||
data, sizeof(data),
|
||||
"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",
|
||||
t->host, t->port, t->piccap_port, t->receiver, t->token);
|
||||
int fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
bool ok = write(fd, data, n) == n && fsync(fd) == 0;
|
||||
if (close(fd) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = rename(tmp, path) == 0;
|
||||
if (!ok)
|
||||
unlink(tmp);
|
||||
return ok;
|
||||
return n > 0 && (size_t)n < sizeof(data) &&
|
||||
atomic_replace(path, data, (size_t)n) != SAVE_UNCHANGED;
|
||||
}
|
||||
static bool target_load(PlaybackJson *j, const char *path, TargetConfig *t) {
|
||||
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||
@@ -178,26 +168,4 @@ static int target_open(const TargetConfig *t) {
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
static int target_connect(const TargetConfig *t) {
|
||||
int fd = target_open(t);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
struct pollfd p = {.fd = fd, .events = POLLOUT};
|
||||
int error = 0;
|
||||
socklen_t size = sizeof(error);
|
||||
if (poll(&p, 1, 1000) <= 0 || getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 ||
|
||||
error) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
struct timeval timeout = {1, 0};
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
@@ -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");
|
||||
}
|
||||
+18
-1
@@ -11,5 +11,22 @@ int main(void){
|
||||
for(unsigned i=0;i<16;i++)black[i]=100;
|
||||
memset(color,0,sizeof(color));assert(letterbox_candidate(black,color)==-1);
|
||||
memset(black,0,sizeof(black));assert(letterbox_candidate(black,color)==-1);
|
||||
puts("PASS: full frame, measured letterbox, one edge only, black frame, dark frame");
|
||||
unsigned thin_black[16]={12,23,1,21,9,6,6,16,18,12,26,23,23,22,37,8};
|
||||
unsigned char thin_color[16][3]={{57,52,48},{97,94,92},{51,42,35},{25,24,21},
|
||||
{71,65,60},{128,122,118},{84,74,67},{34,32,27},{47,41,36},{59,47,43},
|
||||
{37,29,25},{19,18,15},{22,20,16},{22,19,15},{14,13,11},{78,75,72}};
|
||||
assert(letterbox_candidate(thin_black,thin_color)==1);
|
||||
thin_black[1]=thin_black[5]=thin_black[9]=0;
|
||||
assert(letterbox_candidate(thin_black,thin_color)!=1);
|
||||
thin_black[1]=23;thin_black[5]=6;thin_black[9]=12;
|
||||
thin_black[11]=0;
|
||||
assert(letterbox_candidate(thin_black,thin_color)!=1);
|
||||
thin_black[11]=23;
|
||||
memset(thin_color,10,sizeof(thin_color));
|
||||
assert(letterbox_candidate(thin_black,thin_color)==-1);
|
||||
memset(thin_color,100,sizeof(thin_color));
|
||||
assert(letterbox_candidate(thin_black,thin_color)!=1);
|
||||
memset(thin_black,15,sizeof(thin_black));
|
||||
assert(letterbox_candidate(thin_black,thin_color)!=1);
|
||||
puts("PASS: measured wide/thin bars, full frame, one-sided/isolated edges, dark frame and low contrast");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include "dark_color.h"
|
||||
int main(void) {
|
||||
for (unsigned v = 0; v <= 255; v++) {
|
||||
unsigned char gray[3] = {v, v, v};
|
||||
dark_color_filter(gray);
|
||||
unsigned expected = v <= 16 ? 0 : v >= 32 ? v : (v * (v - 16) + 8) / 16;
|
||||
for (unsigned c = 0; c < 3; c++) assert(gray[c] == expected);
|
||||
}
|
||||
unsigned char residual[3] = {4, 8, 2};
|
||||
dark_color_filter(residual);
|
||||
assert(!residual[0] && !residual[1] && !residual[2]);
|
||||
unsigned char measured[3] = {13, 14, 7};
|
||||
dark_color_filter(measured);
|
||||
assert(!measured[0] && !measured[1] && !measured[2]);
|
||||
unsigned char transition[3] = {12, 24, 8};
|
||||
dark_color_filter(transition);
|
||||
assert(transition[0] == 6 && transition[1] == 12 && transition[2] == 4);
|
||||
unsigned char saturated[3] = {0, 32, 0};
|
||||
dark_color_filter(saturated);
|
||||
assert(saturated[0] == 0 && saturated[1] == 32 && saturated[2] == 0);
|
||||
puts("PASS: near-black suppression, gradual transition, unchanged brighter colors");
|
||||
}
|
||||
@@ -32,7 +32,7 @@ static void fixture(PlaybackJson *json,int kind){
|
||||
close(server);assert(test_begin(&config,10000));test_tick(json,10010);assert(connection_test.stage==TEST_FAILED);
|
||||
}
|
||||
int main(void){
|
||||
(void)target_save;(void)target_load;(void)target_connect;(void)test_stage_name;
|
||||
(void)target_save;(void)target_load;(void)target_open;(void)test_stage_name;
|
||||
PlaybackJson json={0};assert(playback_json_init(&json));
|
||||
fixture(&json,0);fixture(&json,1);fixture(&json,2);
|
||||
int peers[2];assert(socketpair(AF_UNIX,SOCK_STREAM,0,peers)==0);
|
||||
|
||||
@@ -44,7 +44,7 @@ static void connected_start(void){
|
||||
media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);assert(capture_source==SOURCE_PICCAP);
|
||||
}
|
||||
int main(void){
|
||||
(void)target_save;(void)target_load;(void)target_connect;(void)handover_save;(void)handover_load;
|
||||
(void)target_save;(void)target_load;(void)target_open;(void)handover_save;(void)handover_load;
|
||||
(void)retry_failure;(void)retry_take;(void)retry_update;
|
||||
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
|
||||
handover_enabled=true;next_media_probe=UINT64_MAX;
|
||||
|
||||
@@ -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 "layout.h"
|
||||
#include "diagnostics.h"
|
||||
static unsigned led_count;
|
||||
static LedBlend *led_blends;
|
||||
#include "runtime_context.h"
|
||||
static ReceiverLayoutContext receiver_layout;
|
||||
#include "layout_refresh.h"
|
||||
static void completed(unsigned count,unsigned generation){
|
||||
layout_probe.mapping=calloc(count,sizeof(LedBlend));assert(layout_probe.mapping);
|
||||
@@ -14,22 +14,22 @@ static void completed(unsigned count,unsigned generation){
|
||||
layout_probe.leds=count;layout_probe.stage=TEST_DONE;layout_probe_generation=generation;
|
||||
}
|
||||
int main(void){
|
||||
(void)target_connect;(void)target_load;(void)target_save;
|
||||
(void)target_open;(void)target_load;(void)target_save;
|
||||
PlaybackJson json={0};assert(playback_json_init(&json));
|
||||
calibration.kind=CAL_G4;next_layout_check=UINT64_MAX;
|
||||
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true);
|
||||
assert(led_count==50 && layout_updates==1 && last_layout_check==1000);
|
||||
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true);
|
||||
assert(led_count==500 && layout_updates==2 && last_layout_check==2000);
|
||||
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true);assert(layout_updates==2);
|
||||
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true,&receiver_layout);
|
||||
assert(receiver_layout.count==50 && layout_updates==1 && last_layout_check==1000);
|
||||
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true,&receiver_layout);
|
||||
assert(receiver_layout.count==500 && layout_updates==2 && last_layout_check==2000);
|
||||
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true,&receiver_layout);assert(layout_updates==2);
|
||||
completed(500,calibration_generation);layout_probe.mapping[42].weight^=512;
|
||||
layout_refresh_tick(&json,4000,true);assert(layout_updates==3);
|
||||
LedBlend *previous=led_blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
|
||||
layout_refresh_tick(&json,5000,true);assert(led_blends==previous && led_count==500 && layout_updates==3 && layout_error[0]);
|
||||
layout_refresh_tick(&json,4000,true,&receiver_layout);assert(layout_updates==3);
|
||||
LedBlend *previous=receiver_layout.blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
|
||||
layout_refresh_tick(&json,5000,true,&receiver_layout);assert(receiver_layout.blends==previous && receiver_layout.count==500 && layout_updates==3 && layout_error[0]);
|
||||
completed(50,calibration_generation);calibration_generation++;
|
||||
layout_refresh_tick(&json,6000,true);assert(led_count==500 && led_blends==previous);assert(probe_busy(&layout_probe));
|
||||
layout_refresh_tick(&json,6100,false);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
|
||||
assert(led_count==500 && led_blends==previous);
|
||||
layout_refresh_reset(7000);free(led_blends);test_close();
|
||||
layout_refresh_tick(&json,6000,true,&receiver_layout);assert(receiver_layout.count==500 && receiver_layout.blends==previous);assert(probe_busy(&layout_probe));
|
||||
layout_refresh_tick(&json,6100,false,&receiver_layout);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
|
||||
assert(receiver_layout.count==500 && receiver_layout.blends==previous);
|
||||
layout_refresh_reset(7000);free(receiver_layout.blends);test_close();
|
||||
puts("PASS: automatic 50/500 LED replacement, same-count position changes, unchanged layout, invalid reply retention, obsolete profile result and cancellation");return 0;
|
||||
}
|
||||
@@ -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_save("/no/such/profile", ©));
|
||||
assert(!target_load(&json, "/no/such/profile", ©));
|
||||
int fd = target_connect(©);
|
||||
int fd = target_open(©);
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
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 {char receiver[16];} target={"Hyperion"};
|
||||
static struct {int stage;bool valid;uint64_t updated;unsigned sequence,count;LedBlend *mapping;unsigned char colors[16][3];} stream;
|
||||
static Continuity continuity;
|
||||
#include "runtime_context.h"
|
||||
static CaptureContext capture_ctx;
|
||||
static bool piccap_connected;
|
||||
static char *queued;
|
||||
static bool stream_queue(const char *line){if(queued)return false;queued=strdup(line);return queued!=NULL;}
|
||||
@@ -23,21 +24,21 @@ static char *spatial_image_command_duration(const unsigned char colors[16][3],un
|
||||
static void consume(void){free(queued);queued=NULL;}
|
||||
int main(void){
|
||||
LedBlend mapping={0,0,0};stream.mapping=&mapping;stream.count=1;stream.stage=4;
|
||||
for(unsigned r=0;r<16;r++){continuity.last[r][0]=200;stream.colors[r][2]=200;}continuity.available=true;continuity.valid_at=1000;
|
||||
capture_source=SOURCE_PICCAP_STARTING;bridge_tick(1000);assert(bridge_phase==1 && strstr(queued,"200,0,0"));consume();
|
||||
capture_source=SOURCE_PICCAP;piccap_connected=true;bridge_tick(1200);assert(bridge_phase==2 && strstr(queued,"clear"));consume();
|
||||
stream.valid=true;stream.sequence=1;stream.updated=1300;bridge_tick(1300);assert(bridge_phase==2);
|
||||
stream.sequence=2;stream.updated=1400;bridge_tick(1400);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume();
|
||||
bridge_tick(1650);assert(strstr(queued,"100,0,100"));consume();
|
||||
bridge_tick(1900);assert(!bridge_phase && strstr(queued,"clear"));consume();
|
||||
capture_source=SOURCE_PICCAP_STOPPING;stream.updated=2000;bridge_tick(2000);assert(continuity.fading && continuity.last[0][2]==200 && strstr(queued,"0,0,200"));consume();
|
||||
capture_source=SOURCE_HISTOGRAM;bridge_tick(2100);assert(!queued);
|
||||
capture_source=SOURCE_PICCAP_STARTING;continuity.valid_at=3000;bridge_tick(3000);consume();bridge_tick(6000);assert(!bridge_phase && strstr(queued,"clear"));consume();
|
||||
capture_source=SOURCE_PICCAP_STARTING;bridge_source=SOURCE_HISTOGRAM;continuity.valid_at=7000;bridge_tick(7000);consume();retries.paused=true;bridge_tick(7100);assert(!bridge_phase && strstr(queued,"clear"));consume();
|
||||
retries.paused=false;bridge_source=SOURCE_HISTOGRAM;continuity.valid_at=8000;capture_source=SOURCE_PICCAP_STARTING;bridge_tick(8000);consume();
|
||||
capture_source=SOURCE_PICCAP;bridge_tick(8100);consume();stream.sequence++;stream.updated=8300;memset(stream.colors,0,sizeof(stream.colors));bridge_tick(8300);assert(!bridge_phase && !queued);
|
||||
bridge_source=SOURCE_HISTOGRAM;capture_source=SOURCE_PICCAP_STARTING;continuity.valid_at=1000;bridge_tick(9000);assert(!bridge_phase && !queued);
|
||||
stream.stage=1;bridge_phase=1;bridge_started=9000;bridge_tick(12000);assert(!queued);retries.paused=true;bridge_tick(12100);assert(!bridge_phase && !queued);
|
||||
for(unsigned r=0;r<16;r++){capture_ctx.continuity.last[r][0]=200;stream.colors[r][2]=200;}capture_ctx.continuity.available=true;capture_ctx.continuity.valid_at=1000;
|
||||
capture_source=SOURCE_PICCAP_STARTING;bridge_tick(1000,&capture_ctx);assert(bridge_phase==1 && strstr(queued,"200,0,0"));consume();
|
||||
capture_source=SOURCE_PICCAP;piccap_connected=true;bridge_tick(1200,&capture_ctx);assert(bridge_phase==2 && strstr(queued,"clear"));consume();
|
||||
stream.valid=true;stream.sequence=1;stream.updated=1300;bridge_tick(1300,&capture_ctx);assert(bridge_phase==2);
|
||||
stream.sequence=2;stream.updated=1400;bridge_tick(1400,&capture_ctx);assert(bridge_phase==3 && strstr(queued,"200,0,0"));consume();
|
||||
bridge_tick(1650,&capture_ctx);assert(strstr(queued,"100,0,100"));consume();
|
||||
bridge_tick(1900,&capture_ctx);assert(!bridge_phase && strstr(queued,"clear"));consume();
|
||||
capture_source=SOURCE_PICCAP_STOPPING;stream.updated=2000;bridge_tick(2000,&capture_ctx);assert(capture_ctx.continuity.fading && capture_ctx.continuity.last[0][2]==200 && strstr(queued,"0,0,200"));consume();
|
||||
capture_source=SOURCE_HISTOGRAM;bridge_tick(2100,&capture_ctx);assert(!queued);
|
||||
capture_source=SOURCE_PICCAP_STARTING;capture_ctx.continuity.valid_at=3000;bridge_tick(3000,&capture_ctx);consume();bridge_tick(6000,&capture_ctx);assert(!bridge_phase && strstr(queued,"clear"));consume();
|
||||
capture_source=SOURCE_PICCAP_STARTING;bridge_source=SOURCE_HISTOGRAM;capture_ctx.continuity.valid_at=7000;bridge_tick(7000,&capture_ctx);consume();retries.paused=true;bridge_tick(7100,&capture_ctx);assert(!bridge_phase && strstr(queued,"clear"));consume();
|
||||
retries.paused=false;bridge_source=SOURCE_HISTOGRAM;capture_ctx.continuity.valid_at=8000;capture_source=SOURCE_PICCAP_STARTING;bridge_tick(8000,&capture_ctx);consume();
|
||||
capture_source=SOURCE_PICCAP;bridge_tick(8100,&capture_ctx);consume();stream.sequence++;stream.updated=8300;memset(stream.colors,0,sizeof(stream.colors));bridge_tick(8300,&capture_ctx);assert(!bridge_phase && !queued);
|
||||
bridge_source=SOURCE_HISTOGRAM;capture_source=SOURCE_PICCAP_STARTING;capture_ctx.continuity.valid_at=1000;bridge_tick(9000,&capture_ctx);assert(!bridge_phase && !queued);
|
||||
stream.stage=1;bridge_phase=1;bridge_started=9000;bridge_tick(12000,&capture_ctx);assert(!queued);retries.paused=true;bridge_tick(12100,&capture_ctx);assert(!bridge_phase && !queued);
|
||||
strcpy(target.receiver,"HyperHDR");char *line=region_command(stream.colors,&mapping,1,500);assert(!strcmp(line,"image"));free(line);assert(!region_command(stream.colors,NULL,0,500));
|
||||
puts("PASS: both source transitions, fresh sample gate, fade midpoint/end, bounded timeout, manual-stop cleanup");return 0;
|
||||
}
|
||||
+10
-3
@@ -3,6 +3,13 @@
|
||||
#include <sys/stat.h>
|
||||
#include <string.h>
|
||||
#include "target.h"
|
||||
static int fixture_connect(const TargetConfig *target){
|
||||
int fd=target_open(target);if(fd<0)return -1;
|
||||
assert(fcntl(fd,F_GETFL,0)&O_NONBLOCK);
|
||||
struct pollfd event={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&event,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size) || error){close(fd);return -1;}
|
||||
return fd;
|
||||
}
|
||||
int main(void){
|
||||
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
|
||||
assert(target_valid(&t));assert(t.piccap_port==19400);assert(target_piccap_local(&t));
|
||||
@@ -22,7 +29,7 @@ int main(void){
|
||||
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
|
||||
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
|
||||
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
|
||||
int client=target_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
|
||||
assert(target_connect(&t)<0);
|
||||
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, bounded TCP connect and refusal");return 0;
|
||||
int client=fixture_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
|
||||
assert(fixture_connect(&t)<0);
|
||||
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, nonblocking socket and refused connection");return 0;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#ifndef HYPERDRM_VERSION_H
|
||||
#define HYPERDRM_VERSION_H
|
||||
#define HYPERDRM_VERSION "0.12.0"
|
||||
#define HYPERDRM_VERSION "0.13.3"
|
||||
#endif
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "org.webosbrew.histogramhyperion",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.3",
|
||||
"vendor": "Local development",
|
||||
"app": "org.webosbrew.histogramhyperion",
|
||||
"services": [
|
||||
|
||||
@@ -53,6 +53,7 @@ def verify_package(version):
|
||||
for directory, filename in [('applications', 'appinfo.json'), ('packages', 'packageinfo.json')]:
|
||||
entry = archive.extractfile(f'usr/palm/{directory}/org.webosbrew.histogramhyperion/{filename}')
|
||||
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()
|
||||
assert binary[:4] == b'\x7fELF' and binary[4] == 1 and binary[18:20] == b'\x28\x00'
|
||||
assert version.encode() in binary
|
||||
@@ -62,6 +63,7 @@ def verify_package(version):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
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()
|
||||
version = synchronize()
|
||||
run(sys.executable, '-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py')
|
||||
@@ -75,6 +77,12 @@ def main():
|
||||
name = '/tmp/hyperdrm-' + test.stem
|
||||
linux('gcc', '-Wall', '-Wextra', '-Werror', '-O2', str(test.relative_to(ROOT)).replace('\\', '/'), '-ldl', '-lm', '-o', 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',
|
||||
'native/service.c', '-ldl', '-o', 'service/histogram-service')
|
||||
run(sys.executable, 'build_package.py')
|
||||
|
||||
Binary file not shown.
@@ -21,6 +21,10 @@ def synchronize(check=False):
|
||||
path = ROOT / 'native/version.h'
|
||||
if not path.exists() or path.read_text(encoding='utf-8') != 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:
|
||||
raise ValueError('Version metadata is stale; run sync_version.py')
|
||||
for path, content in updates.items():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
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={};
|
||||
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=[];
|
||||
|
||||
@@ -34,6 +34,12 @@ class VersionTests(unittest.TestCase):
|
||||
self.assertEqual(app, {'version': '1.2.3', 'id': 'unchanged'})
|
||||
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):
|
||||
(sync_version.ROOT / 'VERSION').write_text('1.2\n')
|
||||
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