Private
Public Access
Add receiver diagnostics and remote PicCap handover
This commit is contained in:
1 parent
ae01879d45
commit
42aeaf56ea
19 files changed
+322
-50
No files matched your search
@@ -33,3 +33,5 @@ test-target
|
||||
tests/receiver-proxy.log
|
||||
|
||||
test-spatial-image
|
||||
|
||||
test-diagnostics
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"id": "org.webosbrew.histogramhyperion",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"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.8.1_arm.ipk",
|
||||
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.9.0_arm.ipk",
|
||||
"ipkHash": {
|
||||
"sha256": "c5a8125d95eeba517f8efe190de8d5304ec2663768db69552e4ea2f0e0b23cc0"
|
||||
"sha256": "f9cb0b4035a0b4f527da814ecb0ea1e0c184fc92154f60899b6425c84e0d2d9b"
|
||||
},
|
||||
"ipkSize": 1173862
|
||||
"ipkSize": 1177866
|
||||
}
|
||||
Binary file not shown.
@@ -21,11 +21,11 @@ DRM playback color output was confirmed on the test TV. Support depends on LG's
|
||||
|
||||
The 16 histogram samples are independent measurements; interpolation produces smoother transitions without adding independently captured detail. Sample positions are approximate G4 calibration values. Top/bottom bar detection is supported; side bars, arbitrary bar thickness and HDR color calibration remain limited.
|
||||
|
||||
Only the own service is modified. Optional Auto PicCap can start and stop the installed PicCap service through its existing API. No Hue, LG library, driver or firmware binaries are included. For histogram initialization, the service temporarily creates LG's developer marker and uses a guardian process to remove its own marker afterward; an existing marker is preserved.
|
||||
Only the own service is modified. Optional Auto PicCap can configure its receiver endpoint and start and stop the installed PicCap service through its existing API. No Hue, LG library, driver or firmware binaries are included. For histogram initialization, the service temporarily creates LG's developer marker and uses a guardian process to remove its own marker afterward; an existing marker is preserved.
|
||||
|
||||
## Installation
|
||||
|
||||
The current package is [HyperDRM_0.8.1_arm.ipk](HyperDRM_0.8.1_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||
The current package is [HyperDRM_0.9.0_arm.ipk](HyperDRM_0.9.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||
|
||||
Elevate the installed own service through Homebrew Channel:
|
||||
|
||||
@@ -88,7 +88,7 @@ Remote navigation regression tests: `node tests/remote-navigation.test.js`.
|
||||
|
||||
## Optional automatic PicCap handover
|
||||
|
||||
Version 0.7.0 adds **Auto PicCap**, disabled by default. The switch is available when PicCap is installed. Keep PicCap's own autostart disabled and configure its output for your Hyperion instance. HyperDRM does not change PicCap's configuration or autostart preference.
|
||||
Version 0.7.0 adds **Auto PicCap**, disabled by default. The switch is available when PicCap is installed. Keep PicCap's own autostart disabled. From 0.9.0, HyperDRM manages PicCap's receiver address, FlatBuffer port and Unix socket setting when automatic handover is enabled; other capture preferences and the autostart preference are preserved.
|
||||
|
||||
When the session is started, the controller polls LG's active media resources. Two consecutive foreground media-decoder samples without a Secure Video Path resource allow PicCap to start. HyperDRM releases its histogram capture and clears priority 149 first, allowing PicCap's configured input priority to take over. Any `SVP_CPB` resource selects protected playback; missing, stale, malformed, idle or HDMI state selects the conservative HyperDRM fallback. PicCap must confirm that it has stopped before histogram capture resumes. Detection is specific to the tested G4 resource model, not a universal DRM-state API.
|
||||
|
||||
@@ -108,7 +108,7 @@ Settings are saved in `target.json` inside the own service container and loaded
|
||||
|
||||
For a receiver requiring authentication, enter its API token. The token file is mode 0600; status only reports whether a token is saved. Leaving the input blank retains it; **Remove saved token** deletes it when saving. A token login happens before requesting the layout or initializing LG capture. JSON TCP traffic is unencrypted, so use a trusted network. Receiver authorization rules are unchanged.
|
||||
|
||||
Auto PicCap remains available for the default local receiver at 127.0.0.1/::1:19444. For custom targets, automatic PicCap capture is paused and HyperDRM supplies colors for all playback; the PicCap preference remains saved. PicCap has a separate FlatBuffer endpoint managed by its own app, which HyperDRM does not rewrite.
|
||||
Auto PicCap uses the selected receiver IP and the separate **PicCap FlatBuffer port** (default 19400), including remote IPv4 receivers and custom ports. Enable the receiver FlatBuffer server. The tested PicCap client supports IPv4 only: local ::1 is mapped to 127.0.0.1 for PicCap, while remote IPv6 keeps histogram capture. API tokens authenticate JSON only; PicCap uses the receiver FlatBuffer protocol.
|
||||
|
||||
`native/test_target.c` verifies validation, token retention/removal, private persistence and TCP connection/refusal. `tests/receiver-proxy.py` is a test-only relay with synthetic token login and fragmented replies. Local HyperHDR 22.0.0 and a remote relay using a different IP/port were tested on the G4; the relay forwarded real color updates to HyperHDR without changing its configuration.
|
||||
|
||||
@@ -117,3 +117,13 @@ Auto PicCap remains available for the default local receiver at 127.0.0.1/::1:19
|
||||
HyperHDR 22's JSON `color` handler ultimately registers only the first RGB triplet as a solid color. Version 0.8.1 uses JSON `image` output for the HyperHDR receiver selection. A cached 160x90 geometry interpolates the calibrated histogram samples into a synthetic color plane; HyperHDR applies its configured LED rectangles to that plane. The payload contains generated colors, not captured or decrypted video frames. HyperHDR's image processing and color calibration affect the resulting LED colors. Select a spatial image mapping mode in HyperHDR; a global single-color mapping mode will still give uniform LEDs.
|
||||
|
||||
Hyperion retains its original per-LED color-array output. Receiver IP, JSON port, optional token, priority, timeout and reconnection behavior remain shared. Tests cover exact RGB base64 encoding, image command metadata and spatial edge variation. Output must be verified through the receiver's LED stream, not solely a successful command acknowledgment.
|
||||
|
||||
## Diagnostics and connection test (0.9.0)
|
||||
|
||||
Save the receiver settings, then select **Test connection**. The test opens a separate nonblocking JSON connection, logs in with the saved token when present, and requests and validates the LED layout. It sends no colors, clears no priorities and acquires no TV capture. Results include success/failure, validated LED count, elapsed time and an actionable error. A successful test verifies connectivity and layout access, not the physical LEDs or FlatBuffer input.
|
||||
|
||||
The diagnostic line reports service version/root status, capture transition, playback classification, capture recovery count/time, age of the last histogram output and scheduled retry timing. PicCap reports its own connection status; histogram output age is not PicCap frame age. Color calibration, saturation, brightness and smoothing belong in Hyperion/HyperHDR; HyperDRM keeps neutral gains.
|
||||
|
||||
Before PicCap starts, its endpoint is read through getSettings. Matching settings are reused. Otherwise setSettings updates only address, port and unix-socket:false; the official implementation restarts PicCap itself, so the controller checks status rather than issuing another start. Endpoint changes are saved by PicCap and remain after disabling handover. Changing the receiver stops the managed PicCap session before configuring the new endpoint. Settings errors/timeouts stop PicCap before histogram fallback, with a cooldown to avoid rapid restarts.
|
||||
|
||||
Feature tests are native/test_diagnostics.c, native/test_target.c, native/test_handover.c and tests/receiver-ui.test.js. The native tests cover fragmented replies, rejected authentication, invalid layout, refused connection, timeout, custom PicCap endpoint, configuration failure and exclusive capture ownership. Test-only JSON and FlatBuffer relays allow a real receiver to be reached through another IP and ports without changing receiver configuration.
|
||||
+12
-5
@@ -11,11 +11,15 @@ function call(method,done,data){
|
||||
}
|
||||
function display(result){
|
||||
if(result.host && !targetDirty){
|
||||
document.getElementById('host').value=result.host;document.getElementById('port').value=result.port;receiver=result.receiver;
|
||||
document.getElementById('host').value=result.host;document.getElementById('port').value=result.port;document.getElementById('piccapPort').value=result.piccapPort||19400;receiver=result.receiver;
|
||||
document.getElementById('receiver').textContent='Receiver: '+receiver;
|
||||
}
|
||||
document.getElementById('token').placeholder=result.tokenSet?'Saved token (leave blank to keep)':'No token saved';
|
||||
if(result.endpoint)document.getElementById('endpoint').textContent=result.receiver+' · '+result.endpoint+' · Priority 149 · 10 updates per second';
|
||||
var t=result.connectionTest;
|
||||
if(t)document.getElementById('testResult').textContent='Connection test: '+t.state+(t.state==='Passed'?' · '+t.ledCount+' LEDs · '+t.elapsedMs+' ms':'')+(t.error?' · '+t.error:'');
|
||||
document.getElementById('testConnection').disabled=!!t && ['Connecting','Sending request','Checking authentication','Reading LED layout'].indexOf(t.state)>=0;
|
||||
if(result.version)document.getElementById('diagnostics').textContent='Version '+result.version+' · '+(result.uid===0?'Root service':'Service needs root elevation')+' · '+result.captureState+' · Playback: '+result.playbackProtection+' · Capture recoveries: '+result.captureRecoveries+' (last: '+result.lastRecoveryMs+' ms)'+(result.lastOutputAgeMs>=0?' · Last histogram output: '+result.lastOutputAgeMs+' ms ago':'')+(result.retryScheduled?' · Retry in '+result.retryInMs+' ms · Attempts: '+result.retryAttempts:'')+(result.piccapError?' · '+result.piccapError:'');
|
||||
var handover=document.getElementById('handover');handover.disabled=!result.piccapInstalled;
|
||||
handover.setAttribute('aria-checked',String(!!result.piccapHandover));
|
||||
handover.textContent=result.piccapInstalled?'Auto PicCap: '+(result.piccapHandover?'On':'Off'):'Auto PicCap: Not installed';
|
||||
@@ -27,7 +31,7 @@ function display(result){
|
||||
document.getElementById('regions').textContent='';return;
|
||||
}
|
||||
if(result.piccapHandover)document.getElementById('status').textContent+=' · HyperDRM · '+result.playbackProtection;
|
||||
if(result.piccapHandover && !result.piccapTargetCompatible)document.getElementById('status').textContent+=' · Auto PicCap paused for custom target';
|
||||
if(result.piccapHandover && !result.piccapTargetCompatible)document.getElementById('status').textContent+=' · Auto PicCap unavailable for remote IPv6 target';
|
||||
if(result.colorValid)document.getElementById('status').textContent+=result.spatialOutput?' · '+result.ledCount+' LEDs from 16 color regions'+(result.receiver==='HyperHDR'?' · Spatial image output':''):' · Output RGB: '+result.outputColor.join(', ');
|
||||
else if(result.returnValue!==false)document.getElementById('status').textContent+=' · No current color output';
|
||||
if(result.blackBarDetection && result.running)document.getElementById('status').textContent+=result.letterboxDetected?' · Top/bottom black bars detected':' · Full-screen capture';
|
||||
@@ -46,13 +50,16 @@ document.getElementById('start').focus();
|
||||
document.getElementById('autostart').onclick=function(){var button=this;var enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;call('autostart',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:enabled});};
|
||||
|
||||
document.getElementById('handover').onclick=function(){var button=this;button.disabled=true;call('handover',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:button.getAttribute('aria-checked')!=='true'});};
|
||||
['host','port','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});});
|
||||
['host','port','piccapPort','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});});
|
||||
document.getElementById('receiver').onclick=function(){receiver=receiver==='Hyperion'?'HyperHDR':'Hyperion';this.textContent='Receiver: '+receiver;targetDirty=true;};
|
||||
document.getElementById('saveTarget').onclick=function(){
|
||||
var port=Number(document.getElementById('port').value),button=this;
|
||||
if(!Number.isInteger(port)||port<1||port>65535){document.getElementById('error').textContent='JSON port must be an integer from 1 to 65535.';return;}
|
||||
var settings={host:document.getElementById('host').value.trim(),port:port,receiver:receiver,clearToken:document.getElementById('clearToken').checked};
|
||||
var flatPort=Number(document.getElementById('piccapPort').value);
|
||||
if(!Number.isInteger(flatPort)||flatPort<1||flatPort>65535){document.getElementById('error').textContent='PicCap FlatBuffer port must be an integer from 1 to 65535.';return;}
|
||||
var settings={piccapPort:flatPort,host:document.getElementById('host').value.trim(),port:port,receiver:receiver,clearToken:document.getElementById('clearToken').checked};
|
||||
var token=document.getElementById('token').value.trim();if(token)settings.token=token;
|
||||
button.disabled=true;call('settings',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}targetDirty=false;document.getElementById('token').value='';document.getElementById('clearToken').checked=false;call('status',display);},settings);
|
||||
};
|
||||
setupRemoteNavigation(['start','stop','refresh','autostart','handover','receiver','host','port','token','clearToken','saveTarget'].map(function(id){return document.getElementById(id);}),document,window);
|
||||
document.getElementById('testConnection').onclick=function(){if(targetDirty){document.getElementById('error').textContent='Save receiver settings before testing the connection.';return;}this.disabled=true;call('testConnection',function(r){if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;call('status',display);});};
|
||||
setupRemoteNavigation(['start','stop','refresh','autostart','handover','receiver','host','port','piccapPort','token','clearToken','saveTarget','testConnection'].map(function(id){return document.getElementById(id);}),document,window);
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.8.1", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.9.0", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
|
||||
+6
-4
@@ -1,5 +1,5 @@
|
||||
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title>
|
||||
<style>body{background:#101827;color:#f1f5ff;font:28px sans-serif;margin:70px;max-width:1400px}h1{font-size:52px;display:flex;align-items:center;gap:24px}h1 img{border-radius:20px}button{font:inherit;padding:20px 32px;margin:12px 12px 12px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:12px}button:focus{outline:5px solid #66ddff}#regions{display:flex;flex-wrap:wrap;gap:8px}.swatch{width:64px;height:64px;border:1px solid #abc}p{line-height:1.5}#error{color:#ffbc99}small{color:#c0cbe0}fieldset{border:2px solid #739be0;border-radius:12px;padding:16px}label{display:inline-flex;flex-direction:column;font-size:24px;margin:8px 20px 8px 0}input{font:inherit;padding:12px;background:#223654;color:white;border:2px solid #739be0;border-radius:8px}input:focus{outline:5px solid #66ddff}#host,#token{width:340px}#port{width:140px}#clearToken{width:28px;height:28px}</style>
|
||||
<style>body{background:#101827;color:#f1f5ff;font:28px sans-serif;margin:70px;max-width:1400px}h1{font-size:52px;display:flex;align-items:center;gap:24px}h1 img{border-radius:20px}button{font:inherit;padding:20px 32px;margin:12px 12px 12px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:12px}button:focus{outline:5px solid #66ddff}#regions{display:flex;flex-wrap:wrap;gap:8px}.swatch{width:64px;height:64px;border:1px solid #abc}p{line-height:1.5}#error{color:#ffbc99}small{color:#c0cbe0}fieldset{border:2px solid #739be0;border-radius:12px;padding:16px}label{display:inline-flex;flex-direction:column;font-size:24px;margin:8px 20px 8px 0}input{font:inherit;padding:12px;background:#223654;color:white;border:2px solid #739be0;border-radius:8px}input:focus{outline:5px solid #66ddff}#host,#token{width:340px}#port,#piccapPort{width:140px}#clearToken{width:28px;height:28px}</style>
|
||||
<h1><img src="logo.png" alt="" width="104" height="104">HyperDRM</h1><p>Ambient lighting for webOS, Hyperion and HyperHDR.</p><p id="status">Checking service …</p><p id="error" role="alert"></p>
|
||||
<div role="group" aria-label="Service controls"><button id="start">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button>
|
||||
<button id="autostart" role="switch" aria-checked="false">Autostart: Off</button>
|
||||
@@ -7,10 +7,12 @@
|
||||
<fieldset><legend>Receiver settings</legend><button id="receiver" type="button">Receiver: Hyperion</button>
|
||||
<label>IP address <input id="host" type="text" maxlength="45" value="127.0.0.1" spellcheck="false"></label>
|
||||
<label>JSON port <input id="port" type="number" min="1" max="65535" value="19444"></label>
|
||||
<label>PicCap FlatBuffer port <input id="piccapPort" type="number" min="1" max="65535" value="19400"></label>
|
||||
<label>API token (optional) <input id="token" type="password" maxlength="256" autocomplete="off"></label>
|
||||
<label><input id="clearToken" type="checkbox"> Remove saved token</label><button id="saveTarget" type="button">Save receiver</button>
|
||||
<label><input id="clearToken" type="checkbox"> Remove saved token</label><button id="saveTarget" type="button">Save receiver</button><button id="testConnection" type="button">Test connection</button><p id="testResult" role="status">Connection test: Not tested</p>
|
||||
<p><small>Use the receiver's JSON TCP port, usually 19444. IPv4 and IPv6 addresses are supported. Leave the token blank to keep a saved token. Save applies the new target to a running session. Use Up/Down to move between fields; Left/Right edit text.</small></p>
|
||||
<p id="piccapTargetHint"><small>Auto PicCap requires the default local receiver; custom targets use HyperDRM for all playback. PicCap settings are managed in PicCap.</small></p></fieldset>
|
||||
<p id="piccapTargetHint"><small>Auto PicCap uses the same receiver IP with its FlatBuffer port, usually 19400. Enable the FlatBuffer server on the receiver. When switching to PicCap, its saved address, port and Unix socket setting are updated; other PicCap preferences are preserved. PicCap supports IPv4 targets; remote IPv6 uses HyperDRM. The API token applies to JSON only.</small></p></fieldset>
|
||||
<p><small id="endpoint">Priority 149 · 10 updates per second</small></p>
|
||||
<div id="regions" aria-label="Histogram colors"></div><p><small>The Hyperion layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. HyperHDR receives a generated color plane, which its image mapping converts to LED colors. Restart the service after changing the LED layout.</small></p>
|
||||
<p><small id="diagnostics"></small></p>
|
||||
<div id="regions" aria-label="Histogram colors"></div><p><small>Adjust saturation, brightness and smoothing in your receiver. The receiver layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. HyperHDR receives a generated color plane, which its image mapping converts to LED colors. Restart the service after changing the LED layout.</small></p>
|
||||
<script src="remote.js"></script><script src="app.js"></script></html>
|
||||
@@ -79,3 +79,13 @@ HyperHDR 22's `HyperHdrInstance::setColor` registers `ledColors[0]` as a solid c
|
||||
For HyperHDR selection, 0.8.1 sends an RGB JSON image generated from the calibrated histogram edge samples on a cached 160x90 geometry. The receiver samples the synthetic plane using its LED rectangles and image processing. Hyperion retains per-LED color-array output. No TV video pixels are captured or transmitted by the generated plane. Original saturation/brightness gains remain 1.0. Receiver configuration is not modified.
|
||||
|
||||
On the G4 with HyperHDR 22.0.0, a temporary spatial test input produced 120 unique colors for 120 LEDs and was cleared. Exact RGB/base64, image command and spatial geometry tests passed on ARM. After installation of 0.8.1, the real histogram source was visible as IMAGE at priority 149. Eleven LED-stream samples from actual playback contained 87-100 unique colors across the 120 LEDs, confirming spatial output rather than a solid color. This plane has approximate calibrated spatial resolution, not full video-frame detail; HyperHDR's configured mapping and calibration affect the output.
|
||||
|
||||
## Receiver diagnostics and remote PicCap (0.9.0)
|
||||
|
||||
The own service exposes a nonblocking read-only JSON connection test, separate from streaming/capture. The UI reports connection/layout/authentication results and capture/retry diagnostics without exposing saved API tokens. Auto PicCap now reads settings and updates only address, port and unix-socket through its existing API when required. Unlike older versions, endpoint configuration is saved by PicCap; other settings and its autostart preference are preserved. PicCap setSettings restarts its capture, so HyperDRM releases VT first and checks status before treating the source as active. Remote IPv6 keeps histogram capture because the tested PicCap network client is IPv4-only.
|
||||
|
||||
ARM tests on the G4 passed connection diagnostics, fragmented replies, invalid layout, rejected token, connection refusal/timeout, receiver setting persistence and remote PicCap controller transitions. The app receiver-field and remote-navigation tests passed. The installed local HyperHDR JSON test validated 120 LEDs in 224 ms; a remote relay with synthetic authentication and fragmented responses validated the same layout in 625 ms. The unprotected test video used PicCap through the PC address and custom FlatBuffer port, with connected:true, videoRunning:true and actual FlatBuffer bytes received by the relay. PicCap retained all non-endpoint settings, including autostart:false. These relays test a remote transport to the actual local HyperHDR instance, not a separately installed remote HyperHDR instance.
|
||||
|
||||
Switching the user playback back to DRM stopped PicCap automatically and resumed histogram output through the remote JSON relay. Seven actual HyperHDR LED-stream samples contained 89-91 distinct colors for 120 LEDs, with no all-black sample. Both test endpoints were restored to local defaults and the synthetic token was removed.
|
||||
|
||||
The remote FlatBuffer relay received over 411 MB from PicCap during the test session. The final port validation also rejects explicit JSON null while retaining the default for older preferences without the new field.
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef HYPERDRM_DIAGNOSTICS_H
|
||||
#define HYPERDRM_DIAGNOSTICS_H
|
||||
/* An independent nonblocking connection. Tests never send colors or acquire
|
||||
* capture. Work per timer iteration is bounded to keep output responsive. */
|
||||
typedef enum {TEST_IDLE,TEST_CONNECT,TEST_SEND,TEST_LOGIN,TEST_LAYOUT,TEST_DONE,TEST_FAILED} TestStage;
|
||||
static struct {
|
||||
int fd;TestStage stage,after_send;TargetConfig target;
|
||||
uint64_t started,deadline,elapsed;unsigned leds;
|
||||
char request[384],error[128];size_t sent,used;char *response;
|
||||
} connection_test={.fd=-1};
|
||||
static const char *test_stage_name(void){
|
||||
static const char *names[]={"Not tested","Connecting","Sending request","Checking authentication","Reading LED layout","Passed","Failed"};
|
||||
return names[connection_test.stage];
|
||||
}
|
||||
static bool test_busy(void){return connection_test.stage>=TEST_CONNECT && connection_test.stage<=TEST_LAYOUT;}
|
||||
static void test_close(void){if(connection_test.fd>=0)close(connection_test.fd);connection_test.fd=-1;free(connection_test.response);connection_test.response=NULL;memset(connection_test.request,0,sizeof(connection_test.request));memset(connection_test.target.token,0,sizeof(connection_test.target.token));}
|
||||
static void test_finish(const char *error,uint64_t now){
|
||||
test_close();connection_test.elapsed=now-connection_test.started;
|
||||
connection_test.stage=error?TEST_FAILED:TEST_DONE;
|
||||
snprintf(connection_test.error,sizeof(connection_test.error),"%s",error?error:"");
|
||||
}
|
||||
static void test_request(bool login,uint64_t now){
|
||||
if(login)snprintf(connection_test.request,sizeof(connection_test.request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",connection_test.target.token);
|
||||
else snprintf(connection_test.request,sizeof(connection_test.request),"{\"command\":\"serverinfo\"}\n");
|
||||
connection_test.sent=connection_test.used=0;connection_test.after_send=login?TEST_LOGIN:TEST_LAYOUT;
|
||||
connection_test.stage=TEST_SEND;connection_test.deadline=now+3000;
|
||||
}
|
||||
static bool test_begin(const TargetConfig *target,uint64_t now){
|
||||
if(test_busy())return false;
|
||||
test_close();memset(&connection_test,0,sizeof(connection_test));connection_test.fd=-1;
|
||||
connection_test.target=*target;connection_test.started=now;
|
||||
connection_test.response=malloc(2*1024*1024+1);
|
||||
if(!connection_test.response){test_finish("Cannot allocate connection test buffer",now);return true;}
|
||||
connection_test.fd=target_open(target);
|
||||
if(connection_test.fd<0){test_finish("Cannot open receiver connection",now);return true;}
|
||||
connection_test.stage=TEST_CONNECT;connection_test.deadline=now+3000;return true;
|
||||
}
|
||||
static void test_tick(PlaybackJson *json,uint64_t now){
|
||||
if(!test_busy())return;
|
||||
if(now>=connection_test.deadline){test_finish("Receiver timed out",now);return;}
|
||||
int fd=connection_test.fd;
|
||||
if(connection_test.stage==TEST_CONNECT){
|
||||
struct pollfd p={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&p,1,0)<=0)return;
|
||||
if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){test_finish("Receiver connection refused or unreachable",now);return;}
|
||||
test_request(connection_test.target.token[0]!=0,now);return;
|
||||
}
|
||||
if(connection_test.stage==TEST_SEND){
|
||||
size_t length=strlen(connection_test.request);
|
||||
ssize_t n=send(fd,connection_test.request+connection_test.sent,length-connection_test.sent,MSG_NOSIGNAL);
|
||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
||||
if(n<=0){test_finish("Receiver closed the test connection",now);return;}
|
||||
connection_test.sent+=(size_t)n;
|
||||
if(connection_test.sent==length){connection_test.stage=connection_test.after_send;connection_test.deadline=now+3000;memset(connection_test.request,0,sizeof(connection_test.request));}
|
||||
return;
|
||||
}
|
||||
size_t available=2*1024*1024-connection_test.used;if(available>65536)available=65536;
|
||||
if(!available){test_finish("Receiver response exceeds 2 MiB",now);return;}
|
||||
ssize_t n=recv(fd,connection_test.response+connection_test.used,available,0);
|
||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
||||
if(n<=0){test_finish("Receiver closed the test connection",now);return;}
|
||||
connection_test.used+=(size_t)n;connection_test.response[connection_test.used]=0;
|
||||
char *end=strchr(connection_test.response,'\n');if(!end)return;*end=0;
|
||||
void *root=json->parse(connection_test.response);bool success=playback_true(json,root,"success");if(root)json->put(root);
|
||||
if(!success){test_finish(connection_test.stage==TEST_LOGIN?"Receiver authentication failed":"Receiver rejected serverinfo; check authorization",now);return;}
|
||||
if(connection_test.stage==TEST_LOGIN){test_request(false,now);return;}
|
||||
LedBlend *mapping=NULL;unsigned count=0;
|
||||
if(!layout_from_json(connection_test.response,&mapping,&count)){test_finish("Receiver returned an invalid or empty LED layout",now);return;}
|
||||
free(mapping);connection_test.leds=count;test_finish(NULL,now);
|
||||
}
|
||||
#endif
|
||||
+20
-8
@@ -17,6 +17,8 @@ static bool (*cancel_call)(void*,unsigned long,LSError*);
|
||||
static unsigned long piccap_token,media_token;
|
||||
static uint64_t piccap_deadline,media_deadline;
|
||||
static unsigned clear_samples;
|
||||
static TargetConfig piccap_target;
|
||||
static char piccap_error[128];
|
||||
static bool piccap_request(int command);
|
||||
static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;}
|
||||
static bool handover_save(bool enabled){
|
||||
@@ -39,7 +41,7 @@ static bool media_reply(void *h,void *m,void *ctx){
|
||||
if(protection==PLAYBACK_CLEAR){if(clear_samples<2)clear_samples++;}else clear_samples=0;
|
||||
return true;
|
||||
}
|
||||
/* Commands: 0 status, 1 start, 2 stop. Confirm stopped status before VT reuse. */
|
||||
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before VT reuse. */
|
||||
static bool piccap_reply(void *h,void *m,void *ctx){
|
||||
(void)h;int command=(int)(intptr_t)ctx;piccap_busy=false;piccap_token=0;
|
||||
void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue");
|
||||
@@ -55,7 +57,16 @@ static bool piccap_reply(void *h,void *m,void *ctx){
|
||||
if(!retries.paused)retries.due=milliseconds();
|
||||
}
|
||||
}else if(!piccap_installed()){piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;}
|
||||
}else if(command==3 && capture_source==SOURCE_PICCAP_STARTING){
|
||||
void *port=playback_field(&playback_json,root,"port"),*socket=playback_field(&playback_json,root,"unix-socket");
|
||||
bool same=ok && playback_is(&playback_json,root,"address",target_piccap_host(&piccap_target)) && port && playback_json.type(port)==3 && strtoul(playback_json.string(port),NULL,10)==piccap_target.piccap_port && socket && playback_json.type(socket)==1 && !playback_json.boolean(socket);
|
||||
if(ok){if(!piccap_request(same?1:4)){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap endpoint request failed");}}
|
||||
else {capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"Cannot read PicCap settings");}
|
||||
}else if(command==4){
|
||||
if(!ok){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap rejected receiver settings");}
|
||||
/* setSettings restarts PicCap itself; status confirms ownership. */
|
||||
}else if(command==1 && !ok){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=milliseconds()+10000;}
|
||||
if(command>=3 && capture_source==SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
|
||||
if(root)playback_json.put(root);
|
||||
next_piccap_probe=milliseconds()+500;
|
||||
if(command==2)piccap_request(0);
|
||||
@@ -63,9 +74,10 @@ static bool piccap_reply(void *h,void *m,void *ctx){
|
||||
}
|
||||
static bool piccap_request(int command){
|
||||
if(piccap_busy)return false;
|
||||
const char *uri=command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
|
||||
const char *uri=command==3?"luna://org.webosbrew.piccap.service/getSettings":command==4?"luna://org.webosbrew.piccap.service/setSettings":command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
|
||||
LSError e;error_init(&e);
|
||||
piccap_busy=call_once(service_handle,uri,"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
|
||||
char data[160];snprintf(data,sizeof(data),"{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",target_piccap_host(&piccap_target),piccap_target.piccap_port);
|
||||
piccap_busy=call_once(service_handle,uri,command==4?data:"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
|
||||
piccap_deadline=milliseconds()+3000;
|
||||
error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy;
|
||||
}
|
||||
@@ -73,20 +85,20 @@ static bool piccap_request(int command){
|
||||
static bool handover_tick(void){
|
||||
uint64_t now=milliseconds();
|
||||
if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;}
|
||||
if(piccap_busy && now>=piccap_deadline){LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
if(piccap_busy && now>=piccap_deadline){snprintf(piccap_error,sizeof(piccap_error),"PicCap request timed out");LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
if(handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && !media_busy && now>=next_media_probe){
|
||||
LSError e;error_init(&e);
|
||||
media_busy=call_once(service_handle,"luna://com.webos.media/getActivePipelines","{}",media_reply,NULL,&media_token,&e);
|
||||
media_deadline=now+3000;
|
||||
error_free(&e);next_media_probe=now+1000;
|
||||
}
|
||||
bool clear=handover_enabled && target_piccap_local(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
|
||||
bool clear=handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
|
||||
protection==PLAYBACK_CLEAR && clear_samples>=2 && now-protection_at<3000 && now>=handover_cooldown;
|
||||
if(capture_source==SOURCE_HISTOGRAM && clear){
|
||||
stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
|
||||
if(!piccap_request(1)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+10000;}
|
||||
piccap_target=target;piccap_error[0]=0;stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
|
||||
if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+10000;}
|
||||
}
|
||||
if(piccap_owned && !clear && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
if(piccap_owned && (!clear || strcmp(piccap_target.host,target.host) || piccap_target.piccap_port!=target.piccap_port) && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
|
||||
if(piccap_owned && !piccap_busy && now>=next_piccap_probe){
|
||||
if(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0);
|
||||
next_piccap_probe=now+1000;
|
||||
|
||||
+16
-6
@@ -26,6 +26,7 @@
|
||||
#include "retry.h"
|
||||
#include "target.h"
|
||||
#include "spatial_image.h"
|
||||
#include "diagnostics.h"
|
||||
#define SERVICE "org.webosbrew.histogramhyperion.service"
|
||||
#define PRIORITY 149
|
||||
typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError;
|
||||
@@ -64,7 +65,7 @@ static LedBlend *led_blends;
|
||||
static bool interpolation_enabled;
|
||||
static RetryState retries;
|
||||
static unsigned capture_recoveries;
|
||||
static uint64_t recovery_started,last_recovery_ms;
|
||||
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;
|
||||
@@ -189,15 +190,23 @@ static bool settings(void *h,void *m,void *ctx){
|
||||
if(!playback_json.lib || !target_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Enter a valid IPv4/IPv6 address, integer port (1-65535), receiver and optional API token\"}");
|
||||
if(!target_save(TARGET_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}");
|
||||
bool active=capture || piccap_owned || retries.due;
|
||||
stop_capture();target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;
|
||||
stop_capture();test_close();connection_test.stage=TEST_IDLE;target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;
|
||||
if(active && !retries.paused && !retries.blocked)retries.due=milliseconds();
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static bool test_connection(void *h,void *m,void *ctx){
|
||||
(void)ctx;
|
||||
if(strcmp(payload(m),"{}") || !playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Test requires an empty object and JSON-C\"}");
|
||||
if(!test_begin(&target,milliseconds()))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"A connection test is already running\"}");
|
||||
return respond(h,m,"{\"returnValue\":true,\"testing\":true}");
|
||||
}
|
||||
static bool status(void *h,void *m,void *ctx){
|
||||
(void)ctx;char json[4096],safe[512],endpoint[80];size_t n=0;
|
||||
snprintf(endpoint,sizeof(endpoint),strchr(target.host,':')?"[%s]:%u":"%s:%u",target.host,target.port);
|
||||
for(size_t i=0;last_error[i] && n+2<sizeof(safe);i++){char c=last_error[i];if(c=='"'||c=='\\')safe[n++]='\\';if((unsigned char)c>=32)safe[n++]=c;}safe[n]=0;
|
||||
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.8.1\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_local(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
|
||||
uint64_t now=milliseconds();
|
||||
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\",\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",target.piccap_port,test_stage_name(),connection_test.leds,(unsigned long long)connection_test.elapsed,connection_test.error,last_output_at?(long long)(now-last_output_at):-1,(unsigned long long)(retries.due>now?retries.due-now:0),capture_source==SOURCE_PICCAP_STARTING?"Starting PicCap":capture_source==SOURCE_PICCAP_STOPPING?"Stopping PicCap":piccap_owned?"PicCap":capture?"Histogram":"Stopped",piccap_error);
|
||||
used+=snprintf(json+used,sizeof(json)-used,"\"version\":\"0.9.0\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_compatible(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
|
||||
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]);
|
||||
used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false");
|
||||
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]);
|
||||
@@ -205,6 +214,7 @@ static bool status(void *h,void *m,void *ctx){
|
||||
}
|
||||
static int tick(void *unused){
|
||||
(void)unused;
|
||||
test_tick(&playback_json,milliseconds());
|
||||
if(handover_tick())return 1;
|
||||
if(!capture){
|
||||
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
|
||||
@@ -265,7 +275,7 @@ static int tick(void *unused){
|
||||
while(used+1<sizeof(answer)){ssize_t len=recv(output,answer+used,sizeof(answer)-used-1,0);if(len<=0)break;used+=(size_t)len;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
|
||||
if(!complete){set_error("Receiver did not acknowledge color data");stop_capture();return 1;}
|
||||
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Receiver rejected the color command");stop_capture();return 1;}
|
||||
color_valid=true;frames++;return 1;
|
||||
color_valid=true;last_output_at=milliseconds();frames++;return 1;
|
||||
}
|
||||
static void *symbol(void *lib,const char *name){void *s=dlsym(lib,name);if(!s){fprintf(stderr,"Missing symbol %s\n",name);exit(2);}return s;}
|
||||
int main(void){
|
||||
@@ -287,13 +297,13 @@ int main(void){
|
||||
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
|
||||
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
|
||||
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
|
||||
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{NULL,NULL,0}};
|
||||
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{"testConnection",test_connection,0},{NULL,NULL,0}};
|
||||
if(!reg(SERVICE,&handle,&e)||!category(handle,"/",methods,NULL,NULL,&e)||!attach(handle,loop,&e)){fprintf(stderr,"Luna registration failed: %s\n",e.message?e.message:"unknown");error_free(&e);return 3;}
|
||||
service_handle=handle;
|
||||
if(!boot_enabled())retry_pause(&retries);
|
||||
timer(100,tick,NULL);
|
||||
while(!quitting){iterate(NULL,0);usleep(10000);}
|
||||
stop_capture();
|
||||
test_close();stop_capture();
|
||||
if(piccap_owned){
|
||||
handover_enabled=false;retry_pause(&retries);
|
||||
uint64_t deadline=milliseconds()+4000;
|
||||
|
||||
+20
-7
@@ -11,12 +11,15 @@
|
||||
#include <unistd.h>
|
||||
#include "playback.h"
|
||||
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
||||
typedef struct {char host[46],receiver[16],token[257];unsigned port;} TargetConfig;
|
||||
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444};
|
||||
typedef struct {char host[46],receiver[16],token[257];unsigned port,piccap_port;} TargetConfig;
|
||||
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444,.piccap_port=19400};
|
||||
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
|
||||
static inline bool target_piccap_compatible(const TargetConfig *t){unsigned char address[4];return !strcmp(t->host,"::1") || inet_pton(AF_INET,t->host,address)==1;}
|
||||
static inline const char *target_piccap_host(const TargetConfig *t){return !strcmp(t->host,"::1")?"127.0.0.1":t->host;}
|
||||
static bool target_valid(const TargetConfig *t){
|
||||
unsigned char address[16];
|
||||
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
|
||||
if(!t->piccap_port || t->piccap_port>65535)return false;
|
||||
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
|
||||
for(const unsigned char *p=(const unsigned char*)t->token;*p;p++)
|
||||
if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false;
|
||||
@@ -37,6 +40,11 @@ static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
|
||||
void *port=playback_field(j,root,"port");
|
||||
if(!port || j->type(port)!=3)valid=false;
|
||||
else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;}
|
||||
void *flat=NULL;
|
||||
if(j->type(root)==4 && j->get(root,"piccapPort",&flat)){
|
||||
if(j->type(flat)!=3)valid=false;
|
||||
else {char *end=NULL;errno=0;long n=strtol(j->string(flat),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.piccap_port=(unsigned)n;}
|
||||
}
|
||||
void *clear=playback_field(j,root,"clearToken");
|
||||
if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;}
|
||||
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
|
||||
@@ -44,7 +52,7 @@ 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;
|
||||
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->receiver,t->token);
|
||||
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;
|
||||
@@ -56,7 +64,7 @@ static bool target_load(PlaybackJson *j,const char *path,TargetConfig *t){
|
||||
char data[1024];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);if(n<=0 || n==(ssize_t)sizeof(data)-1)return false;
|
||||
data[n]=0;return target_parse(j,data,t);
|
||||
}
|
||||
static int target_connect(const TargetConfig *t){
|
||||
static int target_open(const TargetConfig *t){
|
||||
if(!target_valid(t))return -1;
|
||||
struct sockaddr_storage storage={0};socklen_t length;int family;
|
||||
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
|
||||
@@ -66,10 +74,15 @@ static int target_connect(const TargetConfig *t){
|
||||
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
|
||||
if(connect(fd,(void*)&storage,length)!=0){
|
||||
if(errno!=EINPROGRESS){close(fd);return -1;}
|
||||
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;}
|
||||
}
|
||||
if(fcntl(fd,F_SETFL,flags)<0){close(fd);return -1;}
|
||||
return fd;
|
||||
}
|
||||
static int target_connect(const TargetConfig *t){
|
||||
int fd=target_open(t);if(fd<0)return -1;
|
||||
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
|
||||
int flags=fcntl(fd,F_GETFL,0);
|
||||
if(flags<0 || fcntl(fd,F_SETFL,flags & ~O_NONBLOCK)<0){close(fd);return -1;}
|
||||
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
|
||||
return fd;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/wait.h>
|
||||
#include "target.h"
|
||||
#include "layout.h"
|
||||
#include "diagnostics.h"
|
||||
static void fixture(PlaybackJson *json,int kind){
|
||||
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))==0 && listen(server,1)==0);
|
||||
socklen_t length=sizeof(address);assert(getsockname(server,(void*)&address,&length)==0);
|
||||
TargetConfig config=target;config.port=ntohs(address.sin_port);if(kind==1)strcpy(config.token,"fixture-token");
|
||||
pid_t child=fork();assert(child>=0);
|
||||
if(!child){
|
||||
int fd=accept(server,NULL,NULL);if(fd<0)_exit(2);char request[512]={0};size_t used=0;
|
||||
while(used+1<sizeof(request)){ssize_t n=read(fd,request+used,sizeof(request)-used-1);if(n<=0)_exit(3);used+=(size_t)n;if(strchr(request,'\n'))break;}
|
||||
if(!strstr(request,kind==1?"authorize":"serverinfo") || strstr(request,"color") || strstr(request,"image"))_exit(4);
|
||||
const char *response=kind==1?"{\"success\":false}\n":kind==2?"{\"success\":true,\"info\":{\"leds\":[]}}\n":"{\"success\":true,\"info\":{\"leds\":[{\"hmin\":0,\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n";
|
||||
if(write(fd,response,7)!=7)_exit(5);
|
||||
usleep(20000);
|
||||
if(write(fd,response+7,strlen(response)-7)!=(ssize_t)strlen(response)-7)_exit(6);
|
||||
close(fd);close(server);_exit(0);
|
||||
}
|
||||
assert(test_begin(&config,1000));assert(!test_begin(&config,1000));
|
||||
for(unsigned i=0;test_busy() && i<400;i++){test_tick(json,1000+i*10);usleep(1000);}
|
||||
assert(!test_busy());assert(connection_test.stage==(kind?TEST_FAILED:TEST_DONE));
|
||||
if(!kind)assert(connection_test.leds==1 && !connection_test.error[0]);
|
||||
if(kind==1)assert(strstr(connection_test.error,"authentication"));
|
||||
if(kind==2)assert(strstr(connection_test.error,"layout"));
|
||||
int status;assert(waitpid(child,&status,0)==child && WIFEXITED(status) && WEXITSTATUS(status)==0);
|
||||
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;
|
||||
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);
|
||||
connection_test.fd=peers[0];connection_test.stage=TEST_LAYOUT;connection_test.started=20000;connection_test.deadline=23000;
|
||||
test_tick(&json,23000);assert(connection_test.stage==TEST_FAILED && strstr(connection_test.error,"timed out"));close(peers[1]);
|
||||
puts("PASS: read-only connection test, fragmented response, layout validation, rejected token, connection refusal and timeout");return 0;
|
||||
}
|
||||
+24
-9
@@ -10,45 +10,60 @@ typedef bool (*Callback)(void*,void*,void*);
|
||||
static RetryState retries;
|
||||
static uint64_t clock_ms=10000;
|
||||
static unsigned stopped,called,canceled;
|
||||
static char last_uri[128];
|
||||
static char last_uri[128],last_data[256];
|
||||
static uint64_t milliseconds(void){return clock_ms;}
|
||||
static void stop_capture(void){stopped++;}
|
||||
static const char *payload(void *m){return m;}
|
||||
static void error_init(LSError *e){(void)e;}
|
||||
static void error_free(LSError *e){(void)e;}
|
||||
#define PICCAP_BINARY "/bin/sh"
|
||||
static int target;
|
||||
static bool target_piccap_local(const int *t){(void)t;return true;}
|
||||
#include "target.h"
|
||||
#include "handover.h"
|
||||
static bool mock_call(void *h,const char *uri,const char *data,Callback cb,void *ctx,unsigned long *token,LSError *e){
|
||||
(void)h;(void)data;(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
|
||||
(void)h;snprintf(last_data,sizeof(last_data),"%s",data);(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
|
||||
}
|
||||
static bool mock_cancel(void *h,unsigned long token,LSError *e){(void)h;(void)token;(void)e;canceled++;return true;}
|
||||
static void configured(void){
|
||||
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
|
||||
assert(strstr(last_uri,"/start"));
|
||||
}
|
||||
static void answer(int cmd,bool running){
|
||||
char json[128];snprintf(json,sizeof(json),"{\"returnValue\":true,\"isRunning\":%s,\"connected\":true}",running?"true":"false");
|
||||
piccap_reply(NULL,json,(void*)(intptr_t)cmd);
|
||||
}
|
||||
int main(void){
|
||||
(void)target_save;(void)target_load;(void)target_connect;
|
||||
(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;protection=PLAYBACK_PROTECTED;protection_at=clock_ms;next_media_probe=UINT64_MAX;
|
||||
assert(!handover_tick());assert(called==0 && stopped==0);
|
||||
protection=PLAYBACK_CLEAR;clear_samples=1;assert(!handover_tick());assert(called==0);
|
||||
clear_samples=2;assert(handover_tick());assert(stopped==1 && strstr(last_uri,"/start"));
|
||||
answer(1,false);clock_ms+=600;assert(handover_tick());assert(strstr(last_uri,"/status"));answer(0,true);
|
||||
clear_samples=2;assert(handover_tick());assert(stopped==1 && strstr(last_uri,"/getSettings"));
|
||||
configured();answer(1,false);clock_ms+=600;assert(handover_tick());assert(strstr(last_uri,"/status"));answer(0,true);
|
||||
assert(capture_source==SOURCE_PICCAP && piccap_connected);
|
||||
protection=PLAYBACK_PROTECTED;assert(handover_tick());assert(strstr(last_uri,"/stop"));
|
||||
answer(2,true);assert(strstr(last_uri,"/status"));assert(piccap_owned);answer(0,false);
|
||||
assert(!piccap_owned && capture_source==SOURCE_HISTOGRAM && retries.due==clock_ms);
|
||||
protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;assert(handover_tick());answer(1,true);
|
||||
protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;assert(handover_tick());configured();answer(1,true);
|
||||
retry_pause(&retries);assert(handover_tick());assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
|
||||
assert(!piccap_owned && retries.due==0);assert(!handover_tick());
|
||||
retry_reset(&retries);assert(handover_tick());answer(1,true);handover_enabled=false;
|
||||
retry_reset(&retries);assert(handover_tick());configured();answer(1,true);handover_enabled=false;
|
||||
assert(handover_tick());answer(2,false);answer(0,false);assert(!piccap_owned);
|
||||
handover_enabled=true;protection_at=clock_ms;assert(handover_tick());
|
||||
clock_ms+=3100;assert(handover_tick());assert(canceled==1 && capture_source==SOURCE_PICCAP_STOPPING);
|
||||
assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
|
||||
assert(!piccap_owned);assert(!handover_tick());
|
||||
puts("PicCap handover tests passed");return 0;
|
||||
retries.paused=false;handover_cooldown=0;protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;
|
||||
strcpy(target.host,"10.1.0.100");target.piccap_port=19446;
|
||||
assert(handover_tick());assert(strstr(last_uri,"/getSettings"));
|
||||
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
|
||||
assert(strstr(last_uri,"/setSettings"));assert(!strcmp(last_data,"{\"address\":\"10.1.0.100\",\"port\":19446,\"unix-socket\":false}"));
|
||||
answer(4,true);clock_ms+=600;handover_tick();answer(0,true);assert(capture_source==SOURCE_PICCAP);
|
||||
target.piccap_port=19400;assert(handover_tick());assert(capture_source==SOURCE_PICCAP_STOPPING);answer(2,false);answer(0,false);
|
||||
handover_cooldown=0;protection_at=clock_ms;assert(handover_tick());
|
||||
piccap_reply(NULL,"{\"returnValue\":false}",(void*)(intptr_t)3);
|
||||
assert(capture_source==SOURCE_PICCAP_STOPPING && piccap_error[0]);
|
||||
handover_tick();answer(2,false);answer(0,false);assert(!piccap_owned);
|
||||
puts("PicCap handover tests passed: source ownership, remote endpoint, target change and configuration failure");return 0;
|
||||
}
|
||||
@@ -5,12 +5,17 @@
|
||||
#include "target.h"
|
||||
int main(void){
|
||||
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
|
||||
assert(target_valid(&t));assert(target_piccap_local(&t));
|
||||
assert(target_valid(&t));assert(t.piccap_port==19400);assert(target_piccap_local(&t));
|
||||
assert(target_parse(&j,"{\"host\":\"10.1.0.91\",\"port\":19445,\"receiver\":\"HyperHDR\",\"token\":\"test-token\"}",&t));
|
||||
assert(t.port==19445 && !strcmp(t.receiver,"HyperHDR") && !strcmp(t.token,"test-token"));assert(!target_piccap_local(&t));
|
||||
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"Hyperion\"}",&t));assert(!strcmp(t.token,"test-token"));
|
||||
const char *bad[]={"{}","[]","{\"host\":\"localhost\",\"port\":19444,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":0,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":65536,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444.5,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":\"19444\",\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"Other\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"token\":\"bad\\\"token\"}"};
|
||||
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){TargetConfig before=t;assert(!target_parse(&j,bad[i],&t));assert(!memcmp(&t,&before,sizeof(t)));}
|
||||
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":65535,\"receiver\":\"HyperHDR\"}",&t));assert(t.piccap_port==65535);
|
||||
assert(!target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":0,\"receiver\":\"HyperHDR\"}",&t));assert(t.piccap_port==65535);
|
||||
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"HyperHDR\"}",&t));assert(t.piccap_port==65535);
|
||||
assert(!target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":null,\"receiver\":\"HyperHDR\"}",&t));
|
||||
TargetConfig v6=t;strcpy(v6.host,"2001:db8::1");assert(!target_piccap_compatible(&v6));strcpy(v6.host,"::1");assert(target_piccap_compatible(&v6) && !strcmp(target_piccap_host(&v6),"127.0.0.1"));
|
||||
const char *file="/tmp/hyperdrm-target-test.json";assert(access(file,F_OK)!=0);assert(access("/tmp/hyperdrm-target-test.json.tmp",F_OK)!=0);
|
||||
assert(target_save(file,&t));struct stat st;assert(stat(file,&st)==0 && (st.st_mode&0777)==0600);
|
||||
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.8.1", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.9.0", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
|
||||
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
"""Test-only FlatBuffer TCP relay; accepts only the configured TV address."""
|
||||
import argparse
|
||||
import select
|
||||
import socket
|
||||
import socketserver
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--bind', required=True)
|
||||
parser.add_argument('--port', type=int, default=19446)
|
||||
parser.add_argument('--upstream', required=True)
|
||||
parser.add_argument('--upstream-port', type=int, default=19400)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
class Relay(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
if self.client_address[0] != args.upstream:
|
||||
return
|
||||
transferred = 0
|
||||
try:
|
||||
with socket.create_connection((args.upstream, args.upstream_port), timeout=3) as remote:
|
||||
self.request.settimeout(3)
|
||||
while True:
|
||||
readable, _, _ = select.select([self.request, remote], [], [], 3)
|
||||
if not readable:
|
||||
break
|
||||
for source in readable:
|
||||
data = source.recv(65536)
|
||||
if not data:
|
||||
return
|
||||
destination = remote if source is self.request else self.request
|
||||
destination.sendall(data)
|
||||
if source is self.request:
|
||||
if not transferred:
|
||||
print('PicCap FlatBuffer data received', flush=True)
|
||||
transferred += len(data)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
print(f'FlatBuffer connection closed: {transferred} bytes from TV', flush=True)
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingTCPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
with Server((args.bind, args.port), Relay) as server:
|
||||
print('FlatBuffer relay ready', flush=True)
|
||||
server.serve_forever()
|
||||
@@ -0,0 +1,24 @@
|
||||
'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'];
|
||||
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=[];
|
||||
const status={returnValue:true,version:'0.9.0',host:'127.0.0.1',port:19444,piccapPort:19400,receiver:'HyperHDR',tokenSet:true,piccapInstalled:true,autostart:true,captureState:'Histogram',playbackProtection:'protected',connectionTest:{state:'Passed',ledCount:120,elapsedMs:300,error:''},uid:0};
|
||||
class Bridge {
|
||||
call(uri,data){requests.push({uri,data:JSON.parse(data)});this.onservicecallback(JSON.stringify(uri.endsWith('/status')?status:{returnValue:true}));}
|
||||
cancel(){}
|
||||
}
|
||||
let controls;
|
||||
const context={PalmServiceBridge:Bridge,document:{getElementById:id=>elements[id],createElement:()=>({style:{}})},window:{},setTimeout:()=>1,clearTimeout(){},setInterval(){},setupRemoteNavigation(list){controls=list;}};
|
||||
vm.runInNewContext(fs.readFileSync('app/app.js','utf8'),context);
|
||||
assert(elements.testResult.textContent.includes('120 LEDs'));
|
||||
assert(elements.diagnostics.textContent.includes('Root service'));
|
||||
assert(controls.includes(elements.piccapPort)&&controls.includes(elements.testConnection));
|
||||
context.targetDirty=true;elements.testConnection.onclick();assert(elements.error.textContent.includes('Save receiver'));
|
||||
elements.piccapPort.value='0';elements.saveTarget.onclick();assert(elements.error.textContent.includes('FlatBuffer port'));
|
||||
elements.piccapPort.value='19446';elements.host.value='10.1.0.100';elements.token.value='';elements.saveTarget.onclick();
|
||||
const saved=requests.find(r=>r.uri.endsWith('/settings'));assert.strictEqual(saved.data.piccapPort,19446);assert.strictEqual(saved.data.host,'10.1.0.100');assert(!('token' in saved.data));
|
||||
elements.testConnection.onclick();assert(requests.some(r=>r.uri.endsWith('/testConnection')));
|
||||
assert.strictEqual(elements.token.value,'');assert(!elements.diagnostics.textContent.includes('fixture-token'));
|
||||
console.log('PASS: receiver fields, port validation, saved-token retention, test control and diagnostics');
|
||||
Reference in new issue
Block a user