Private
Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcdfc4b431 | ||
|
|
ef03258e22 | ||
|
|
2f6efd9864 | ||
|
|
c3040680a0 | ||
|
|
c0d891b372 | ||
|
|
7f6eeab39e | ||
|
|
f74a658097 |
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
BasedOnStyle: LLVM
|
||||||
|
IndentWidth: 4
|
||||||
|
ColumnLimit: 100
|
||||||
|
AllowShortFunctionsOnASingleLine: Empty
|
||||||
|
AllowShortIfStatementsOnASingleLine: Never
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
SortIncludes: Never
|
||||||
@@ -39,3 +39,11 @@ test-diagnostics
|
|||||||
test-calibration
|
test-calibration
|
||||||
test-layout_refresh
|
test-layout_refresh
|
||||||
test-spatial_image
|
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",
|
"id": "org.webosbrew.histogramhyperion",
|
||||||
"version": "0.10.0",
|
"version": "0.13.4",
|
||||||
"type": "web",
|
"type": "web",
|
||||||
"title": "HyperDRM",
|
"title": "HyperDRM",
|
||||||
"appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR",
|
"appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR",
|
||||||
"iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png",
|
"iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png",
|
||||||
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM",
|
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM",
|
||||||
"rootRequired": true,
|
"rootRequired": true,
|
||||||
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.10.0_arm.ipk",
|
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.13.4_arm.ipk",
|
||||||
"ipkHash": {
|
"ipkHash": {
|
||||||
"sha256": "0ad040adfc0be402f5c83e783a8f9b4e78b33b59e5b04fa3d52652bf48ddda70"
|
"sha256": "3028ea164e05ff6b9c601aa7f3ff0f05b17781ba603eaebe3395abae503f7bb7"
|
||||||
},
|
},
|
||||||
"ipkSize": 1183836
|
"ipkSize": 1208554
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -9,6 +9,8 @@ Ambient Light for webOS, Hyperion and HyperHDR. An own webOS app and native ARM
|
|||||||
- Spatial colors from 16 LG histogram regions with interpolated transitions along the screen perimeter.
|
- Spatial colors from 16 LG histogram regions with interpolated transitions along the screen perimeter.
|
||||||
- LED count, ordering and screen rectangles read from the receiver at startup and refreshed every five seconds without capture restart. No fixed 200-LED layout; layouts with 50 and 500 LEDs were tested.
|
- LED count, ordering and screen rectangles read from the receiver at startup and refreshed every five seconds without capture restart. No fixed 200-LED layout; layouts with 50 and 500 LEDs were tested.
|
||||||
- Automatic detection of top/bottom black bars on the calibrated LG G4.
|
- Automatic detection of top/bottom black bars on the calibrated LG G4.
|
||||||
|
- Bounded last-color hold on empty/failed histogram reads, real black detection and 500 ms source transitions using a raw receiver LED subscription.
|
||||||
|
- Guided full-screen video calibration for additional TV models, with review before saving and a four-color validation video.
|
||||||
- Original saturation and brightness (both gains 1.0).
|
- Original saturation and brightness (both gains 1.0).
|
||||||
- Start, stop, status and persistent boot autostart switch in the TV app.
|
- Start, stop, status and persistent boot autostart switch in the TV app.
|
||||||
- Configurable IPv4/IPv6 receiver and JSON TCP port, optional token authentication, priority 149, up to 10 updates per second.
|
- Configurable IPv4/IPv6 receiver and JSON TCP port, optional token authentication, priority 149, up to 10 updates per second.
|
||||||
@@ -25,7 +27,7 @@ Only the own service is modified. Optional Auto PicCap can configure its receive
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The current package is [HyperDRM_0.10.0_arm.ipk](HyperDRM_0.10.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
The current package is [HyperDRM_0.13.4_arm.ipk](HyperDRM_0.13.4_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||||
|
|
||||||
Elevate the installed own service through Homebrew Channel:
|
Elevate the installed own service through Homebrew Channel:
|
||||||
|
|
||||||
@@ -139,3 +141,101 @@ While a capture session runs, a separate nonblocking authenticated JSON connecti
|
|||||||
Select Auto, LG G4 (measured), Average (uniform), or Custom model in TV calibration and select Save profile. The app obtains the model identifier through LG's system-property service. Auto selects an imported profile matching that exact identifier, then the built-in G4 series profile, otherwise uniform average colors. Unknown models are not assigned an invented spatial calibration. A manual G4 selection is available when model detection is unavailable.
|
Select Auto, LG G4 (measured), Average (uniform), or Custom model in TV calibration and select Save profile. The app obtains the model identifier through LG's system-property service. Auto selects an imported profile matching that exact identifier, then the built-in G4 series profile, otherwise uniform average colors. Unknown models are not assigned an invented spatial calibration. A manual G4 selection is available when model detection is unavailable.
|
||||||
|
|
||||||
Custom imports accept measured model JSON with 2-16 sorted clockwise perimeter points and histogram region indices 0-15. Profiles persist privately in the own service container and apply to the current session without restarting capture. See [profile format and calibration guidance](profiles/README.md). Only G4 hardware has been validated; importing a profile does not guarantee histogram support on another TV. Color calibration remains in Hyperion/HyperHDR.
|
Custom imports accept measured model JSON with 2-16 sorted clockwise perimeter points and histogram region indices 0-15. Profiles persist privately in the own service container and apply to the current session without restarting capture. See [profile format and calibration guidance](profiles/README.md). Only G4 hardware has been validated; importing a profile does not guarantee histogram support on another TV. Color calibration remains in Hyperion/HyperHDR.
|
||||||
|
|
||||||
|
## Color continuity and guided calibration
|
||||||
|
|
||||||
|
A populated histogram containing black pixels is a valid dark scene and is sent
|
||||||
|
immediately. Empty channel populations and failed reads are missing data, not
|
||||||
|
black. The last good colors are held for at most 1.5 seconds while capture remains
|
||||||
|
available; a longer failure triggers bounded capture recovery. Receiver failure
|
||||||
|
cannot be hidden by color holding. Real black does not wait for a recovery fade.
|
||||||
|
|
||||||
|
Source handovers retain the previous output briefly instead of clearing it before
|
||||||
|
PicCap connects. An independent nonblocking raw LED subscription provides the
|
||||||
|
previous PicCap colors for the return to HyperDRM, and a fresh PicCap target for
|
||||||
|
the forward transition. The transition lasts 500 ms, with a three-second overall
|
||||||
|
handover bound. Conversion from LED samples to histogram regions is approximate;
|
||||||
|
receiver color correction and configured smoothing remain in the receiver. If
|
||||||
|
streaming is unavailable, ordinary bounded source selection still works.
|
||||||
|
|
||||||
|
In **TV calibration**, select **Calibration** then **Start Auto Calibration**. The bundled H.264 video
|
||||||
|
shows a black reference and 32 white patches around all four edges. Keep the app
|
||||||
|
visible and the picture full-screen without zoom or bars. The service temporarily
|
||||||
|
disables PicCap handover and bar detection for this measurement, then restores
|
||||||
|
normal operation. Cancellation, Back, playback errors and a 110-second service
|
||||||
|
lease protect against leaving calibration active after the app closes.
|
||||||
|
|
||||||
|
Fresh measurements identify responsive edge regions. Global-only, absent or
|
||||||
|
ambiguous responses are rejected without overwriting a saved profile. Successful
|
||||||
|
measurements fill the Custom profile editor; choose **Save profile** to apply.
|
||||||
|
Select **Validate profile** afterwards: the top-left quadrant is red, top-right
|
||||||
|
green, bottom-right blue and bottom-left yellow. Check LED positions on every
|
||||||
|
edge. The measured profile is specific to the exact reported model; other models
|
||||||
|
still require their own measurement. This does not add HDR color calibration or
|
||||||
|
more independently captured regions.
|
||||||
|
|
||||||
|
An explicit app launch with `{"action":"calibrate"}` starts the same visible
|
||||||
|
wizard. The calibration video can be reproduced with
|
||||||
|
`python build_calibration_video.py --ffmpeg /path/to/ffmpeg`.
|
||||||
|
|
||||||
|
The TV UI separates Overview, Receiver and Calibration. Auto Calibration is also
|
||||||
|
linked from Overview. Fields use focusable selection frames; highlighting or
|
||||||
|
hovering a frame never focuses its input. OK or a click starts editing and opens
|
||||||
|
the TV keyboard. Keyboard dismissal or Back returns to the selection frame.
|
||||||
|
Only visible controls participate in arrow navigation. Custom profile JSON is
|
||||||
|
available through Edit profile JSON after selecting a custom profile.
|
||||||
|
|
||||||
|
If an update leaves an old process running without root, Overview shows Enable
|
||||||
|
Root Access. Changing autostart also handles a root-required reply automatically:
|
||||||
|
the app asks Homebrew Channel to elevate only its own service, restarts that
|
||||||
|
service, verifies UID 0, and retries the switch. Root and boot-hook failures have
|
||||||
|
separate messages. The explicit launch action `{"action":"repairRoot"}` invokes
|
||||||
|
the same visible repair flow. Homebrew Channel must be installed and permit the
|
||||||
|
request; denied elevation is reported without changing autostart.
|
||||||
|
|
||||||
|
### Development and releases
|
||||||
|
|
||||||
|
`VERSION` is the single source for app, package and native runtime versions.
|
||||||
|
Run `python sync_version.py` after editing it; `--check` rejects stale metadata.
|
||||||
|
Run `python release.py` to execute every JavaScript and portable native test,
|
||||||
|
compile the ARM service, build a reproducible IPK and verify its embedded versions,
|
||||||
|
ARM architecture, size and SHA-256 against the manifest. On Windows this requires
|
||||||
|
Node.js and Debian WSL with GCC, JSON-C and arm-linux-gnueabi-gcc; on Linux the
|
||||||
|
same tools must be on PATH. No TV connection or installation occurs.
|
||||||
|
|
||||||
|
After reviewing the working tree, `python release.py --push` also commits changes,
|
||||||
|
creates a new version tag and atomically pushes main and the tag to origin.
|
||||||
|
An existing tag or a branch other than main is rejected. Attach the exact verified
|
||||||
|
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.
|
||||||
|
|
||||||
|
Histogram capture defaults to 50 ms. The Receiver panel provides a saved Capture interval (ms) setting from 25 to 1000 in steps of 25. Existing settings without this field use 50 ms. PicCap capture timing is unchanged.
|
||||||
+369
-42
@@ -1,26 +1,132 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
var pending = [];
|
var pending = [];
|
||||||
var targetDirty=false,receiver='Hyperion';
|
var actionError = '';
|
||||||
var profileDirty=false,profile='auto';
|
var versionCompatible = typeof HYPERDRM_APP_VERSION === 'undefined';
|
||||||
var profileNames={auto:'Auto',g4:'LG G4 (measured)',average:'Average (uniform)',custom:'Custom model'};
|
|
||||||
function showProfile(){document.getElementById('profile').textContent='Profile: '+profileNames[profile];document.getElementById('customEditor').style.display=profile==='custom'?'block':'none';document.getElementById('customProfile').disabled=profile!=='custom';}
|
function checkServiceVersion(result) {
|
||||||
function call(method,done,data){
|
if (typeof HYPERDRM_APP_VERSION === 'undefined' || result.returnValue === false) return;
|
||||||
if(typeof PalmServiceBridge==='undefined'){done({returnValue:false,errorText:'This app requires an LG TV with the native service installed.'});return;}
|
versionCompatible = result.version === HYPERDRM_APP_VERSION;
|
||||||
var bridge=new PalmServiceBridge(),timer,finished=false;
|
var warning = document.getElementById('versionWarning');
|
||||||
function finish(value){if(finished)return;finished=true;clearTimeout(timer);var i=pending.indexOf(bridge);if(i>=0)pending.splice(i,1);done(value);}
|
warning.style.display = versionCompatible ? 'none' : 'block';
|
||||||
bridge.onservicecallback=function(raw){try{finish(JSON.parse(raw));}catch(e){finish({returnValue:false,errorText:'Invalid service response'});}};
|
document.getElementById('reloadService').style.display = versionCompatible ? 'none' : 'inline-block';
|
||||||
pending.push(bridge);timer=setTimeout(function(){bridge.cancel();finish({returnValue:false,errorText:'The service is not responding. Check the installation and service log.'});},10000);
|
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.';
|
||||||
bridge.call('luna://org.webosbrew.histogramhyperion.service/'+method,JSON.stringify(data||{}));
|
['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 || '';
|
||||||
|
document.getElementById('error').textContent = actionError;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearActionError() {
|
||||||
|
showActionError('');
|
||||||
|
}
|
||||||
|
var targetDirty = false,
|
||||||
|
receiver = 'Hyperion';
|
||||||
|
var profileDirty = false,
|
||||||
|
profile = 'auto',
|
||||||
|
advancedProfile = false;
|
||||||
|
var profileNames = {
|
||||||
|
auto: 'Auto',
|
||||||
|
g4: 'LG G4 (measured)',
|
||||||
|
average: 'Average (uniform)',
|
||||||
|
custom: 'Custom model'
|
||||||
|
};
|
||||||
|
|
||||||
|
function showProfile() {
|
||||||
|
document.getElementById('profile').textContent = 'Profile: ' + profileNames[profile];
|
||||||
|
var visible = profile === 'custom' && advancedProfile;
|
||||||
|
document.getElementById('customEditor').style.display = visible ? 'block' : 'none';
|
||||||
|
document.getElementById('customProfile').disabled = !visible;
|
||||||
|
document.getElementById('editProfile').disabled = profile !== 'custom';
|
||||||
|
document.getElementById('editProfile').textContent = visible ? 'Hide profile JSON' : 'Edit profile JSON';
|
||||||
|
}
|
||||||
|
|
||||||
|
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.');
|
||||||
|
done(result);
|
||||||
|
}, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function callUri(uri, done, data) {
|
||||||
|
if (typeof PalmServiceBridge === 'undefined') {
|
||||||
|
done({
|
||||||
|
returnValue: false,
|
||||||
|
errorText: 'This app requires an LG TV with the native service installed.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bridge = new PalmServiceBridge(),
|
||||||
|
timer, finished = false;
|
||||||
|
|
||||||
|
function finish(value) {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
var i = pending.indexOf(bridge);
|
||||||
|
if (i >= 0) pending.splice(i, 1);
|
||||||
|
done(value);
|
||||||
|
}
|
||||||
|
bridge.onservicecallback = function(raw) {
|
||||||
|
try {
|
||||||
|
finish(JSON.parse(raw));
|
||||||
|
} catch (e) {
|
||||||
|
finish({
|
||||||
|
returnValue: false,
|
||||||
|
errorText: 'Invalid service response'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pending.push(bridge);
|
||||||
|
timer = setTimeout(function() {
|
||||||
|
bridge.cancel();
|
||||||
|
finish({
|
||||||
|
returnValue: false,
|
||||||
|
errorText: 'The service is not responding. Check the installation and service log.'
|
||||||
|
});
|
||||||
|
}, uri.indexOf('hbchannel') >= 0 ? 30000 : 10000);
|
||||||
|
bridge.call(uri, JSON.stringify(data || {}));
|
||||||
|
}
|
||||||
|
|
||||||
function display(result) {
|
function display(result) {
|
||||||
|
if (typeof result.uid === 'number') {
|
||||||
|
document.getElementById('grantRoot').style.display = result.uid === 0 ? 'none' : 'inline-block';
|
||||||
|
document.getElementById('rootHint').style.display = result.uid === 0 ? 'none' : 'block';
|
||||||
|
}
|
||||||
if (result.calibrationProfile && !profileDirty) {
|
if (result.calibrationProfile && !profileDirty) {
|
||||||
profile=result.calibrationProfile;showProfile();
|
profile = result.calibrationProfile;
|
||||||
document.getElementById('customProfile').value=JSON.stringify({model:result.customModel||result.tvModel||'TV-MODEL',points:result.customPoints&&result.customPoints.length?result.customPoints:[]},null,2);
|
showProfile();
|
||||||
|
document.getElementById('customProfile').value = JSON.stringify({
|
||||||
|
model: result.customModel || result.tvModel || 'TV-MODEL',
|
||||||
|
points: result.customPoints && result.customPoints.length ? result.customPoints : []
|
||||||
|
}, null, 2);
|
||||||
}
|
}
|
||||||
if (result.effectiveCalibration) document.getElementById('profileStatus').textContent = 'TV: ' + (result.tvModel || 'Unknown') + ' · Active profile: ' + profileNames[result.effectiveCalibration] + (result.effectiveCalibration === 'average' ? ' · Spatial calibration needed for this model' : '');
|
if (result.effectiveCalibration) document.getElementById('profileStatus').textContent = 'TV: ' + (result.tvModel || 'Unknown') + ' · Active profile: ' + profileNames[result.effectiveCalibration] + (result.effectiveCalibration === 'average' ? ' · Spatial calibration needed for this model' : '');
|
||||||
|
|
||||||
if (result.host && !targetDirty) {
|
if (result.host && !targetDirty) {
|
||||||
document.getElementById('host').value=result.host;document.getElementById('port').value=result.port;document.getElementById('piccapPort').value=result.piccapPort||19400;receiver=result.receiver;
|
document.getElementById('host').value = result.host;
|
||||||
|
document.getElementById('port').value = result.port;
|
||||||
|
document.getElementById('piccapPort').value = result.piccapPort || 19400;
|
||||||
|
document.getElementById('captureInterval').value = result.captureIntervalMs || 50;
|
||||||
|
receiver = result.receiver;
|
||||||
document.getElementById('receiver').textContent = 'Receiver: ' + receiver;
|
document.getElementById('receiver').textContent = 'Receiver: ' + receiver;
|
||||||
}
|
}
|
||||||
document.getElementById('token').placeholder = result.tokenSet ? 'Saved token (leave blank to keep)' : 'No token saved';
|
document.getElementById('token').placeholder = result.tokenSet ? 'Saved token (leave blank to keep)' : 'No token saved';
|
||||||
@@ -29,15 +135,22 @@ function display(result){
|
|||||||
if (t) document.getElementById('testResult').textContent = 'Connection test: ' + t.state + (t.state === 'Passed' ? ' · ' + t.ledCount + ' LEDs · ' + t.elapsedMs + ' ms' : '') + (t.error ? ' · ' + t.error : '');
|
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;
|
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 : '') + (result.piccapCooldownMs ? ' · PicCap retry in ' + Math.ceil(result.piccapCooldownMs / 1000) + ' s' : '') + ' · Layout updates: ' + (result.layoutUpdates || 0) + (result.layoutError ? ' · Layout: ' + result.layoutError : '');
|
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 : '') + (result.piccapCooldownMs ? ' · PicCap retry in ' + Math.ceil(result.piccapCooldownMs / 1000) + ' s' : '') + ' · Layout updates: ' + (result.layoutUpdates || 0) + (result.layoutError ? ' · Layout: ' + result.layoutError : '');
|
||||||
var handover=document.getElementById('handover');handover.disabled=!result.piccapInstalled;
|
var handover = document.getElementById('handover');
|
||||||
|
handover.disabled = !result.piccapInstalled;
|
||||||
handover.setAttribute('aria-checked', String(!!result.piccapHandover));
|
handover.setAttribute('aria-checked', String(!!result.piccapHandover));
|
||||||
handover.textContent = result.piccapInstalled ? 'Auto PicCap: ' + (result.piccapHandover ? 'On' : 'Off') : 'Auto PicCap: Not installed';
|
handover.textContent = result.piccapInstalled ? 'Auto PicCap: ' + (result.piccapHandover ? 'On' : 'Off') : 'Auto PicCap: Not installed';
|
||||||
if(typeof result.autostart==='boolean'){var toggle=document.getElementById('autostart');toggle.setAttribute('aria-checked',String(result.autostart));toggle.textContent='Autostart: '+(result.autostart?'On':'Off');}
|
if (typeof result.autostart === 'boolean') {
|
||||||
document.getElementById('error').textContent=result.errorText||result.error||'';
|
var toggle = document.getElementById('autostart');
|
||||||
document.getElementById('status').textContent=result.returnValue===false?'Service unavailable':(result.running?'Running':'Stopped')+' · '+(result.frames||0)+' acknowledged color updates';
|
toggle.setAttribute('aria-checked', String(result.autostart));
|
||||||
|
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.receiverConnecting ? 'Connecting to receiver' : result.running ? 'Running' : 'Stopped') + ' · ' + (result.frames || 0) + ' acknowledged color updates';
|
||||||
if (result.activeSource === 'PicCap') {
|
if (result.activeSource === 'PicCap') {
|
||||||
document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver');
|
document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver');
|
||||||
document.getElementById('regions').textContent='';return;
|
document.getElementById('regions').textContent = '';
|
||||||
|
checkServiceVersion(result);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (result.piccapHandover) document.getElementById('status').textContent += ' · HyperDRM · ' + result.playbackProtection;
|
if (result.piccapHandover) document.getElementById('status').textContent += ' · HyperDRM · ' + result.playbackProtection;
|
||||||
if (result.piccapHandover && !result.piccapTargetCompatible) document.getElementById('status').textContent += ' · Auto PicCap unavailable for remote IPv6 target';
|
if (result.piccapHandover && !result.piccapTargetCompatible) document.getElementById('status').textContent += ' · Auto PicCap unavailable for remote IPv6 target';
|
||||||
@@ -46,33 +159,247 @@ function display(result){
|
|||||||
else if (result.returnValue !== false) document.getElementById('status').textContent += ' · No current color output';
|
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';
|
if (result.blackBarDetection && result.running) document.getElementById('status').textContent += result.letterboxDetected ? ' · Top/bottom black bars detected' : ' · Full-screen capture';
|
||||||
if (result.spatialInterpolation) document.getElementById('status').textContent += ' · Smooth color transitions';
|
if (result.spatialInterpolation) document.getElementById('status').textContent += ' · Smooth color transitions';
|
||||||
|
if (result.holdingColors) document.getElementById('status').textContent += ' · Holding last valid colors (missing data)';
|
||||||
|
if (result.sourceFade) document.getElementById('status').textContent += ' · Blending capture sources';
|
||||||
if (result.retryScheduled) document.getElementById('status').textContent += ' · Retrying receiver or capture startup';
|
if (result.retryScheduled) document.getElementById('status').textContent += ' · Retrying receiver or capture startup';
|
||||||
if (result.retryPaused) document.getElementById('status').textContent += ' · Automatic retry paused until Start';
|
if (result.retryPaused) document.getElementById('status').textContent += ' · Automatic retry paused until Start';
|
||||||
var box=document.getElementById('regions');box.textContent='';
|
var box = document.getElementById('regions');
|
||||||
(result.regions||[]).forEach(function(rgb,index){var e=document.createElement('div');e.className='swatch';e.style.backgroundColor='rgb('+rgb.join(',')+')';e.title='Histogram '+index;box.appendChild(e);});
|
box.textContent = '';
|
||||||
|
(result.regions || []).forEach(function(rgb, index) {
|
||||||
|
var e = document.createElement('div');
|
||||||
|
e.className = 'swatch';
|
||||||
|
e.style.backgroundColor = 'rgb(' + rgb.join(',') + ')';
|
||||||
|
e.title = 'Histogram ' + index;
|
||||||
|
box.appendChild(e);
|
||||||
|
});
|
||||||
|
checkServiceVersion(result);
|
||||||
}
|
}
|
||||||
document.getElementById('start').onclick=function(){call('start',function(r){if(r.returnValue===false)display(r);else call('status',display);});};
|
document.getElementById('reloadService').onclick = function() {
|
||||||
document.getElementById('stop').onclick=function(){call('stop',function(){call('status',display);});};
|
var button = this;
|
||||||
document.getElementById('refresh').onclick=function(){call('status',display);};
|
button.disabled = true;
|
||||||
call('status',display);setInterval(function(){if(!pending.length)call('status',display);},3000);
|
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);
|
||||||
|
else call('status', display);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.getElementById('stop').onclick = function() {
|
||||||
|
call('stop', function() {
|
||||||
|
call('status', display);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.getElementById('refresh').onclick = function() {
|
||||||
|
call('status', display);
|
||||||
|
};
|
||||||
|
call('status', display);
|
||||||
|
setInterval(function() {
|
||||||
|
if (!pending.length) call('status', display);
|
||||||
|
}, 3000);
|
||||||
document.getElementById('start').focus();
|
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});};
|
var repairingRoot = false,
|
||||||
|
rootWaiters = [];
|
||||||
|
|
||||||
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'});};
|
function repairRoot(done) {
|
||||||
['host','port','piccapPort','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});});
|
clearActionError();
|
||||||
document.getElementById('receiver').onclick=function(){receiver=receiver==='Hyperion'?'HyperHDR':'Hyperion';this.textContent='Receiver: '+receiver;targetDirty=true;};
|
rootWaiters.push(done);
|
||||||
document.getElementById('saveTarget').onclick=function(){
|
if (repairingRoot) return;
|
||||||
var port=Number(document.getElementById('port').value),button=this;
|
repairingRoot = true;
|
||||||
if(!Number.isInteger(port)||port<1||port>65535){document.getElementById('error').textContent='JSON port must be an integer from 1 to 65535.';return;}
|
document.getElementById('grantRoot').disabled = true;
|
||||||
var flatPort=Number(document.getElementById('piccapPort').value);
|
showActionError('Granting root access to HyperDRM...');
|
||||||
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};
|
function finish(error) {
|
||||||
var token=document.getElementById('token').value.trim();if(token)settings.token=token;
|
repairingRoot = false;
|
||||||
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);
|
document.getElementById('grantRoot').disabled = false;
|
||||||
|
if (error) showActionError(error);
|
||||||
|
var waiters = rootWaiters;
|
||||||
|
rootWaiters = [];
|
||||||
|
waiters.forEach(function(callback) {
|
||||||
|
callback(!error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
callUri('luna://org.webosbrew.hbchannel.service/elevateService', function(r) {
|
||||||
|
if (r.returnValue === false) {
|
||||||
|
finish('Homebrew Channel could not grant root access: ' + (r.errorText || 'Check Homebrew Channel installation.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
call('restartService', function(r) {
|
||||||
|
if (r.returnValue === false) {
|
||||||
|
finish(r.errorText || 'Could not restart the HyperDRM service.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var attempts = 0;
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
call('status', function(s) {
|
||||||
|
if (s.uid === 0) {
|
||||||
|
display(s);
|
||||||
|
finish();
|
||||||
|
} else if (++attempts < 12) setTimeout(check, 1000);
|
||||||
|
else finish('The service is still missing root access. Elevate HyperDRM in Homebrew Channel and reopen the app.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(check, 1500);
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
id: 'org.webosbrew.histogramhyperion.service'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('grantRoot').onclick = function() {
|
||||||
|
repairRoot(function(ok) {
|
||||||
|
if (ok) call('status', display);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
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);});};
|
document.getElementById('autostart').onclick = function() {
|
||||||
document.getElementById('profile').onclick=function(){var keys=['auto','g4','average','custom'];profile=keys[(keys.indexOf(profile)+1)%keys.length];profileDirty=true;showProfile();};
|
var button = this,
|
||||||
document.getElementById('customProfile').addEventListener('input',function(){profileDirty=true;});
|
enabled = button.getAttribute('aria-checked') !== 'true';
|
||||||
document.getElementById('saveProfile').onclick=function(){var data={profile:profile},button=this;if(profile==='custom'){try{var parsed=JSON.parse(document.getElementById('customProfile').value);data.model=parsed.model;data.points=parsed.points;}catch(e){document.getElementById('error').textContent='Custom profile must be valid JSON.';return;}}button.disabled=true;call('calibration',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}profileDirty=false;call('status',display);},data);};
|
button.disabled = true;
|
||||||
setupRemoteNavigation(['start','stop','refresh','autostart','handover','receiver','host','port','piccapPort','token','clearToken','saveTarget','testConnection','profile','customProfile','saveProfile'].map(function(id){return document.getElementById(id);}),document,window);
|
|
||||||
|
function save() {
|
||||||
|
call('autostart', function(r) {
|
||||||
|
if (r.rootRequired) {
|
||||||
|
repairRoot(function(ok) {
|
||||||
|
if (ok) save();
|
||||||
|
else button.disabled = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
if (r.returnValue === false) showActionError(r.errorText);
|
||||||
|
else call('status', display);
|
||||||
|
}, {
|
||||||
|
enabled: enabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('handover').onclick = function() {
|
||||||
|
var button = this;
|
||||||
|
button.disabled = true;
|
||||||
|
call('handover', function(r) {
|
||||||
|
button.disabled = false;
|
||||||
|
if (r.returnValue === false) showActionError(r.errorText);
|
||||||
|
else call('status', display);
|
||||||
|
}, {
|
||||||
|
enabled: button.getAttribute('aria-checked') !== 'true'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
['host', 'port', 'piccapPort', 'captureInterval', '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) {
|
||||||
|
showActionError('JSON port must be an integer from 1 to 65535.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var flatPort = Number(document.getElementById('piccapPort').value);
|
||||||
|
if (!Number.isInteger(flatPort) || flatPort < 1 || flatPort > 65535) {
|
||||||
|
showActionError('PicCap FlatBuffer port must be an integer from 1 to 65535.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var interval = Number(document.getElementById('captureInterval').value);
|
||||||
|
if (!Number.isInteger(interval) || interval < 25 || interval > 1000 || interval % 25) {
|
||||||
|
showActionError('Capture interval must be 25–1000 ms in steps of 25.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var settings = {
|
||||||
|
captureIntervalMs: interval,
|
||||||
|
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) {
|
||||||
|
showActionError(r.errorText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetDirty = false;
|
||||||
|
document.getElementById('token').value = '';
|
||||||
|
document.getElementById('clearToken').checked = false;
|
||||||
|
call('status', display);
|
||||||
|
}, settings);
|
||||||
|
};
|
||||||
|
document.getElementById('testConnection').onclick = function() {
|
||||||
|
if (targetDirty) {
|
||||||
|
showActionError('Save receiver settings before testing the connection.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.disabled = true;
|
||||||
|
call('testConnection', function(r) {
|
||||||
|
if (r.returnValue === false) showActionError(r.errorText);
|
||||||
|
call('status', display);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.getElementById('profile').onclick = function() {
|
||||||
|
var keys = ['auto', 'g4', 'average', 'custom'];
|
||||||
|
profile = keys[(keys.indexOf(profile) + 1) % keys.length];
|
||||||
|
profileDirty = true;
|
||||||
|
showProfile();
|
||||||
|
};
|
||||||
|
document.getElementById('customProfile').addEventListener('input', function() {
|
||||||
|
profileDirty = true;
|
||||||
|
});
|
||||||
|
document.getElementById('saveProfile').onclick = function() {
|
||||||
|
var data = {
|
||||||
|
profile: profile
|
||||||
|
},
|
||||||
|
button = this;
|
||||||
|
if (profile === 'custom') {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(document.getElementById('customProfile').value);
|
||||||
|
data.model = parsed.model;
|
||||||
|
data.points = parsed.points;
|
||||||
|
} catch (e) {
|
||||||
|
showActionError('Custom profile must be valid JSON.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.disabled = true;
|
||||||
|
call('calibration', function(r) {
|
||||||
|
button.disabled = false;
|
||||||
|
if (r.returnValue === false) {
|
||||||
|
showActionError(r.errorText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profileDirty = false;
|
||||||
|
call('status', display);
|
||||||
|
}, data);
|
||||||
|
};
|
||||||
|
document.getElementById('editProfile').onclick = function() {
|
||||||
|
advancedProfile = !advancedProfile;
|
||||||
|
showProfile();
|
||||||
|
if (advancedProfile) document.getElementById('customProfileField').focus();
|
||||||
|
};
|
||||||
|
setupRemoteNavigation(['overviewTab', 'receiverTab', 'calibrationTab', 'reloadService', 'grantRoot', 'start', 'stop', 'refresh', 'autostart', 'handover', 'calibrationShortcut', 'receiver', 'hostField', 'portField', 'piccapPortField', 'captureIntervalField', 'tokenField', 'clearToken', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'profile', 'saveProfile', 'editProfile', 'customProfileField', 'cancelCalibration'].map(function(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}), document, window);
|
||||||
+14
-1
@@ -1 +1,14 @@
|
|||||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.10.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"}
|
{
|
||||||
|
"id": "org.webosbrew.histogramhyperion",
|
||||||
|
"version": "0.13.4",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/* Pure measurement processing; the video uses 32 clockwise edge patches. */
|
||||||
|
function calibrationFit(baseline, patches, model) {
|
||||||
|
if (!model || baseline.length !== 16 || patches.length !== 32) throw new Error('Incomplete calibration measurements.');
|
||||||
|
var candidates = [],
|
||||||
|
edges = [0, 0, 0, 0];
|
||||||
|
for (var region = 0; region < 16; region++) {
|
||||||
|
var responses = patches.map(function(p) {
|
||||||
|
if (!p || p.length !== 16) throw new Error('Missing video measurements.');
|
||||||
|
return Math.max(0, p[region] - baseline[region]);
|
||||||
|
});
|
||||||
|
var peak = Math.max.apply(Math, responses),
|
||||||
|
best = responses.indexOf(peak);
|
||||||
|
if (peak < 8) continue;
|
||||||
|
// Reject regions responding equally around the screen (global histograms).
|
||||||
|
var strong = responses.filter(function(v) {
|
||||||
|
return v >= peak * .65;
|
||||||
|
}).length;
|
||||||
|
if (strong > 10) continue;
|
||||||
|
var distant = responses.some(function(v, i) {
|
||||||
|
var d = Math.min(Math.abs(i - best), 32 - Math.abs(i - best));
|
||||||
|
return v >= peak * .65 && d > 6;
|
||||||
|
});
|
||||||
|
if (distant) continue;
|
||||||
|
var sum = 0,
|
||||||
|
weighted = 0;
|
||||||
|
for (var offset = -3; offset <= 3; offset++) {
|
||||||
|
var index = (best + offset + 32) % 32,
|
||||||
|
weight = Math.max(0, responses[index] - peak * .35);
|
||||||
|
weighted += offset * weight;
|
||||||
|
sum += weight;
|
||||||
|
}
|
||||||
|
var position = ((best + .5 + weighted / sum) / 8 + 4) % 4;
|
||||||
|
candidates.push({
|
||||||
|
position: position,
|
||||||
|
region: region,
|
||||||
|
score: peak / strong
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Nested LG regions can measure the same edge position. Keep the most
|
||||||
|
// localized response rather than making duplicate interpolation anchors.
|
||||||
|
candidates.sort(function(a, b) {
|
||||||
|
return b.score - a.score || a.region - b.region;
|
||||||
|
});
|
||||||
|
var points = [];
|
||||||
|
candidates.forEach(function(p) {
|
||||||
|
if (points.some(function(q) {
|
||||||
|
var d = Math.abs(p.position - q.position);
|
||||||
|
return Math.min(d, 4 - d) < .06;
|
||||||
|
})) return;
|
||||||
|
points.push({
|
||||||
|
position: p.position,
|
||||||
|
region: p.region
|
||||||
|
});
|
||||||
|
edges[Math.floor(p.position)]++;
|
||||||
|
});
|
||||||
|
points.sort(function(a, b) {
|
||||||
|
return a.position - b.position;
|
||||||
|
});
|
||||||
|
if (points.length < 4 || edges.some(function(n) {
|
||||||
|
return !n;
|
||||||
|
})) throw new Error('No reliable spatial response on every edge. Keep the video full-screen; this TV may not expose local histogram regions. Your saved profile is unchanged.');
|
||||||
|
return {
|
||||||
|
model: model,
|
||||||
|
points: points
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function calibrationLuma(regions) {
|
||||||
|
return regions.map(function(c) {
|
||||||
|
return (c[0] + c[1] + c[2]) / 3;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calibrationAverage(samples) {
|
||||||
|
if (samples.length < 3) throw new Error('Not enough fresh histogram samples. Keep the app visible and repeat calibration.');
|
||||||
|
return samples[0].map(function(_, r) {
|
||||||
|
return samples.reduce(function(sum, s) {
|
||||||
|
return sum + s[r];
|
||||||
|
}, 0) / samples.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
Binary file not shown.
+60
-21
@@ -1,21 +1,60 @@
|
|||||||
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title>
|
<!doctype html>
|
||||||
<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,textarea{font:inherit;padding:12px;background:#223654;color:white;border:2px solid #739be0;border-radius:8px}input:focus,textarea:focus{outline:5px solid #66ddff}#host,#token{width:340px}#port,#piccapPort{width:140px}#clearToken{width:28px;height:28px}</style>
|
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title>
|
||||||
<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>
|
<style>
|
||||||
<div role="group" aria-label="Service controls"><button id="start">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button>
|
body{background:#101827;color:#f1f5ff;font:24px sans-serif;margin:36px 54px;max-width:1800px}
|
||||||
<button id="autostart" role="switch" aria-checked="false">Autostart: Off</button>
|
h1{font-size:40px;display:flex;align-items:center;gap:18px;margin:0 0 16px}h1 img{border-radius:16px}
|
||||||
<button id="handover" role="switch" aria-checked="false" disabled>Auto PicCap: Off</button></div><p><small>Use the arrow buttons to select a control and OK to activate it.</small></p><p><small>Autostart starts the service when the TV boots. Stop ends the current session. Auto PicCap uses installed PicCap for non-secure video and HyperDRM for protected or unknown playback. Keep PicCap's own autostart off. Stable unprotected playback selects PicCap; five seconds of connection loss restores HyperDRM, with a 30-second retry cooldown.</small></p>
|
button{font:inherit;padding:14px 24px;margin:8px 12px 8px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:10px}
|
||||||
<fieldset><legend>Receiver settings</legend><button id="receiver" type="button">Receiver: Hyperion</button>
|
button:disabled{opacity:.45}button:focus,.field:focus{outline:4px solid #66ddff;outline-offset:3px}
|
||||||
<label>IP address <input id="host" type="text" maxlength="45" value="127.0.0.1" spellcheck="false"></label>
|
nav{border-bottom:1px solid #456185;padding-bottom:10px;margin-bottom:18px}nav button[aria-pressed="true"]{background:#126b85;border-color:#66ddff}
|
||||||
<label>JSON port <input id="port" type="number" min="1" max="65535" value="19444"></label>
|
.primary{background:#126b85;border-color:#66ddff}p{line-height:1.4;margin:12px 0}small{color:#c0cbe0;font-size:21px}
|
||||||
<label>PicCap FlatBuffer port <input id="piccapPort" type="number" min="1" max="65535" value="19400"></label>
|
#error{color:#ffbc99}fieldset{border:1px solid #456185;border-radius:12px;padding:18px 24px;margin:16px 0}
|
||||||
<label>API token (optional) <input id="token" type="password" maxlength="256" autocomplete="off"></label>
|
.fields{display:flex;flex-wrap:wrap;gap:18px;margin:18px 0}.field{border:2px solid #739be0;border-radius:10px;padding:12px;background:#1b2b43;width:360px;cursor:pointer}
|
||||||
<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>
|
.field label{display:flex;flex-direction:column;gap:10px;font-size:22px}.field input,.field textarea{font:inherit;color:white;background:transparent;border:0;outline:0;width:100%;padding:0;box-sizing:border-box}
|
||||||
<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>
|
.field:focus-within{border-color:#66ddff}.field input:read-write,.field textarea:read-write{background:#223654;outline:2px solid #66ddff}
|
||||||
<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>
|
.narrow{width:225px}.wide{width:95%;max-width:1200px}#clearToken{width:24px;height:24px}
|
||||||
<fieldset><legend>TV calibration</legend><button id="profile" type="button">Profile: Auto</button><button id="saveProfile" type="button">Save profile</button>
|
#regions{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0}.swatch{width:50px;height:50px;border:1px solid #abc;border-radius:6px}
|
||||||
<p id="profileStatus">Detecting TV model...</p><div id="customEditor" style="display:none"><label>Custom model profile JSON <textarea id="customProfile" rows="5" cols="68" maxlength="2048" disabled spellcheck="false"></textarea></label></div>
|
#diagnostics{display:block;margin-top:14px;max-width:1500px}#cancelCalibration{display:none}
|
||||||
<p><small>Auto selects an imported profile matching this exact TV model, or the measured LG G4 profile. Other models use uniform average colors until calibrated. Select Custom and paste a measured model profile to import it. G4 can also be selected manually. Profiles change spatial mapping; adjust colors in Hyperion/HyperHDR.</small></p></fieldset>
|
#cancelCalibration:not(:disabled){display:block;position:fixed;right:24px;top:24px;z-index:101;font-size:20px;opacity:.65}
|
||||||
<p><small id="endpoint">Priority 149 · 10 updates per second</small></p>
|
#calibrationVideo{display:none;position:fixed;inset:0;width:100vw;height:100vh;object-fit:fill;background:black;z-index:100}
|
||||||
<p><small id="diagnostics"></small></p>
|
.steps{display:flex;gap:24px;flex-wrap:wrap}.steps p{flex:1;min-width:260px;color:#c0cbe0}.steps strong{display:block;color:#fff;margin-bottom:6px}
|
||||||
<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. The LED layout is checked every five seconds and updated automatically.</small></p>
|
</style>
|
||||||
<script src="remote.js"></script><script src="app.js"></script></html>
|
<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>
|
||||||
|
<fieldset><legend>Automatic operation</legend><button id="autostart" role="switch" aria-checked="false">Autostart: Off</button><button id="handover" role="switch" aria-checked="false" disabled>Auto PicCap: Off</button>
|
||||||
|
<p><small>Autostart retries until the receiver is available. Auto PicCap uses PicCap for unprotected playback and HyperDRM for protected playback. Keep PicCap's own autostart off.</small></p></fieldset>
|
||||||
|
<button id="calibrationShortcut">Auto Calibration</button><small>Measure a spatial profile for your TV.</small>
|
||||||
|
<p><small id="endpoint">Priority 149 · 10 updates per second</small></p><div id="regions" aria-label="Histogram colors"></div>
|
||||||
|
<p><small>Adjust brightness, saturation and normal smoothing in Hyperion or HyperHDR.</small></p><small id="diagnostics"></small>
|
||||||
|
</section>
|
||||||
|
<section id="receiverPanel" aria-label="Receiver settings" style="display:none">
|
||||||
|
<fieldset><legend>Receiver connection</legend><button id="receiver">Receiver: Hyperion</button>
|
||||||
|
<p><small>Select a field with the arrow buttons. Press OK or click to open the TV keyboard.</small></p>
|
||||||
|
<div class="fields">
|
||||||
|
<div id="hostField" class="field" tabindex="0" role="button" aria-label="IP address, press OK to edit"><label>IP address<input id="host" tabindex="-1" readonly type="text" maxlength="45" value="127.0.0.1" spellcheck="false"></label></div>
|
||||||
|
<div id="portField" class="field narrow" tabindex="0" role="button" aria-label="JSON port, press OK to edit"><label>JSON port<input id="port" tabindex="-1" readonly type="number" min="1" max="65535" value="19444"></label></div>
|
||||||
|
<div id="piccapPortField" class="field narrow" tabindex="0" role="button" aria-label="FlatBuffer port, press OK to edit"><label>PicCap FlatBuffer port<input id="piccapPort" tabindex="-1" readonly type="number" min="1" max="65535" value="19400"></label></div>
|
||||||
|
<div id="captureIntervalField" class="field narrow" tabindex="0" role="button" aria-label="Capture interval in milliseconds, press OK to edit"><label>Capture interval (ms)<input id="captureInterval" tabindex="-1" readonly type="number" min="25" max="1000" step="25" value="50"></label></div>
|
||||||
|
<div id="tokenField" class="field" tabindex="0" role="button" aria-label="API token, press OK to edit"><label>API token (optional)<input id="token" tabindex="-1" readonly type="password" maxlength="256" autocomplete="off"></label></div>
|
||||||
|
</div>
|
||||||
|
<label><input id="clearToken" type="checkbox"> Remove saved token</label>
|
||||||
|
<div><button id="saveTarget" class="primary">Save receiver</button><button id="testConnection">Test connection</button></div><p id="testResult" role="status">Connection test: Not tested</p>
|
||||||
|
<p><small>The JSON TCP port is usually 19444. Leave the token blank to keep the saved token. Saving updates the current session. Histogram capture defaults to 50 ms; choose 25–1000 ms in steps of 25. Lower values reduce latency and increase TV load. PicCap uses its own capture rate.</small></p>
|
||||||
|
<p id="piccapTargetHint"><small>Auto PicCap uses the same IP and its FlatBuffer port, usually 19400. Enable the receiver's FlatBuffer server. Remote IPv6 uses HyperDRM.</small></p></fieldset>
|
||||||
|
</section>
|
||||||
|
<section id="calibrationPanel" aria-label="TV calibration" style="display:none">
|
||||||
|
<p id="profileStatus">Detecting TV model...</p>
|
||||||
|
<fieldset><legend>Auto Calibration</legend>
|
||||||
|
<div class="steps"><p><strong>1. Measure</strong>Play the built-in edge test. Keep the picture full-screen without bars.</p><p><strong>2. Save</strong>Review the result and save the measured profile.</p><p><strong>3. Check</strong>Validate all four colored screen corners against the LEDs.</p></div>
|
||||||
|
<button id="calibrate" class="primary">Start Auto Calibration</button><button id="validateCalibration">Validate profile</button><p id="wizardStatus" role="status">Measurement takes about 70 seconds. Your saved profile is preserved until you select Save profile.</p>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset><legend>Spatial profile</legend><button id="profile">Profile: Auto</button><button id="saveProfile" class="primary">Save profile</button><button id="editProfile" disabled>Edit profile JSON</button>
|
||||||
|
<p><small>Auto uses your measured model profile or the built-in LG G4 profile. Other uncalibrated TVs use average colors.</small></p>
|
||||||
|
<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="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,67 @@
|
|||||||
|
function showPanel(name, focus) {
|
||||||
|
['overview', 'receiver', 'calibration'].forEach(function(key) {
|
||||||
|
document.getElementById(key + 'Panel').style.display = key === name ? 'block' : 'none';
|
||||||
|
document.getElementById(key + 'Tab').setAttribute('aria-pressed', String(key === name));
|
||||||
|
});
|
||||||
|
if (focus !== false) document.getElementById({
|
||||||
|
overview: 'start',
|
||||||
|
receiver: 'receiver',
|
||||||
|
calibration: 'calibrate'
|
||||||
|
} [name]).focus();
|
||||||
|
}
|
||||||
|
['overview', 'receiver', 'calibration'].forEach(function(name) {
|
||||||
|
document.getElementById(name + 'Tab').onclick = function() {
|
||||||
|
showPanel(name);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
document.getElementById('calibrationShortcut').onclick = function() {
|
||||||
|
showPanel('calibration');
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupFormEditors(ids, doc, win) {
|
||||||
|
var pairs = ids.map(function(id) {
|
||||||
|
return {
|
||||||
|
input: doc.getElementById(id),
|
||||||
|
field: doc.getElementById(id + 'Field')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
pairs.forEach(function(pair) {
|
||||||
|
var input = pair.input,
|
||||||
|
field = pair.field;
|
||||||
|
input.readOnly = true;
|
||||||
|
field.onclick = function() {
|
||||||
|
if (input.disabled || !input.readOnly) return;
|
||||||
|
input.readOnly = false;
|
||||||
|
input.focus();
|
||||||
|
};
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
input.readOnly = true;
|
||||||
|
});
|
||||||
|
input.addEventListener('mousedown', function(event) {
|
||||||
|
if (input.readOnly) event.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function finishEditing() {
|
||||||
|
pairs.forEach(function(pair) {
|
||||||
|
if (!pair.input.readOnly) {
|
||||||
|
pair.input.readOnly = true;
|
||||||
|
pair.input.blur();
|
||||||
|
pair.field.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
doc.addEventListener('keyboardStateChange', function(event) {
|
||||||
|
var detail = event.detail || {};
|
||||||
|
if (detail.visibility === false || detail.isShowing === false) finishEditing();
|
||||||
|
});
|
||||||
|
doc.addEventListener('keydown', function(event) {
|
||||||
|
if ((event.keyCode === 461 || event.key === 'Escape') && pairs.some(function(pair) {
|
||||||
|
return !pair.input.readOnly;
|
||||||
|
})) {
|
||||||
|
event.preventDefault();
|
||||||
|
finishEditing();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setupFormEditors(['host', 'port', 'piccapPort', 'captureInterval', 'token', 'customProfile'], document, window);
|
||||||
+30
-13
@@ -1,9 +1,14 @@
|
|||||||
/* Directional control for webOS remotes and standard keyboards. */
|
/* Directional control for webOS remotes and standard keyboards. */
|
||||||
function setupRemoteNavigation(buttons, doc, win) {
|
function setupRemoteNavigation(buttons, doc, win) {
|
||||||
var lastFocused=buttons[0], enterDown=false;
|
var lastFocused = buttons[0],
|
||||||
|
enterDown = false;
|
||||||
buttons.forEach(function(button) {
|
buttons.forEach(function(button) {
|
||||||
button.addEventListener('focus',function(){lastFocused=button;});
|
button.addEventListener('focus', function() {
|
||||||
button.addEventListener('mouseenter',function(){if(!button.disabled)button.focus();});
|
lastFocused = button;
|
||||||
|
});
|
||||||
|
button.addEventListener('mouseenter', function() {
|
||||||
|
if (!button.disabled) button.focus();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
doc.addEventListener('keydown', function(event) {
|
doc.addEventListener('keydown', function(event) {
|
||||||
var code = event.keyCode || event.which;
|
var code = event.keyCode || event.which;
|
||||||
@@ -11,22 +16,34 @@ function setupRemoteNavigation(buttons, doc, win) {
|
|||||||
event.key === 'ArrowRight' || event.key === 'ArrowDown' || code === 39 || code === 40 ? 1 : 0;
|
event.key === 'ArrowRight' || event.key === 'ArrowDown' || code === 39 || code === 40 ? 1 : 0;
|
||||||
var enter = event.key === 'Enter' || code === 13;
|
var enter = event.key === 'Enter' || code === 13;
|
||||||
if (!direction && !enter) return;
|
if (!direction && !enter) return;
|
||||||
var editing=doc.activeElement && (doc.activeElement.tagName==='INPUT' && doc.activeElement.type!=='checkbox' || doc.activeElement.tagName==='TEXTAREA');
|
var editing = doc.activeElement && !doc.activeElement.readOnly && (doc.activeElement.tagName === 'INPUT' && doc.activeElement.type !== 'checkbox' || doc.activeElement.tagName === 'TEXTAREA');
|
||||||
if(editing && (enter||code===37||code===39||event.key==='ArrowLeft'||event.key==='ArrowRight'))return;
|
if (editing) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var enabled=buttons.filter(function(button){return !button.disabled;});
|
var enabled = buttons.filter(function(button) {
|
||||||
|
return !button.disabled && (typeof button.getClientRects !== 'function' || button.getClientRects().length) && (!win.remoteNavigationScope || win.remoteNavigationScope.indexOf(button.id) >= 0);
|
||||||
|
});
|
||||||
if (!enabled.length) return;
|
if (!enabled.length) return;
|
||||||
var focused=doc.activeElement,index=enabled.indexOf(focused);
|
var focused = doc.activeElement,
|
||||||
|
index = enabled.indexOf(focused);
|
||||||
if (direction) {
|
if (direction) {
|
||||||
if(index<0){index=enabled.indexOf(lastFocused);if(index<0)index=0;enabled[index].focus();}
|
if (index < 0) {
|
||||||
else enabled[(index+direction+enabled.length)%enabled.length].focus();
|
index = enabled.indexOf(lastFocused);
|
||||||
|
if (index < 0) index = 0;
|
||||||
|
enabled[index].focus();
|
||||||
|
} else enabled[(index + direction + enabled.length) % enabled.length].focus();
|
||||||
} else {
|
} else {
|
||||||
if (enterDown || event.repeat) return;
|
if (enterDown || event.repeat) return;
|
||||||
enterDown = true;
|
enterDown = true;
|
||||||
if(index<0){index=enabled.indexOf(lastFocused);enabled[index<0?0:index].focus();}
|
if (index < 0) {
|
||||||
else focused.click();
|
index = enabled.indexOf(lastFocused);
|
||||||
|
enabled[index < 0 ? 0 : index].focus();
|
||||||
|
} else focused.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
doc.addEventListener('keyup',function(event){if(event.key==='Enter'||event.keyCode===13||event.which===13)enterDown=false;});
|
doc.addEventListener('keyup', function(event) {
|
||||||
win.addEventListener('blur',function(){enterDown=false;});
|
if (event.key === 'Enter' || event.keyCode === 13 || event.which === 13) enterDown = false;
|
||||||
|
});
|
||||||
|
win.addEventListener('blur', function() {
|
||||||
|
enterDown = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
'use strict';
|
||||||
|
var HYPERDRM_APP_VERSION = '0.13.4';
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
'use strict';
|
||||||
|
var WizardPhase = {
|
||||||
|
STARTING: 'starting',
|
||||||
|
WAITING: 'waiting',
|
||||||
|
MEASURING: 'measuring',
|
||||||
|
VALIDATING: 'validating',
|
||||||
|
COMMITTING: 'committing'
|
||||||
|
};
|
||||||
|
var wizard = null;
|
||||||
|
var video = document.getElementById('calibrationVideo');
|
||||||
|
|
||||||
|
function wizardFinish(error) {
|
||||||
|
if (!wizard) return;
|
||||||
|
clearInterval(wizard.timer);
|
||||||
|
wizard = null;
|
||||||
|
window.remoteNavigationScope = null;
|
||||||
|
video.pause();
|
||||||
|
video.style.display = 'none';
|
||||||
|
if (typeof video.removeAttribute === 'function') {
|
||||||
|
video.removeAttribute('src');
|
||||||
|
video.load();
|
||||||
|
}
|
||||||
|
document.getElementById('cancelCalibration').disabled = true;
|
||||||
|
document.getElementById('calibrate').disabled = false;
|
||||||
|
document.getElementById('validateCalibration').disabled = false;
|
||||||
|
call('calibrationSession', function(r) {
|
||||||
|
if (r.returnValue === false) document.getElementById('error').textContent = r.errorText;
|
||||||
|
else call('status', display);
|
||||||
|
}, {
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
call('calibrationResult', function() {}, {
|
||||||
|
message: error || document.getElementById('wizardStatus').textContent
|
||||||
|
});
|
||||||
|
if (error) document.getElementById('wizardStatus').textContent = error;
|
||||||
|
document.getElementById('calibrate').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wizardSample() {
|
||||||
|
if (!wizard || wizard.busy) return;
|
||||||
|
var session = wizard;
|
||||||
|
if (Date.now() - wizard.started > 100000) {
|
||||||
|
wizardFinish('Calibration timed out. Saved profile unchanged.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wizard.phase !== WizardPhase.MEASURING) return;
|
||||||
|
var t = video.currentTime;
|
||||||
|
if (t >= 68) {
|
||||||
|
try {
|
||||||
|
var measured = calibrationFit(calibrationAverage(wizard.baseline), wizard.patches.map(calibrationAverage), wizard.model);
|
||||||
|
profile = 'custom';
|
||||||
|
profileDirty = true;
|
||||||
|
showProfile();
|
||||||
|
document.getElementById('customProfile').value = JSON.stringify(measured, null, 2);
|
||||||
|
document.getElementById('wizardStatus').textContent = 'Measured ' + measured.points.length + ' edge regions. Select Save profile, then Validate profile to check all four colored edges. The previous saved profile is still active.';
|
||||||
|
wizard.busy = true;
|
||||||
|
wizard.phase = WizardPhase.COMMITTING;
|
||||||
|
call('calibrationDraft', function(r) {
|
||||||
|
if (wizard === session) wizardFinish(r.returnValue === false ? r.errorText : undefined);
|
||||||
|
}, {
|
||||||
|
profile: 'custom',
|
||||||
|
model: measured.model,
|
||||||
|
points: measured.points
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
wizardFinish(e.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var step = t < 4 ? -1 : Math.floor((t - 4) / 2),
|
||||||
|
local = t < 4 ? t : (t - 4) % 2;
|
||||||
|
if (step < 0 && (t < .8 || t > 3.2) || step >= 0 && (local < .65 || local > 1.75)) return;
|
||||||
|
wizard.busy = true;
|
||||||
|
call('status', function(s) {
|
||||||
|
if (wizard !== session) return;
|
||||||
|
wizard.busy = false;
|
||||||
|
if (s.returnValue === false || !s.calibrating) {
|
||||||
|
wizardFinish(s.errorText || 'Calibration session ended. Saved profile unchanged.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ignore duplicates, held output and replies crossing a stimulus boundary.
|
||||||
|
var current = video.currentTime,
|
||||||
|
nowStep = current < 4 ? -1 : Math.floor((current - 4) / 2);
|
||||||
|
if (nowStep !== step || s.sampleSequence === wizard.sequence || s.sampleAgeMs < 0 || s.sampleAgeMs > 350 || !s.colorValid || !Array.isArray(s.regions) || s.regions.length !== 16) return;
|
||||||
|
wizard.sequence = s.sampleSequence;
|
||||||
|
(step < 0 ? wizard.baseline : wizard.patches[step]).push(calibrationLuma(s.regions));
|
||||||
|
document.getElementById('wizardStatus').textContent = step < 0 ? 'Measuring black reference...' : 'Measuring edge patch ' + (step + 1) + ' of 32...';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
document.getElementById('wizardStatus').textContent = 'Save receiver and profile edits before starting.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('calibrate').disabled = true;
|
||||||
|
document.getElementById('validateCalibration').disabled = true;
|
||||||
|
document.getElementById('cancelCalibration').disabled = false;
|
||||||
|
window.remoteNavigationScope = ['cancelCalibration'];
|
||||||
|
wizard = {
|
||||||
|
phase: WizardPhase.STARTING,
|
||||||
|
validation: validation,
|
||||||
|
started: Date.now(),
|
||||||
|
busy: false,
|
||||||
|
sequence: -1,
|
||||||
|
model: '',
|
||||||
|
baseline: [],
|
||||||
|
patches: Array.from({
|
||||||
|
length: 32
|
||||||
|
}, function() {
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
};
|
||||||
|
var session = wizard;
|
||||||
|
document.getElementById('wizardStatus').textContent = 'Starting full-screen capture. Keep the app visible; do not change zoom or aspect ratio.';
|
||||||
|
call('calibrationSession', function(r) {
|
||||||
|
if (wizard !== session) return;
|
||||||
|
if (r.returnValue === false) {
|
||||||
|
wizardFinish(r.errorText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Start the video plane before waiting for histogram data. Without another
|
||||||
|
// running video, waiting first would prevent calibration from ever starting.
|
||||||
|
video.src = 'calibration.mp4';
|
||||||
|
if (typeof video.load === 'function') video.load();
|
||||||
|
video.style.display = 'block';
|
||||||
|
var warmup = video.play();
|
||||||
|
if (warmup && warmup.catch) warmup.catch(function() {
|
||||||
|
if (wizard !== session) return;
|
||||||
|
wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');
|
||||||
|
});
|
||||||
|
wizard.phase = WizardPhase.WAITING;
|
||||||
|
call('start', function() {
|
||||||
|
if (wizard === session) waitCapture();
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function waitCapture() {
|
||||||
|
if (wizard !== session) return;
|
||||||
|
call('status', function(s) {
|
||||||
|
if (wizard !== session) return;
|
||||||
|
if (s.running && s.activeSource !== 'PicCap' && s.tvModel && s.colorValid) {
|
||||||
|
wizard.phase = validation ? WizardPhase.VALIDATING : WizardPhase.MEASURING;
|
||||||
|
wizard.model = s.tvModel;
|
||||||
|
video.currentTime = validation ? 68 : 0;
|
||||||
|
video.style.display = 'block';
|
||||||
|
document.getElementById('cancelCalibration').focus();
|
||||||
|
var playing = video.play();
|
||||||
|
if (playing && playing.catch) playing.catch(function() {
|
||||||
|
if (wizard !== session) return;
|
||||||
|
wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');
|
||||||
|
});
|
||||||
|
wizard.timer = setInterval(wizardSample, 250);
|
||||||
|
} else if (Date.now() - wizard.started < 15000) setTimeout(waitCapture, 500);
|
||||||
|
else wizardFinish('No fresh full-screen histogram capture or TV model. Saved profile unchanged.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('calibrate').onclick = function() {
|
||||||
|
wizardStart(false);
|
||||||
|
};
|
||||||
|
document.getElementById('validateCalibration').onclick = function() {
|
||||||
|
wizardStart(true);
|
||||||
|
};
|
||||||
|
document.getElementById('cancelCalibration').onclick = function() {
|
||||||
|
wizardFinish('Calibration cancelled. Saved profile unchanged.');
|
||||||
|
};
|
||||||
|
video.addEventListener('ended', function() {
|
||||||
|
if (wizard && wizard.validation) wizardFinish('Validation finished. Top-left red, top-right green, bottom-right blue, bottom-left yellow. Check matching LED positions.');
|
||||||
|
else wizardSample();
|
||||||
|
});
|
||||||
|
video.addEventListener('error', function() {
|
||||||
|
wizardFinish('Calibration video playback failed. Saved profile unchanged.');
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (wizard && (e.keyCode === 461 || e.key === 'Escape')) {
|
||||||
|
e.preventDefault();
|
||||||
|
wizardFinish('Calibration cancelled. Saved profile unchanged.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('pagehide', function() {
|
||||||
|
wizardFinish();
|
||||||
|
});
|
||||||
|
// An explicit launch action also supports starting the TV's guided test remotely.
|
||||||
|
function wizardLaunch(raw) {
|
||||||
|
try {
|
||||||
|
var params = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||||
|
if (params && params.action === 'calibrate') wizardStart(false);
|
||||||
|
else if (params && params.action === 'repairRoot') {
|
||||||
|
showPanel('overview');
|
||||||
|
repairRoot(function(ok) {
|
||||||
|
if (ok) call('status', display);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('wizardStatus').textContent = 'Invalid launch parameters.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof PalmSystem !== 'undefined' && PalmSystem.launchParams) setTimeout(function() {
|
||||||
|
wizardLaunch(PalmSystem.launchParams);
|
||||||
|
}, 1000);
|
||||||
|
window.addEventListener('webOSRelaunch', function() {
|
||||||
|
if (typeof PalmSystem !== 'undefined') wizardLaunch(PalmSystem.launchParams);
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Build the deterministic native video-plane calibration stimulus (H.264)."""
|
||||||
|
import argparse, subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--ffmpeg',default='ffmpeg')
|
||||||
|
args=parser.parse_args()
|
||||||
|
root=Path(__file__).resolve().parent
|
||||||
|
width,height=1920,1080
|
||||||
|
filters=[]
|
||||||
|
for step in range(32):
|
||||||
|
edge,index=divmod(step,8)
|
||||||
|
center=(index+.5)/8
|
||||||
|
if edge==0: x,y,w,h=int(center*width-192),0,384,162
|
||||||
|
elif edge==1: x,y,w,h=1632,int(center*height-108),288,216
|
||||||
|
elif edge==2: x,y,w,h=int((1-center)*width-192),918,384,162
|
||||||
|
else: x,y,w,h=0,int((1-center)*height-108),288,216
|
||||||
|
left,top=max(0,x),max(0,y)
|
||||||
|
w,h=min(width,x+w)-left,min(height,y+h)-top
|
||||||
|
start=4+step*2
|
||||||
|
filters.append(f"drawbox=x={left}:y={top}:w={w}:h={h}:color=white:t=fill:enable='gte(t,{start})*lt(t,{start+2})'")
|
||||||
|
# Four color quadrants for visual validation after measurement.
|
||||||
|
for x,y,color in [(0,0,'red'),(960,0,'lime'),(960,540,'blue'),(0,540,'yellow')]:
|
||||||
|
filters.append(f"drawbox=x={x}:y={y}:w=960:h=540:color={color}:t=fill:enable='gte(t,68)'")
|
||||||
|
subprocess.run([args.ffmpeg,'-hide_banner','-loglevel','error','-y','-f','lavfi','-i',
|
||||||
|
'color=black:s=1920x1080:r=30:d=78','-vf',','.join(filters),
|
||||||
|
'-c:v','libx264','-preset','fast','-crf','16','-pix_fmt','yuv420p',
|
||||||
|
'-profile:v','main','-level','4.0','-g','30','-an','-movflags','+faststart',
|
||||||
|
str(root/'app/calibration.mp4')],check=True)
|
||||||
|
print(root/'app/calibration.mp4')
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Describe the exact built IPK for Homebrew Channel."""
|
"""Describe the exact built IPK for Homebrew Channel."""
|
||||||
import hashlib, json
|
import hashlib, json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from sync_version import synchronize
|
||||||
|
synchronize(check=True)
|
||||||
ROOT=Path(__file__).resolve().parent
|
ROOT=Path(__file__).resolve().parent
|
||||||
BASE='https://gitea.adnau.de/matze19999/HyperDRM'
|
BASE='https://gitea.adnau.de/matze19999/HyperDRM'
|
||||||
RAW=BASE+'/raw/branch/main/'
|
RAW=BASE+'/raw/branch/main/'
|
||||||
|
|||||||
+5
-3
@@ -1,16 +1,18 @@
|
|||||||
"""Package own app and executable; no Signify or LG binaries are included."""
|
"""Package own app and executable; no Signify or LG binaries are included."""
|
||||||
import io, json, tarfile
|
import gzip, io, json, tarfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from sync_version import synchronize
|
||||||
|
synchronize(check=True)
|
||||||
ROOT=Path(__file__).resolve().parent
|
ROOT=Path(__file__).resolve().parent
|
||||||
ID='org.webosbrew.histogramhyperion'
|
ID='org.webosbrew.histogramhyperion'
|
||||||
VERSION=json.loads((ROOT/'app/appinfo.json').read_text(encoding='utf-8'))['version']
|
VERSION=json.loads((ROOT/'app/appinfo.json').read_text(encoding='utf-8'))['version']
|
||||||
def tar_bytes(entries):
|
def tar_bytes(entries):
|
||||||
stream=io.BytesIO()
|
stream=io.BytesIO()
|
||||||
with tarfile.open(fileobj=stream,mode='w:gz') as tar:
|
with tarfile.open(fileobj=stream,mode='w') as tar:
|
||||||
for name,body,mode in entries:
|
for name,body,mode in entries:
|
||||||
info=tarfile.TarInfo(name);info.size=len(body);info.mode=mode;info.mtime=0
|
info=tarfile.TarInfo(name);info.size=len(body);info.mode=mode;info.mtime=0
|
||||||
tar.addfile(info,io.BytesIO(body))
|
tar.addfile(info,io.BytesIO(body))
|
||||||
return stream.getvalue()
|
return gzip.compress(stream.getvalue(), mtime=0)
|
||||||
entries=[]
|
entries=[]
|
||||||
for directory,target in [('app','applications/'+ID),('service','services/'+ID+'.service'),('package','packages/'+ID)]:
|
for directory,target in [('app','applications/'+ID),('service','services/'+ID+'.service'),('package','packages/'+ID)]:
|
||||||
for path in sorted((ROOT/directory).iterdir()):
|
for path in sorted((ROOT/directory).iterdir()):
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Runtime boundaries
|
||||||
|
|
||||||
|
The native service remains one translation unit because LG symbols are resolved
|
||||||
|
at runtime. Its implementation headers share the context declared in service.c;
|
||||||
|
they are included once in dependency order, not independent public libraries.
|
||||||
|
Formatting must preserve include order (`SortIncludes: Never`).
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| service.c | Runtime context, LG/Luna loading, registration and shutdown |
|
||||||
|
| service_api.h | Start/stop, root restart, settings and autostart actions |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
Connection stages are explicitly named in runtime_states.h:
|
||||||
|
Disconnected → Connecting → Authenticating (when configured) → Layout → Subscribed.
|
||||||
|
Any connection, authorization or parsing failure closes the connection and
|
||||||
|
schedules a bounded retry. Up to eight buffered messages are consumed per tick,
|
||||||
|
including ticks where no new bytes arrive. Buffer capacity and read budgets remain
|
||||||
|
bounded. LED count/profile changes trigger a fresh layout subscription.
|
||||||
|
|
||||||
|
The color bridge uses Idle → Holding → WaitingFrame → Fading → Idle. It holds the
|
||||||
|
last histogram frame while PicCap starts, clears its priority before observing a
|
||||||
|
fresh PicCap frame, then blends for 500 ms. The transition expires after 3 seconds;
|
||||||
|
real black bypasses the fade. The opposite direction seeds histogram continuity
|
||||||
|
from a fresh receiver frame. Capture ownership remains in handover.h; the observer
|
||||||
|
never acquires LG capture. Stop cancels the bridge and releases owned resources.
|
||||||
|
|
||||||
|
The UI calibration session uses Starting → Waiting → Measuring/Validating →
|
||||||
|
Committing → finished. Each asynchronous callback checks its original session
|
||||||
|
identity, so a cancelled session cannot modify a newly started session. Proposals
|
||||||
|
remain unsaved until the user selects Save profile. The native lease still bounds
|
||||||
|
the session if the UI disappears.
|
||||||
|
|
||||||
|
UI action errors take precedence over transient status errors. Polling cannot
|
||||||
|
erase them; starting the next action clears them. Receiver settings, capture state,
|
||||||
|
calibration state and presentation errors remain distinct.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -99,3 +99,94 @@ A separate nonblocking authenticated receiver probe refreshes the LED mapping ev
|
|||||||
The app automatically detected OLED65G42LW.DEUQLJP and selected the measured G4 profile. A live switch to Average produced one unique output color across four HyperHDR LED-stream samples; switching back to G4 produced 80-97 unique colors. Auto and the original receiver/handover preferences were restored afterward. Custom profiles validate 2-16 sorted rim positions, bounded region indices and model identifiers, persist privately, and are selected automatically only for the exact imported model identifier. No measured spatial profile for other hardware is claimed. Native tests passed profile parsing/persistence, model selection and average fallback. The synthetic-image test verifies geometry cache replacement after a custom-profile change.
|
The app automatically detected OLED65G42LW.DEUQLJP and selected the measured G4 profile. A live switch to Average produced one unique output color across four HyperHDR LED-stream samples; switching back to G4 produced 80-97 unique colors. Auto and the original receiver/handover preferences were restored afterward. Custom profiles validate 2-16 sorted rim positions, bounded region indices and model identifiers, persist privately, and are selected automatically only for the exact imported model identifier. No measured spatial profile for other hardware is claimed. Native tests passed profile parsing/persistence, model selection and average fallback. The synthetic-image test verifies geometry cache replacement after a custom-profile change.
|
||||||
|
|
||||||
The receiver UI and remote-navigation tests passed, including custom JSON input and profile controls. Diagnosis export was deliberately excluded from this release. Cold boot and other TV models remain unverified.
|
The receiver UI and remote-navigation tests passed, including custom JSON input and profile controls. Diagnosis export was deliberately excluded from this release. Cold boot and other TV models remain unverified.
|
||||||
|
|
||||||
|
## Color continuity, calibration wizard and TV navigation (0.11.0)
|
||||||
|
|
||||||
|
Empty histogram populations and failed reads are distinguished from populated
|
||||||
|
black frames. A bounded 1.5-second hold expires into capture recovery; valid dark
|
||||||
|
frames bypass the 500 ms recovery blend without altering their RGB values. ARM
|
||||||
|
tests verify hold timing, black populations, recovery mixing and near-black codec
|
||||||
|
rounding. An independent nonblocking raw receiver LED subscription accepts old
|
||||||
|
result and current data envelopes, authenticates when required, handles fragmented
|
||||||
|
messages, validates array values/counts and rebuilds mappings on layout/profile
|
||||||
|
changes. Source bridge tests cover both directions, fresh-sample gating, midpoint
|
||||||
|
and completion, genuine black, stale samples, timeout, connection setup and Stop.
|
||||||
|
|
||||||
|
The bundled reproducible H.264 calibration video measures 32 edge positions.
|
||||||
|
PicCap handover and bar detection are suppressed only for the bounded calibration
|
||||||
|
session. The wizard starts the video plane before waiting for capture, requires
|
||||||
|
fresh samples, rejects global/multimodal responses and selects the most localized
|
||||||
|
anchor when nested regions overlap. The live G4 run produced ten sorted anchors
|
||||||
|
covering all four edges and preserved the saved Auto/G4 profile. One source
|
||||||
|
revocation recovered in 252 ms. Other TV hardware remains unverified; candidates
|
||||||
|
require user review/save and four-corner validation. Failed measurements and
|
||||||
|
cancellation preserve the saved profile.
|
||||||
|
|
||||||
|
Overview, Receiver and Calibration are separate sections. Start Auto Calibration
|
||||||
|
is directly visible in Calibration and linked from Overview. Focusable field
|
||||||
|
frames keep native inputs unfocused during highlighting; OK/click explicitly
|
||||||
|
starts keyboard editing. The documented keyboardStateChange event and Back
|
||||||
|
restore frame navigation. Tests cover section selection, hidden/disabled controls,
|
||||||
|
input focus only after activation, keyboard dismissal, wizard scope and rejection,
|
||||||
|
proposal versus saved state, JSON validation and root-repair retries/denials.
|
||||||
|
|
||||||
|
The root repair targets only org.webosbrew.histogramhyperion.service via Homebrew
|
||||||
|
Channel and restarts only this service. The TV's current root process and boot
|
||||||
|
symlink were verified. Actual autostart disable/enable requests succeeded, with
|
||||||
|
Autostart restored to On. No receiver color correction, WLED configuration or
|
||||||
|
PicCap autostart setting was changed. A local browser preview was unavailable
|
||||||
|
because the browser policy rejected file URLs; no alternate browser path was used.
|
||||||
|
|
||||||
|
## Version 0.12.0
|
||||||
|
|
||||||
|
Receiver stream processing now drains complete buffered messages when no new
|
||||||
|
bytes arrive. A regression fixture buffers seventeen updates and verifies all
|
||||||
|
three processing batches without additional socket writes. Action errors survive
|
||||||
|
status polling and are cleared by the next user action. Tests also cover delayed
|
||||||
|
calibration replies after cancellation and a new session.
|
||||||
|
|
||||||
|
Connection and bridge states are named enums; UI calibration phases and session
|
||||||
|
identity checks isolate asynchronous work. The native service is separated into
|
||||||
|
runtime bootstrap, Luna actions, calibration, status and capture-loop modules.
|
||||||
|
Production C and JavaScript are formatted consistently; include dependency order
|
||||||
|
is preserved explicitly.
|
||||||
|
|
||||||
|
VERSION generates app/package/runtime versions. release.py passes all JavaScript,
|
||||||
|
portable native and Python version-drift tests, cross-compiles the ARM service and
|
||||||
|
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,11 @@
|
|||||||
|
# HyperDRM 0.12.0
|
||||||
|
|
||||||
|
- Process buffered receiver updates even when no additional network data arrives.
|
||||||
|
- Keep action errors visible across background status polling.
|
||||||
|
- Name connection, transition and calibration states; ignore stale calibration callbacks.
|
||||||
|
- Split and format native service modules and application JavaScript.
|
||||||
|
- Generate versions from VERSION and add a tested, reproducible release workflow.
|
||||||
|
|
||||||
|
Validation: all portable native, JavaScript and Python version tests pass; the
|
||||||
|
ARM IPK's embedded versions, architecture, size and SHA-256 match its manifest.
|
||||||
|
This release has not yet been installed or validated 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,3 @@
|
|||||||
|
# HyperDRM 0.13.4
|
||||||
|
|
||||||
|
Default histogram capture to 50 ms and expose a persisted Capture interval (ms) in the Receiver panel (25–1000, steps of 25). Remote highlighting leaves the keyboard closed; OK opens editing. A 25 ms scheduler keeps network processing responsive independently of slower capture settings. Missing legacy values default to 50 ms. PicCap timing is unchanged. Native tests check persistence, legacy defaults and invalid values.
|
||||||
@@ -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
|
||||||
+49
-17
@@ -4,34 +4,66 @@
|
|||||||
#define BOOT_DIRECTORY "/var/lib/webosbrew/init.d"
|
#define BOOT_DIRECTORY "/var/lib/webosbrew/init.d"
|
||||||
#endif
|
#endif
|
||||||
#ifndef BOOT_SCRIPT
|
#ifndef BOOT_SCRIPT
|
||||||
#define BOOT_SCRIPT "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/boot.sh"
|
#define BOOT_SCRIPT \
|
||||||
|
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/boot.sh"
|
||||||
#endif
|
#endif
|
||||||
#define BOOT_HOOK BOOT_DIRECTORY "/60-histogramhyperion"
|
#define BOOT_HOOK BOOT_DIRECTORY "/60-histogramhyperion"
|
||||||
/* Exact pre-0.6.1 hook; only this known own file may be migrated. */
|
/* Exact pre-0.6.1 hook; only this known own file may be migrated. */
|
||||||
static const char legacy_boot_script[] = "#!/bin/sh\n# Histogram Hyperion own boot hook v1\n(\n attempt=0\n while [ -x /var/lib/webosbrew/init.d/60-histogramhyperion ] && [ \"$attempt\" -lt 120 ]; do\n result=$(timeout 12 luna-send -n 1 luna://org.webosbrew.histogramhyperion.service/start '{}' 2>&1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 </dev/null &\nexit 0\n";
|
static const char legacy_boot_script[] =
|
||||||
|
"#!/bin/sh\n# Histogram Hyperion own boot hook v1\n(\n attempt=0\n while [ -x "
|
||||||
|
"/var/lib/webosbrew/init.d/60-histogramhyperion ] && [ \"$attempt\" -lt 120 ]; do\n "
|
||||||
|
"result=$(timeout 12 luna-send -n 1 luna://org.webosbrew.histogramhyperion.service/start '{}' "
|
||||||
|
"2>&1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n "
|
||||||
|
"attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start "
|
||||||
|
"cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 </dev/null "
|
||||||
|
"&\nexit 0\n";
|
||||||
/* 0: absent, 1: own legacy copy, 2: own symlink, -1: foreign/error. */
|
/* 0: absent, 1: own legacy copy, 2: own symlink, -1: foreign/error. */
|
||||||
static int boot_hook_kind(void) {
|
static int boot_hook_kind(void) {
|
||||||
struct stat st;if(lstat(BOOT_HOOK,&st)!=0)return errno==ENOENT?0:-1;
|
struct stat st;
|
||||||
|
if (lstat(BOOT_HOOK, &st) != 0)
|
||||||
|
return errno == ENOENT ? 0 : -1;
|
||||||
if (S_ISLNK(st.st_mode)) {
|
if (S_ISLNK(st.st_mode)) {
|
||||||
char target[sizeof(BOOT_SCRIPT)+1];ssize_t n=readlink(BOOT_HOOK,target,sizeof(target));
|
char target[sizeof(BOOT_SCRIPT) + 1];
|
||||||
return n==(ssize_t)(sizeof(BOOT_SCRIPT)-1) && memcmp(target,BOOT_SCRIPT,(size_t)n)==0?2:-1;
|
ssize_t n = readlink(BOOT_HOOK, target, sizeof(target));
|
||||||
|
return n == (ssize_t)(sizeof(BOOT_SCRIPT) - 1) &&
|
||||||
|
memcmp(target, BOOT_SCRIPT, (size_t)n) == 0
|
||||||
|
? 2
|
||||||
|
: -1;
|
||||||
}
|
}
|
||||||
if(!S_ISREG(st.st_mode))return -1;
|
if (!S_ISREG(st.st_mode))
|
||||||
char data[sizeof(legacy_boot_script)];int fd=open(BOOT_HOOK,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return -1;
|
return -1;
|
||||||
ssize_t n=read(fd,data,sizeof(data));close(fd);
|
char data[sizeof(legacy_boot_script)];
|
||||||
return n==(ssize_t)(sizeof(legacy_boot_script)-1) && memcmp(data,legacy_boot_script,(size_t)n)==0?1:-1;
|
int fd = open(BOOT_HOOK, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
if (fd < 0)
|
||||||
|
return -1;
|
||||||
|
ssize_t n = read(fd, data, sizeof(data));
|
||||||
|
close(fd);
|
||||||
|
return n == (ssize_t)(sizeof(legacy_boot_script) - 1) &&
|
||||||
|
memcmp(data, legacy_boot_script, (size_t)n) == 0
|
||||||
|
? 1
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
static bool boot_enabled(void) {
|
||||||
|
return boot_hook_kind() > 0 && access(BOOT_HOOK, X_OK) == 0;
|
||||||
}
|
}
|
||||||
static bool boot_enabled(void){return boot_hook_kind()>0 && access(BOOT_HOOK,X_OK)==0;}
|
|
||||||
static bool set_boot(bool enabled) {
|
static bool set_boot(bool enabled) {
|
||||||
int kind=boot_hook_kind();if(kind<0)return false;
|
int kind = boot_hook_kind();
|
||||||
if(!enabled)return kind==0 || unlink(BOOT_HOOK)==0;
|
if (kind < 0)
|
||||||
if(access(BOOT_SCRIPT,X_OK)!=0)return false;
|
return false;
|
||||||
if(kind==2)return true;
|
if (!enabled)
|
||||||
if(mkdir(BOOT_DIRECTORY,0755)!=0 && errno!=EEXIST)return false;
|
return kind == 0 || unlink(BOOT_HOOK) == 0;
|
||||||
|
if (access(BOOT_SCRIPT, X_OK) != 0)
|
||||||
|
return false;
|
||||||
|
if (kind == 2)
|
||||||
|
return true;
|
||||||
|
if (mkdir(BOOT_DIRECTORY, 0755) != 0 && errno != EEXIST)
|
||||||
|
return false;
|
||||||
/* Never overwrite another process's temporary file or a foreign hook. */
|
/* Never overwrite another process's temporary file or a foreign hook. */
|
||||||
if(symlink(BOOT_SCRIPT,BOOT_HOOK ".tmp")!=0)return false;
|
if (symlink(BOOT_SCRIPT, BOOT_HOOK ".tmp") != 0)
|
||||||
|
return false;
|
||||||
bool ok = rename(BOOT_HOOK ".tmp", BOOT_HOOK) == 0;
|
bool ok = rename(BOOT_HOOK ".tmp", BOOT_HOOK) == 0;
|
||||||
if(!ok)unlink(BOOT_HOOK ".tmp");
|
if (!ok)
|
||||||
|
unlink(BOOT_HOOK ".tmp");
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+36
-6
@@ -3,17 +3,47 @@
|
|||||||
/* Calibrated preset-0 edge probes on this G4. Return -1 for an ambiguous
|
/* Calibrated preset-0 edge probes on this G4. Return -1 for an ambiguous
|
||||||
* or dark frame so darkness alone cannot switch the active capture mode. */
|
* 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]) {
|
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;
|
const unsigned interior[] = {2, 6, 10, 15};
|
||||||
|
unsigned lit = 0, interior_black = 0, interior_peak = 0;
|
||||||
for (unsigned i = 0; i < 4; i++) {
|
for (unsigned i = 0; i < 4; i++) {
|
||||||
unsigned r = interior[i], peak = color[r][0];
|
unsigned r = interior[i], peak = color[r][0];
|
||||||
for(unsigned c=1;c<3;c++)if(color[r][c]>peak)peak=color[r][c];
|
for (unsigned c = 1; c < 3; c++)
|
||||||
if(black[r]<20 && peak>35)lit++;
|
if (color[r][c] > peak)
|
||||||
|
peak = color[r][c];
|
||||||
|
if (black[r] < 20 && peak > 35) {
|
||||||
|
lit++;
|
||||||
|
interior_black += black[r];
|
||||||
|
interior_peak += peak;
|
||||||
}
|
}
|
||||||
if(lit<2)return -1;
|
}
|
||||||
|
if (lit < 2)
|
||||||
|
return -1;
|
||||||
unsigned top = (black[3] + black[11] + black[12]) / 3;
|
unsigned top = (black[3] + black[11] + black[12]) / 3;
|
||||||
unsigned bottom = (black[1] + black[5] + black[9]) / 3;
|
unsigned bottom = (black[1] + black[5] + black[9]) / 3;
|
||||||
if(top>=55 && bottom>=25)return 1;
|
if (top >= 55 && bottom >= 25)
|
||||||
if(top<20 && bottom<20)return 0;
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+117
-45
@@ -2,83 +2,155 @@
|
|||||||
#define HYPERDRM_CALIBRATION_H
|
#define HYPERDRM_CALIBRATION_H
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "playback.h"
|
#include "playback.h"
|
||||||
#define CALIBRATION_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/calibration.json"
|
#include "atomic_file.h"
|
||||||
typedef struct {double position;unsigned char region;} RimPoint;
|
#define CALIBRATION_PREF \
|
||||||
|
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
|
||||||
|
"calibration.json"
|
||||||
|
typedef struct {
|
||||||
|
double position;
|
||||||
|
unsigned char region;
|
||||||
|
} RimPoint;
|
||||||
typedef enum { CAL_AUTO, CAL_G4, CAL_AVERAGE, CAL_CUSTOM } CalibrationKind;
|
typedef enum { CAL_AUTO, CAL_G4, CAL_AVERAGE, CAL_CUSTOM } CalibrationKind;
|
||||||
typedef struct {CalibrationKind kind;char model[64];unsigned count;RimPoint points[16];} Calibration;
|
typedef struct {
|
||||||
|
CalibrationKind kind;
|
||||||
|
char model[64];
|
||||||
|
unsigned count;
|
||||||
|
RimPoint points[16];
|
||||||
|
} Calibration;
|
||||||
static Calibration calibration;
|
static Calibration calibration;
|
||||||
static char tv_model[64];
|
static char tv_model[64];
|
||||||
static unsigned calibration_generation = 1;
|
static unsigned calibration_generation = 1;
|
||||||
static inline unsigned calibration_revision(void){return calibration_generation;}
|
static inline unsigned calibration_revision(void) {
|
||||||
static const RimPoint g4_rim[]={
|
return calibration_generation;
|
||||||
{0.2078431373,7},{0.3176470588,3},{0.5490196078,11},{0.7529411765,12},
|
}
|
||||||
{0.8392156863,13},{1.4941176471,10},{2.1843137255,9},{2.5490196078,1},
|
static const RimPoint g4_rim[] = {{0.2078431373, 7}, {0.3176470588, 3}, {0.5490196078, 11},
|
||||||
{2.8666666667,5},{3.4,15}
|
{0.7529411765, 12}, {0.8392156863, 13}, {1.4941176471, 10},
|
||||||
};
|
{2.1843137255, 9}, {2.5490196078, 1}, {2.8666666667, 5},
|
||||||
|
{3.4, 15}};
|
||||||
static inline const char *calibration_key(CalibrationKind kind) {
|
static inline const char *calibration_key(CalibrationKind kind) {
|
||||||
const char *keys[]={"auto","g4","average","custom"};return keys[kind];
|
const char *keys[] = {"auto", "g4", "average", "custom"};
|
||||||
|
return keys[kind];
|
||||||
}
|
}
|
||||||
static inline bool calibration_model_valid(const char *model) {
|
static inline bool calibration_model_valid(const char *model) {
|
||||||
if(!model[0] || strlen(model)>63)return false;
|
if (!model[0] || strlen(model) > 63)
|
||||||
|
return false;
|
||||||
for (const unsigned char *p = (const unsigned char *)model; *p; p++)
|
for (const unsigned char *p = (const unsigned char *)model; *p; p++)
|
||||||
if(!((*p>='A'&&*p<='Z')||(*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')||*p=='.'||*p=='-'||*p=='_'||*p==' '))return false;
|
if (!((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') ||
|
||||||
|
*p == '.' || *p == '-' || *p == '_' || *p == ' '))
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/* LG retail model identifiers: OLED + two-digit panel size + G4 series. */
|
/* LG retail model identifiers: OLED + two-digit panel size + G4 series. */
|
||||||
static inline bool calibration_is_g4(const char *model) {
|
static inline bool calibration_is_g4(const char *model) {
|
||||||
return strlen(model)>=8 && !strncmp(model,"OLED",4) && model[4]>='0' && model[4]<='9' && model[5]>='0' && model[5]<='9' && model[6]=='G' && model[7]=='4';
|
return strlen(model) >= 8 && !strncmp(model, "OLED", 4) && model[4] >= '0' && model[4] <= '9' &&
|
||||||
|
model[5] >= '0' && model[5] <= '9' && model[6] == 'G' && model[7] == '4';
|
||||||
}
|
}
|
||||||
static inline CalibrationKind calibration_effective(void) {
|
static inline CalibrationKind calibration_effective(void) {
|
||||||
if(calibration.kind!=CAL_AUTO)return calibration.kind;
|
if (calibration.kind != CAL_AUTO)
|
||||||
if(calibration.count>=2 && tv_model[0] && !strcmp(calibration.model,tv_model))return CAL_CUSTOM;
|
return calibration.kind;
|
||||||
|
if (calibration.count >= 2 && tv_model[0] && !strcmp(calibration.model, tv_model))
|
||||||
|
return CAL_CUSTOM;
|
||||||
return calibration_is_g4(tv_model) ? CAL_G4 : CAL_AVERAGE;
|
return calibration_is_g4(tv_model) ? CAL_G4 : CAL_AVERAGE;
|
||||||
}
|
}
|
||||||
static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibration *result) {
|
static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibration *result) {
|
||||||
void *root=j->parse(text);if(!root)return false;
|
void *root = playback_parse_exact(j, text);
|
||||||
Calibration next=*result;bool valid=false;
|
if (!root)
|
||||||
for(unsigned k=0;k<4;k++)if(playback_is(j,root,"profile",calibration_key(k))){next.kind=k;valid=true;break;}
|
return false;
|
||||||
|
Calibration next = *result;
|
||||||
|
bool valid = false;
|
||||||
|
for (unsigned k = 0; k < 4; k++)
|
||||||
|
if (playback_is(j, root, "profile", calibration_key(k))) {
|
||||||
|
next.kind = k;
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
void *points = NULL;
|
void *points = NULL;
|
||||||
if (j->type(root) == 4 && j->get(root, "points", &points)) {
|
if (j->type(root) == 4 && j->get(root, "points", &points)) {
|
||||||
void *model = playback_field(j, root, "model");
|
void *model = playback_field(j, root, "model");
|
||||||
valid=valid && points && j->type(points)==5 && model && j->type(model)==6 && calibration_model_valid(j->string(model));
|
valid = valid && points && j->type(points) == 5 && model && j->type(model) == 6 &&
|
||||||
size_t count=valid?j->length(points):0;valid=valid && count>=2 && count<=16;
|
calibration_model_valid(j->string(model));
|
||||||
if(valid){strcpy(next.model,j->string(model));next.count=count;memset(next.points,0,sizeof(next.points));}
|
size_t count = valid ? j->length(points) : 0;
|
||||||
|
valid = valid && count >= 2 && count <= 16;
|
||||||
|
if (valid) {
|
||||||
|
strcpy(next.model, j->string(model));
|
||||||
|
next.count = count;
|
||||||
|
memset(next.points, 0, sizeof(next.points));
|
||||||
|
}
|
||||||
for (size_t i = 0; valid && i < count; i++) {
|
for (size_t i = 0; valid && i < count; i++) {
|
||||||
void *point=j->at(points,i),*position=playback_field(j,point,"position"),*region=playback_field(j,point,"region");
|
void *point = j->at(points, i), *position = playback_field(j, point, "position"),
|
||||||
valid=position && (j->type(position)==2 || j->type(position)==3) && region && j->type(region)==3;
|
*region = playback_field(j, point, "region");
|
||||||
if(valid){char *end;double x=strtod(j->string(position),&end);valid=end && !*end && isfinite(x) && x>=0 && x<4;
|
valid = position && (j->type(position) == 2 || j->type(position) == 3) && region &&
|
||||||
long r=strtol(j->string(region),&end,10);valid=valid && end && !*end && r>=0 && r<16 && (!i || x>next.points[i-1].position);
|
j->type(region) == 3;
|
||||||
if(valid){next.points[i].position=x;next.points[i].region=r;}
|
if (valid) {
|
||||||
|
char *end;
|
||||||
|
double x = strtod(j->string(position), &end);
|
||||||
|
valid = end && !*end && isfinite(x) && x >= 0 && x < 4;
|
||||||
|
long r = strtol(j->string(region), &end, 10);
|
||||||
|
valid = valid && end && !*end && r >= 0 && r < 16 &&
|
||||||
|
(!i || x > next.points[i - 1].position);
|
||||||
|
if (valid) {
|
||||||
|
next.points[i].position = x;
|
||||||
|
next.points[i].region = r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(next.kind==CAL_CUSTOM && next.count<2)valid=false;
|
}
|
||||||
j->put(root);if(valid)*result=next;return valid;
|
if (next.kind == CAL_CUSTOM && next.count < 2)
|
||||||
|
valid = false;
|
||||||
|
j->put(root);
|
||||||
|
if (valid)
|
||||||
|
*result = next;
|
||||||
|
return valid;
|
||||||
}
|
}
|
||||||
static inline bool calibration_save(const char *path, const Calibration *value) {
|
static inline bool calibration_save(const char *path, const Calibration *value) {
|
||||||
char temp[512],data[2048];int used=snprintf(data,sizeof(data),"{\"profile\":\"%s\"",calibration_key(value->kind));
|
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) {
|
if (value->count) {
|
||||||
used+=snprintf(data+used,sizeof(data)-used,",\"model\":\"%s\",\"points\":[",value->model);
|
used += snprintf(data + used, sizeof(data) - used, ",\"model\":\"%s\",\"points\":[",
|
||||||
for(unsigned i=0;i<value->count;i++)used+=snprintf(data+used,sizeof(data)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",value->points[i].position,value->points[i].region);
|
value->model);
|
||||||
|
for (unsigned i = 0; i < value->count; i++)
|
||||||
|
used +=
|
||||||
|
snprintf(data + used, sizeof(data) - used, "%s{\"position\":%.12g,\"region\":%u}",
|
||||||
|
i ? "," : "", value->points[i].position, value->points[i].region);
|
||||||
used += snprintf(data + used, sizeof(data) - used, "]");
|
used += snprintf(data + used, sizeof(data) - used, "]");
|
||||||
}
|
}
|
||||||
used += snprintf(data + used, sizeof(data) - used, "}\n");
|
used += snprintf(data + used, sizeof(data) - used, "}\n");
|
||||||
if(snprintf(temp,sizeof(temp),"%s.tmp",path)>=(int)sizeof(temp))return false;
|
return used > 0 && (size_t)used < sizeof(data) &&
|
||||||
int fd=open(temp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
atomic_replace(path, data, (size_t)used) != SAVE_UNCHANGED;
|
||||||
bool ok=write(fd,data,used)==used && fsync(fd)==0;if(close(fd)!=0)ok=false;
|
|
||||||
if(ok)ok=rename(temp,path)==0;
|
|
||||||
if(!ok)unlink(temp);
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
static inline void calibration_load(PlaybackJson *j, const char *path) {
|
static inline void calibration_load(PlaybackJson *j, const char *path) {
|
||||||
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return;
|
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
char data[2048];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);
|
if (fd < 0)
|
||||||
if(n<=0 || n==(ssize_t)sizeof(data)-1)return;
|
return;
|
||||||
data[n]=0;calibration_parse(j,data,&calibration);
|
char data[2048];
|
||||||
|
ssize_t n = read(fd, data, sizeof(data) - 1);
|
||||||
|
close(fd);
|
||||||
|
if (n <= 0 || n == (ssize_t)sizeof(data) - 1)
|
||||||
|
return;
|
||||||
|
data[n] = 0;
|
||||||
|
calibration_parse(j, data, &calibration);
|
||||||
}
|
}
|
||||||
static inline void calibration_colors(const unsigned char input[16][3],unsigned char output[16][3]){
|
static inline void calibration_colors(const unsigned char input[16][3],
|
||||||
memcpy(output,input,48);if(calibration_effective()!=CAL_AVERAGE)return;
|
unsigned char output[16][3]) {
|
||||||
unsigned sum[3]={0};for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)sum[c]+=input[i][c];
|
memcpy(output, input, 48);
|
||||||
for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)output[i][c]=sum[c]/16;
|
if (calibration_effective() != CAL_AVERAGE)
|
||||||
|
return;
|
||||||
|
unsigned sum[3] = {0};
|
||||||
|
for (unsigned i = 0; i < 16; i++)
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
sum[c] += input[i][c];
|
||||||
|
for (unsigned i = 0; i < 16; i++)
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
output[i][c] = sum[c] / 16;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/* Capture scheduler and histogram processing. */
|
||||||
|
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_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, &capture_ctx);
|
||||||
|
if (owned)
|
||||||
|
return 1;
|
||||||
|
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_ctx.handle && !receiver_runtime.starting)
|
||||||
|
retry_failure(&retries, milliseconds());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (capture_ctx.revoked) {
|
||||||
|
set_error("LG revoked capture; recovering after source transition");
|
||||||
|
capture_ctx.recoveries++;
|
||||||
|
capture_ctx.recovery_started = milliseconds();
|
||||||
|
release_capture(false);
|
||||||
|
retry_source_transition(&retries, milliseconds());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (capture_ctx.settling) {
|
||||||
|
capture_ctx.settling--;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
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_ctx.preset = 0;
|
||||||
|
capture_ctx.settling = 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (capture_ctx.sample_at && now - capture_ctx.sample_at < target_capture_interval(&target))
|
||||||
|
return 1;
|
||||||
|
Histogram hist[16] = {0};
|
||||||
|
int rc = vt_get(capture_ctx.handle, hist);
|
||||||
|
bool populated = !rc && histogram_populated(hist);
|
||||||
|
if (!populated) {
|
||||||
|
unsigned char held[16][3];
|
||||||
|
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, 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);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
capture_ctx.sample_at = now;
|
||||||
|
capture_ctx.sample_sequence++;
|
||||||
|
unsigned aggregate[3] = {0};
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
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++) {
|
||||||
|
total += hist[i].bins[c][b];
|
||||||
|
if (b <= 12)
|
||||||
|
black += hist[i].bins[c][b];
|
||||||
|
}
|
||||||
|
unsigned fraction = total ? (unsigned)(100 * black / total) : 0;
|
||||||
|
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++) {
|
||||||
|
capture_ctx.rgb[i][c] = histogram_mean(hist[i].bins[c]);
|
||||||
|
aggregate[c] += capture_ctx.rgb[i][c];
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
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_ctx.preset = 1;
|
||||||
|
capture_ctx.settling = 2;
|
||||||
|
capture_ctx.next_border_probe = now + 2000;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int c = 0; c < 3; c++)
|
||||||
|
capture_ctx.sent_rgb[c] = (unsigned char)(aggregate[c] / 16);
|
||||||
|
dark_color_filter(capture_ctx.sent_rgb);
|
||||||
|
unsigned char 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(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 = (!output_frame(&receiver_runtime.output, line));
|
||||||
|
free(line);
|
||||||
|
if (failed) {
|
||||||
|
set_error("Receiver connection lost");
|
||||||
|
stop_capture();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
capture_ctx.color_valid = true;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#define COLOR_BOOST_H
|
#define COLOR_BOOST_H
|
||||||
/* Preserve the original saturation and brightness. */
|
/* Preserve the original saturation and brightness. */
|
||||||
static inline void boost_color(const unsigned char in[3], unsigned char out[3]) {
|
static inline void boost_color(const unsigned char in[3], unsigned char out[3]) {
|
||||||
for(unsigned c=0;c<3;c++)out[c]=in[c];
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
out[c] = in[c];
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#ifndef HYPERDRM_CONTINUITY_H
|
||||||
|
#define HYPERDRM_CONTINUITY_H
|
||||||
|
/* Zero population is missing data, while populated bin zero is real black. */
|
||||||
|
static inline bool histogram_populated(const Histogram hist[16]) {
|
||||||
|
for (unsigned r = 0; r < 16; r++)
|
||||||
|
for (unsigned c = 0; c < 3; c++) {
|
||||||
|
uint64_t total = 0;
|
||||||
|
for (unsigned b = 0; b < 256; b++)
|
||||||
|
total += hist[r].bins[c][b];
|
||||||
|
if (!total)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
typedef struct {
|
||||||
|
unsigned char last[16][3], from[16][3];
|
||||||
|
uint64_t valid_at, fade_at, missing_at;
|
||||||
|
bool available, fading, holding;
|
||||||
|
} Continuity;
|
||||||
|
static inline void continuity_seed(Continuity *s, const unsigned char colors[16][3], uint64_t now) {
|
||||||
|
memcpy(s->last, colors, sizeof(s->last));
|
||||||
|
memcpy(s->from, colors, sizeof(s->from));
|
||||||
|
s->valid_at = now;
|
||||||
|
s->available = true;
|
||||||
|
s->fading = true;
|
||||||
|
s->fade_at = 0;
|
||||||
|
s->missing_at = 0;
|
||||||
|
s->holding = false;
|
||||||
|
}
|
||||||
|
static inline void continuity_mix(const unsigned char from[16][3], const unsigned char to[16][3],
|
||||||
|
unsigned elapsed, unsigned char out[16][3]) {
|
||||||
|
if (elapsed > 500)
|
||||||
|
elapsed = 500;
|
||||||
|
for (unsigned r = 0; r < 16; r++)
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
out[r][c] = (from[r][c] * (500 - elapsed) + to[r][c] * elapsed + 250) / 500;
|
||||||
|
}
|
||||||
|
static inline bool continuity_frame(Continuity *s, bool valid, const unsigned char input[16][3],
|
||||||
|
uint64_t now, unsigned char out[16][3]) {
|
||||||
|
if (!valid) {
|
||||||
|
if (!s->missing_at)
|
||||||
|
s->missing_at = now;
|
||||||
|
s->holding = s->available && now - s->missing_at < 1500;
|
||||||
|
if (s->holding)
|
||||||
|
memcpy(out, s->last, sizeof(s->last));
|
||||||
|
return s->holding;
|
||||||
|
}
|
||||||
|
if (s->holding) {
|
||||||
|
memcpy(s->from, s->last, sizeof(s->from));
|
||||||
|
s->fading = true;
|
||||||
|
s->fade_at = 0;
|
||||||
|
}
|
||||||
|
s->holding = false;
|
||||||
|
s->missing_at = 0;
|
||||||
|
/* Near-black codec rounding is valid darkness, not a lost measurement. */
|
||||||
|
bool black = true;
|
||||||
|
for (unsigned r = 0; r < 16; r++)
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
if (input[r][c] > 3)
|
||||||
|
black = false;
|
||||||
|
if (black)
|
||||||
|
s->fading = false;
|
||||||
|
if (s->fading) {
|
||||||
|
if (!s->fade_at)
|
||||||
|
s->fade_at = now;
|
||||||
|
continuity_mix(s->from, input, (unsigned)(now - s->fade_at), out);
|
||||||
|
if (now - s->fade_at >= 500)
|
||||||
|
s->fading = false;
|
||||||
|
} else
|
||||||
|
memcpy(out, input, sizeof(s->last));
|
||||||
|
memcpy(s->last, out, sizeof(s->last));
|
||||||
|
s->available = true;
|
||||||
|
s->valid_at = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -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
|
||||||
+156
-45
@@ -2,77 +2,188 @@
|
|||||||
#define HYPERDRM_DIAGNOSTICS_H
|
#define HYPERDRM_DIAGNOSTICS_H
|
||||||
/* An independent nonblocking connection. Tests never send colors or acquire
|
/* An independent nonblocking connection. Tests never send colors or acquire
|
||||||
* capture. Work per timer iteration is bounded to keep output responsive. */
|
* 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;
|
typedef enum {
|
||||||
|
TEST_IDLE,
|
||||||
|
TEST_CONNECT,
|
||||||
|
TEST_SEND,
|
||||||
|
TEST_LOGIN,
|
||||||
|
TEST_LAYOUT,
|
||||||
|
TEST_DONE,
|
||||||
|
TEST_FAILED
|
||||||
|
} TestStage;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int fd;TestStage stage,after_send;TargetConfig target;
|
int fd;
|
||||||
uint64_t started,deadline,elapsed;unsigned leds;
|
TestStage stage, after_send;
|
||||||
char request[384],error[128];size_t sent,used;char *response;
|
TargetConfig target;
|
||||||
|
uint64_t started, deadline, elapsed;
|
||||||
|
unsigned leds;
|
||||||
|
char request[384], error[128];
|
||||||
|
size_t sent, used;
|
||||||
|
char *response;
|
||||||
LedBlend *mapping;
|
LedBlend *mapping;
|
||||||
} ReceiverProbe;
|
} ReceiverProbe;
|
||||||
static ReceiverProbe connection_test = {.fd = -1};
|
static ReceiverProbe connection_test = {.fd = -1};
|
||||||
static const char *probe_stage_name(const ReceiverProbe *probe) {
|
static const char *probe_stage_name(const ReceiverProbe *probe) {
|
||||||
static const char *names[]={"Not tested","Connecting","Sending request","Checking authentication","Reading LED layout","Passed","Failed"};
|
static const char *names[] = {
|
||||||
|
"Not tested", "Connecting", "Sending request", "Checking authentication",
|
||||||
|
"Reading LED layout", "Passed", "Failed"};
|
||||||
return names[probe->stage];
|
return names[probe->stage];
|
||||||
}
|
}
|
||||||
static bool probe_busy(const ReceiverProbe *probe){return probe->stage>=TEST_CONNECT && probe->stage<=TEST_LAYOUT;}
|
static bool probe_busy(const ReceiverProbe *probe) {
|
||||||
static void probe_close(ReceiverProbe *probe){if(probe->fd>=0)close(probe->fd);probe->fd=-1;free(probe->mapping);probe->mapping=NULL;free(probe->response);probe->response=NULL;memset(probe->request,0,sizeof(probe->request));memset(probe->target.token,0,sizeof(probe->target.token));}
|
return probe->stage >= TEST_CONNECT && probe->stage <= TEST_LAYOUT;
|
||||||
|
}
|
||||||
|
static void probe_close(ReceiverProbe *probe) {
|
||||||
|
if (probe->fd >= 0)
|
||||||
|
close(probe->fd);
|
||||||
|
probe->fd = -1;
|
||||||
|
free(probe->mapping);
|
||||||
|
probe->mapping = NULL;
|
||||||
|
free(probe->response);
|
||||||
|
probe->response = NULL;
|
||||||
|
memset(probe->request, 0, sizeof(probe->request));
|
||||||
|
memset(probe->target.token, 0, sizeof(probe->target.token));
|
||||||
|
}
|
||||||
static void probe_finish(ReceiverProbe *probe, const char *error, uint64_t now) {
|
static void probe_finish(ReceiverProbe *probe, const char *error, uint64_t now) {
|
||||||
probe_close(probe);probe->elapsed=now-probe->started;
|
probe_close(probe);
|
||||||
|
probe->elapsed = now - probe->started;
|
||||||
probe->stage = error ? TEST_FAILED : TEST_DONE;
|
probe->stage = error ? TEST_FAILED : TEST_DONE;
|
||||||
snprintf(probe->error, sizeof(probe->error), "%s", error ? error : "");
|
snprintf(probe->error, sizeof(probe->error), "%s", error ? error : "");
|
||||||
}
|
}
|
||||||
static void probe_request(ReceiverProbe *probe, bool login, uint64_t now) {
|
static void probe_request(ReceiverProbe *probe, bool login, uint64_t now) {
|
||||||
if(login)snprintf(probe->request,sizeof(probe->request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",probe->target.token);
|
if (login)
|
||||||
else snprintf(probe->request,sizeof(probe->request),"{\"command\":\"serverinfo\"}\n");
|
snprintf(probe->request, sizeof(probe->request),
|
||||||
probe->sent=probe->used=0;probe->after_send=login?TEST_LOGIN:TEST_LAYOUT;
|
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
|
||||||
probe->stage=TEST_SEND;probe->deadline=now+3000;
|
probe->target.token);
|
||||||
|
else
|
||||||
|
snprintf(probe->request, sizeof(probe->request), "{\"command\":\"serverinfo\"}\n");
|
||||||
|
probe->sent = probe->used = 0;
|
||||||
|
probe->after_send = login ? TEST_LOGIN : TEST_LAYOUT;
|
||||||
|
probe->stage = TEST_SEND;
|
||||||
|
probe->deadline = now + 3000;
|
||||||
}
|
}
|
||||||
static bool probe_begin(ReceiverProbe *probe, const TargetConfig *target, uint64_t now) {
|
static bool probe_begin(ReceiverProbe *probe, const TargetConfig *target, uint64_t now) {
|
||||||
if(probe_busy(probe))return false;
|
if (probe_busy(probe))
|
||||||
probe_close(probe);memset(probe,0,sizeof(*probe));probe->fd=-1;
|
return false;
|
||||||
probe->target=*target;probe->started=now;
|
probe_close(probe);
|
||||||
|
memset(probe, 0, sizeof(*probe));
|
||||||
|
probe->fd = -1;
|
||||||
|
probe->target = *target;
|
||||||
|
probe->started = now;
|
||||||
probe->response = malloc(2 * 1024 * 1024 + 1);
|
probe->response = malloc(2 * 1024 * 1024 + 1);
|
||||||
if(!probe->response){probe_finish(probe,"Cannot allocate connection test buffer",now);return true;}
|
if (!probe->response) {
|
||||||
|
probe_finish(probe, "Cannot allocate connection test buffer", now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
probe->fd = target_open(target);
|
probe->fd = target_open(target);
|
||||||
if(probe->fd<0){probe_finish(probe,"Cannot open receiver connection",now);return true;}
|
if (probe->fd < 0) {
|
||||||
probe->stage=TEST_CONNECT;probe->deadline=now+3000;return true;
|
probe_finish(probe, "Cannot open receiver connection", now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
probe->stage = TEST_CONNECT;
|
||||||
|
probe->deadline = now + 3000;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
static void probe_tick(ReceiverProbe *probe, PlaybackJson *json, uint64_t now) {
|
static void probe_tick(ReceiverProbe *probe, PlaybackJson *json, uint64_t now) {
|
||||||
if(!probe_busy(probe))return;
|
if (!probe_busy(probe))
|
||||||
if(now>=probe->deadline){probe_finish(probe,"Receiver timed out",now);return;}
|
return;
|
||||||
|
if (now >= probe->deadline) {
|
||||||
|
probe_finish(probe, "Receiver timed out", now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
int fd = probe->fd;
|
int fd = probe->fd;
|
||||||
if (probe->stage == TEST_CONNECT) {
|
if (probe->stage == TEST_CONNECT) {
|
||||||
struct pollfd p={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
|
struct pollfd p = {fd, POLLOUT, 0};
|
||||||
if(poll(&p,1,0)<=0)return;
|
int error = 0;
|
||||||
if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){probe_finish(probe,"Receiver connection refused or unreachable",now);return;}
|
socklen_t size = sizeof(error);
|
||||||
probe_request(probe,probe->target.token[0]!=0,now);return;
|
if (poll(&p, 1, 0) <= 0)
|
||||||
|
return;
|
||||||
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || error) {
|
||||||
|
probe_finish(probe, "Receiver connection refused or unreachable", now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probe_request(probe, probe->target.token[0] != 0, now);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (probe->stage == TEST_SEND) {
|
if (probe->stage == TEST_SEND) {
|
||||||
size_t length = strlen(probe->request);
|
size_t length = strlen(probe->request);
|
||||||
ssize_t n = send(fd, probe->request + probe->sent, length - probe->sent, MSG_NOSIGNAL);
|
ssize_t n = send(fd, probe->request + probe->sent, length - probe->sent, MSG_NOSIGNAL);
|
||||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||||
if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;}
|
return;
|
||||||
probe->sent+=(size_t)n;
|
if (n <= 0) {
|
||||||
if(probe->sent==length){probe->stage=probe->after_send;probe->deadline=now+3000;memset(probe->request,0,sizeof(probe->request));}
|
probe_finish(probe, "Receiver closed the test connection", now);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
size_t available=2*1024*1024-probe->used;if(available>65536)available=65536;
|
probe->sent += (size_t)n;
|
||||||
if(!available){probe_finish(probe,"Receiver response exceeds 2 MiB",now);return;}
|
if (probe->sent == length) {
|
||||||
|
probe->stage = probe->after_send;
|
||||||
|
probe->deadline = now + 3000;
|
||||||
|
memset(probe->request, 0, sizeof(probe->request));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t available = 2 * 1024 * 1024 - probe->used;
|
||||||
|
if (available > 65536)
|
||||||
|
available = 65536;
|
||||||
|
if (!available) {
|
||||||
|
probe_finish(probe, "Receiver response exceeds 2 MiB", now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ssize_t n = recv(fd, probe->response + probe->used, available, 0);
|
ssize_t n = recv(fd, probe->response + probe->used, available, 0);
|
||||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||||
if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;}
|
return;
|
||||||
probe->used+=(size_t)n;probe->response[probe->used]=0;
|
if (n <= 0) {
|
||||||
char *end=strchr(probe->response,'\n');if(!end)return;*end=0;
|
probe_finish(probe, "Receiver closed the test connection", now);
|
||||||
void *root=json->parse(probe->response);bool success=playback_true(json,root,"success");if(root)json->put(root);
|
return;
|
||||||
if(!success){probe_finish(probe,probe->stage==TEST_LOGIN?"Receiver authentication failed":"Receiver rejected serverinfo; check authorization",now);return;}
|
}
|
||||||
if(probe->stage==TEST_LOGIN){probe_request(probe,false,now);return;}
|
if (memchr(probe->response + probe->used, 0, (size_t)n)) {
|
||||||
LedBlend *mapping=NULL;unsigned count=0;
|
probe_finish(probe, "Receiver sent an embedded NUL", now);
|
||||||
if(!layout_from_json(probe->response,&mapping,&count)){probe_finish(probe,"Receiver returned an invalid or empty LED layout",now);return;}
|
return;
|
||||||
probe->leds=count;probe_finish(probe,NULL,now);probe->mapping=mapping;
|
}
|
||||||
|
probe->used += (size_t)n;
|
||||||
|
probe->response[probe->used] = 0;
|
||||||
|
char *end = strchr(probe->response, '\n');
|
||||||
|
if (!end)
|
||||||
|
return;
|
||||||
|
*end = 0;
|
||||||
|
void *root = playback_parse_exact(json, probe->response);
|
||||||
|
bool success = playback_true(json, root, "success");
|
||||||
|
if (root)
|
||||||
|
json->put(root);
|
||||||
|
if (!success) {
|
||||||
|
probe_finish(probe,
|
||||||
|
probe->stage == TEST_LOGIN
|
||||||
|
? "Receiver authentication failed"
|
||||||
|
: "Receiver rejected serverinfo; check authorization",
|
||||||
|
now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (probe->stage == TEST_LOGIN) {
|
||||||
|
probe_request(probe, false, now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LedBlend *mapping = NULL;
|
||||||
|
unsigned count = 0;
|
||||||
|
if (!layout_from_json(probe->response, &mapping, &count)) {
|
||||||
|
probe_finish(probe, "Receiver returned an invalid or empty LED layout", now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probe->leds = count;
|
||||||
|
probe_finish(probe, NULL, now);
|
||||||
|
probe->mapping = mapping;
|
||||||
|
}
|
||||||
|
static inline const char *test_stage_name(void) {
|
||||||
|
return probe_stage_name(&connection_test);
|
||||||
|
}
|
||||||
|
static inline bool test_busy(void) {
|
||||||
|
return probe_busy(&connection_test);
|
||||||
|
}
|
||||||
|
static inline void test_close(void) {
|
||||||
|
probe_close(&connection_test);
|
||||||
|
}
|
||||||
|
static inline bool test_begin(const TargetConfig *target, uint64_t now) {
|
||||||
|
return probe_begin(&connection_test, target, now);
|
||||||
|
}
|
||||||
|
static inline void test_tick(PlaybackJson *json, uint64_t now) {
|
||||||
|
probe_tick(&connection_test, json, now);
|
||||||
}
|
}
|
||||||
static inline const char *test_stage_name(void){return probe_stage_name(&connection_test);}
|
|
||||||
static inline bool test_busy(void){return probe_busy(&connection_test);}
|
|
||||||
static inline void test_close(void){probe_close(&connection_test);}
|
|
||||||
static inline bool test_begin(const TargetConfig *target,uint64_t now){return probe_begin(&connection_test,target,now);}
|
|
||||||
static inline void test_tick(PlaybackJson *json,uint64_t now){probe_tick(&connection_test,json,now);}
|
|
||||||
#endif
|
#endif
|
||||||
+207
-64
@@ -1,18 +1,32 @@
|
|||||||
#ifndef HYPERDRM_HANDOVER_H
|
#ifndef HYPERDRM_HANDOVER_H
|
||||||
#define HYPERDRM_HANDOVER_H
|
#define HYPERDRM_HANDOVER_H
|
||||||
#include "playback.h"
|
#include "playback.h"
|
||||||
#ifndef PICCAP_BINARY
|
#include "atomic_file.h"
|
||||||
#define PICCAP_BINARY "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
|
#ifndef CALIBRATION_ACTIVE
|
||||||
|
#define CALIBRATION_ACTIVE false
|
||||||
#endif
|
#endif
|
||||||
#define HANDOVER_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/handover.json"
|
#ifndef PICCAP_BINARY
|
||||||
|
#define PICCAP_BINARY \
|
||||||
|
"/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
|
||||||
|
#endif
|
||||||
|
#define HANDOVER_PREF \
|
||||||
|
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
|
||||||
|
"handover.json"
|
||||||
static PlaybackJson playback_json;
|
static PlaybackJson playback_json;
|
||||||
static bool handover_enabled, piccap_owned, piccap_busy, media_busy, piccap_connected;
|
static bool handover_enabled, piccap_owned, piccap_busy, media_busy, piccap_connected;
|
||||||
static PlaybackProtection protection;
|
static PlaybackProtection protection;
|
||||||
static uint64_t protection_at,next_media_probe,next_piccap_probe,handover_cooldown,piccap_action_at;
|
static uint64_t protection_at, next_media_probe, next_piccap_probe, handover_cooldown,
|
||||||
typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource;
|
piccap_action_at;
|
||||||
|
typedef enum {
|
||||||
|
SOURCE_HISTOGRAM,
|
||||||
|
SOURCE_PICCAP_STARTING,
|
||||||
|
SOURCE_PICCAP,
|
||||||
|
SOURCE_PICCAP_STOPPING
|
||||||
|
} CaptureSource;
|
||||||
static CaptureSource capture_source;
|
static CaptureSource capture_source;
|
||||||
static void *service_handle;
|
static void *service_handle;
|
||||||
static bool (*call_once)(void*,const char*,const char*,Callback,void*,unsigned long*,LSError*);
|
static bool (*call_once)(void *, const char *, const char *, Callback, void *, unsigned long *,
|
||||||
|
LSError *);
|
||||||
static bool (*cancel_call)(void *, unsigned long, LSError *);
|
static bool (*cancel_call)(void *, unsigned long, LSError *);
|
||||||
static unsigned long piccap_token, media_token;
|
static unsigned long piccap_token, media_token;
|
||||||
static uint64_t piccap_deadline, media_deadline;
|
static uint64_t piccap_deadline, media_deadline;
|
||||||
@@ -25,103 +39,232 @@ static uint64_t clear_since,unknown_since,piccap_disconnected_since;
|
|||||||
static TargetConfig piccap_target;
|
static TargetConfig piccap_target;
|
||||||
static char piccap_error[128];
|
static char piccap_error[128];
|
||||||
static bool piccap_request(int command);
|
static bool piccap_request(int command);
|
||||||
static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;}
|
static inline bool piccap_installed(void) {
|
||||||
|
return access(PICCAP_BINARY, X_OK) == 0;
|
||||||
|
}
|
||||||
static bool handover_save(bool enabled) {
|
static bool handover_save(bool enabled) {
|
||||||
const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n";
|
const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n";
|
||||||
int fd=open(HANDOVER_PREF ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
return atomic_replace(HANDOVER_PREF, data, strlen(data)) != SAVE_UNCHANGED;
|
||||||
ssize_t n=write(fd,data,strlen(data));bool ok=n==(ssize_t)strlen(data) && fsync(fd)==0;
|
|
||||||
if(close(fd)!=0)ok=false;
|
|
||||||
if(ok)ok=rename(HANDOVER_PREF ".tmp",HANDOVER_PREF)==0;
|
|
||||||
if(!ok)unlink(HANDOVER_PREF ".tmp");
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
static void handover_load(void) {
|
static void handover_load(void) {
|
||||||
int fd=open(HANDOVER_PREF,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return;
|
int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
char text[128];ssize_t n=read(fd,text,sizeof(text)-1);close(fd);
|
if (fd < 0)
|
||||||
if(n>0){text[n]=0;void *root=playback_json.parse(text);handover_enabled=playback_true(&playback_json,root,"piccapHandover");if(root)playback_json.put(root);}
|
return;
|
||||||
|
char text[128];
|
||||||
|
ssize_t n = read(fd, text, sizeof(text) - 1);
|
||||||
|
close(fd);
|
||||||
|
if (n > 0) {
|
||||||
|
text[n] = 0;
|
||||||
|
void *root = playback_json.parse(text);
|
||||||
|
handover_enabled = playback_true(&playback_json, root, "piccapHandover");
|
||||||
|
if (root)
|
||||||
|
playback_json.put(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
static bool media_reply(void *h, void *m, void *ctx) {
|
static bool media_reply(void *h, void *m, void *ctx) {
|
||||||
(void)h;(void)ctx;media_busy=false;media_token=0;
|
(void)h;
|
||||||
protection=playback_classify(&playback_json,payload(m));protection_at=milliseconds();
|
(void)ctx;
|
||||||
|
media_busy = false;
|
||||||
|
media_token = 0;
|
||||||
|
protection = playback_classify(&playback_json, payload(m));
|
||||||
|
protection_at = milliseconds();
|
||||||
if (protection == PLAYBACK_CLEAR) {
|
if (protection == PLAYBACK_CLEAR) {
|
||||||
if(!clear_samples)clear_since=protection_at;
|
if (!clear_samples)
|
||||||
if(clear_samples<3)clear_samples++;
|
clear_since = protection_at;
|
||||||
|
if (clear_samples < 3)
|
||||||
|
clear_samples++;
|
||||||
unknown_since = 0;
|
unknown_since = 0;
|
||||||
}else {clear_samples=0;clear_since=0;if(protection==PLAYBACK_UNKNOWN && !unknown_since)unknown_since=protection_at;else if(protection==PLAYBACK_PROTECTED)unknown_since=0;}
|
} else {
|
||||||
|
clear_samples = 0;
|
||||||
|
clear_since = 0;
|
||||||
|
if (protection == PLAYBACK_UNKNOWN && !unknown_since)
|
||||||
|
unknown_since = protection_at;
|
||||||
|
else if (protection == PLAYBACK_PROTECTED)
|
||||||
|
unknown_since = 0;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. 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) {
|
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)h;
|
||||||
void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue");
|
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");
|
||||||
if (command == 0) {
|
if (command == 0) {
|
||||||
void *running = playback_field(&playback_json, root, "isRunning");
|
void *running = playback_field(&playback_json, root, "isRunning");
|
||||||
if (ok && running && playback_json.type(running) == 1) {
|
if (ok && running && playback_json.type(running) == 1) {
|
||||||
bool active = playback_json.boolean(running);
|
bool active = playback_json.boolean(running);
|
||||||
piccap_connected = playback_true(&playback_json, root, "connected");
|
piccap_connected = playback_true(&playback_json, root, "connected");
|
||||||
if (active) {
|
if (active) {
|
||||||
if(capture_source==SOURCE_PICCAP_STARTING)capture_source=SOURCE_PICCAP;
|
if (capture_source == SOURCE_PICCAP_STARTING)
|
||||||
if(piccap_connected)piccap_disconnected_since=0;
|
capture_source = SOURCE_PICCAP;
|
||||||
else if(!piccap_disconnected_since)piccap_disconnected_since=milliseconds();
|
if (piccap_connected)
|
||||||
|
piccap_disconnected_since = 0;
|
||||||
|
else if (!piccap_disconnected_since)
|
||||||
|
piccap_disconnected_since = milliseconds();
|
||||||
|
} else if (capture_source != SOURCE_PICCAP_STARTING ||
|
||||||
|
milliseconds() - piccap_action_at >= 2000) {
|
||||||
|
if (capture_source != SOURCE_PICCAP_STOPPING)
|
||||||
|
handover_cooldown = milliseconds() + 10000;
|
||||||
|
piccap_owned = false;
|
||||||
|
capture_source = SOURCE_HISTOGRAM;
|
||||||
|
piccap_connected = false;
|
||||||
|
piccap_disconnected_since = 0;
|
||||||
|
if (handover_cooldown < milliseconds() + 3000)
|
||||||
|
handover_cooldown = milliseconds() + 3000;
|
||||||
|
if (!retries.paused)
|
||||||
|
retries.due = milliseconds();
|
||||||
}
|
}
|
||||||
else if(capture_source!=SOURCE_PICCAP_STARTING || milliseconds()-piccap_action_at>=2000){
|
} else if (!piccap_installed()) {
|
||||||
if(capture_source!=SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
|
piccap_owned = false;
|
||||||
piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;piccap_disconnected_since=0;
|
capture_source = SOURCE_HISTOGRAM;
|
||||||
if(handover_cooldown<milliseconds()+3000)handover_cooldown=milliseconds()+3000;
|
piccap_connected = false;
|
||||||
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) {
|
} 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");
|
void *port = playback_field(&playback_json, root, "port"),
|
||||||
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);
|
*socket = playback_field(&playback_json, root, "unix-socket");
|
||||||
if(ok){if(!piccap_request(same?1:4)){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap endpoint request failed");}}
|
bool same =
|
||||||
else {capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"Cannot read PicCap settings");}
|
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) {
|
} else if (command == 4) {
|
||||||
if(!ok){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap rejected receiver settings");}
|
if (!ok) {
|
||||||
|
capture_source = SOURCE_PICCAP_STOPPING;
|
||||||
|
snprintf(piccap_error, sizeof(piccap_error), "PicCap rejected receiver settings");
|
||||||
|
}
|
||||||
/* setSettings restarts PicCap itself; status confirms ownership. */
|
/* setSettings restarts PicCap itself; status confirms ownership. */
|
||||||
}else if(command==1 && !ok){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=milliseconds()+10000;}
|
} else if (command == 1 && !ok) {
|
||||||
if(command>=3 && capture_source==SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
|
capture_source = SOURCE_PICCAP_STOPPING;
|
||||||
if(root)playback_json.put(root);
|
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;
|
next_piccap_probe = milliseconds() + 500;
|
||||||
if(command==2)piccap_request(0);
|
if (command == 2)
|
||||||
|
piccap_request(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
static bool piccap_request(int command) {
|
static bool piccap_request(int command) {
|
||||||
if(piccap_busy)return false;
|
if (piccap_busy)
|
||||||
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";
|
return false;
|
||||||
LSError e;error_init(&e);
|
const char *uri = command == 3 ? "luna://org.webosbrew.piccap.service/getSettings"
|
||||||
char data[160];snprintf(data,sizeof(data),"{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",target_piccap_host(&piccap_target),piccap_target.piccap_port);
|
: command == 4 ? "luna://org.webosbrew.piccap.service/setSettings"
|
||||||
piccap_busy=call_once(service_handle,uri,command==4?data:"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
|
: 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);
|
||||||
|
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;
|
piccap_deadline = milliseconds() + 3000;
|
||||||
error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy;
|
error_free(&e);
|
||||||
|
if (command)
|
||||||
|
piccap_action_at = milliseconds();
|
||||||
|
return piccap_busy;
|
||||||
}
|
}
|
||||||
/* Return true while PicCap owns capture or a stop confirmation is pending. */
|
/* Return true while PicCap owns capture or a stop confirmation is pending. */
|
||||||
static bool handover_tick(void) {
|
static bool handover_tick(void) {
|
||||||
uint64_t now = milliseconds();
|
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;clear_since=0;if(!unknown_since)unknown_since=now;}
|
if (media_busy && now >= media_deadline) {
|
||||||
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;}
|
LSError e;
|
||||||
if(handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && !media_busy && now>=next_media_probe){
|
error_init(&e);
|
||||||
LSError e;error_init(&e);
|
cancel_call(service_handle, media_token, &e);
|
||||||
media_busy=call_once(service_handle,"luna://com.webos.media/getActivePipelines","{}",media_reply,NULL,&media_token,&e);
|
error_free(&e);
|
||||||
|
media_busy = false;
|
||||||
|
media_token = 0;
|
||||||
|
protection = PLAYBACK_UNKNOWN;
|
||||||
|
clear_samples = 0;
|
||||||
|
clear_since = 0;
|
||||||
|
if (!unknown_since)
|
||||||
|
unknown_since = now;
|
||||||
|
}
|
||||||
|
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;
|
media_deadline = now + 3000;
|
||||||
error_free(&e);next_media_probe=now+1000;
|
error_free(&e);
|
||||||
|
next_media_probe = now + 1000;
|
||||||
}
|
}
|
||||||
if(protection==PLAYBACK_CLEAR && now-protection_at>=3000){protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=0;if(!unknown_since)unknown_since=now;}
|
if (protection == PLAYBACK_CLEAR && now - protection_at >= 3000) {
|
||||||
bool session=handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked;
|
protection = PLAYBACK_UNKNOWN;
|
||||||
|
clear_samples = 0;
|
||||||
|
clear_since = 0;
|
||||||
|
if (!unknown_since)
|
||||||
|
unknown_since = now;
|
||||||
|
}
|
||||||
|
bool session = !CALIBRATION_ACTIVE && handover_enabled && target_piccap_compatible(&target) &&
|
||||||
|
piccap_installed() && !retries.paused && !retries.blocked;
|
||||||
bool clear = session && protection == PLAYBACK_CLEAR && now - protection_at < 3000;
|
bool clear = session && protection == PLAYBACK_CLEAR && now - protection_at < 3000;
|
||||||
bool stable=clear && clear_samples>=3 && now-clear_since>=CLEAR_STABLE_MS && now>=handover_cooldown;
|
bool stable = clear && clear_samples >= 3 && now - clear_since >= CLEAR_STABLE_MS &&
|
||||||
|
now >= handover_cooldown;
|
||||||
if (capture_source == SOURCE_HISTOGRAM && stable) {
|
if (capture_source == SOURCE_HISTOGRAM && stable) {
|
||||||
piccap_target=target;piccap_error[0]=0;piccap_disconnected_since=now;stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
|
piccap_target = target;
|
||||||
if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;}
|
piccap_error[0] = 0;
|
||||||
|
piccap_disconnected_since = now;
|
||||||
|
release_capture(false);
|
||||||
|
retries.due = 0;
|
||||||
|
piccap_owned = true;
|
||||||
|
capture_source = SOURCE_PICCAP_STARTING;
|
||||||
|
if (!piccap_request(3)) {
|
||||||
|
capture_source = SOURCE_PICCAP_STOPPING;
|
||||||
|
handover_cooldown = now + PICCAP_FAILURE_COOLDOWN_MS;
|
||||||
}
|
}
|
||||||
bool grace=protection==PLAYBACK_UNKNOWN && unknown_since && now-unknown_since<UNKNOWN_GRACE_MS && capture_source==SOURCE_PICCAP;
|
}
|
||||||
bool lost=piccap_owned && capture_source==SOURCE_PICCAP && piccap_disconnected_since && now-piccap_disconnected_since>=PICCAP_LOSS_MS;
|
bool grace = protection == PLAYBACK_UNKNOWN && unknown_since &&
|
||||||
|
now - unknown_since < UNKNOWN_GRACE_MS && capture_source == SOURCE_PICCAP;
|
||||||
|
bool lost = piccap_owned && capture_source == SOURCE_PICCAP && piccap_disconnected_since &&
|
||||||
|
now - piccap_disconnected_since >= PICCAP_LOSS_MS;
|
||||||
if (lost) {
|
if (lost) {
|
||||||
snprintf(piccap_error,sizeof(piccap_error),"PicCap receiver disconnected; histogram fallback");piccap_fallbacks++;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;
|
snprintf(piccap_error, sizeof(piccap_error),
|
||||||
|
"PicCap receiver disconnected; histogram fallback");
|
||||||
|
piccap_fallbacks++;
|
||||||
|
handover_cooldown = now + PICCAP_FAILURE_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
if (piccap_owned &&
|
||||||
|
(!session || (!clear && !grace) || lost || 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 && (!session || (!clear && !grace) || lost || 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 (piccap_owned && !piccap_busy && now >= next_piccap_probe) {
|
||||||
if(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0);
|
if (capture_source == SOURCE_PICCAP_STOPPING)
|
||||||
|
piccap_request(2);
|
||||||
|
else
|
||||||
|
piccap_request(0);
|
||||||
next_piccap_probe = now + 1000;
|
next_piccap_probe = now + 1000;
|
||||||
}
|
}
|
||||||
return piccap_owned;
|
return piccap_owned;
|
||||||
|
|||||||
+8
-2
@@ -4,12 +4,18 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
/* LG ABI: region ID followed by three planar channels, 256 uint16 bins
|
/* LG ABI: region ID followed by three planar channels, 256 uint16 bins
|
||||||
* per channel. Bin index is the 8-bit intensity (0..255). */
|
* per channel. Bin index is the 8-bit intensity (0..255). */
|
||||||
typedef struct { uint32_t id; uint16_t bins[3][256]; } Histogram;
|
typedef struct {
|
||||||
|
uint32_t id;
|
||||||
|
uint16_t bins[3][256];
|
||||||
|
} Histogram;
|
||||||
_Static_assert(sizeof(Histogram) == 0x604, "LG ABI record size");
|
_Static_assert(sizeof(Histogram) == 0x604, "LG ABI record size");
|
||||||
_Static_assert(offsetof(Histogram, bins) == 4, "LG ABI channel offset");
|
_Static_assert(offsetof(Histogram, bins) == 4, "LG ABI channel offset");
|
||||||
static inline unsigned char histogram_mean(const uint16_t bins[256]) {
|
static inline unsigned char histogram_mean(const uint16_t bins[256]) {
|
||||||
uint64_t count = 0, weight = 0;
|
uint64_t count = 0, weight = 0;
|
||||||
for(unsigned b=0;b<256;b++){count+=bins[b];weight+=(uint64_t)bins[b]*b;}
|
for (unsigned b = 0; b < 256; b++) {
|
||||||
|
count += bins[b];
|
||||||
|
weight += (uint64_t)bins[b] * b;
|
||||||
|
}
|
||||||
return count ? (unsigned char)((weight + count / 2) / count) : 0;
|
return count ? (unsigned char)((weight + count / 2) / count) : 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
#ifndef SPATIAL_INTERPOLATION_H
|
#ifndef SPATIAL_INTERPOLATION_H
|
||||||
#define SPATIAL_INTERPOLATION_H
|
#define SPATIAL_INTERPOLATION_H
|
||||||
static inline void blend_color(const unsigned char a[3],const unsigned char b[3],unsigned weight,unsigned char out[3]){
|
static inline void blend_color(const unsigned char a[3], const unsigned char b[3], unsigned weight,
|
||||||
for(unsigned c=0;c<3;c++)out[c]=(unsigned char)(((unsigned)a[c]*(65535-weight)+(unsigned)b[c]*weight+32767)/65535);
|
unsigned char out[3]) {
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
out[c] =
|
||||||
|
(unsigned char)(((unsigned)a[c] * (65535 - weight) + (unsigned)b[c] * weight + 32767) /
|
||||||
|
65535);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+60
-22
@@ -5,30 +5,46 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
typedef struct {unsigned char a,b;unsigned weight;} LedBlend;
|
typedef struct {
|
||||||
|
unsigned char a, b;
|
||||||
|
unsigned weight;
|
||||||
|
} LedBlend;
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include "calibration.h"
|
#include "calibration.h"
|
||||||
static double screen_perimeter(double x, double y) {
|
static double screen_perimeter(double x, double y) {
|
||||||
double distances[]={y,1-x,1-y,x},positions[]={x,1+y,3-x,4-y};unsigned edge=0;
|
double distances[] = {y, 1 - x, 1 - y, x}, positions[] = {x, 1 + y, 3 - x, 4 - y};
|
||||||
for(unsigned i=1;i<4;i++)if(distances[i]<distances[edge])edge=i;
|
unsigned edge = 0;
|
||||||
|
for (unsigned i = 1; i < 4; i++)
|
||||||
|
if (distances[i] < distances[edge])
|
||||||
|
edge = i;
|
||||||
return positions[edge] >= 4 ? positions[edge] - 4 : positions[edge];
|
return positions[edge] >= 4 ? positions[edge] - 4 : positions[edge];
|
||||||
}
|
}
|
||||||
static LedBlend layout_blend(double x, double y) {
|
static LedBlend layout_blend(double x, double y) {
|
||||||
const RimPoint *rim = calibration_effective() == CAL_CUSTOM ? calibration.points : g4_rim;
|
const RimPoint *rim = calibration_effective() == CAL_CUSTOM ? calibration.points : g4_rim;
|
||||||
double t=screen_perimeter(x,y);unsigned n=calibration_effective()==CAL_CUSTOM?calibration.count:sizeof(g4_rim)/sizeof(*g4_rim),right=0;
|
double t = screen_perimeter(x, y);
|
||||||
while(right<n && rim[right].position<=t)right++;
|
unsigned n = calibration_effective() == CAL_CUSTOM ? calibration.count
|
||||||
right%=n;unsigned left=(right+n-1)%n;
|
: sizeof(g4_rim) / sizeof(*g4_rim),
|
||||||
|
right = 0;
|
||||||
|
while (right < n && rim[right].position <= t)
|
||||||
|
right++;
|
||||||
|
right %= n;
|
||||||
|
unsigned left = (right + n - 1) % n;
|
||||||
double span = rim[right].position - rim[left].position, distance = t - rim[left].position;
|
double span = rim[right].position - rim[left].position, distance = t - rim[left].position;
|
||||||
if(span<=0)span+=4;
|
if (span <= 0)
|
||||||
if(distance<0)distance+=4;
|
span += 4;
|
||||||
return (LedBlend){rim[left].region,rim[right].region,(unsigned)(distance/span*65535+0.5)};
|
if (distance < 0)
|
||||||
|
distance += 4;
|
||||||
|
return (LedBlend){rim[left].region, rim[right].region,
|
||||||
|
(unsigned)(distance / span * 65535 + 0.5)};
|
||||||
}
|
}
|
||||||
/* Use the TV's JSON-C library; validate every rectangle before replacing the
|
/* Use the TV's JSON-C library; validate every rectangle before replacing the
|
||||||
* layout. The safety limit is independent of the installed LED count. */
|
* layout. The safety limit is independent of the installed LED count. */
|
||||||
static bool layout_from_json(const char *text, LedBlend **result, unsigned *count) {
|
static bool layout_from_json(const char *text, LedBlend **result, unsigned *count) {
|
||||||
void *lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!lib)return false;
|
void *lib = dlopen("libjson-c.so.5", RTLD_NOW);
|
||||||
|
if (!lib)
|
||||||
|
return false;
|
||||||
void *(*parse)(const char *) = dlsym(lib, "json_tokener_parse");
|
void *(*parse)(const char *) = dlsym(lib, "json_tokener_parse");
|
||||||
int (*get)(const void *, const char *, void **) = dlsym(lib, "json_object_object_get_ex");
|
int (*get)(const void *, const char *, void **) = dlsym(lib, "json_object_object_get_ex");
|
||||||
int (*type)(const void *) = dlsym(lib, "json_object_get_type");
|
int (*type)(const void *) = dlsym(lib, "json_object_get_type");
|
||||||
@@ -36,23 +52,45 @@ static bool layout_from_json(const char *text,LedBlend **result,unsigned *count)
|
|||||||
void *(*at)(const void *, size_t) = dlsym(lib, "json_object_array_get_idx");
|
void *(*at)(const void *, size_t) = dlsym(lib, "json_object_array_get_idx");
|
||||||
double (*number)(const void *) = dlsym(lib, "json_object_get_double");
|
double (*number)(const void *) = dlsym(lib, "json_object_get_double");
|
||||||
int (*put)(void *) = dlsym(lib, "json_object_put");
|
int (*put)(void *) = dlsym(lib, "json_object_put");
|
||||||
if(!parse||!get||!type||!length||!at||!number||!put){dlclose(lib);return false;}
|
if (!parse || !get || !type || !length || !at || !number || !put) {
|
||||||
void *root=parse(text),*info=NULL,*leds=NULL;bool valid=root && type(root)==4 && get(root,"info",&info) && type(info)==4 && get(info,"leds",&leds) && type(leds)==5;
|
dlclose(lib);
|
||||||
size_t n=valid?length(leds):0;valid=valid && n>0 && n<=16384;
|
return false;
|
||||||
LedBlend *mapping=valid?calloc(n,sizeof(*mapping)):NULL;valid=valid && mapping;
|
}
|
||||||
|
void *root = parse(text), *info = NULL, *leds = NULL;
|
||||||
|
bool valid = root && type(root) == 4 && get(root, "info", &info) && type(info) == 4 &&
|
||||||
|
get(info, "leds", &leds) && type(leds) == 5;
|
||||||
|
size_t n = valid ? length(leds) : 0;
|
||||||
|
valid = valid && n > 0 && n <= 16384;
|
||||||
|
LedBlend *mapping = valid ? calloc(n, sizeof(*mapping)) : NULL;
|
||||||
|
valid = valid && mapping;
|
||||||
const char *keys[] = {"hmin", "hmax", "vmin", "vmax"};
|
const char *keys[] = {"hmin", "hmax", "vmin", "vmax"};
|
||||||
for (size_t i = 0; valid && i < n; i++) {
|
for (size_t i = 0; valid && i < n; i++) {
|
||||||
void *led=at(leds,i);double coordinates[4];valid=led && type(led)==4;
|
void *led = at(leds, i);
|
||||||
|
double coordinates[4];
|
||||||
|
valid = led && type(led) == 4;
|
||||||
for (unsigned c = 0; valid && c < 4; c++) {
|
for (unsigned c = 0; valid && c < 4; c++) {
|
||||||
void *v=NULL;valid=get(led,keys[c],&v) && (type(v)==2||type(v)==3);
|
void *v = NULL;
|
||||||
if(valid){coordinates[c]=number(v);valid=isfinite(coordinates[c]) && coordinates[c]>=0 && coordinates[c]<=1;}
|
valid = get(led, keys[c], &v) && (type(v) == 2 || type(v) == 3);
|
||||||
|
if (valid) {
|
||||||
|
coordinates[c] = number(v);
|
||||||
|
valid = isfinite(coordinates[c]) && coordinates[c] >= 0 && coordinates[c] <= 1;
|
||||||
}
|
}
|
||||||
if(valid)valid=coordinates[0]<=coordinates[1] && coordinates[2]<=coordinates[3];
|
|
||||||
if(valid)mapping[i]=layout_blend((coordinates[0]+coordinates[1])/2,(coordinates[2]+coordinates[3])/2);
|
|
||||||
}
|
}
|
||||||
if(root)put(root);
|
if (valid)
|
||||||
|
valid = coordinates[0] <= coordinates[1] && coordinates[2] <= coordinates[3];
|
||||||
|
if (valid)
|
||||||
|
mapping[i] = layout_blend((coordinates[0] + coordinates[1]) / 2,
|
||||||
|
(coordinates[2] + coordinates[3]) / 2);
|
||||||
|
}
|
||||||
|
if (root)
|
||||||
|
put(root);
|
||||||
dlclose(lib);
|
dlclose(lib);
|
||||||
if(!valid){free(mapping);return false;}
|
if (!valid) {
|
||||||
*result=mapping;*count=(unsigned)n;return true;
|
free(mapping);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*result = mapping;
|
||||||
|
*count = (unsigned)n;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+34
-13
@@ -6,29 +6,50 @@ static uint64_t next_layout_check,last_layout_check;
|
|||||||
static unsigned layout_updates, layout_probe_generation;
|
static unsigned layout_updates, layout_probe_generation;
|
||||||
static char layout_error[128];
|
static char layout_error[128];
|
||||||
static void layout_refresh_reset(uint64_t due) {
|
static void layout_refresh_reset(uint64_t due) {
|
||||||
probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;next_layout_check=due;
|
probe_close(&layout_probe);
|
||||||
|
layout_probe.stage = TEST_IDLE;
|
||||||
|
next_layout_check = due;
|
||||||
}
|
}
|
||||||
static bool layout_mapping_changed(const LedBlend *next,unsigned count){
|
static bool layout_mapping_changed(const ReceiverLayoutContext *layout, const LedBlend *next,
|
||||||
if(count!=led_count || !led_blends)return true;
|
unsigned count) {
|
||||||
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)return true;
|
if (count != layout->count || !layout->blends)
|
||||||
|
return true;
|
||||||
|
for (unsigned i = 0; i < count; i++)
|
||||||
|
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;
|
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,
|
||||||
if(!running){layout_refresh_reset(now+LAYOUT_REFRESH_MS);return;}
|
ReceiverLayoutContext *layout) {
|
||||||
|
if (!running) {
|
||||||
|
layout_refresh_reset(now + LAYOUT_REFRESH_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
probe_tick(&layout_probe, json, now);
|
probe_tick(&layout_probe, json, now);
|
||||||
if (layout_probe.stage == TEST_DONE) {
|
if (layout_probe.stage == TEST_DONE) {
|
||||||
if (layout_probe_generation == calibration_generation) {
|
if (layout_probe_generation == calibration_generation) {
|
||||||
if(layout_mapping_changed(layout_probe.mapping,layout_probe.leds)){
|
if (layout_mapping_changed(layout, layout_probe.mapping, layout_probe.leds)) {
|
||||||
free(led_blends);led_blends=layout_probe.mapping;layout_probe.mapping=NULL;led_count=layout_probe.leds;layout_updates++;
|
free(layout->blends);
|
||||||
|
layout->blends = layout_probe.mapping;
|
||||||
|
layout_probe.mapping = NULL;
|
||||||
|
layout->count = layout_probe.leds;
|
||||||
|
layout_updates++;
|
||||||
}
|
}
|
||||||
last_layout_check=now;layout_error[0]=0;
|
last_layout_check = now;
|
||||||
}else next_layout_check=now;
|
layout_error[0] = 0;
|
||||||
probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;
|
} else
|
||||||
|
next_layout_check = now;
|
||||||
|
probe_close(&layout_probe);
|
||||||
|
layout_probe.stage = TEST_IDLE;
|
||||||
} else if (layout_probe.stage == TEST_FAILED) {
|
} else if (layout_probe.stage == TEST_FAILED) {
|
||||||
snprintf(layout_error,sizeof(layout_error),"%s",layout_probe.error);layout_probe.stage=TEST_IDLE;
|
snprintf(layout_error, sizeof(layout_error), "%s", layout_probe.error);
|
||||||
|
layout_probe.stage = TEST_IDLE;
|
||||||
}
|
}
|
||||||
if (!probe_busy(&layout_probe) && now >= next_layout_check) {
|
if (!probe_busy(&layout_probe) && now >= next_layout_check) {
|
||||||
layout_probe_generation=calibration_generation;probe_begin(&layout_probe,&target,now);next_layout_check=now+LAYOUT_REFRESH_MS;
|
layout_probe_generation = calibration_generation;
|
||||||
|
probe_begin(&layout_probe, &target, now);
|
||||||
|
next_layout_check = now + LAYOUT_REFRESH_MS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+105
-22
@@ -5,43 +5,126 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void *lib;void *(*parse)(const char*);int (*get)(const void*,const char*,void**);
|
void *lib;
|
||||||
int (*type)(const void*);size_t (*length)(const void*);void *(*at)(const void*,size_t);
|
void *(*parse)(const char *);
|
||||||
const char *(*string)(const void*);int (*boolean)(const void*);int (*put)(void*);
|
int (*get)(const void *, const char *, void **);
|
||||||
|
int (*type)(const void *);
|
||||||
|
size_t (*length)(const void *);
|
||||||
|
void *(*at)(const void *, size_t);
|
||||||
|
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;
|
} PlaybackJson;
|
||||||
static inline bool playback_json_init(PlaybackJson *j) {
|
static inline bool playback_json_init(PlaybackJson *j) {
|
||||||
j->lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!j->lib)return false;
|
j->lib = dlopen("libjson-c.so.5", RTLD_NOW);
|
||||||
j->parse=dlsym(j->lib,"json_tokener_parse");j->get=dlsym(j->lib,"json_object_object_get_ex");
|
if (!j->lib)
|
||||||
j->type=dlsym(j->lib,"json_object_get_type");j->length=dlsym(j->lib,"json_object_array_length");
|
return false;
|
||||||
j->at=dlsym(j->lib,"json_object_array_get_idx");j->string=dlsym(j->lib,"json_object_get_string");
|
j->parse = dlsym(j->lib, "json_tokener_parse");
|
||||||
j->boolean=dlsym(j->lib,"json_object_get_boolean");j->put=dlsym(j->lib,"json_object_put");
|
j->get = dlsym(j->lib, "json_object_object_get_ex");
|
||||||
if(!j->parse||!j->get||!j->type||!j->length||!j->at||!j->string||!j->boolean||!j->put){dlclose(j->lib);j->lib=NULL;return false;}return true;
|
j->type = dlsym(j->lib, "json_object_get_type");
|
||||||
|
j->length = dlsym(j->lib, "json_object_array_length");
|
||||||
|
j->at = dlsym(j->lib, "json_object_array_get_idx");
|
||||||
|
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->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;
|
||||||
|
}
|
||||||
|
static inline bool playback_true(PlaybackJson *j, void *o, const char *key) {
|
||||||
|
void *v = playback_field(j, o, key);
|
||||||
|
return v && j->type(v) == 1 && j->boolean(v);
|
||||||
|
}
|
||||||
|
static inline bool playback_is(PlaybackJson *j, void *o, const char *key, const char *value) {
|
||||||
|
void *v = playback_field(j, o, key);
|
||||||
|
return v && j->type(v) == 6 && strcmp(j->string(v), value) == 0;
|
||||||
}
|
}
|
||||||
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;}
|
|
||||||
static inline bool playback_true(PlaybackJson *j,void *o,const char *key){void *v=playback_field(j,o,key);return v && j->type(v)==1 && j->boolean(v);}
|
|
||||||
static inline bool playback_is(PlaybackJson *j,void *o,const char *key,const char *value){void *v=playback_field(j,o,key);return v && j->type(v)==6 && strcmp(j->string(v),value)==0;}
|
|
||||||
typedef enum { PLAYBACK_UNKNOWN, PLAYBACK_CLEAR, PLAYBACK_PROTECTED } PlaybackProtection;
|
typedef enum { PLAYBACK_UNKNOWN, PLAYBACK_CLEAR, PLAYBACK_PROTECTED } PlaybackProtection;
|
||||||
/* G4 resource-state classification, not black-frame or app-name guessing.
|
/* G4 resource-state classification, not black-frame or app-name guessing.
|
||||||
* Any active Secure Video Path wins. Only a foreground media decoder without
|
* Any active Secure Video Path wins. Only a foreground media decoder without
|
||||||
* a secure resource is eligible; HDMI, idle and malformed replies are unknown. */
|
* a secure resource is eligible; HDMI, idle and malformed replies are unknown. */
|
||||||
static inline PlaybackProtection playback_classify(PlaybackJson *j, const char *text) {
|
static inline PlaybackProtection playback_classify(PlaybackJson *j, const char *text) {
|
||||||
void *root=j->parse(text);if(!root)return PLAYBACK_UNKNOWN;
|
void *root = j->parse(text);
|
||||||
|
if (!root)
|
||||||
|
return PLAYBACK_UNKNOWN;
|
||||||
bool clear = false, invalid = j->type(root) != 5, secure = false;
|
bool clear = false, invalid = j->type(root) != 5, secure = false;
|
||||||
size_t n = invalid ? 0 : j->length(root);
|
size_t n = invalid ? 0 : j->length(root);
|
||||||
for (size_t i = 0; i < n; i++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
void *p=j->at(root,i);if(!p || j->type(p)!=4){invalid=true;continue;}
|
void *p = j->at(root, i);
|
||||||
void *resources=playback_field(j,p,"resource");if(!resources)resources=playback_field(j,p,"resources");
|
if (!p || j->type(p) != 4) {
|
||||||
if(!resources || j->type(resources)!=5){invalid=true;continue;}
|
invalid = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
void *resources = playback_field(j, p, "resource");
|
||||||
|
if (!resources)
|
||||||
|
resources = playback_field(j, p, "resources");
|
||||||
|
if (!resources || j->type(resources) != 5) {
|
||||||
|
invalid = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
bool decoder = false;
|
bool decoder = false;
|
||||||
for (size_t k = 0; k < j->length(resources); k++) {
|
for (size_t k = 0; k < j->length(resources); k++) {
|
||||||
void *resource=j->at(resources,k);void *name=playback_field(j,resource,"resource");
|
void *resource = j->at(resources, k);
|
||||||
if(!name || j->type(name)!=6){invalid=true;continue;}
|
void *name = playback_field(j, resource, "resource");
|
||||||
|
if (!name || j->type(name) != 6) {
|
||||||
|
invalid = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const char *s = j->string(name);
|
const char *s = j->string(name);
|
||||||
if(strcmp(s,"SVP_CPB")==0)secure=true;
|
if (strcmp(s, "SVP_CPB") == 0)
|
||||||
if(strcmp(s,"VDEC")==0)decoder=true;
|
secure = true;
|
||||||
|
if (strcmp(s, "VDEC") == 0)
|
||||||
|
decoder = true;
|
||||||
}
|
}
|
||||||
if(decoder && playback_true(j,p,"is_foreground") && playback_is(j,p,"type","media"))clear=true;
|
if (decoder && playback_true(j, p, "is_foreground") && playback_is(j, p, "type", "media"))
|
||||||
|
clear = true;
|
||||||
}
|
}
|
||||||
j->put(root);return secure?PLAYBACK_PROTECTED:clear&&!invalid?PLAYBACK_CLEAR:PLAYBACK_UNKNOWN;
|
j->put(root);
|
||||||
|
return secure ? PLAYBACK_PROTECTED : clear && !invalid ? PLAYBACK_CLEAR : PLAYBACK_UNKNOWN;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+46
-13
@@ -7,28 +7,61 @@
|
|||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include "histogram.h"
|
#include "histogram.h"
|
||||||
/* Process-local diagnostic sink. It changes logging only, not capture policy. */
|
/* Process-local diagnostic sink. It changes logging only, not capture policy. */
|
||||||
void printLog(const char *context,int level,const char *message_id,
|
void printLog(const char *context, int level, const char *message_id, const char *file,
|
||||||
const char *file,const char *function,int line,const char *format,...) {
|
const char *function, int line, const char *format, ...) {
|
||||||
(void)context;(void)level;(void)message_id;(void)file;
|
(void)context;
|
||||||
|
(void)level;
|
||||||
|
(void)message_id;
|
||||||
|
(void)file;
|
||||||
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
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);
|
if (format) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vfprintf(stderr, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
fputc('\n', stderr);
|
||||||
|
}
|
||||||
|
static bool policy(void) {
|
||||||
|
fprintf(stderr, "Capture revoked by LG policy\n");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
static bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; }
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
void *lib = dlopen("libvtcapture.so.1", RTLD_NOW);
|
void *lib = dlopen("libvtcapture.so.1", RTLD_NOW);
|
||||||
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;}
|
if (!lib) {
|
||||||
|
fprintf(stderr, "dlopen: %s\n", dlerror());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
void *(*init)(bool (*)(void)) = dlsym(lib, "vtCapture_initHistogram");
|
void *(*init)(bool (*)(void)) = dlsym(lib, "vtCapture_initHistogram");
|
||||||
int (*preset)(void *, int) = dlsym(lib, "vtCapture_setPresetHistogram");
|
int (*preset)(void *, int) = dlsym(lib, "vtCapture_setPresetHistogram");
|
||||||
int (*get)(void *, Histogram *) = dlsym(lib, "vtCapture_getHistogram");
|
int (*get)(void *, Histogram *) = dlsym(lib, "vtCapture_getHistogram");
|
||||||
int (*finish)(void *) = dlsym(lib, "vtCapture_finalizeHistogram");
|
int (*finish)(void *) = dlsym(lib, "vtCapture_finalizeHistogram");
|
||||||
if(!init||!preset||!get||!finish)return 2;
|
if (!init || !preset || !get || !finish)
|
||||||
void *handle=init(policy); fprintf(stderr,"handle=%p\n",handle);
|
return 2;
|
||||||
if(!handle)return 3;
|
void *handle = init(policy);
|
||||||
int rc=preset(handle,0);fprintf(stderr,"preset=%d\n",rc);
|
fprintf(stderr, "handle=%p\n", handle);
|
||||||
|
if (!handle)
|
||||||
|
return 3;
|
||||||
|
int rc = preset(handle, 0);
|
||||||
|
fprintf(stderr, "preset=%d\n", rc);
|
||||||
Histogram hist[16];
|
Histogram hist[16];
|
||||||
for (int frame = 0; rc == 0 && frame < 3; frame++) {
|
for (int frame = 0; rc == 0 && frame < 3; frame++) {
|
||||||
usleep(100000); rc=get(handle,hist); printf("frame=%d rc=%d\n",frame,rc);
|
usleep(100000);
|
||||||
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("");}
|
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]));
|
||||||
}
|
}
|
||||||
fprintf(stderr,"finalize=%d\n",finish(handle)); return rc?4:0;
|
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
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
#ifndef HYPERDRM_RECEIVER_STREAM_H
|
||||||
|
#define HYPERDRM_RECEIVER_STREAM_H
|
||||||
|
#ifndef RECEIVER_LAYOUT_REVISION
|
||||||
|
#define RECEIVER_LAYOUT_REVISION 0
|
||||||
|
#endif
|
||||||
|
/* Independent raw LED subscription: bounded, nonblocking, before receiver
|
||||||
|
* color correction. Old result and newer data envelopes are both accepted. */
|
||||||
|
#include "runtime_states.h"
|
||||||
|
typedef struct {
|
||||||
|
int fd;
|
||||||
|
StreamStage stage;
|
||||||
|
uint64_t deadline, retry_at, updated;
|
||||||
|
unsigned sequence, count, revision, layout_revision;
|
||||||
|
LedBlend *mapping;
|
||||||
|
char *data, *request;
|
||||||
|
size_t used, sent, length;
|
||||||
|
unsigned char colors[16][3];
|
||||||
|
bool valid;
|
||||||
|
} ReceiverStream;
|
||||||
|
static ReceiverStream stream = {.fd = -1};
|
||||||
|
static void stream_close(uint64_t now) {
|
||||||
|
if (stream.fd >= 0)
|
||||||
|
close(stream.fd);
|
||||||
|
free(stream.mapping);
|
||||||
|
free(stream.data);
|
||||||
|
free(stream.request);
|
||||||
|
stream = (ReceiverStream){.fd = -1, .retry_at = now + 5000};
|
||||||
|
}
|
||||||
|
static bool stream_queue(const char *line) {
|
||||||
|
if (stream.fd < 0 || stream.request)
|
||||||
|
return false;
|
||||||
|
stream.request = strdup(line);
|
||||||
|
if (!stream.request)
|
||||||
|
return false;
|
||||||
|
stream.length = strlen(line);
|
||||||
|
stream.sent = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static void stream_start(uint64_t now) {
|
||||||
|
stream.fd = target_open(&target);
|
||||||
|
if (stream.fd < 0) {
|
||||||
|
stream.retry_at = now + 5000;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.data = malloc(262145);
|
||||||
|
if (!stream.data) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.stage = STREAM_CONNECTING;
|
||||||
|
stream.deadline = now + 3000;
|
||||||
|
stream.revision = calibration_revision();
|
||||||
|
stream.layout_revision = RECEIVER_LAYOUT_REVISION;
|
||||||
|
}
|
||||||
|
static bool stream_colors(PlaybackJson *j, void *root, uint64_t now) {
|
||||||
|
void *body = playback_field(j, root, "result");
|
||||||
|
if (!body)
|
||||||
|
body = playback_field(j, root, "data");
|
||||||
|
void *leds = playback_field(j, body, "leds");
|
||||||
|
if (!leds || j->type(leds) != 5)
|
||||||
|
return false;
|
||||||
|
if (j->length(leds) != (size_t)stream.count * 3) {
|
||||||
|
stream.revision = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint64_t sums[16][3] = {{0}}, weights[16] = {0};
|
||||||
|
for (unsigned i = 0; i < stream.count; i++) {
|
||||||
|
unsigned values[3];
|
||||||
|
for (unsigned c = 0; c < 3; c++) {
|
||||||
|
void *v = j->at(leds, i * 3 + c);
|
||||||
|
if (!v || j->type(v) != 3)
|
||||||
|
return false;
|
||||||
|
char *end;
|
||||||
|
long n = strtol(j->string(v), &end, 10);
|
||||||
|
if (*end || n < 0 || n > 255)
|
||||||
|
return false;
|
||||||
|
values[c] = (unsigned)n;
|
||||||
|
}
|
||||||
|
LedBlend b = stream.mapping[i];
|
||||||
|
unsigned w[2] = {65535 - b.weight, b.weight}, r[2] = {b.a, b.b};
|
||||||
|
for (unsigned k = 0; k < 2; k++) {
|
||||||
|
weights[r[k]] += w[k];
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
sums[r[k]][c] += (uint64_t)values[c] * w[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsigned char mean[3] = {0};
|
||||||
|
uint64_t total = 0, all[3] = {0};
|
||||||
|
for (unsigned r = 0; r < 16; r++) {
|
||||||
|
total += weights[r];
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
all[c] += sums[r][c];
|
||||||
|
}
|
||||||
|
if (!total)
|
||||||
|
return false;
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
mean[c] = (unsigned char)(all[c] / total);
|
||||||
|
for (unsigned r = 0; r < 16; r++)
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
stream.colors[r][c] = weights[r] ? (unsigned char)(sums[r][c] / weights[r]) : mean[c];
|
||||||
|
stream.valid = true;
|
||||||
|
stream.updated = now;
|
||||||
|
stream.sequence++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) {
|
||||||
|
if (!enabled) {
|
||||||
|
if (stream.fd >= 0)
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stream.fd >= 0 && (stream.revision != calibration_revision() ||
|
||||||
|
stream.layout_revision != RECEIVER_LAYOUT_REVISION)) {
|
||||||
|
stream_close(now);
|
||||||
|
stream.retry_at = now;
|
||||||
|
}
|
||||||
|
if (stream.fd < 0) {
|
||||||
|
if (now >= stream.retry_at)
|
||||||
|
stream_start(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stream.stage < STREAM_SUBSCRIBED && now >= stream.deadline) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stream.stage == STREAM_CONNECTING) {
|
||||||
|
struct pollfd p = {stream.fd, POLLOUT, 0};
|
||||||
|
int error = 0;
|
||||||
|
socklen_t size = sizeof(error);
|
||||||
|
if (poll(&p, 1, 0) <= 0)
|
||||||
|
return;
|
||||||
|
if (getsockopt(stream.fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || error) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char line[384];
|
||||||
|
if (target.token[0]) {
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
|
||||||
|
target.token);
|
||||||
|
stream.stage = STREAM_AUTHENTICATING;
|
||||||
|
} else {
|
||||||
|
snprintf(line, sizeof(line), "{\"command\":\"serverinfo\"}\n");
|
||||||
|
stream.stage = STREAM_LAYOUT;
|
||||||
|
}
|
||||||
|
if (!stream_queue(line)) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stream.request) {
|
||||||
|
ssize_t n = send(stream.fd, stream.request + stream.sent, stream.length - stream.sent,
|
||||||
|
MSG_NOSIGNAL);
|
||||||
|
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||||
|
return;
|
||||||
|
if (n <= 0) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.sent += (size_t)n;
|
||||||
|
if (stream.sent == stream.length) {
|
||||||
|
free(stream.request);
|
||||||
|
stream.request = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Drain complete buffered replies even when recv would return EAGAIN.
|
||||||
|
* The per-tick budget applies independently of incoming network traffic. */
|
||||||
|
if (stream.used == 262144 && !strchr(stream.data, '\n')) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t available = 262144 - stream.used;
|
||||||
|
if (available > 65536)
|
||||||
|
available = 65536;
|
||||||
|
ssize_t n = recv(stream.fd, stream.data + stream.used, available, 0);
|
||||||
|
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n == 0 && available) {
|
||||||
|
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;
|
||||||
|
for (unsigned messages = 0; messages < 8; messages++) {
|
||||||
|
char *end = strchr(stream.data, '\n');
|
||||||
|
if (!end)
|
||||||
|
break;
|
||||||
|
*end = 0;
|
||||||
|
void *root = playback_parse_exact(j, stream.data);
|
||||||
|
if (stream.stage == STREAM_AUTHENTICATING) {
|
||||||
|
if (!playback_true(j, root, "success")) {
|
||||||
|
if (root)
|
||||||
|
j->put(root);
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.stage = STREAM_LAYOUT;
|
||||||
|
stream_queue("{\"command\":\"serverinfo\"}\n");
|
||||||
|
stream.deadline = now + 3000;
|
||||||
|
} else if (stream.stage == STREAM_LAYOUT) {
|
||||||
|
LedBlend *mapping = NULL;
|
||||||
|
unsigned count = 0;
|
||||||
|
if (!layout_from_json(stream.data, &mapping, &count)) {
|
||||||
|
if (root)
|
||||||
|
j->put(root);
|
||||||
|
stream_close(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.mapping = mapping;
|
||||||
|
stream.count = count;
|
||||||
|
stream.stage = STREAM_SUBSCRIBED;
|
||||||
|
stream_queue("{\"command\":\"ledcolors\",\"subcommand\":\"ledstream-start\","
|
||||||
|
"\"interval\":100}\n");
|
||||||
|
} else if (playback_is(j, root, "command", "ledcolors-ledstream-update"))
|
||||||
|
stream_colors(j, root, now);
|
||||||
|
if (root)
|
||||||
|
j->put(root);
|
||||||
|
size_t consumed = (size_t)(end - stream.data) + 1;
|
||||||
|
memmove(stream.data, stream.data + consumed, stream.used - consumed);
|
||||||
|
stream.used -= consumed;
|
||||||
|
stream.data[stream.used] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
+34
-10
@@ -5,26 +5,50 @@
|
|||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#define RETRY_DELAY_MS 5000
|
#define RETRY_DELAY_MS 5000
|
||||||
/* Continuous autostart retries and bounded manual source recovery. */
|
/* Continuous autostart retries and bounded manual source recovery. */
|
||||||
typedef struct {uint64_t due;unsigned attempts,quick_retries;bool automatic,paused,blocked;} RetryState;
|
typedef struct {
|
||||||
static void retry_reset(RetryState *state){*state=(RetryState){0};}
|
uint64_t due;
|
||||||
static void retry_pause(RetryState *state){retry_reset(state);state->paused=true;}
|
unsigned attempts, quick_retries;
|
||||||
|
bool automatic, paused, blocked;
|
||||||
|
} RetryState;
|
||||||
|
static void retry_reset(RetryState *state) {
|
||||||
|
*state = (RetryState){0};
|
||||||
|
}
|
||||||
|
static void retry_pause(RetryState *state) {
|
||||||
|
retry_reset(state);
|
||||||
|
state->paused = true;
|
||||||
|
}
|
||||||
static void retry_update(RetryState *state, bool enabled, bool running, uint64_t now) {
|
static void retry_update(RetryState *state, bool enabled, bool running, uint64_t now) {
|
||||||
bool previous = state->automatic;
|
bool previous = state->automatic;
|
||||||
state->automatic = enabled && !state->paused && !state->blocked;
|
state->automatic = enabled && !state->paused && !state->blocked;
|
||||||
if(previous && !state->automatic){state->due=0;state->attempts=0;}
|
if (previous && !state->automatic) {
|
||||||
if(state->automatic && !running && !state->due)state->due=now+RETRY_DELAY_MS;
|
state->due = 0;
|
||||||
|
state->attempts = 0;
|
||||||
|
}
|
||||||
|
if (state->automatic && !running && !state->due)
|
||||||
|
state->due = now + RETRY_DELAY_MS;
|
||||||
}
|
}
|
||||||
static bool retry_take(RetryState *state, uint64_t now) {
|
static bool retry_take(RetryState *state, uint64_t now) {
|
||||||
if(!state->due || now<state->due || state->paused || state->blocked)return false;
|
if (!state->due || now < state->due || state->paused || state->blocked)
|
||||||
state->due=0;if(state->attempts<UINT_MAX)state->attempts++;return true;
|
return false;
|
||||||
|
state->due = 0;
|
||||||
|
if (state->attempts < UINT_MAX)
|
||||||
|
state->attempts++;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
static void retry_failure(RetryState *state, uint64_t now) {
|
static void retry_failure(RetryState *state, uint64_t now) {
|
||||||
if (!state->paused && !state->blocked && (state->automatic || state->attempts < 6)) {
|
if (!state->paused && !state->blocked && (state->automatic || state->attempts < 6)) {
|
||||||
if(state->quick_retries){state->quick_retries--;state->due=now+400;}
|
if (state->quick_retries) {
|
||||||
else state->due=now+RETRY_DELAY_MS;
|
state->quick_retries--;
|
||||||
|
state->due = now + 400;
|
||||||
|
} else
|
||||||
|
state->due = now + RETRY_DELAY_MS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static inline void retry_source_transition(RetryState *state, uint64_t now) {
|
static inline void retry_source_transition(RetryState *state, uint64_t now) {
|
||||||
if(!state->paused && !state->blocked){state->attempts=0;state->quick_retries=2;state->due=now+200;}
|
if (!state->paused && !state->blocked) {
|
||||||
|
state->attempts = 0;
|
||||||
|
state->quick_retries = 2;
|
||||||
|
state->due = now + 200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef HYPERDRM_RUNTIME_STATES_H
|
||||||
|
#define HYPERDRM_RUNTIME_STATES_H
|
||||||
|
/* Connection and source transitions are independent state machines. */
|
||||||
|
typedef enum {
|
||||||
|
STREAM_DISCONNECTED,
|
||||||
|
STREAM_CONNECTING,
|
||||||
|
STREAM_AUTHENTICATING,
|
||||||
|
STREAM_LAYOUT,
|
||||||
|
STREAM_SUBSCRIBED
|
||||||
|
} StreamStage;
|
||||||
|
typedef enum { BRIDGE_IDLE, BRIDGE_HOLDING, BRIDGE_WAITING_FRAME, BRIDGE_FADING } BridgePhase;
|
||||||
|
#endif
|
||||||
+207
-259
@@ -17,6 +17,7 @@
|
|||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include "version.h"
|
||||||
#include "histogram.h"
|
#include "histogram.h"
|
||||||
#include "blackbars.h"
|
#include "blackbars.h"
|
||||||
#include "color_boost.h"
|
#include "color_boost.h"
|
||||||
@@ -27,322 +28,269 @@
|
|||||||
#include "target.h"
|
#include "target.h"
|
||||||
#include "spatial_image.h"
|
#include "spatial_image.h"
|
||||||
#include "diagnostics.h"
|
#include "diagnostics.h"
|
||||||
|
#include "continuity.h"
|
||||||
|
#include "service_context.h"
|
||||||
#define SERVICE "org.webosbrew.histogramhyperion.service"
|
#define SERVICE "org.webosbrew.histogramhyperion.service"
|
||||||
#define PRIORITY 149
|
#define PRIORITY 149
|
||||||
typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError;
|
typedef struct {
|
||||||
|
int code;
|
||||||
|
char *message;
|
||||||
|
const char *file;
|
||||||
|
int line;
|
||||||
|
const char *func;
|
||||||
|
void *padding;
|
||||||
|
unsigned long magic;
|
||||||
|
} LSError;
|
||||||
typedef bool (*Callback)(void *, void *, void *);
|
typedef bool (*Callback)(void *, void *, void *);
|
||||||
typedef struct {const char *name;Callback function;int flags;} Method;
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
Callback function;
|
||||||
|
int flags;
|
||||||
|
} Method;
|
||||||
static bool (*reply)(void *, void *, const char *, LSError *);
|
static bool (*reply)(void *, void *, const char *, LSError *);
|
||||||
static const char *(*payload)(void *);
|
static const char *(*payload)(void *);
|
||||||
static bool (*error_init)(LSError *);
|
static bool (*error_init)(LSError *);
|
||||||
static void (*error_free)(LSError *);
|
static void (*error_free)(LSError *);
|
||||||
static void *(*vt_init)(bool (*)(void));
|
static void *(*vt_init)(bool (*)(void));
|
||||||
static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *);
|
static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *);
|
||||||
static void *capture;
|
static CaptureContext capture_ctx = {.pending_letterbox = -1};
|
||||||
static char lg_diagnostic[256];
|
static ReceiverLayoutContext receiver_layout;
|
||||||
|
static CalibrationSessionContext calibration_ctx;
|
||||||
/* Process-local LG diagnostics; no changes to the loaded LG library. */
|
/* Process-local LG diagnostics; no changes to the loaded LG library. */
|
||||||
void printLog(const char *context,int level,const char *message_id,
|
void printLog(const char *context, int level, const char *message_id, const char *file,
|
||||||
const char *file,const char *function,int line,const char *format,...) {
|
const char *function, int line, const char *format, ...) {
|
||||||
(void)context;(void)message_id;(void)file;
|
(void)context;
|
||||||
if(capture && level>3)return;
|
(void)message_id;
|
||||||
if(format){va_list args;va_start(args,format);vsnprintf(lg_diagnostic,sizeof(lg_diagnostic),format,args);va_end(args);}
|
(void)file;
|
||||||
|
if (capture_ctx.handle && level > 3)
|
||||||
|
return;
|
||||||
|
if (format) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(capture_ctx.diagnostic, sizeof(capture_ctx.diagnostic), format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
||||||
if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);}
|
if (format) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vfprintf(stderr, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
fputc('\n', stderr);
|
fputc('\n', stderr);
|
||||||
}
|
}
|
||||||
static int output=-1;
|
#define CALIBRATION_ACTIVE (calibration_ctx.until > milliseconds())
|
||||||
static unsigned long frames;
|
|
||||||
static unsigned char rgb[16][3];
|
|
||||||
static unsigned char sent_rgb[3];
|
|
||||||
static unsigned black_percent[16];
|
|
||||||
static unsigned capture_preset;
|
|
||||||
static unsigned letterbox_mode,settling;
|
|
||||||
static int pending_letterbox=-1;
|
|
||||||
static uint64_t candidate_since,next_border_probe;
|
|
||||||
static bool color_valid;
|
|
||||||
static unsigned led_count;
|
|
||||||
static LedBlend *led_blends;
|
|
||||||
static bool interpolation_enabled;
|
|
||||||
static RetryState retries;
|
static RetryState retries;
|
||||||
static unsigned capture_recoveries;
|
static uint64_t milliseconds(void) {
|
||||||
static uint64_t recovery_started,last_recovery_ms,last_output_at;
|
struct timespec t;
|
||||||
static uint64_t milliseconds(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000+t.tv_nsec/1000000;}
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||||
|
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
static char last_error[256];
|
static char last_error[256];
|
||||||
static volatile sig_atomic_t revoked,quitting;
|
static volatile sig_atomic_t quitting;
|
||||||
static int guard_pipe = -1;
|
static int guard_pipe = -1;
|
||||||
static pid_t guard_pid = -1;
|
static pid_t guard_pid = -1;
|
||||||
|
#ifndef DEV_MODE
|
||||||
#define DEV_MODE "/var/luna/preferences/vt_devmode"
|
#define DEV_MODE "/var/luna/preferences/vt_devmode"
|
||||||
|
#endif
|
||||||
/* LG's developer gate is enabled only during initialization. A separate
|
/* LG's developer gate is enabled only during initialization. A separate
|
||||||
* guardian removes our own marker even if this process aborts or is killed.
|
* guardian removes our own marker even if this process aborts or is killed.
|
||||||
* An existing marker is preserved, as are files replaced by another owner. */
|
* An existing marker is preserved, as are files replaced by another owner. */
|
||||||
static bool enter_developer_mode(void) {
|
static bool enter_developer_mode(void) {
|
||||||
int marker = open(DEV_MODE, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
|
int marker = open(DEV_MODE, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
|
||||||
if(marker<0)return errno==EEXIST;
|
if (marker < 0)
|
||||||
|
return errno == EEXIST;
|
||||||
struct stat owned;
|
struct stat owned;
|
||||||
if(fstat(marker,&owned)!=0){close(marker);unlink(DEV_MODE);return false;}
|
if (fstat(marker, &owned) != 0) {
|
||||||
close(marker);int p[2];
|
close(marker);
|
||||||
if(pipe(p)!=0){unlink(DEV_MODE);return false;}
|
unlink(DEV_MODE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close(marker);
|
||||||
|
int p[2];
|
||||||
|
if (pipe(p) != 0) {
|
||||||
|
unlink(DEV_MODE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if(child<0){close(p[0]);close(p[1]);unlink(DEV_MODE);return false;}
|
if (child < 0) {
|
||||||
|
close(p[0]);
|
||||||
|
close(p[1]);
|
||||||
|
unlink(DEV_MODE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
close(p[1]);struct pollfd fd={p[0],POLLIN|POLLHUP,0};
|
close(p[1]);
|
||||||
|
struct pollfd fd = {p[0], POLLIN | POLLHUP, 0};
|
||||||
/* Bounded cleanup also covers an indefinitely blocked initialization. */
|
/* Bounded cleanup also covers an indefinitely blocked initialization. */
|
||||||
poll(&fd,1,10000);close(p[0]);struct stat current;
|
poll(&fd, 1, 10000);
|
||||||
|
close(p[0]);
|
||||||
|
struct stat current;
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
if(lstat(DEV_MODE,¤t)==0 && current.st_dev==owned.st_dev && current.st_ino==owned.st_ino)rc=unlink(DEV_MODE);
|
if (lstat(DEV_MODE, ¤t) == 0 && current.st_dev == owned.st_dev &&
|
||||||
|
current.st_ino == owned.st_ino)
|
||||||
|
rc = unlink(DEV_MODE);
|
||||||
_exit(rc == 0 ? 0 : 1);
|
_exit(rc == 0 ? 0 : 1);
|
||||||
}
|
}
|
||||||
close(p[0]);guard_pipe=p[1];guard_pid=child;return true;
|
close(p[0]);
|
||||||
|
guard_pipe = p[1];
|
||||||
|
guard_pid = child;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
static bool leave_developer_mode(void) {
|
static bool leave_developer_mode(void) {
|
||||||
if(guard_pipe<0)return true;
|
if (guard_pipe < 0)
|
||||||
close(guard_pipe);guard_pipe=-1;int status;
|
return true;
|
||||||
while(waitpid(guard_pid,&status,0)<0){if(errno!=EINTR)return false;}
|
close(guard_pipe);
|
||||||
guard_pid=-1;return WIFEXITED(status)&&WEXITSTATUS(status)==0;
|
guard_pipe = -1;
|
||||||
|
int status;
|
||||||
|
while (waitpid(guard_pid, &status, 0) < 0) {
|
||||||
|
if (errno != EINTR)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
static void quit_signal(int s){(void)s;quitting=1;}
|
guard_pid = -1;
|
||||||
static bool policy(void){revoked=1;return true;}
|
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||||
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 read_layout(void){
|
static void quit_signal(int s) {
|
||||||
const size_t capacity=2*1024*1024;char *data=malloc(capacity+1);if(!data)return false;
|
(void)s;
|
||||||
bool complete=false;size_t used=0;
|
quitting = 1;
|
||||||
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);
|
static bool policy(void) {
|
||||||
if(ok){free(led_blends);led_blends=mapping;led_count=count;}free(data);interpolation_enabled=ok && calibration_effective()!=CAL_AVERAGE;return ok;
|
capture_ctx.revoked = 1;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
static bool target_login(void){
|
static void set_error(const char *s) {
|
||||||
if(!target.token[0])return true;
|
snprintf(last_error, sizeof(last_error), "%s", s);
|
||||||
char request[384],answer[4096];size_t used=0;
|
fprintf(stderr, "%s\n", s);
|
||||||
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 layout_refresh_reset(uint64_t due);
|
||||||
static void release_capture(bool clear_output) {
|
static void release_capture(bool clear_output) {
|
||||||
layout_refresh_reset(milliseconds() + 5000);
|
layout_refresh_reset(milliseconds() + 5000);
|
||||||
free(led_blends);led_blends=NULL;led_count=0;interpolation_enabled=false;
|
free(receiver_layout.blends);
|
||||||
color_valid=false;memset(rgb,0,sizeof(rgb));memset(sent_rgb,0,sizeof(sent_rgb));
|
receiver_layout.blends = NULL;
|
||||||
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;}
|
receiver_layout.count = 0;
|
||||||
if(capture){vt_finish(capture);capture=NULL;}
|
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) {
|
||||||
|
release_capture(true);
|
||||||
|
}
|
||||||
|
static bool respond(void *h, void *m, const char *s) {
|
||||||
|
if (!h || !m)
|
||||||
|
return true;
|
||||||
|
LSError e;
|
||||||
|
error_init(&e);
|
||||||
|
bool ok = reply(h, m, s, &e);
|
||||||
|
error_free(&e);
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
static void stop_capture(void){release_capture(true);}
|
|
||||||
static bool respond(void *h,void *m,const char *s){if(!h||!m)return true;LSError e;error_init(&e);bool ok=reply(h,m,s,&e);error_free(&e);return ok;}
|
|
||||||
#include "handover.h"
|
#include "handover.h"
|
||||||
#include "layout_refresh.h"
|
#include "layout_refresh.h"
|
||||||
static bool model_busy;
|
#define RECEIVER_LAYOUT_REVISION layout_updates
|
||||||
static unsigned long model_token;
|
#include "receiver_stream.h"
|
||||||
static uint64_t model_deadline,next_model_probe;
|
#include "source_bridge.h"
|
||||||
static bool model_reply(void *h,void *m,void *ctx){
|
#include "service_api.h"
|
||||||
(void)h;(void)ctx;model_busy=false;model_token=0;
|
#include "dark_color.h"
|
||||||
void *root=playback_json.parse(payload(m)),*model=playback_field(&playback_json,root,"modelName");
|
#include "capture_loop.h"
|
||||||
if(playback_true(&playback_json,root,"returnValue") && model && playback_json.type(model)==6 && calibration_model_valid(playback_json.string(model))){
|
static void *symbol(void *lib, const char *name) {
|
||||||
snprintf(tv_model,sizeof(tv_model),"%s",playback_json.string(model));calibration_generation++;layout_refresh_reset(milliseconds());
|
void *s = dlsym(lib, name);
|
||||||
|
if (!s) {
|
||||||
|
fprintf(stderr, "Missing symbol %s\n", name);
|
||||||
|
exit(2);
|
||||||
}
|
}
|
||||||
if(root)playback_json.put(root);
|
return s;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
static void model_tick(uint64_t now){
|
|
||||||
if(model_busy && now>=model_deadline){LSError e;error_init(&e);cancel_call(service_handle,model_token,&e);error_free(&e);model_busy=false;model_token=0;}
|
|
||||||
if(!tv_model[0] && !model_busy && now>=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);error_free(&e);model_deadline=now+3000;next_model_probe=now+60000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static bool set_calibration(void *h,void *m,void *ctx){
|
|
||||||
(void)ctx;Calibration next=calibration;
|
|
||||||
if(!playback_json.lib || !calibration_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Select a valid profile; custom profiles need a model and 2-16 sorted rim points (position 0-4, region 0-15)\"}");
|
|
||||||
if(!calibration_save(CALIBRATION_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot save calibration profile\"}");
|
|
||||||
calibration=next;calibration_generation++;layout_refresh_reset(milliseconds());
|
|
||||||
return respond(h,m,"{\"returnValue\":true}");
|
|
||||||
}
|
|
||||||
static bool start_failure(void *h,void *m,const char *message){
|
|
||||||
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
|
|
||||||
char json[512];snprintf(json,sizeof(json),"{\"returnValue\":false,\"errorText\":\"%s\",\"retryScheduled\":%s,\"retryBlocked\":%s}",message,retries.due?"true":"false",retries.blocked?"true":"false");
|
|
||||||
return respond(h,m,json);
|
|
||||||
}
|
|
||||||
static bool start(void *h,void *m,void *ctx){
|
|
||||||
(void)ctx;const char *p=m?payload(m):NULL;
|
|
||||||
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use settings to configure the receiver\"}");
|
|
||||||
if(m)retry_reset(&retries);
|
|
||||||
if(piccap_owned)return respond(h,m,"{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
|
|
||||||
if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}");
|
|
||||||
frames=0;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");}
|
|
||||||
if(!enter_developer_mode()){stop_capture();set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return start_failure(h,m,"Root access is required for temporary LG developer initialization");}
|
|
||||||
capture=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){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){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;
|
|
||||||
retries.due=0;retries.attempts=0;retries.quick_retries=0;
|
|
||||||
if(recovery_started){last_recovery_ms=milliseconds()-recovery_started;recovery_started=0;}
|
|
||||||
return respond(h,m,"{\"returnValue\":true,\"running\":true}");
|
|
||||||
}
|
|
||||||
static bool stop(void *h,void *m,void *ctx){(void)ctx;retry_pause(&retries);stop_capture();return respond(h,m,"{\"returnValue\":true,\"running\":false}");}
|
|
||||||
static bool autostart(void *h,void *m,void *ctx){
|
|
||||||
(void)ctx;const char *p=m?payload(m):NULL;bool enabled;
|
|
||||||
if(p && strcmp(p,"{\"enabled\":true}")==0)enabled=true;
|
|
||||||
else if(p && strcmp(p,"{\"enabled\":false}")==0)enabled=false;
|
|
||||||
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
|
||||||
if(geteuid()!=0 || !set_boot(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Autostart could not be saved. Check own service root elevation and boot hook.\"}");
|
|
||||||
if(enabled)retry_reset(&retries);
|
|
||||||
retry_update(&retries,enabled,capture!=NULL,milliseconds());
|
|
||||||
return respond(h,m,enabled?"{\"returnValue\":true,\"autostart\":true}":"{\"returnValue\":true,\"autostart\":false}");
|
|
||||||
}
|
|
||||||
static bool handover(void *h,void *m,void *ctx){
|
|
||||||
(void)ctx;const char *p=payload(m);bool enabled;
|
|
||||||
if(strcmp(p,"{\"enabled\":true}")==0)enabled=true;
|
|
||||||
else if(strcmp(p,"{\"enabled\":false}")==0)enabled=false;
|
|
||||||
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
|
||||||
if(!playback_json.lib || geteuid()!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Root service and JSON-C are required\"}");
|
|
||||||
if(enabled && !piccap_installed())return respond(h,m,"{\"returnValue\":false,\"errorText\":\"PicCap is not installed\"}");
|
|
||||||
if(!handover_save(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save PicCap handover preference\"}");
|
|
||||||
handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=0;next_media_probe=0;
|
|
||||||
return respond(h,m,"{\"returnValue\":true}");
|
|
||||||
}
|
|
||||||
static bool settings(void *h,void *m,void *ctx){
|
|
||||||
(void)ctx;TargetConfig next=target;
|
|
||||||
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();test_close();connection_test.stage=TEST_IDLE;target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=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;
|
|
||||||
uint64_t now=milliseconds();
|
|
||||||
int used=snprintf(json,sizeof(json),"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\",\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",calibration_key(calibration.kind),calibration_key(calibration_effective()),tv_model,calibration.model);
|
|
||||||
for(unsigned i=0;i<calibration.count;i++)used+=snprintf(json+used,sizeof(json)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",calibration.points[i].position,calibration.points[i].region);
|
|
||||||
used+=snprintf(json+used,sizeof(json)-used,"],\"layoutUpdates\":%u,\"lastLayoutCheckAgeMs\":%lld,\"layoutError\":\"%s\",\"piccapFallbacks\":%u,\"piccapCooldownMs\":%llu,",layout_updates,last_layout_check?(long long)(now-last_layout_check):-1,layout_error,piccap_fallbacks,(unsigned long long)(handover_cooldown>now?handover_cooldown-now:0));
|
|
||||||
used+=snprintf(json+used,sizeof(json)-used,"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\",\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",target.piccap_port,test_stage_name(),connection_test.leds,(unsigned long long)connection_test.elapsed,connection_test.error,last_output_at?(long long)(now-last_output_at):-1,(unsigned long long)(retries.due>now?retries.due-now:0),capture_source==SOURCE_PICCAP_STARTING?"Starting PicCap":capture_source==SOURCE_PICCAP_STOPPING?"Stopping PicCap":piccap_owned?"PicCap":capture?"Histogram":"Stopped",piccap_error);
|
|
||||||
used+=snprintf(json+used,sizeof(json)-used,"\"version\":\"0.10.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]);
|
|
||||||
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",boot_enabled()?"true":"false",retries.due?"true":"false",retries.paused?"true":"false",retries.blocked?"true":"false",retries.attempts,piccap_installed()?"true":"false",handover_enabled?"true":"false",piccap_connected?"true":"false",piccap_owned?"PicCap":"HyperDRM",protection==PLAYBACK_PROTECTED?"protected":protection==PLAYBACK_CLEAR?"non-secure decoder":"unknown",capture&&letterbox_mode?"true":"false");return respond(h,m,json);
|
|
||||||
}
|
|
||||||
static int tick(void *unused){
|
|
||||||
(void)unused;
|
|
||||||
uint64_t now=milliseconds();
|
|
||||||
model_tick(now);test_tick(&playback_json,now);layout_refresh_tick(&playback_json,now,capture || capture_source==SOURCE_PICCAP);
|
|
||||||
if(handover_tick())return 1;
|
|
||||||
if(!capture){
|
|
||||||
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
|
|
||||||
if(retry_take(&retries,milliseconds())){
|
|
||||||
start(NULL,NULL,NULL);
|
|
||||||
if(!capture)retry_failure(&retries,milliseconds());
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if(revoked){set_error("LG revoked capture; recovering after source transition");capture_recoveries++;recovery_started=milliseconds();release_capture(false);retry_source_transition(&retries,milliseconds());return 1;}
|
|
||||||
if(settling){settling--;return 1;}
|
|
||||||
if(letterbox_mode && capture_preset==1 && milliseconds()>=next_border_probe){
|
|
||||||
if(vt_preset(capture,0)!=0){set_error("Border probe preset failed");stop_capture();return 1;}
|
|
||||||
capture_preset=0;settling=2;return 1;
|
|
||||||
}
|
|
||||||
Histogram hist[16]={0};int rc=vt_get(capture,hist);
|
|
||||||
if(rc){set_error("LG histogram read failed; streaming stopped");stop_capture();return 1;}
|
|
||||||
unsigned aggregate[3]={0};
|
|
||||||
for(int i=0;i<16;i++){
|
|
||||||
black_percent[i]=100;
|
|
||||||
for(int c=0;c<3;c++){
|
|
||||||
uint64_t total=0,black=0;
|
|
||||||
for(int b=0;b<256;b++){total+=hist[i].bins[c][b];if(b<=12)black+=hist[i].bins[c][b];}
|
|
||||||
unsigned fraction=total?(unsigned)(100*black/total):0;
|
|
||||||
if(fraction<black_percent[i])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];
|
|
||||||
}
|
|
||||||
if(capture_preset==0){
|
|
||||||
int candidate=letterbox_candidate(black_percent,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){set_error("Letterbox preset failed");stop_capture();return 1;}
|
|
||||||
capture_preset=1;settling=2;next_border_probe=now+2000;return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(int c=0;c<3;c++)sent_rgb[c]=(unsigned char)(aggregate[c]/16);
|
|
||||||
unsigned char boosted_mean[3];boost_color(sent_rgb,boosted_mean);memcpy(sent_rgb,boosted_mean,3);
|
|
||||||
unsigned char mapped_rgb[16][3];calibration_colors(rgb,mapped_rgb);
|
|
||||||
interpolation_enabled=led_count && calibration_effective()!=CAL_AVERAGE;
|
|
||||||
bool image_output=strcmp(target.receiver,"HyperHDR")==0;
|
|
||||||
size_t capacity=256+(size_t)led_count*12;char *line=image_output?spatial_image_command(mapped_rgb,PRIORITY):malloc(capacity);
|
|
||||||
if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;}
|
|
||||||
if(!image_output){
|
|
||||||
size_t written=(size_t)snprintf(line,capacity,"{\"command\":\"color\",\"priority\":%d,\"duration\":1500,\"origin\":\"HyperDRM\",\"color\":[",PRIORITY);
|
|
||||||
for(unsigned i=0;i<led_count;i++){
|
|
||||||
unsigned char interpolated[3],boosted[3];LedBlend blend=led_blends[i];
|
|
||||||
blend_color(mapped_rgb[blend.a],mapped_rgb[blend.b],blend.weight,interpolated);boost_color(interpolated,boosted);
|
|
||||||
written+=(size_t)snprintf(line+written,capacity-written,"%s%u,%u,%u",i?",":"",boosted[0],boosted[1],boosted[2]);
|
|
||||||
}
|
|
||||||
snprintf(line+written,capacity-written,"]}\n");
|
|
||||||
}
|
|
||||||
int failed=send_line(line);free(line);
|
|
||||||
if(failed){set_error("Receiver connection lost");stop_capture();return 1;}
|
|
||||||
char answer[4096];size_t used=0;bool complete=false;
|
|
||||||
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;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) {
|
int main(void) {
|
||||||
signal(SIGTERM,quit_signal);signal(SIGINT,quit_signal);
|
signal(SIGTERM, quit_signal);
|
||||||
if(boot_enabled() && !set_boot(true))fprintf(stderr,"Own legacy boot hook migration failed\n");
|
signal(SIGINT, quit_signal);
|
||||||
void *vt=dlopen("libvtcapture.so.1",RTLD_NOW),*ls=dlopen("libluna-service2.so.3",RTLD_NOW),*glib=dlopen("libglib-2.0.so.0",RTLD_NOW);
|
if (boot_enabled() && !set_boot(true))
|
||||||
if(!vt||!ls||!glib){fprintf(stderr,"Missing LG library: %s\n",dlerror());return 2;}
|
fprintf(stderr, "Own legacy boot hook migration failed\n");
|
||||||
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
|
void *vt = dlopen("libvtcapture.so.1", RTLD_NOW),
|
||||||
|
*ls = dlopen("libluna-service2.so.3", RTLD_NOW),
|
||||||
|
*glib = dlopen("libglib-2.0.so.0", RTLD_NOW);
|
||||||
|
if (!vt || !ls || !glib) {
|
||||||
|
fprintf(stderr, "Missing LG library: %s\n", dlerror());
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
vt_init = symbol(vt, "vtCapture_initHistogram");
|
||||||
|
vt_preset = symbol(vt, "vtCapture_setPresetHistogram");
|
||||||
|
vt_get = symbol(vt, "vtCapture_getHistogram");
|
||||||
|
vt_finish = symbol(vt, "vtCapture_finalizeHistogram");
|
||||||
call_once = symbol(ls, "LSCallOneReply");
|
call_once = symbol(ls, "LSCallOneReply");
|
||||||
cancel_call = symbol(ls, "LSCallCancel");
|
cancel_call = symbol(ls, "LSCallCancel");
|
||||||
if(playback_json_init(&playback_json)){handover_load();target_load(&playback_json,TARGET_PREF,&target);calibration_load(&playback_json,CALIBRATION_PREF);}
|
if (playback_json_init(&playback_json)) {
|
||||||
reply=symbol(ls,"LSMessageReply");payload=symbol(ls,"LSMessageGetPayload");error_init=symbol(ls,"LSErrorInit");error_free=symbol(ls,"LSErrorFree");
|
handover_load();
|
||||||
|
target_load(&playback_json, TARGET_PREF, &target);
|
||||||
|
calibration_load(&playback_json, CALIBRATION_PREF);
|
||||||
|
}
|
||||||
|
reply = symbol(ls, "LSMessageReply");
|
||||||
|
payload = symbol(ls, "LSMessageGetPayload");
|
||||||
|
error_init = symbol(ls, "LSErrorInit");
|
||||||
|
error_free = symbol(ls, "LSErrorFree");
|
||||||
bool (*reg)(const char *, void **, LSError *) = symbol(ls, "LSRegister");
|
bool (*reg)(const char *, void **, LSError *) = symbol(ls, "LSRegister");
|
||||||
bool (*category)(void*,const char*,Method*,void*,void*,LSError*)=symbol(ls,"LSRegisterCategory");
|
bool (*category)(void *, const char *, Method *, void *, void *, LSError *) =
|
||||||
|
symbol(ls, "LSRegisterCategory");
|
||||||
bool (*attach)(void *, void *, LSError *) = symbol(ls, "LSGmainAttach");
|
bool (*attach)(void *, void *, LSError *) = symbol(ls, "LSGmainAttach");
|
||||||
bool (*unreg)(void *, LSError *) = symbol(ls, "LSUnregister");
|
bool (*unreg)(void *, LSError *) = symbol(ls, "LSUnregister");
|
||||||
void *(*loop_new)(void *, int) = symbol(glib, "g_main_loop_new");
|
void *(*loop_new)(void *, int) = symbol(glib, "g_main_loop_new");
|
||||||
int (*iterate)(void *, int) = symbol(glib, "g_main_context_iteration");
|
int (*iterate)(void *, int) = symbol(glib, "g_main_context_iteration");
|
||||||
unsigned (*timer)(unsigned, int (*)(void *), void *) = symbol(glib, "g_timeout_add");
|
unsigned (*timer)(unsigned, int (*)(void *), void *) = symbol(glib, "g_timeout_add");
|
||||||
void (*loop_unref)(void *) = symbol(glib, "g_main_loop_unref");
|
void (*loop_unref)(void *) = symbol(glib, "g_main_loop_unref");
|
||||||
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
|
void *handle = NULL, *loop = loop_new(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},{"calibration",set_calibration,0},{NULL,NULL,0}};
|
LSError e;
|
||||||
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;}
|
error_init(&e);
|
||||||
|
Method methods[] = {{"start", start, 0},
|
||||||
|
{"stop", stop, 0},
|
||||||
|
{"status", status, 0},
|
||||||
|
{"autostart", autostart, 0},
|
||||||
|
{"restartService", restart_service, 0},
|
||||||
|
{"handover", handover, 0},
|
||||||
|
{"settings", settings, 0},
|
||||||
|
{"testConnection", test_connection, 0},
|
||||||
|
{"calibration", set_calibration, 0},
|
||||||
|
{"calibrationSession", calibration_session, 0},
|
||||||
|
{"calibrationDraft", draft_calibration, 0},
|
||||||
|
{"calibrationResult", report_calibration, 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;
|
service_handle = handle;
|
||||||
if(!boot_enabled())retry_pause(&retries);
|
if (!boot_enabled())
|
||||||
timer(100,tick,NULL);
|
retry_pause(&retries);
|
||||||
while(!quitting){iterate(NULL,0);usleep(10000);}
|
timer(25, tick, NULL);
|
||||||
test_close();stop_capture();
|
while (!quitting) {
|
||||||
|
iterate(NULL, 0);
|
||||||
|
usleep(10000);
|
||||||
|
}
|
||||||
|
stream_close(milliseconds());
|
||||||
|
test_close();
|
||||||
|
stop_capture();
|
||||||
|
output_close(&receiver_runtime.output);
|
||||||
if (piccap_owned) {
|
if (piccap_owned) {
|
||||||
handover_enabled=false;retry_pause(&retries);
|
handover_enabled = false;
|
||||||
|
retry_pause(&retries);
|
||||||
uint64_t deadline = milliseconds() + 4000;
|
uint64_t deadline = milliseconds() + 4000;
|
||||||
while(piccap_owned && milliseconds()<deadline){handover_tick();iterate(NULL,0);usleep(10000);}
|
while (piccap_owned && milliseconds() < deadline) {
|
||||||
|
handover_tick();
|
||||||
|
iterate(NULL, 0);
|
||||||
|
usleep(10000);
|
||||||
}
|
}
|
||||||
unreg(handle,&e);error_free(&e);loop_unref(loop);return 0;
|
}
|
||||||
|
unreg(handle, &e);
|
||||||
|
error_free(&e);
|
||||||
|
loop_unref(loop);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/* Luna API handlers. Included after the shared runtime context. */
|
||||||
|
#include "service_calibration.h"
|
||||||
|
static bool start_failure(void *h, void *m, const char *message) {
|
||||||
|
retry_update(&retries, boot_enabled() && geteuid() == 0, false, milliseconds());
|
||||||
|
char json[512];
|
||||||
|
snprintf(
|
||||||
|
json, sizeof(json),
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"%s\",\"retryScheduled\":%s,\"retryBlocked\":%s}",
|
||||||
|
message, retries.due ? "true" : "false", retries.blocked ? "true" : "false");
|
||||||
|
return respond(h, m, json);
|
||||||
|
}
|
||||||
|
static bool start(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
const char *p = m ? payload(m) : NULL;
|
||||||
|
if (p && strcmp(p, "{}") != 0)
|
||||||
|
return respond(h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use "
|
||||||
|
"settings to configure the receiver\"}");
|
||||||
|
if (m)
|
||||||
|
retry_reset(&retries);
|
||||||
|
if (piccap_owned)
|
||||||
|
return respond(h, m, "{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
|
||||||
|
if (capture_ctx.handle)
|
||||||
|
return respond(h, m, "{\"returnValue\":true,\"running\":true}");
|
||||||
|
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;
|
||||||
|
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 "
|
||||||
|
"Channel.");
|
||||||
|
return start_failure(h, m,
|
||||||
|
"Root access is required for temporary LG developer initialization");
|
||||||
|
}
|
||||||
|
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_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_ctx.handle, 0) != 0) {
|
||||||
|
stop_capture();
|
||||||
|
set_error("LG histogram preset initialization failed");
|
||||||
|
return start_failure(h, m, "Histogram preset failed");
|
||||||
|
}
|
||||||
|
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 (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_ctx.until = 0;
|
||||||
|
retry_pause(&retries);
|
||||||
|
bool bridging = bridge_phase != BRIDGE_IDLE;
|
||||||
|
stream_close(milliseconds());
|
||||||
|
bridge_phase = BRIDGE_IDLE;
|
||||||
|
memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
|
||||||
|
stop_capture();
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
static bool autostart(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
const char *p = m ? payload(m) : NULL;
|
||||||
|
bool enabled;
|
||||||
|
if (p && strcmp(p, "{\"enabled\":true}") == 0)
|
||||||
|
enabled = true;
|
||||||
|
else if (p && strcmp(p, "{\"enabled\":false}") == 0)
|
||||||
|
enabled = false;
|
||||||
|
else
|
||||||
|
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||||
|
if (geteuid() != 0)
|
||||||
|
return respond(
|
||||||
|
h, m,
|
||||||
|
"{\"returnValue\":false,\"rootRequired\":true,\"errorText\":\"Root access is required "
|
||||||
|
"to change autostart. Select Enable Root Access in Overview.\"}");
|
||||||
|
if (!set_boot(enabled))
|
||||||
|
return respond(
|
||||||
|
h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Cannot update the HyperDRM boot hook. Check "
|
||||||
|
"the installed boot script and whether another file occupies the hook path.\"}");
|
||||||
|
if (enabled)
|
||||||
|
retry_reset(&retries);
|
||||||
|
retry_update(&retries, enabled, capture_ctx.handle != NULL, milliseconds());
|
||||||
|
return respond(h, m,
|
||||||
|
enabled ? "{\"returnValue\":true,\"autostart\":true}"
|
||||||
|
: "{\"returnValue\":true,\"autostart\":false}");
|
||||||
|
}
|
||||||
|
static bool restart_service(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
if (strcmp(payload(m), "{}"))
|
||||||
|
return respond(h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Restart expects an empty object\"}");
|
||||||
|
bool ok = respond(h, m, "{\"returnValue\":true,\"restarting\":true}");
|
||||||
|
quitting = 1;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
static bool handover(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
const char *p = payload(m);
|
||||||
|
bool enabled;
|
||||||
|
if (strcmp(p, "{\"enabled\":true}") == 0)
|
||||||
|
enabled = true;
|
||||||
|
else if (strcmp(p, "{\"enabled\":false}") == 0)
|
||||||
|
enabled = false;
|
||||||
|
else
|
||||||
|
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||||
|
if (!playback_json.lib || geteuid() != 0)
|
||||||
|
return respond(
|
||||||
|
h, m, "{\"returnValue\":false,\"errorText\":\"Root service and JSON-C are required\"}");
|
||||||
|
if (enabled && !piccap_installed())
|
||||||
|
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"PicCap is not installed\"}");
|
||||||
|
if (!handover_save(enabled))
|
||||||
|
return respond(
|
||||||
|
h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Could not save PicCap handover preference\"}");
|
||||||
|
handover_enabled = enabled;
|
||||||
|
protection = PLAYBACK_UNKNOWN;
|
||||||
|
clear_samples = 0;
|
||||||
|
clear_since = unknown_since = 0;
|
||||||
|
next_media_probe = 0;
|
||||||
|
return respond(h, m, "{\"returnValue\":true}");
|
||||||
|
}
|
||||||
|
static bool settings(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
TargetConfig next = target;
|
||||||
|
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_ctx.handle || receiver_runtime.starting || piccap_owned || retries.due;
|
||||||
|
stop_capture();
|
||||||
|
stream_close(milliseconds());
|
||||||
|
bridge_phase = BRIDGE_IDLE;
|
||||||
|
memset(&capture_ctx.continuity, 0, sizeof(capture_ctx.continuity));
|
||||||
|
test_close();
|
||||||
|
connection_test.stage = TEST_IDLE;
|
||||||
|
target = next;
|
||||||
|
last_error[0] = 0;
|
||||||
|
protection = PLAYBACK_UNKNOWN;
|
||||||
|
clear_samples = 0;
|
||||||
|
clear_since = unknown_since = 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}");
|
||||||
|
}
|
||||||
|
#include "service_status.h"
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/* Calibration session, proposals, saved profiles and model discovery. */
|
||||||
|
static bool calibration_session(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
const char *p = payload(m);
|
||||||
|
if (!strcmp(p, "{\"enabled\":true}")) {
|
||||||
|
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_ctx.preset = capture_ctx.letterbox_mode = 0;
|
||||||
|
capture_ctx.settling = 2;
|
||||||
|
capture_ctx.pending_letterbox = -1;
|
||||||
|
} else if (!strcmp(p, "{\"enabled\":false}"))
|
||||||
|
calibration_ctx.until = 0;
|
||||||
|
else
|
||||||
|
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||||
|
return respond(h, m, "{\"returnValue\":true}");
|
||||||
|
}
|
||||||
|
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\"}");
|
||||||
|
(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_ctx.message);
|
||||||
|
if (valid)
|
||||||
|
snprintf(calibration_ctx.message, sizeof(calibration_ctx.message), "%s",
|
||||||
|
playback_json.string(message));
|
||||||
|
if (root)
|
||||||
|
playback_json.put(root);
|
||||||
|
return respond(
|
||||||
|
h, m,
|
||||||
|
valid ? "{\"returnValue\":true}"
|
||||||
|
: "{\"returnValue\":false,\"errorText\":\"Invalid calibration result message\"}");
|
||||||
|
}
|
||||||
|
static bool draft_calibration(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
Calibration next = {0};
|
||||||
|
if (!playback_json.lib || !calibration_parse(&playback_json, payload(m), &next) ||
|
||||||
|
next.kind != CAL_CUSTOM || next.count < 4 || strcmp(next.model, tv_model))
|
||||||
|
return respond(h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Invalid measured model profile\"}");
|
||||||
|
calibration_ctx.draft = next;
|
||||||
|
return respond(h, m, "{\"returnValue\":true}");
|
||||||
|
}
|
||||||
|
static bool model_reply(void *h, void *m, void *ctx) {
|
||||||
|
(void)h;
|
||||||
|
(void)ctx;
|
||||||
|
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 &&
|
||||||
|
playback_json.type(model) == 6 && calibration_model_valid(playback_json.string(model))) {
|
||||||
|
snprintf(tv_model, sizeof(tv_model), "%s", playback_json.string(model));
|
||||||
|
calibration_generation++;
|
||||||
|
layout_refresh_reset(milliseconds());
|
||||||
|
}
|
||||||
|
if (root)
|
||||||
|
playback_json.put(root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static void model_tick(uint64_t now) {
|
||||||
|
if (calibration_ctx.model_busy && now >= calibration_ctx.model_deadline) {
|
||||||
|
LSError e;
|
||||||
|
error_init(&e);
|
||||||
|
cancel_call(service_handle, calibration_ctx.model_token, &e);
|
||||||
|
error_free(&e);
|
||||||
|
calibration_ctx.model_busy = false;
|
||||||
|
calibration_ctx.model_token = 0;
|
||||||
|
}
|
||||||
|
if (!tv_model[0] && !calibration_ctx.model_busy && now >= calibration_ctx.next_model_probe) {
|
||||||
|
LSError e;
|
||||||
|
error_init(&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);
|
||||||
|
calibration_ctx.model_deadline = now + 3000;
|
||||||
|
calibration_ctx.next_model_probe = now + 60000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static bool set_calibration(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
Calibration next = calibration;
|
||||||
|
if (!playback_json.lib || !calibration_parse(&playback_json, payload(m), &next))
|
||||||
|
return respond(
|
||||||
|
h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Select a valid profile; custom profiles need a "
|
||||||
|
"model and 2-16 sorted rim points (position 0-4, region 0-15)\"}");
|
||||||
|
if (!calibration_save(CALIBRATION_PREF, &next))
|
||||||
|
return respond(h, m,
|
||||||
|
"{\"returnValue\":false,\"errorText\":\"Cannot save calibration profile\"}");
|
||||||
|
calibration = next;
|
||||||
|
calibration_generation++;
|
||||||
|
layout_refresh_reset(milliseconds());
|
||||||
|
return respond(h, m, "{\"returnValue\":true}");
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/* Read-only runtime status serialization. */
|
||||||
|
static bool status(void *h, void *m, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
char json[8192], 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;
|
||||||
|
uint64_t now = milliseconds();
|
||||||
|
char report[512];
|
||||||
|
size_t k = 0;
|
||||||
|
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),
|
||||||
|
"{\"receiverConnecting\":%s,\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
|
||||||
|
"\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",
|
||||||
|
receiver_runtime.starting ? "true" : "false", calibration_key(calibration.kind),
|
||||||
|
calibration_key(calibration_effective()), tv_model, calibration.model);
|
||||||
|
for (unsigned i = 0; i < calibration.count; i++)
|
||||||
|
used +=
|
||||||
|
snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}",
|
||||||
|
i ? "," : "", calibration.points[i].position, calibration.points[i].region);
|
||||||
|
used += snprintf(json + used, sizeof(json) - used,
|
||||||
|
"],\"layoutUpdates\":%u,\"lastLayoutCheckAgeMs\":%lld,\"layoutError\":\"%s\","
|
||||||
|
"\"piccapFallbacks\":%u,\"piccapCooldownMs\":%llu,",
|
||||||
|
layout_updates, last_layout_check ? (long long)(now - last_layout_check) : -1,
|
||||||
|
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_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_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,",
|
||||||
|
capture_ctx.sample_sequence,
|
||||||
|
capture_ctx.sample_at ? (long long)(now - capture_ctx.sample_at) : -1,
|
||||||
|
capture_ctx.continuity.holding ? "true" : "false",
|
||||||
|
(capture_ctx.continuity.fading || bridge_phase == BRIDGE_FADING) ? "true"
|
||||||
|
: "false",
|
||||||
|
CALIBRATION_ACTIVE ? "true" : "false");
|
||||||
|
used +=
|
||||||
|
snprintf(json + used, sizeof(json) - used,
|
||||||
|
"\"returnValue\":true,\"captureIntervalMs\":%u,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\","
|
||||||
|
"\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%"
|
||||||
|
"lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",
|
||||||
|
target_capture_interval(&target), target.piccap_port, test_stage_name(), connection_test.leds,
|
||||||
|
(unsigned long long)connection_test.elapsed, connection_test.error,
|
||||||
|
capture_ctx.last_output_at ? (long long)(now - capture_ctx.last_output_at) : -1,
|
||||||
|
(unsigned long long)(retries.due > now ? retries.due - now : 0),
|
||||||
|
capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap"
|
||||||
|
: capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap"
|
||||||
|
: piccap_owned ? "PicCap"
|
||||||
|
: capture_ctx.handle ? "Histogram"
|
||||||
|
: "Stopped",
|
||||||
|
piccap_error);
|
||||||
|
used += snprintf(
|
||||||
|
json + used, sizeof(json) - used,
|
||||||
|
"\"version\":\"" HYPERDRM_VERSION
|
||||||
|
"\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,"
|
||||||
|
"\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,"
|
||||||
|
"\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,"
|
||||||
|
"\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,"
|
||||||
|
"\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",
|
||||||
|
(capture_ctx.handle || capture_source == SOURCE_PICCAP) ? "true" : "false",
|
||||||
|
capture_ctx.color_valid ? "true" : "false", capture_ctx.frames, capture_ctx.recoveries,
|
||||||
|
(unsigned long long)capture_ctx.last_recovery_ms, endpoint, target.host, target.port,
|
||||||
|
target.receiver, target.token[0] ? "true" : "false",
|
||||||
|
target_piccap_compatible(&target) ? "true" : "false", PRIORITY, (unsigned)geteuid(),
|
||||||
|
capture_ctx.sent_rgb[0], capture_ctx.sent_rgb[1], capture_ctx.sent_rgb[2], safe);
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
used += snprintf(json + used, sizeof(json) - used, "%s[%u,%u,%u]", i ? "," : "",
|
||||||
|
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\":[",
|
||||||
|
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 ? "," : "",
|
||||||
|
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_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",
|
||||||
|
handover_enabled ? "true" : "false", piccap_connected ? "true" : "false",
|
||||||
|
piccap_owned ? "PicCap" : "HyperDRM",
|
||||||
|
protection == PLAYBACK_PROTECTED ? "protected"
|
||||||
|
: protection == PLAYBACK_CLEAR ? "non-secure decoder"
|
||||||
|
: "unknown",
|
||||||
|
capture_ctx.handle && capture_ctx.letterbox_mode ? "true" : "false");
|
||||||
|
return respond(h, m, json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#ifndef HYPERDRM_SOURCE_BRIDGE_H
|
||||||
|
#define HYPERDRM_SOURCE_BRIDGE_H
|
||||||
|
#include "runtime_states.h"
|
||||||
|
static BridgePhase bridge_phase;
|
||||||
|
static unsigned bridge_sequence;
|
||||||
|
static uint64_t bridge_started, bridge_clear_at, bridge_fade_at, bridge_last_sent;
|
||||||
|
static unsigned char bridge_from[16][3], bridge_to[16][3];
|
||||||
|
static CaptureSource bridge_source;
|
||||||
|
static char *region_command(const unsigned char colors[16][3], const LedBlend *mapping,
|
||||||
|
unsigned count, unsigned duration) {
|
||||||
|
if (!count || !mapping)
|
||||||
|
return NULL;
|
||||||
|
if (!strcmp(target.receiver, "HyperHDR"))
|
||||||
|
return spatial_image_command_duration(colors, PRIORITY, duration);
|
||||||
|
size_t capacity = 256 + (size_t)count * 12;
|
||||||
|
char *line = malloc(capacity);
|
||||||
|
if (!line)
|
||||||
|
return NULL;
|
||||||
|
size_t n = (size_t)snprintf(line, capacity,
|
||||||
|
"{\"command\":\"color\",\"priority\":%d,\"duration\":%u,\"origin\":"
|
||||||
|
"\"HyperDRM\",\"color\":[",
|
||||||
|
PRIORITY, duration);
|
||||||
|
for (unsigned i = 0; i < count; i++) {
|
||||||
|
unsigned char value[3];
|
||||||
|
LedBlend b = mapping[i];
|
||||||
|
blend_color(colors[b.a], colors[b.b], b.weight, value);
|
||||||
|
n += (size_t)snprintf(line + n, capacity - n, "%s%u,%u,%u", i ? "," : "", value[0],
|
||||||
|
value[1], value[2]);
|
||||||
|
}
|
||||||
|
snprintf(line + n, capacity - n, "]}\n");
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
static bool bridge_clear(void) {
|
||||||
|
return stream.stage == STREAM_SUBSCRIBED &&
|
||||||
|
stream_queue("{\"command\":\"clear\",\"priority\":149}\n");
|
||||||
|
}
|
||||||
|
static bool bridge_send(const unsigned char colors[16][3]) {
|
||||||
|
if (stream.stage != STREAM_SUBSCRIBED)
|
||||||
|
return false;
|
||||||
|
char *line = region_command(colors, stream.mapping, stream.count, 1500);
|
||||||
|
if (!line)
|
||||||
|
return false;
|
||||||
|
bool ok = stream_queue(line);
|
||||||
|
free(line);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
static void bridge_tick(uint64_t now, CaptureContext *capture) {
|
||||||
|
if (retries.paused || retries.blocked) {
|
||||||
|
if (bridge_phase)
|
||||||
|
bridge_clear();
|
||||||
|
bridge_phase = BRIDGE_IDLE;
|
||||||
|
bridge_source = capture_source;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (capture_source != bridge_source) {
|
||||||
|
if (capture_source == SOURCE_PICCAP_STARTING) {
|
||||||
|
bridge_phase =
|
||||||
|
capture->continuity.available && now - capture->continuity.valid_at < 2000
|
||||||
|
? BRIDGE_HOLDING
|
||||||
|
: BRIDGE_IDLE;
|
||||||
|
bridge_started = now;
|
||||||
|
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(&capture->continuity, stream.colors, now);
|
||||||
|
bridge_send(stream.colors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bridge_source = capture_source;
|
||||||
|
}
|
||||||
|
if (bridge_phase && now - bridge_started >= 3000) {
|
||||||
|
if (bridge_clear())
|
||||||
|
bridge_phase = BRIDGE_IDLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bridge_phase == BRIDGE_HOLDING && capture_source == SOURCE_PICCAP && piccap_connected &&
|
||||||
|
stream.stage == STREAM_SUBSCRIBED) {
|
||||||
|
if (bridge_clear()) {
|
||||||
|
bridge_sequence = stream.sequence;
|
||||||
|
bridge_clear_at = now;
|
||||||
|
bridge_phase = BRIDGE_WAITING_FRAME;
|
||||||
|
}
|
||||||
|
} else if (bridge_phase == BRIDGE_HOLDING && now - bridge_last_sent >= 100 &&
|
||||||
|
stream.stage == STREAM_SUBSCRIBED) {
|
||||||
|
if (bridge_send(bridge_from))
|
||||||
|
bridge_last_sent = now;
|
||||||
|
} else if (bridge_phase == BRIDGE_WAITING_FRAME && stream.valid &&
|
||||||
|
stream.sequence > bridge_sequence && stream.updated >= bridge_clear_at + 150) {
|
||||||
|
memcpy(bridge_to, stream.colors, sizeof(bridge_to));
|
||||||
|
bridge_fade_at = now;
|
||||||
|
bridge_phase = BRIDGE_FADING;
|
||||||
|
bool black = true;
|
||||||
|
for (unsigned r = 0; r < 16; r++)
|
||||||
|
for (unsigned c = 0; c < 3; c++)
|
||||||
|
if (bridge_to[r][c] > 3)
|
||||||
|
black = false;
|
||||||
|
if (black)
|
||||||
|
bridge_phase = BRIDGE_IDLE;
|
||||||
|
}
|
||||||
|
if (bridge_phase == BRIDGE_FADING && now - bridge_last_sent >= 100) {
|
||||||
|
if (now - bridge_fade_at >= 500) {
|
||||||
|
if (bridge_clear())
|
||||||
|
bridge_phase = BRIDGE_IDLE;
|
||||||
|
} else {
|
||||||
|
unsigned char colors[16][3];
|
||||||
|
continuity_mix(bridge_from, bridge_to, (unsigned)(now - bridge_fade_at), colors);
|
||||||
|
if (bridge_send(colors))
|
||||||
|
bridge_last_sent = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
+33
-10
@@ -9,21 +9,44 @@ enum {SPATIAL_WIDTH=160,SPATIAL_HEIGHT=90,SPATIAL_PIXELS=SPATIAL_WIDTH*SPATIAL_H
|
|||||||
/* Synthetic color plane from histogram samples, never captured video pixels.
|
/* Synthetic color plane from histogram samples, never captured video pixels.
|
||||||
* HyperHDR 22 treats JSON color as a solid color; image input retains space.
|
* HyperHDR 22 treats JSON color as a solid color; image input retains space.
|
||||||
* The receiver samples this plane using its configured LED rectangles. */
|
* The receiver samples this plane using its configured LED rectangles. */
|
||||||
static char *spatial_image_command(const unsigned char regions[16][3],unsigned priority){
|
static char *spatial_image_command_duration(const unsigned char regions[16][3], unsigned priority,
|
||||||
static LedBlend blends[SPATIAL_PIXELS];static unsigned generation;
|
unsigned duration) {
|
||||||
|
static LedBlend blends[SPATIAL_PIXELS];
|
||||||
|
static unsigned generation;
|
||||||
if (generation != calibration_generation) {
|
if (generation != calibration_generation) {
|
||||||
for(unsigned y=0;y<SPATIAL_HEIGHT;y++)for(unsigned x=0;x<SPATIAL_WIDTH;x++)
|
for (unsigned y = 0; y < SPATIAL_HEIGHT; y++)
|
||||||
blends[y*SPATIAL_WIDTH+x]=layout_blend((x+0.5)/SPATIAL_WIDTH,(y+0.5)/SPATIAL_HEIGHT);
|
for (unsigned x = 0; x < SPATIAL_WIDTH; x++)
|
||||||
|
blends[y * SPATIAL_WIDTH + x] =
|
||||||
|
layout_blend((x + 0.5) / SPATIAL_WIDTH, (y + 0.5) / SPATIAL_HEIGHT);
|
||||||
generation = calibration_generation;
|
generation = calibration_generation;
|
||||||
}
|
}
|
||||||
const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
size_t capacity=SPATIAL_PIXELS*4+256;char *line=malloc(capacity);if(!line)return NULL;
|
size_t capacity = SPATIAL_PIXELS * 4 + 256;
|
||||||
size_t used=snprintf(line,capacity,"{\"command\":\"image\",\"priority\":%u,\"duration\":1500,\"origin\":\"HyperDRM\",\"imagewidth\":%u,\"imageheight\":%u,\"format\":\"rgb\",\"name\":\"Histogram colors\",\"imagedata\":\"",priority,SPATIAL_WIDTH,SPATIAL_HEIGHT);
|
char *line = malloc(capacity);
|
||||||
|
if (!line)
|
||||||
|
return NULL;
|
||||||
|
size_t used = snprintf(line, capacity,
|
||||||
|
"{\"command\":\"image\",\"priority\":%u,\"duration\":%u,\"origin\":"
|
||||||
|
"\"HyperDRM\",\"imagewidth\":%u,\"imageheight\":%u,\"format\":\"rgb\","
|
||||||
|
"\"name\":\"Histogram colors\",\"imagedata\":\"",
|
||||||
|
priority, duration, SPATIAL_WIDTH, SPATIAL_HEIGHT);
|
||||||
for (unsigned i = 0; i < SPATIAL_PIXELS; i++) {
|
for (unsigned i = 0; i < SPATIAL_PIXELS; i++) {
|
||||||
unsigned char rgb[3],color[3];LedBlend b=blends[i];blend_color(regions[b.a],regions[b.b],b.weight,rgb);boost_color(rgb,color);
|
unsigned char rgb[3], color[3];
|
||||||
line[used++]=alphabet[color[0]>>2];line[used++]=alphabet[((color[0]&3)<<4)|(color[1]>>4)];
|
LedBlend b = blends[i];
|
||||||
line[used++]=alphabet[((color[1]&15)<<2)|(color[2]>>6)];line[used++]=alphabet[color[2]&63];
|
blend_color(regions[b.a], regions[b.b], b.weight, rgb);
|
||||||
|
boost_color(rgb, color);
|
||||||
|
line[used++] = alphabet[color[0] >> 2];
|
||||||
|
line[used++] = alphabet[((color[0] & 3) << 4) | (color[1] >> 4)];
|
||||||
|
line[used++] = alphabet[((color[1] & 15) << 2) | (color[2] >> 6)];
|
||||||
|
line[used++] = alphabet[color[2] & 63];
|
||||||
}
|
}
|
||||||
line[used++]='"';line[used++]='}';line[used++]='\n';line[used]=0;return line;
|
line[used++] = '"';
|
||||||
|
line[used++] = '}';
|
||||||
|
line[used++] = '\n';
|
||||||
|
line[used] = 0;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
static inline char *spatial_image_command(const unsigned char regions[16][3], unsigned priority) {
|
||||||
|
return spatial_image_command_duration(regions, priority, 1500);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
+153
-50
@@ -10,80 +10,183 @@
|
|||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "playback.h"
|
#include "playback.h"
|
||||||
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
#include "atomic_file.h"
|
||||||
typedef struct {char host[46],receiver[16],token[257];unsigned port,piccap_port;} TargetConfig;
|
#define TARGET_PREF \
|
||||||
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444,.piccap_port=19400};
|
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
||||||
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
|
typedef struct {
|
||||||
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;}
|
char host[46], receiver[16], token[257];
|
||||||
static inline const char *target_piccap_host(const TargetConfig *t){return !strcmp(t->host,"::1")?"127.0.0.1":t->host;}
|
unsigned port, piccap_port, capture_interval;
|
||||||
|
} TargetConfig;
|
||||||
|
static TargetConfig target = {
|
||||||
|
.host = "127.0.0.1", .receiver = "Hyperion", .port = 19444, .piccap_port = 19400,
|
||||||
|
.capture_interval = 50};
|
||||||
|
static inline unsigned target_capture_interval(const TargetConfig *t) {
|
||||||
|
return t->capture_interval ? t->capture_interval : 50;
|
||||||
|
}
|
||||||
|
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) {
|
static bool target_valid(const TargetConfig *t) {
|
||||||
unsigned char address[16];
|
unsigned char address[16];
|
||||||
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
|
unsigned interval = target_capture_interval(t);
|
||||||
if(!t->piccap_port || t->piccap_port>65535)return false;
|
if (interval < 25 || interval > 1000 || interval % 25)
|
||||||
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
|
return false;
|
||||||
|
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++)
|
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;
|
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
|
||||||
|
*p == '-' || *p == '_' || *p == '.'))
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
|
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
|
||||||
void *root=j->parse(text);if(!root)return false;
|
void *root = playback_parse_exact(j, text);
|
||||||
TargetConfig next=*out;bool valid=j->type(root)==4;
|
if (!root)
|
||||||
const char *names[]={"host","receiver","token"};char *fields[]={next.host,next.receiver,next.token};
|
return false;
|
||||||
|
TargetConfig next = *out;
|
||||||
|
bool valid = j->type(root) == 4;
|
||||||
|
const char *names[] = {"host", "receiver", "token"};
|
||||||
|
char *fields[] = {next.host, next.receiver, next.token};
|
||||||
size_t sizes[] = {sizeof(next.host), sizeof(next.receiver), sizeof(next.token)};
|
size_t sizes[] = {sizeof(next.host), sizeof(next.receiver), sizeof(next.token)};
|
||||||
for (unsigned i = 0; valid && i < 3; i++) {
|
for (unsigned i = 0; valid && i < 3; i++) {
|
||||||
void *v = playback_field(j, root, names[i]);
|
void *v = playback_field(j, root, names[i]);
|
||||||
if(!v){if(i<2)valid=false;continue;}
|
if (!v) {
|
||||||
if(j->type(v)!=6){valid=false;break;}
|
if (i < 2)
|
||||||
const char *s=j->string(v);if(strlen(s)>=sizes[i]){valid=false;break;}
|
valid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (j->type(v) != 6) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const char *s = j->string(v);
|
||||||
|
if (strlen(s) >= sizes[i]) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
strcpy(fields[i], s);
|
strcpy(fields[i], s);
|
||||||
}
|
}
|
||||||
void *port = playback_field(j, root, "port");
|
void *port = playback_field(j, root, "port");
|
||||||
if(!port || j->type(port)!=3)valid=false;
|
if (!port || j->type(port) != 3)
|
||||||
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;}
|
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;
|
void *flat = NULL;
|
||||||
if (j->type(root) == 4 && j->get(root, "piccapPort", &flat)) {
|
if (j->type(root) == 4 && j->get(root, "piccapPort", &flat)) {
|
||||||
if(j->type(flat)!=3)valid=false;
|
if (j->type(flat) != 3)
|
||||||
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;}
|
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 *interval = NULL;
|
||||||
|
if (j->type(root) == 4 && j->get(root, "captureIntervalMs", &interval)) {
|
||||||
|
if (j->type(interval) != 3)
|
||||||
|
valid = false;
|
||||||
|
else {
|
||||||
|
char *end = NULL;
|
||||||
|
errno = 0;
|
||||||
|
long n = strtol(j->string(interval), &end, 10);
|
||||||
|
if (errno || !end || *end || n < 25 || n > 1000 || n % 25)
|
||||||
|
valid = false;
|
||||||
|
else
|
||||||
|
next.capture_interval = (unsigned)n;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void *clear = playback_field(j, root, "clearToken");
|
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;}
|
if (clear) {
|
||||||
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
|
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;
|
||||||
}
|
}
|
||||||
static bool target_save(const char *path, const TargetConfig *t) {
|
static bool target_save(const char *path, const TargetConfig *t) {
|
||||||
if(!target_valid(t))return false;
|
if (!target_valid(t))
|
||||||
char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false;
|
return false;
|
||||||
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);
|
char data[512];
|
||||||
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
int n = snprintf(
|
||||||
bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false;
|
data, sizeof(data),
|
||||||
if(ok)ok=rename(tmp,path)==0;
|
"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\",\"captureIntervalMs\":%u}\n",
|
||||||
if(!ok)unlink(tmp);
|
t->host, t->port, t->piccap_port, t->receiver, t->token, target_capture_interval(t));
|
||||||
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) {
|
static bool target_load(PlaybackJson *j, const char *path, TargetConfig *t) {
|
||||||
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false;
|
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
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;
|
if (fd < 0)
|
||||||
data[n]=0;return target_parse(j,data,t);
|
return false;
|
||||||
|
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_open(const TargetConfig *t) {
|
static int target_open(const TargetConfig *t) {
|
||||||
if(!target_valid(t))return -1;
|
if (!target_valid(t))
|
||||||
struct sockaddr_storage storage={0};socklen_t length;int family;
|
return -1;
|
||||||
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
|
struct sockaddr_storage storage = {0};
|
||||||
if(inet_pton(AF_INET,t->host,&v4->sin_addr)==1){family=AF_INET;v4->sin_family=family;v4->sin_port=htons(t->port);length=sizeof(*v4);}
|
socklen_t length;
|
||||||
else {family=AF_INET6;v6->sin6_family=family;v6->sin6_port=htons(t->port);length=sizeof(*v6);}
|
int family;
|
||||||
int fd=socket(family,SOCK_STREAM,0);if(fd<0)return -1;
|
struct sockaddr_in *v4 = (void *)&storage;
|
||||||
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
|
struct sockaddr_in6 *v6 = (void *)&storage;
|
||||||
if(connect(fd,(void*)&storage,length)!=0){
|
if (inet_pton(AF_INET, t->host, &v4->sin_addr) == 1) {
|
||||||
if(errno!=EINPROGRESS){close(fd);return -1;}
|
family = AF_INET;
|
||||||
|
v4->sin_family = family;
|
||||||
|
v4->sin_port = htons(t->port);
|
||||||
|
length = sizeof(*v4);
|
||||||
|
} else {
|
||||||
|
family = AF_INET6;
|
||||||
|
v6->sin6_family = family;
|
||||||
|
v6->sin6_port = htons(t->port);
|
||||||
|
length = sizeof(*v6);
|
||||||
}
|
}
|
||||||
return fd;
|
int fd = socket(family, SOCK_STREAM, 0);
|
||||||
}
|
if (fd < 0)
|
||||||
static int target_connect(const TargetConfig *t){
|
return -1;
|
||||||
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);
|
int flags = fcntl(fd, F_GETFL, 0);
|
||||||
if(flags<0 || fcntl(fd,F_SETFL,flags & ~O_NONBLOCK)<0){close(fd);return -1;}
|
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
|
||||||
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (connect(fd, (void *)&storage, length) != 0) {
|
||||||
|
if (errno != EINPROGRESS) {
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#include <assert.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
static bool interrupt_write, short_write, fail_write, fail_sync, fail_rename, fail_commit_sync;
|
||||||
|
static unsigned directory_syncs;
|
||||||
|
static ssize_t fixture_write(int fd, const void *data, size_t n) {
|
||||||
|
if (interrupt_write) {
|
||||||
|
interrupt_write = false;
|
||||||
|
errno = EINTR;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (fail_write) {
|
||||||
|
errno = ENOSPC;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return write(fd, data, short_write && n > 2 ? 2 : n);
|
||||||
|
}
|
||||||
|
static int fixture_sync(int fd) {
|
||||||
|
struct stat info;
|
||||||
|
assert(!fstat(fd, &info));
|
||||||
|
if (S_ISDIR(info.st_mode)) {
|
||||||
|
directory_syncs++;
|
||||||
|
if (fail_commit_sync && directory_syncs == 2) {
|
||||||
|
errno = EIO;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (fail_sync) {
|
||||||
|
errno = EIO;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return fsync(fd);
|
||||||
|
}
|
||||||
|
static int fixture_rename(int from, const char *a, int to, const char *b) {
|
||||||
|
if (fail_rename) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return renameat(from, a, to, b);
|
||||||
|
}
|
||||||
|
#define ATOMIC_WRITE fixture_write
|
||||||
|
#define ATOMIC_SYNC fixture_sync
|
||||||
|
#define ATOMIC_RENAME fixture_rename
|
||||||
|
#include "atomic_file.h"
|
||||||
|
static void contents(const char *path, const char *expected) {
|
||||||
|
char data[64] = {0};
|
||||||
|
int fd = open(path, O_RDONLY);
|
||||||
|
assert(fd >= 0);
|
||||||
|
assert(read(fd, data, sizeof(data) - 1) == (ssize_t)strlen(expected));
|
||||||
|
close(fd);
|
||||||
|
assert(!strcmp(data, expected));
|
||||||
|
}
|
||||||
|
int main(void) {
|
||||||
|
char directory[] = "/tmp/hyperdrm-save-XXXXXX";
|
||||||
|
assert(mkdtemp(directory));
|
||||||
|
char path[128], stale[128], link[128];
|
||||||
|
snprintf(path, sizeof(path), "%s/settings.json", directory);
|
||||||
|
snprintf(stale, sizeof(stale), "%s/settings.json.tmp", directory);
|
||||||
|
snprintf(link, sizeof(link), "%s/link", directory);
|
||||||
|
int fd = open(stale, O_CREAT | O_WRONLY, 0600);
|
||||||
|
assert(fd >= 0);
|
||||||
|
close(fd);
|
||||||
|
assert(atomic_replace(path, "old", 3) == SAVE_DURABLE);
|
||||||
|
interrupt_write = short_write = true;
|
||||||
|
assert(atomic_replace(path, "partial writes handled", 22) == SAVE_DURABLE);
|
||||||
|
contents(path, "partial writes handled");
|
||||||
|
short_write = false;
|
||||||
|
fail_write = true;
|
||||||
|
assert(atomic_replace(path, "new", 3) == SAVE_UNCHANGED);
|
||||||
|
fail_write = false;
|
||||||
|
contents(path, "partial writes handled");
|
||||||
|
fail_sync = true;
|
||||||
|
assert(atomic_replace(path, "new", 3) == SAVE_UNCHANGED);
|
||||||
|
fail_sync = false;
|
||||||
|
fail_rename = true;
|
||||||
|
assert(atomic_replace(path, "new", 3) == SAVE_UNCHANGED);
|
||||||
|
fail_rename = false;
|
||||||
|
contents(path, "partial writes handled");
|
||||||
|
directory_syncs = 0;
|
||||||
|
fail_commit_sync = true;
|
||||||
|
assert(atomic_replace(path, "committed", 9) == SAVE_COMMITTED);
|
||||||
|
contents(path, "committed");
|
||||||
|
fail_commit_sync = false;
|
||||||
|
assert(!symlink(path, link));
|
||||||
|
assert(atomic_replace(link, "bad", 3) == SAVE_UNCHANGED);
|
||||||
|
contents(path, "committed");
|
||||||
|
struct stat info;
|
||||||
|
assert(!stat(path, &info) && (info.st_mode & 0777) == 0600);
|
||||||
|
assert(!access(stale, F_OK));
|
||||||
|
unlink(link);
|
||||||
|
unlink(path);
|
||||||
|
unlink(stale);
|
||||||
|
assert(!rmdir(directory));
|
||||||
|
puts("PASS: interrupted/partial writes, write/sync/rename failures, atomic replacement, commit "
|
||||||
|
"durability, private permissions, symlinks and stale files");
|
||||||
|
}
|
||||||
+18
-1
@@ -11,5 +11,22 @@ int main(void){
|
|||||||
for(unsigned i=0;i<16;i++)black[i]=100;
|
for(unsigned i=0;i<16;i++)black[i]=100;
|
||||||
memset(color,0,sizeof(color));assert(letterbox_candidate(black,color)==-1);
|
memset(color,0,sizeof(color));assert(letterbox_candidate(black,color)==-1);
|
||||||
memset(black,0,sizeof(black));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);
|
||||||
|
for(unsigned i=0;i<16;i++)thin_black[i]=15;
|
||||||
|
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,27 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "histogram.h"
|
||||||
|
#include "continuity.h"
|
||||||
|
int main(void){
|
||||||
|
Histogram hist[16]={0};assert(!histogram_populated(hist));
|
||||||
|
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)hist[r].bins[c][0]=100;
|
||||||
|
assert(histogram_populated(hist));hist[4].bins[2][0]=0;assert(!histogram_populated(hist));
|
||||||
|
Continuity s={0};unsigned char red[16][3]={{0}},blue[16][3]={{0}},black[16][3]={{0}},out[16][3];
|
||||||
|
for(unsigned r=0;r<16;r++){red[r][0]=200;blue[r][2]=200;}
|
||||||
|
assert(!continuity_frame(&s,false,red,1000,out));
|
||||||
|
assert(continuity_frame(&s,true,red,1100,out));assert(!memcmp(out,red,sizeof(out)));
|
||||||
|
assert(continuity_frame(&s,false,black,1200,out));assert(!memcmp(out,red,sizeof(out)));
|
||||||
|
assert(continuity_frame(&s,false,black,2699,out));assert(!continuity_frame(&s,false,black,2700,out));
|
||||||
|
assert(continuity_frame(&s,true,blue,2800,out));
|
||||||
|
continuity_seed(&s,red,3000);continuity_frame(&s,true,blue,3100,out);assert(out[0][0]==200 && out[0][2]==0);
|
||||||
|
continuity_frame(&s,true,blue,3350,out);assert(out[0][0]==100 && out[0][2]==100);
|
||||||
|
continuity_frame(&s,true,blue,3600,out);assert(out[0][0]==0 && out[0][2]==200 && !s.fading);
|
||||||
|
continuity_seed(&s,red,4000);continuity_frame(&s,true,black,4100,out);assert(!memcmp(out,black,sizeof(out)) && !s.fading);
|
||||||
|
continuity_frame(&s,false,red,4200,out);assert(!memcmp(out,black,sizeof(out)));
|
||||||
|
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)black[r][c]=2;
|
||||||
|
continuity_seed(&s,red,4300);continuity_frame(&s,true,black,4400,out);assert(out[0][0]==2 && out[0][1]==2 && !s.fading);
|
||||||
|
puts("PASS: empty data versus populated black, bounded hold, recovery fade, immediate real black");return 0;
|
||||||
|
}
|
||||||
@@ -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);
|
close(server);assert(test_begin(&config,10000));test_tick(json,10010);assert(connection_test.stage==TEST_FAILED);
|
||||||
}
|
}
|
||||||
int main(void){
|
int main(void){
|
||||||
(void)target_save;(void)target_load;(void)target_connect;(void)test_stage_name;
|
(void)target_save;(void)target_load;(void)target_open;(void)test_stage_name;
|
||||||
PlaybackJson json={0};assert(playback_json_init(&json));
|
PlaybackJson json={0};assert(playback_json_init(&json));
|
||||||
fixture(&json,0);fixture(&json,1);fixture(&json,2);
|
fixture(&json,0);fixture(&json,1);fixture(&json,2);
|
||||||
int peers[2];assert(socketpair(AF_UNIX,SOCK_STREAM,0,peers)==0);
|
int peers[2];assert(socketpair(AF_UNIX,SOCK_STREAM,0,peers)==0);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ static uint64_t clock_ms=10000;
|
|||||||
static unsigned stopped,called,canceled;
|
static unsigned stopped,called,canceled;
|
||||||
static char last_uri[128],last_data[256];
|
static char last_uri[128],last_data[256];
|
||||||
static uint64_t milliseconds(void){return clock_ms;}
|
static uint64_t milliseconds(void){return clock_ms;}
|
||||||
static void stop_capture(void){stopped++;}
|
static void release_capture(bool clear){(void)clear;stopped++;}
|
||||||
static const char *payload(void *m){return m;}
|
static const char *payload(void *m){return m;}
|
||||||
static void error_init(LSError *e){(void)e;}
|
static void error_init(LSError *e){(void)e;}
|
||||||
static void error_free(LSError *e){(void)e;}
|
static void error_free(LSError *e){(void)e;}
|
||||||
@@ -44,7 +44,7 @@ static void connected_start(void){
|
|||||||
media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);assert(capture_source==SOURCE_PICCAP);
|
media(PLAYBACK_CLEAR);handover_tick();answer(0,true,true);assert(capture_source==SOURCE_PICCAP);
|
||||||
}
|
}
|
||||||
int main(void){
|
int main(void){
|
||||||
(void)target_save;(void)target_load;(void)target_connect;(void)handover_save;(void)handover_load;
|
(void)target_save;(void)target_load;(void)target_open;(void)handover_save;(void)handover_load;
|
||||||
(void)retry_failure;(void)retry_take;(void)retry_update;
|
(void)retry_failure;(void)retry_take;(void)retry_update;
|
||||||
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
|
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
|
||||||
handover_enabled=true;next_media_probe=UINT64_MAX;
|
handover_enabled=true;next_media_probe=UINT64_MAX;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "target.h"
|
||||||
|
#include "layout.h"
|
||||||
|
int main(void) {
|
||||||
|
(void)target_open;
|
||||||
|
(void)target_save;
|
||||||
|
(void)target_load;
|
||||||
|
(void)layout_from_json;
|
||||||
|
PlaybackJson json = {0};
|
||||||
|
assert(!playback_parse_exact(&json, "{}"));
|
||||||
|
assert(playback_json_init(&json));
|
||||||
|
const char *invalid[] = {"{",
|
||||||
|
"{\"success\":true",
|
||||||
|
"{}garbage",
|
||||||
|
"{} {}",
|
||||||
|
"[1,",
|
||||||
|
"{\"success\":tru}",
|
||||||
|
"{\"a\":\"unterminated}"};
|
||||||
|
for (unsigned i = 0; i < sizeof(invalid) / sizeof(*invalid); i++)
|
||||||
|
assert(!playback_parse_exact(&json, invalid[i]));
|
||||||
|
void *root = playback_parse_exact(&json, " {\"success\":true} \t\r");
|
||||||
|
assert(root && playback_true(&json, root, "success"));
|
||||||
|
json.put(root);
|
||||||
|
const char *wrong[] = {
|
||||||
|
"[]", "true", "1", "\"success\"", "{\"success\":1}", "{\"success\":\"true\"}"};
|
||||||
|
for (unsigned i = 0; i < sizeof(wrong) / sizeof(*wrong); i++) {
|
||||||
|
root = playback_parse_exact(&json, wrong[i]);
|
||||||
|
assert(!playback_true(&json, root, "success"));
|
||||||
|
if (root)
|
||||||
|
json.put(root);
|
||||||
|
}
|
||||||
|
TargetConfig original = target, next = original;
|
||||||
|
assert(!target_parse(
|
||||||
|
&json, "{\"host\":\"127.0.0.1\",\"receiver\":\"Hyperion\",\"port\":19444}garbage", &next));
|
||||||
|
assert(!memcmp(&next, &original, sizeof(next)));
|
||||||
|
Calibration profile = calibration;
|
||||||
|
assert(!calibration_parse(&json, "{\"profile\":\"average\"}{}", &profile));
|
||||||
|
assert(!memcmp(&profile, &calibration, sizeof(profile)));
|
||||||
|
/* Deterministic malformed-input mutations; every successful parse is freed.
|
||||||
|
* This is regression coverage, not a claim of exhaustive fuzzing. */
|
||||||
|
const char seed[] = "{\"success\":true,\"tan\":1,\"data\":{\"leds\":[0,255,128]}}";
|
||||||
|
uint32_t random = 0x12345678;
|
||||||
|
for (unsigned i = 0; i < 5000; i++) {
|
||||||
|
char text[sizeof(seed) + 4];
|
||||||
|
memcpy(text, seed, sizeof(seed));
|
||||||
|
random = random * 1664525u + 1013904223u;
|
||||||
|
unsigned position = random % (sizeof(seed) - 1);
|
||||||
|
text[position] = (char)(1 + (random >> 16) % 126);
|
||||||
|
if (i % 3 == 0)
|
||||||
|
text[position] = 0;
|
||||||
|
root = playback_parse_exact(&json, text);
|
||||||
|
if (root)
|
||||||
|
json.put(root);
|
||||||
|
}
|
||||||
|
dlclose(json.lib);
|
||||||
|
puts("PASS: exact JSON framing, truncated/trailing data, unexpected types, unchanged settings "
|
||||||
|
"on rejection and 5000 deterministic parser mutations");
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
#include "target.h"
|
#include "target.h"
|
||||||
#include "layout.h"
|
#include "layout.h"
|
||||||
#include "diagnostics.h"
|
#include "diagnostics.h"
|
||||||
static unsigned led_count;
|
#include "runtime_context.h"
|
||||||
static LedBlend *led_blends;
|
static ReceiverLayoutContext receiver_layout;
|
||||||
#include "layout_refresh.h"
|
#include "layout_refresh.h"
|
||||||
static void completed(unsigned count,unsigned generation){
|
static void completed(unsigned count,unsigned generation){
|
||||||
layout_probe.mapping=calloc(count,sizeof(LedBlend));assert(layout_probe.mapping);
|
layout_probe.mapping=calloc(count,sizeof(LedBlend));assert(layout_probe.mapping);
|
||||||
@@ -14,22 +14,22 @@ static void completed(unsigned count,unsigned generation){
|
|||||||
layout_probe.leds=count;layout_probe.stage=TEST_DONE;layout_probe_generation=generation;
|
layout_probe.leds=count;layout_probe.stage=TEST_DONE;layout_probe_generation=generation;
|
||||||
}
|
}
|
||||||
int main(void){
|
int main(void){
|
||||||
(void)target_connect;(void)target_load;(void)target_save;
|
(void)target_open;(void)target_load;(void)target_save;
|
||||||
PlaybackJson json={0};assert(playback_json_init(&json));
|
PlaybackJson json={0};assert(playback_json_init(&json));
|
||||||
calibration.kind=CAL_G4;next_layout_check=UINT64_MAX;
|
calibration.kind=CAL_G4;next_layout_check=UINT64_MAX;
|
||||||
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true);
|
completed(50,calibration_generation);layout_refresh_tick(&json,1000,true,&receiver_layout);
|
||||||
assert(led_count==50 && layout_updates==1 && last_layout_check==1000);
|
assert(receiver_layout.count==50 && layout_updates==1 && last_layout_check==1000);
|
||||||
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true);
|
completed(500,calibration_generation);layout_refresh_tick(&json,2000,true,&receiver_layout);
|
||||||
assert(led_count==500 && layout_updates==2 && last_layout_check==2000);
|
assert(receiver_layout.count==500 && layout_updates==2 && last_layout_check==2000);
|
||||||
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true);assert(layout_updates==2);
|
completed(500,calibration_generation);layout_refresh_tick(&json,3000,true,&receiver_layout);assert(layout_updates==2);
|
||||||
completed(500,calibration_generation);layout_probe.mapping[42].weight^=512;
|
completed(500,calibration_generation);layout_probe.mapping[42].weight^=512;
|
||||||
layout_refresh_tick(&json,4000,true);assert(layout_updates==3);
|
layout_refresh_tick(&json,4000,true,&receiver_layout);assert(layout_updates==3);
|
||||||
LedBlend *previous=led_blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
|
LedBlend *previous=receiver_layout.blends;layout_probe.stage=TEST_FAILED;strcpy(layout_probe.error,"Invalid layout");
|
||||||
layout_refresh_tick(&json,5000,true);assert(led_blends==previous && led_count==500 && layout_updates==3 && layout_error[0]);
|
layout_refresh_tick(&json,5000,true,&receiver_layout);assert(receiver_layout.blends==previous && receiver_layout.count==500 && layout_updates==3 && layout_error[0]);
|
||||||
completed(50,calibration_generation);calibration_generation++;
|
completed(50,calibration_generation);calibration_generation++;
|
||||||
layout_refresh_tick(&json,6000,true);assert(led_count==500 && led_blends==previous);assert(probe_busy(&layout_probe));
|
layout_refresh_tick(&json,6000,true,&receiver_layout);assert(receiver_layout.count==500 && receiver_layout.blends==previous);assert(probe_busy(&layout_probe));
|
||||||
layout_refresh_tick(&json,6100,false);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
|
layout_refresh_tick(&json,6100,false,&receiver_layout);assert(!probe_busy(&layout_probe) && layout_probe.fd==-1);
|
||||||
assert(led_count==500 && led_blends==previous);
|
assert(receiver_layout.count==500 && receiver_layout.blends==previous);
|
||||||
layout_refresh_reset(7000);free(led_blends);test_close();
|
layout_refresh_reset(7000);free(receiver_layout.blends);test_close();
|
||||||
puts("PASS: automatic 50/500 LED replacement, same-count position changes, unchanged layout, invalid reply retention, obsolete profile result and cancellation");return 0;
|
puts("PASS: automatic 50/500 LED replacement, same-count position changes, unchanged layout, invalid reply retention, obsolete profile result and cancellation");return 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include "target.h"
|
||||||
|
#include "layout.h"
|
||||||
|
#include "receiver_output.h"
|
||||||
|
static unsigned request_tan(const char *request) {
|
||||||
|
const char *p = strstr(request, "\"tan\":");
|
||||||
|
assert(p);
|
||||||
|
return (unsigned)strtoul(p + 6, NULL, 10);
|
||||||
|
}
|
||||||
|
static void ack(int fd, unsigned id, bool layout) {
|
||||||
|
char data[256];
|
||||||
|
int n = snprintf(data, sizeof(data),
|
||||||
|
layout ? "{\"success\":true,\"tan\":%u,\"info\":{\"leds\":[{\"hmin\":0,"
|
||||||
|
"\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n"
|
||||||
|
: "{\"success\":true,\"tan\":%u}\n",
|
||||||
|
id);
|
||||||
|
assert(send(fd, data, 7, MSG_NOSIGNAL) == 7);
|
||||||
|
usleep(1000);
|
||||||
|
assert(send(fd, data + 7, n - 7, MSG_NOSIGNAL) == n - 7);
|
||||||
|
}
|
||||||
|
static void fixture(PlaybackJson *json, int mode) {
|
||||||
|
int server = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
assert(server >= 0);
|
||||||
|
struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr = {htonl(INADDR_LOOPBACK)}};
|
||||||
|
assert(!bind(server, (void *)&address, sizeof(address)) && !listen(server, 1));
|
||||||
|
socklen_t size = sizeof(address);
|
||||||
|
assert(!getsockname(server, (void *)&address, &size));
|
||||||
|
TargetConfig config = target;
|
||||||
|
config.port = ntohs(address.sin_port);
|
||||||
|
strcpy(config.token, "fixture");
|
||||||
|
pid_t child = fork();
|
||||||
|
assert(child >= 0);
|
||||||
|
if (!child) {
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
int fd = accept(server, NULL, NULL);
|
||||||
|
assert(fd >= 0);
|
||||||
|
FILE *input = fdopen(dup(fd), "r");
|
||||||
|
char line[1024];
|
||||||
|
assert(fgets(line, sizeof(line), input));
|
||||||
|
assert(strstr(line, "authorize"));
|
||||||
|
ack(fd, request_tan(line), false);
|
||||||
|
assert(fgets(line, sizeof(line), input) && strstr(line, "serverinfo"));
|
||||||
|
unsigned id = request_tan(line);
|
||||||
|
if (mode == 1) {
|
||||||
|
send(fd, "{\"success\":true", 15, MSG_NOSIGNAL);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (mode == 2) {
|
||||||
|
send(fd, "{} garbage\n", 11, MSG_NOSIGNAL);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (mode == 3) {
|
||||||
|
send(fd, "[]\n", 3, MSG_NOSIGNAL);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (mode == 4) {
|
||||||
|
char *huge = malloc(2 * 1024 * 1024 + 64);
|
||||||
|
assert(huge);
|
||||||
|
memset(huge, 'x', 2 * 1024 * 1024 + 64);
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < 2 * 1024 * 1024 + 64) {
|
||||||
|
ssize_t n = send(fd, huge + sent, 2 * 1024 * 1024 + 64 - sent, MSG_NOSIGNAL);
|
||||||
|
if (n <= 0)
|
||||||
|
break;
|
||||||
|
sent += (size_t)n;
|
||||||
|
}
|
||||||
|
free(huge);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (mode == 6) {
|
||||||
|
const char bytes[] = "{}\0ignored\n";
|
||||||
|
send(fd, bytes, sizeof(bytes) - 1, MSG_NOSIGNAL);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (mode == 5) {
|
||||||
|
char bad[128];
|
||||||
|
int n = snprintf(bad, sizeof(bad), "{\"success\":\"true\",\"tan\":%u}\n", id);
|
||||||
|
send(fd, bad, n, MSG_NOSIGNAL);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
ack(fd, id, true);
|
||||||
|
assert(fgets(line, sizeof(line), input) && strstr(line, "[1,2,3]"));
|
||||||
|
id = request_tan(line);
|
||||||
|
/* Unrelated/stale acknowledgements must not release the in-flight frame. */
|
||||||
|
ack(fd, id + 99, false);
|
||||||
|
usleep(50000);
|
||||||
|
ack(fd, id, false);
|
||||||
|
assert(fgets(line, sizeof(line), input) && strstr(line, "[7,8,9]"));
|
||||||
|
ack(fd, request_tan(line), false);
|
||||||
|
assert(fgets(line, sizeof(line), input) && strstr(line, "clear"));
|
||||||
|
ack(fd, request_tan(line), false);
|
||||||
|
done:
|
||||||
|
usleep(20000);
|
||||||
|
fclose(input);
|
||||||
|
close(fd);
|
||||||
|
close(server);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
ReceiverOutput output = {.fd = -1};
|
||||||
|
assert(output_begin(&output, &config, 1000));
|
||||||
|
bool first = false, queued = false, stopping = false;
|
||||||
|
unsigned iterations = 0;
|
||||||
|
for (; iterations < 5000; iterations++) {
|
||||||
|
output_tick(&output, json, 1000 + iterations);
|
||||||
|
if (output.acknowledgements == 1 && queued && !stopping)
|
||||||
|
assert(output.awaiting && !output.latest);
|
||||||
|
if (output.stage == OUTPUT_FAILED || (stopping && output.stage == OUTPUT_IDLE))
|
||||||
|
break;
|
||||||
|
if (output.stage == OUTPUT_READY && !first) {
|
||||||
|
first = true;
|
||||||
|
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[1,2,3]}\n"));
|
||||||
|
} else if (first && output.awaiting && !queued) {
|
||||||
|
queued = true;
|
||||||
|
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[4,5,6]}\n"));
|
||||||
|
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[7,8,9]}\n"));
|
||||||
|
}
|
||||||
|
if (output.acknowledgements == 2 && !stopping) {
|
||||||
|
stopping = true;
|
||||||
|
output_stop(&output, 1000 + iterations, true);
|
||||||
|
}
|
||||||
|
usleep(1000);
|
||||||
|
}
|
||||||
|
assert(iterations < 5000);
|
||||||
|
if (mode)
|
||||||
|
assert(output.stage == OUTPUT_FAILED);
|
||||||
|
else
|
||||||
|
assert(first && queued && stopping && output.stage == OUTPUT_IDLE);
|
||||||
|
output_close(&output);
|
||||||
|
int status;
|
||||||
|
waitpid(child, &status, 0);
|
||||||
|
assert(WIFEXITED(status) && !WEXITSTATUS(status));
|
||||||
|
close(server);
|
||||||
|
}
|
||||||
|
int main(void) {
|
||||||
|
(void)target_open;
|
||||||
|
(void)target_parse;
|
||||||
|
(void)target_load;
|
||||||
|
(void)target_save;
|
||||||
|
PlaybackJson json = {0};
|
||||||
|
assert(playback_json_init(&json));
|
||||||
|
for (int mode = 0; mode <= 6; mode++)
|
||||||
|
fixture(&json, mode);
|
||||||
|
ReceiverOutput output = {.fd = -1};
|
||||||
|
int sockets[2];
|
||||||
|
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, sockets));
|
||||||
|
output.fd = sockets[0];
|
||||||
|
output.stage = OUTPUT_READY;
|
||||||
|
output.incoming = malloc(2 * 1024 * 1024 + 1);
|
||||||
|
assert(output.incoming);
|
||||||
|
assert(!fcntl(sockets[0], F_SETFL, O_NONBLOCK));
|
||||||
|
char block[4096] = {0};
|
||||||
|
while (send(sockets[0], block, sizeof(block), MSG_NOSIGNAL) > 0) {
|
||||||
|
}
|
||||||
|
assert(errno == EAGAIN || errno == EWOULDBLOCK);
|
||||||
|
assert(output_frame(&output, "{\"command\":\"color\",\"color\":[0,0,0]}\n"));
|
||||||
|
output_tick(&output, &json, 1000);
|
||||||
|
assert(output.stage == OUTPUT_READY && output.request && output.sent == 0);
|
||||||
|
output_tick(&output, &json, 4000);
|
||||||
|
assert(output.stage == OUTPUT_FAILED);
|
||||||
|
close(sockets[1]);
|
||||||
|
output_close(&output);
|
||||||
|
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, sockets));
|
||||||
|
output.fd = sockets[0];
|
||||||
|
output.stage = OUTPUT_READY;
|
||||||
|
output.incoming = malloc(2 * 1024 * 1024 + 1);
|
||||||
|
assert(output.incoming);
|
||||||
|
close(sockets[1]);
|
||||||
|
assert(output_frame(&output, "{\"command\":\"clear\"}\n"));
|
||||||
|
output_tick(&output, &json, 5000);
|
||||||
|
assert(output.stage == OUTPUT_FAILED);
|
||||||
|
output_close(&output);
|
||||||
|
dlclose(json.lib);
|
||||||
|
puts("PASS: asynchronous login/layout, fragmented and unrelated replies, latest-frame "
|
||||||
|
"coalescing, clear/stop, truncation, invalid JSON/types, oversized responses, send "
|
||||||
|
"backpressure, timeout and disconnect");
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include "target.h"
|
||||||
|
#include "layout.h"
|
||||||
|
#include "receiver_stream.h"
|
||||||
|
static void fixture(PlaybackJson *json, bool modern, bool login) {
|
||||||
|
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));
|
||||||
|
target.port = ntohs(address.sin_port);
|
||||||
|
snprintf(target.token, sizeof(target.token), "%s", login ? "fixture-token" : "");
|
||||||
|
pid_t child = fork();
|
||||||
|
assert(child >= 0);
|
||||||
|
if (!child) {
|
||||||
|
int fd = accept(server, NULL, NULL);
|
||||||
|
if (fd < 0)
|
||||||
|
_exit(1);
|
||||||
|
FILE *input = fdopen(dup(fd), "r");
|
||||||
|
char request[512];
|
||||||
|
if (login) {
|
||||||
|
if (!fgets(request, sizeof(request), input) || !strstr(request, "authorize"))
|
||||||
|
_exit(2);
|
||||||
|
write(fd, "{\"success\":true}\n", 17);
|
||||||
|
}
|
||||||
|
if (!fgets(request, sizeof(request), input) || !strstr(request, "serverinfo"))
|
||||||
|
_exit(3);
|
||||||
|
const char *layout = "{\"success\":true,\"info\":{\"leds\":[{\"hmin\":0,\"hmax\":1,"
|
||||||
|
"\"vmin\":0,\"vmax\":0.1}]}}\n";
|
||||||
|
write(fd, layout, strlen(layout));
|
||||||
|
if (!fgets(request, sizeof(request), input) || !strstr(request, "ledstream-start"))
|
||||||
|
_exit(4);
|
||||||
|
const char *reply = modern ? "{\"success\":true,\"command\":\"ledcolors-ledstream-update\","
|
||||||
|
"\"data\":{\"leds\":[20,30,40]}}\n"
|
||||||
|
: "{\"success\":true,\"command\":\"ledcolors-ledstream-update\","
|
||||||
|
"\"result\":{\"leds\":[20,30,40]}}\n";
|
||||||
|
write(fd, reply, 13);
|
||||||
|
usleep(20000);
|
||||||
|
write(fd, reply + 13, strlen(reply) - 13);
|
||||||
|
usleep(500000);
|
||||||
|
close(fd);
|
||||||
|
fclose(input);
|
||||||
|
close(server);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
stream_close(0);
|
||||||
|
stream.retry_at = 0;
|
||||||
|
for (unsigned i = 0; i < 500 && !stream.valid; i++) {
|
||||||
|
stream_tick(json, 1000 + i * 5, true);
|
||||||
|
usleep(1000);
|
||||||
|
}
|
||||||
|
assert(stream.valid && stream.sequence == 1 && stream.count == 1);
|
||||||
|
for (unsigned r = 0; r < 16; r++) {
|
||||||
|
assert(stream.colors[r][0] == 20 && stream.colors[r][1] == 30 && stream.colors[r][2] == 40);
|
||||||
|
}
|
||||||
|
/* More than the eight-message budget, with no further socket writes. */
|
||||||
|
const char *buffered =
|
||||||
|
"{\"command\":\"ledcolors-ledstream-update\",\"data\":{\"leds\":[50,60,70]}}\n";
|
||||||
|
stream.used = 0;
|
||||||
|
for (unsigned i = 0; i < 17; i++) {
|
||||||
|
memcpy(stream.data + stream.used, buffered, strlen(buffered));
|
||||||
|
stream.used += strlen(buffered);
|
||||||
|
}
|
||||||
|
stream.data[stream.used] = 0;
|
||||||
|
for (unsigned i = 0; i < 3; i++)
|
||||||
|
stream_tick(json, 1900 + i, true);
|
||||||
|
assert(stream.sequence == 18 && stream.used == 0 && stream.colors[0][0] == 50);
|
||||||
|
void *bad = json->parse("{\"result\":{\"leds\":[0,0,999]}}");
|
||||||
|
assert(!stream_colors(json, bad, 2000));
|
||||||
|
json->put(bad);
|
||||||
|
assert(stream.colors[0][2] == 70);
|
||||||
|
bad = json->parse("{\"data\":{\"leds\":[0,0,0]}}");
|
||||||
|
assert(stream_colors(json, bad, 2000));
|
||||||
|
json->put(bad);
|
||||||
|
assert(stream.colors[0][2] == 0);
|
||||||
|
bad = json->parse("{\"data\":{\"leds\":[1,2,3,4,5,6]}}");
|
||||||
|
assert(!stream_colors(json, bad, 2000) && !stream.revision);
|
||||||
|
json->put(bad);
|
||||||
|
stream_tick(json, 2001, false);
|
||||||
|
assert(stream.fd < 0 && !stream.valid);
|
||||||
|
int status;
|
||||||
|
waitpid(child, &status, 0);
|
||||||
|
assert(WIFEXITED(status) && !WEXITSTATUS(status));
|
||||||
|
close(server);
|
||||||
|
}
|
||||||
|
int main(void) {
|
||||||
|
PlaybackJson json = {0};
|
||||||
|
assert(playback_json_init(&json));
|
||||||
|
fixture(&json, false, false);
|
||||||
|
fixture(&json, true, true);
|
||||||
|
stream_start(1000);
|
||||||
|
stream_tick(&json, 5000, true);
|
||||||
|
assert(stream.fd < 0);
|
||||||
|
stream_close(0);
|
||||||
|
TargetConfig copy = target;
|
||||||
|
assert(target_valid(©));
|
||||||
|
assert(target_piccap_compatible(©));
|
||||||
|
assert(!target_piccap_local(©));
|
||||||
|
assert(target_piccap_host(©));
|
||||||
|
assert(!target_parse(&json, "{}", ©));
|
||||||
|
assert(!target_save("/no/such/profile", ©));
|
||||||
|
assert(!target_load(&json, "/no/such/profile", ©));
|
||||||
|
int fd = target_open(©);
|
||||||
|
if (fd >= 0)
|
||||||
|
close(fd);
|
||||||
|
dlclose(json.lib);
|
||||||
|
puts("PASS: fragmented raw LED subscription, token login, old/new envelopes, invalid samples, "
|
||||||
|
"real black, timeout and cleanup");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "histogram.h"
|
||||||
|
#include "continuity.h"
|
||||||
|
#include "interpolation.h"
|
||||||
|
#define PRIORITY 149
|
||||||
|
typedef struct {unsigned char a,b;unsigned weight;} LedBlend;
|
||||||
|
typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource;
|
||||||
|
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;
|
||||||
|
#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;}
|
||||||
|
static char *spatial_image_command_duration(const unsigned char colors[16][3],unsigned priority,unsigned duration){(void)colors;(void)priority;(void)duration;return strdup("image");}
|
||||||
|
#include "source_bridge.h"
|
||||||
|
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++){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;
|
||||||
|
}
|
||||||
+19
-4
@@ -3,6 +3,13 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "target.h"
|
#include "target.h"
|
||||||
|
static int fixture_connect(const TargetConfig *target){
|
||||||
|
int fd=target_open(target);if(fd<0)return -1;
|
||||||
|
assert(fcntl(fd,F_GETFL,0)&O_NONBLOCK);
|
||||||
|
struct pollfd event={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
|
||||||
|
if(poll(&event,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size) || error){close(fd);return -1;}
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
int main(void){
|
int main(void){
|
||||||
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
|
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
|
||||||
assert(target_valid(&t));assert(t.piccap_port==19400);assert(target_piccap_local(&t));
|
assert(target_valid(&t));assert(t.piccap_port==19400);assert(target_piccap_local(&t));
|
||||||
@@ -17,12 +24,20 @@ int main(void){
|
|||||||
assert(!target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":null,\"receiver\":\"HyperHDR\"}",&t));
|
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"));
|
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);
|
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_capture_interval(&t)==50);
|
||||||
|
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"HyperHDR\",\"captureIntervalMs\":100}",&t));
|
||||||
|
assert(t.capture_interval==100);
|
||||||
|
const char *invalid_intervals[]={"0","24","26","1001","50.5","null","\"50\""};
|
||||||
|
for(unsigned i=0;i<sizeof(invalid_intervals)/sizeof(*invalid_intervals);i++){
|
||||||
|
char request[256];snprintf(request,sizeof(request),"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"HyperHDR\",\"captureIntervalMs\":%s}",invalid_intervals[i]);
|
||||||
|
TargetConfig before=t;assert(!target_parse(&j,request,&t));assert(!memcmp(&before,&t,sizeof(t)));
|
||||||
|
}
|
||||||
assert(target_save(file,&t));struct stat st;assert(stat(file,&st)==0 && (st.st_mode&0777)==0600);
|
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"));
|
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(restored.capture_interval==100);assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));
|
||||||
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
|
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
|
||||||
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
|
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
|
||||||
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
|
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
|
||||||
int client=target_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
|
int client=fixture_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
|
||||||
assert(target_connect(&t)<0);
|
assert(fixture_connect(&t)<0);
|
||||||
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, bounded TCP connect and refusal");return 0;
|
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, nonblocking socket and refused connection");return 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#ifndef HYPERDRM_VERSION_H
|
||||||
|
#define HYPERDRM_VERSION_H
|
||||||
|
#define HYPERDRM_VERSION "0.13.4"
|
||||||
|
#endif
|
||||||
@@ -1 +1,10 @@
|
|||||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.10.0", "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.13.4",
|
||||||
|
"vendor": "Local development",
|
||||||
|
"app": "org.webosbrew.histogramhyperion",
|
||||||
|
"services": [
|
||||||
|
"org.webosbrew.histogramhyperion.service"
|
||||||
|
],
|
||||||
|
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"
|
||||||
|
}
|
||||||
@@ -33,3 +33,13 @@ SoC or firmware; document that test context with contributed profiles.
|
|||||||
Profiles only control spatial region assignment. Saturation, brightness and
|
Profiles only control spatial region assignment. Saturation, brightness and
|
||||||
smoothing remain receiver settings. Runtime selection invalidates the generated
|
smoothing remain receiver settings. Runtime selection invalidates the generated
|
||||||
HyperHDR geometry and refreshes the Hyperion LED mapping without capture restart.
|
HyperHDR geometry and refreshes the Hyperion LED mapping without capture restart.
|
||||||
|
|
||||||
|
## Guided measurement
|
||||||
|
|
||||||
|
The app's auto calibration uses an actual local video plane, not UI overlays.
|
||||||
|
It collects a black reference and fresh samples from 32 perimeter patches,
|
||||||
|
rejects global responses, and derives sorted positions from each region's local
|
||||||
|
response peak. A candidate must cover all four edges. The resulting JSON is a
|
||||||
|
proposal until Save profile is selected. Use Validate profile after saving and
|
||||||
|
check all four color quadrants before contributing the profile. Cancellation and
|
||||||
|
failed measurement preserve the saved profile.
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
"""Test, compile and verify a release locally. Never contacts the TV.
|
||||||
|
|
||||||
|
Use --push to commit the reviewed tree and push main and its version tag.
|
||||||
|
Release attachment publication requires the separately authorized Gitea session.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from sync_version import synchronize
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
GIT = ['git', '-c', 'safe.directory=' + ROOT.as_posix()]
|
||||||
|
|
||||||
|
def run(*args):
|
||||||
|
if args[0] == 'git':
|
||||||
|
args = (*GIT, *args[1:])
|
||||||
|
subprocess.run(args, cwd=ROOT, check=True)
|
||||||
|
|
||||||
|
def linux(*args):
|
||||||
|
if os.name == 'nt':
|
||||||
|
path = '/mnt/' + str(ROOT)[0].lower() + str(ROOT)[2:].replace('\\', '/')
|
||||||
|
run('wsl', '-d', 'Debian', '--cd', path, '--exec', *args)
|
||||||
|
else:
|
||||||
|
run(*args)
|
||||||
|
|
||||||
|
def verify_package(version):
|
||||||
|
import io
|
||||||
|
import tarfile
|
||||||
|
package = ROOT / f'HyperDRM_{version}_arm.ipk'
|
||||||
|
data = package.read_bytes()
|
||||||
|
manifest = json.loads((ROOT / 'HyperDRM.manifest.json').read_text(encoding='utf-8'))
|
||||||
|
assert manifest['version'] == version
|
||||||
|
assert manifest['ipkSize'] == len(data)
|
||||||
|
assert manifest['ipkHash']['sha256'] == hashlib.sha256(data).hexdigest()
|
||||||
|
assert data.startswith(b'!<arch>\n')
|
||||||
|
offset = 8
|
||||||
|
members = {}
|
||||||
|
while offset < len(data):
|
||||||
|
header = data[offset:offset + 60]
|
||||||
|
assert len(header) == 60 and header[58:60] == b'`\n'
|
||||||
|
size = int(header[48:58])
|
||||||
|
offset += 60
|
||||||
|
members[header[:16].decode().strip().rstrip('/')] = data[offset:offset + size]
|
||||||
|
offset += size + size % 2
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(members['data.tar.gz']), mode='r:gz') as archive:
|
||||||
|
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
|
||||||
|
print(f'Verified {package.name}: {len(data)} bytes, SHA256 {manifest["ipkHash"]["sha256"]}')
|
||||||
|
return package
|
||||||
|
|
||||||
|
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')
|
||||||
|
node = shutil.which('node')
|
||||||
|
if not node:
|
||||||
|
raise RuntimeError('Node.js is required for UI tests')
|
||||||
|
for test in sorted((ROOT / 'tests').glob('*.test.js')):
|
||||||
|
run(node, str(test))
|
||||||
|
# Host tests exercise the same portable code without LG libraries or a TV.
|
||||||
|
for test in sorted((ROOT / 'native').glob('test_*.c')):
|
||||||
|
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')
|
||||||
|
run(sys.executable, 'build_manifest.py')
|
||||||
|
verify_package(version)
|
||||||
|
run('git', 'diff', '--check')
|
||||||
|
if options.push:
|
||||||
|
branch = subprocess.check_output(GIT + ['branch', '--show-current'], cwd=ROOT, text=True).strip()
|
||||||
|
if branch != 'main':
|
||||||
|
raise RuntimeError('Release pushes require main')
|
||||||
|
# Fail before committing when this version already exists.
|
||||||
|
exists = subprocess.run(GIT + ['rev-parse', '--verify', 'refs/tags/v' + version], cwd=ROOT, capture_output=True)
|
||||||
|
if exists.returncode == 0:
|
||||||
|
raise RuntimeError('Version tag already exists; bump VERSION')
|
||||||
|
run('git', 'add', '--all')
|
||||||
|
run('git', 'commit', '-m', f'Release {version}')
|
||||||
|
run('git', 'tag', 'v' + version)
|
||||||
|
run('git', 'push', '--atomic', 'origin', 'main', 'v' + version)
|
||||||
|
print('Attach the verified IPK to the matching Gitea release through an authorized session.')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
|||||||
|
"""Generate runtime version metadata from VERSION; --check detects drift."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
def synchronize(check=False):
|
||||||
|
version = (ROOT / 'VERSION').read_text(encoding='utf-8').strip()
|
||||||
|
if not re.fullmatch(r'\d+\.\d+\.\d+', version):
|
||||||
|
raise ValueError('VERSION must contain a semantic release version')
|
||||||
|
updates = {}
|
||||||
|
for filename in ('app/appinfo.json', 'package/packageinfo.json'):
|
||||||
|
path = ROOT / filename
|
||||||
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
if data['version'] != version:
|
||||||
|
data['version'] = version
|
||||||
|
updates[path] = json.dumps(data, indent=2, ensure_ascii=False) + '\n'
|
||||||
|
header = '#ifndef HYPERDRM_VERSION_H\n#define HYPERDRM_VERSION_H\n#define HYPERDRM_VERSION "' + version + '"\n#endif\n'
|
||||||
|
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():
|
||||||
|
path.write_text(content, encoding='utf-8')
|
||||||
|
return version
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--check', action='store_true')
|
||||||
|
synchronize(parser.parse_args().check)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||||
|
const c={};vm.createContext(c);vm.runInContext(fs.readFileSync('app/calibration.js','utf8'),c);
|
||||||
|
const baseline=Array(16).fill(10);
|
||||||
|
const patches=Array.from({length:32},(_,i)=>Array.from({length:16},(_,r)=>{
|
||||||
|
const center=r*2+.5,distance=Math.min(Math.abs(i-center),32-Math.abs(i-center));
|
||||||
|
return 10+80*Math.max(0,1-distance/2);
|
||||||
|
}));
|
||||||
|
const fit=c.calibrationFit(baseline,patches,'OTHER-TV');assert.equal(fit.points.length,16);
|
||||||
|
fit.points.forEach((p,i)=>{assert.equal(p.region,i);assert(Math.abs(p.position-(i*2+1)/8)<.05);});
|
||||||
|
assert.throws(()=>c.calibrationFit(baseline,Array.from({length:32},()=>Array(16).fill(90)),'OTHER-TV'),/reliable spatial/);
|
||||||
|
assert.throws(()=>c.calibrationFit(baseline,Array.from({length:32},()=>baseline),'OTHER-TV'),/reliable spatial/);
|
||||||
|
assert.throws(()=>c.calibrationFit(baseline,patches.slice(1),'OTHER-TV'),/Incomplete/);
|
||||||
|
assert.throws(()=>c.calibrationAverage([baseline,baseline]),/fresh/);
|
||||||
|
const nested=patches.map(p=>p.map((v,r)=>r===15?p[0]:v));
|
||||||
|
const reduced=c.calibrationFit(baseline,nested,'NESTED-TV');assert.equal(reduced.points.length,15);assert(!reduced.points.some(p=>p.region===15));
|
||||||
|
const duplicate=patches.map(p=>Array(16).fill(p[0]));assert.throws(()=>c.calibrationFit(baseline,duplicate,'DUPLICATE-TV'),/reliable spatial/);
|
||||||
|
assert.deepEqual(Array.from(c.calibrationAverage([baseline,baseline,baseline])),baseline);
|
||||||
|
console.log('PASS: measured edge positions, global/absent response rejection and fresh sample minimum');
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||||
|
const events={},windowEvents={},elements={};let inputFocuses=0;
|
||||||
|
const doc={activeElement:null,getElementById(id){return elements[id];},addEventListener(k,f){(events[k]||(events[k]=[])).push(f);}};
|
||||||
|
function element(id,input){return {id,tagName:input?'INPUT':'DIV',type:'text',readOnly:true,disabled:false,style:{},attributes:{},handlers:{},
|
||||||
|
addEventListener(k,f){this.handlers[k]=f;},setAttribute(k,v){this.attributes[k]=v;},
|
||||||
|
focus(){if(doc.activeElement && doc.activeElement!==this)doc.activeElement.blur();doc.activeElement=this;if(input)inputFocuses++;},
|
||||||
|
blur(){if(this.handlers.blur)this.handlers.blur();if(doc.activeElement===this)doc.activeElement=null;}
|
||||||
|
};}
|
||||||
|
['host','port','piccapPort','captureInterval','token','customProfile'].forEach(id=>{elements[id]=element(id,true);elements[id+'Field']=element(id+'Field',false);});
|
||||||
|
['overview','receiver','calibration'].forEach(key=>{elements[key+'Panel']=element(key+'Panel');elements[key+'Tab']=element(key+'Tab');});
|
||||||
|
['start','receiver','calibrate','calibrationShortcut'].forEach(id=>elements[id]=element(id));
|
||||||
|
const win={addEventListener(k,f){windowEvents[k]=f;}};
|
||||||
|
const c={document:doc,window:win};vm.createContext(c);vm.runInContext(fs.readFileSync('app/navigation.js','utf8'),c);
|
||||||
|
elements.receiverTab.onclick();assert.equal(elements.receiverPanel.style.display,'block');assert.equal(elements.overviewPanel.style.display,'none');assert.equal(doc.activeElement.id,'receiver');
|
||||||
|
elements.hostField.focus();assert.equal(inputFocuses,0);assert(elements.host.readOnly);
|
||||||
|
elements.hostField.onclick();assert.equal(inputFocuses,1);assert(!elements.host.readOnly);assert.equal(doc.activeElement.id,'host');
|
||||||
|
events.keyboardStateChange[0]({detail:{visibility:false}});assert(elements.host.readOnly);assert.equal(doc.activeElement.id,'hostField');
|
||||||
|
elements.portField.focus();assert.equal(inputFocuses,1);elements.portField.onclick();assert.equal(inputFocuses,2);
|
||||||
|
let prevented=false;events.keydown[0]({keyCode:461,preventDefault(){prevented=true;}});assert(prevented && elements.port.readOnly && doc.activeElement.id==='portField');
|
||||||
|
elements.token.disabled=true;elements.tokenField.onclick();assert.equal(inputFocuses,2);
|
||||||
|
elements.calibrationShortcut.onclick();assert.equal(doc.activeElement.id,'calibrate');assert.equal(elements.calibrationPanel.style.display,'block');assert.equal(elements.receiverPanel.style.display,'none');
|
||||||
|
elements.customProfileField.onclick();assert.equal(inputFocuses,3);elements.start.focus();assert(elements.customProfile.readOnly);
|
||||||
|
console.log('PASS: three sections, calibration shortcut, highlight without input focus, explicit editing, keyboard close/Back and disabled input');
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||||
const ids=['host','port','piccapPort','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics','profile','customEditor','customProfile','saveProfile','profileStatus'];
|
const ids=['reloadService','versionWarning','host','port','piccapPort','captureInterval','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','captureIntervalField','tokenField','grantRoot','rootHint'];
|
||||||
const elements={};
|
const elements={};
|
||||||
ids.forEach(id=>elements[id]={value:'',textContent:'',disabled:false,checked:false,attributes:{},style:{},addEventListener(){},focus(){},appendChild(){},setAttribute(k,v){this.attributes[k]=v;},getAttribute(k){return this.attributes[k];}});
|
ids.forEach(id=>elements[id]={value:'',textContent:'',disabled:false,checked:false,attributes:{},style:{},addEventListener(){},focus(){},appendChild(){},setAttribute(k,v){this.attributes[k]=v;},getAttribute(k){return this.attributes[k];}});
|
||||||
const requests=[];
|
const requests=[];
|
||||||
@@ -14,11 +14,11 @@ const context={PalmServiceBridge:Bridge,document:{getElementById:id=>elements[id
|
|||||||
vm.runInNewContext(fs.readFileSync('app/app.js','utf8'),context);
|
vm.runInNewContext(fs.readFileSync('app/app.js','utf8'),context);
|
||||||
assert(elements.testResult.textContent.includes('120 LEDs'));
|
assert(elements.testResult.textContent.includes('120 LEDs'));
|
||||||
assert(elements.diagnostics.textContent.includes('Root service'));
|
assert(elements.diagnostics.textContent.includes('Root service'));
|
||||||
assert(controls.includes(elements.piccapPort)&&controls.includes(elements.testConnection));
|
assert(controls.includes(elements.piccapPortField)&&controls.includes(elements.testConnection));
|
||||||
context.targetDirty=true;elements.testConnection.onclick();assert(elements.error.textContent.includes('Save receiver'));
|
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='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();
|
elements.piccapPort.value='19446';elements.captureInterval.value='26';elements.saveTarget.onclick();assert(elements.error.textContent.includes('Capture interval'));elements.captureInterval.value='100';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));
|
const saved=requests.find(r=>r.uri.endsWith('/settings'));assert.strictEqual(saved.data.piccapPort,19446);assert.strictEqual(saved.data.captureIntervalMs,100);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')));
|
elements.testConnection.onclick();assert(requests.some(r=>r.uri.endsWith('/testConnection')));
|
||||||
assert.strictEqual(elements.token.value,'');assert(!elements.diagnostics.textContent.includes('fixture-token'));
|
assert.strictEqual(elements.token.value,'');assert(!elements.diagnostics.textContent.includes('fixture-token'));
|
||||||
elements.profile.onclick();assert(elements.profile.textContent.includes('LG G4'));elements.saveProfile.onclick();assert(requests.some(r=>r.uri.endsWith('/calibration')&&r.data.profile==='g4'));
|
elements.profile.onclick();assert(elements.profile.textContent.includes('LG G4'));elements.saveProfile.onclick();assert(requests.some(r=>r.uri.endsWith('/calibration')&&r.data.profile==='g4'));
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ buttons[4].disabled=false;buttons[3].focus();key(39);assert.equal(doc.activeElem
|
|||||||
key(39);assert.equal(doc.activeElement.id,'start');key(37);assert.equal(doc.activeElement.id,'handover');
|
key(39);assert.equal(doc.activeElement.id,'start');key(37);assert.equal(doc.activeElement.id,'handover');
|
||||||
buttons[2].tagName='INPUT';buttons[2].type='text';buttons[2].focus();
|
buttons[2].tagName='INPUT';buttons[2].type='text';buttons[2].focus();
|
||||||
assert(!key(37));assert(!key(39));assert(!key(13));assert.equal(doc.activeElement.id,'refresh');
|
assert(!key(37));assert(!key(39));assert(!key(13));assert.equal(doc.activeElement.id,'refresh');
|
||||||
key(40);assert.equal(doc.activeElement.id,'autostart');key(38);assert.equal(doc.activeElement.id,'refresh');
|
assert(!key(40));assert(!key(38));assert.equal(doc.activeElement.id,'refresh');
|
||||||
buttons[2].tagName='TEXTAREA';buttons[2].focus();
|
buttons[2].tagName='TEXTAREA';buttons[2].focus();
|
||||||
assert(!key(37));assert(!key(39));assert(!key(13));
|
assert(!key(37));assert(!key(39));assert(!key(13));
|
||||||
key(40);assert.equal(doc.activeElement.id,'autostart');
|
assert(!key(40));assert(!key(38));assert.equal(doc.activeElement.id,'refresh');
|
||||||
key(38);assert.equal(doc.activeElement.id,'refresh');
|
buttons[2].readOnly=true;assert(key(40));assert.equal(doc.activeElement.id,'autostart');
|
||||||
buttons[2].tagName='BUTTON';
|
buttons[2].tagName='BUTTON';
|
||||||
buttons.forEach(button=>button.disabled=true);assert(key(39));
|
buttons.forEach(button=>button.disabled=true);assert(key(39));
|
||||||
console.log('PASS: arrow navigation, wraparound, disabled controls, OK once per press, pointer handoff, focus recovery, Back preserved');
|
console.log('PASS: arrow navigation, wraparound, disabled controls, OK once per press, pointer handoff, focus recovery, Back preserved');
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||||
|
const elements={},requests=[],timers=new Map();let next=0,uid=1000,enabled=false,deny=false,delayRoot=false,pendingRoot=null;
|
||||||
|
for(const match of fs.readFileSync('app/index.html','utf8').matchAll(/id="([^"]+)"/g))elements[match[1]]={style:{},value:'',textContent:'',attributes:{},disabled:false,addEventListener(){},focus(){},appendChild(){},setAttribute(k,v){this.attributes[k]=v;},getAttribute(k){return this.attributes[k];}};
|
||||||
|
class Bridge {
|
||||||
|
call(uri,raw){
|
||||||
|
const data=JSON.parse(raw);requests.push({uri,data});let r={returnValue:true};
|
||||||
|
if(uri.endsWith('/status'))r={returnValue:true,uid,autostart:enabled,piccapInstalled:true,connectionTest:{state:'Not tested'},calibrationProfile:'auto',effectiveCalibration:'g4',customPoints:[],tvModel:'OLED65G42LW.DEUQLJP'};
|
||||||
|
if(uri.endsWith('/autostart')){if(uid)r={returnValue:false,rootRequired:true};else enabled=data.enabled;}
|
||||||
|
if(uri.includes('hbchannel')){if(delayRoot){pendingRoot=this;return;}if(deny)r={returnValue:false,errorText:'Denied by Homebrew Channel'};}
|
||||||
|
if(uri.endsWith('/restartService'))uid=0;
|
||||||
|
this.onservicecallback(JSON.stringify(r));
|
||||||
|
}
|
||||||
|
cancel(){}
|
||||||
|
}
|
||||||
|
const c={document:{getElementById(id){assert(elements[id],id);return elements[id];},createElement(){return {};}},window:{},PalmServiceBridge:Bridge,setupRemoteNavigation(){},setInterval(){},setTimeout(fn){const id=++next;timers.set(id,fn);return id;},clearTimeout(id){timers.delete(id);}};
|
||||||
|
vm.createContext(c);vm.runInContext(fs.readFileSync('app/app.js','utf8'),c);
|
||||||
|
function flush(){for(let i=0;timers.size && i<20;i++){const [id,fn]=timers.entries().next().value;timers.delete(id);fn();}assert(!timers.size);}
|
||||||
|
assert.equal(elements.grantRoot.style.display,'inline-block');elements.autostart.onclick();flush();
|
||||||
|
assert(enabled && !elements.autostart.disabled && uid===0);assert.equal(elements.grantRoot.style.display,'none');
|
||||||
|
const grant=requests.find(r=>r.uri.includes('hbchannel'));assert.deepEqual(grant.data,{id:'org.webosbrew.histogramhyperion.service'});assert(requests.some(r=>r.uri.endsWith('/restartService')));
|
||||||
|
const grantCount=requests.filter(r=>r.uri.includes('hbchannel')).length;
|
||||||
|
elements.autostart.onclick();flush();assert(!enabled);assert.equal(requests.filter(r=>r.uri.includes('hbchannel')).length,grantCount);
|
||||||
|
uid=1000;deny=true;elements.autostart.onclick();flush();assert(!enabled && !elements.autostart.disabled && !elements.grantRoot.disabled);assert(elements.error.textContent.includes('could not grant root'));
|
||||||
|
const retainedError=elements.error.textContent;c.call('status',c.display);assert.equal(elements.error.textContent,retainedError);
|
||||||
|
c.showActionError('Invalid receiver settings');c.call('status',c.display);assert.equal(elements.error.textContent,'Invalid receiver settings');
|
||||||
|
deny=false;delayRoot=true;elements.grantRoot.onclick();elements.autostart.onclick();assert(pendingRoot && c.rootWaiters.length===2);
|
||||||
|
delayRoot=false;pendingRoot.onservicecallback(JSON.stringify({returnValue:true}));flush();assert(enabled && !elements.autostart.disabled && !c.rootWaiters.length);
|
||||||
|
assert.equal(c.actionError,'');
|
||||||
|
console.log('PASS: root-required autostart repairs only own service, confirms root after restart, retries setting, avoids unnecessary elevation and reports denial');
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Version drift must fail before an inconsistent package can be built."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import sync_version
|
||||||
|
|
||||||
|
|
||||||
|
class VersionTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.directory = tempfile.TemporaryDirectory()
|
||||||
|
self.previous_root = sync_version.ROOT
|
||||||
|
sync_version.ROOT = Path(self.directory.name)
|
||||||
|
for name in ('app', 'package', 'native'):
|
||||||
|
(sync_version.ROOT / name).mkdir()
|
||||||
|
(sync_version.ROOT / 'VERSION').write_text('1.2.3\n')
|
||||||
|
for name in ('app/appinfo.json', 'package/packageinfo.json'):
|
||||||
|
(sync_version.ROOT / name).write_text(json.dumps({'version': '1.0.0', 'id': 'unchanged'}))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
sync_version.ROOT = self.previous_root
|
||||||
|
self.directory.cleanup()
|
||||||
|
|
||||||
|
def test_check_rejects_drift_without_writing(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
sync_version.synchronize(check=True)
|
||||||
|
self.assertFalse((sync_version.ROOT / 'native/version.h').exists())
|
||||||
|
|
||||||
|
def test_sync_preserves_metadata_and_produces_runtime_version(self):
|
||||||
|
self.assertEqual(sync_version.synchronize(), '1.2.3')
|
||||||
|
sync_version.synchronize(check=True)
|
||||||
|
app = json.loads((sync_version.ROOT / 'app/appinfo.json').read_text())
|
||||||
|
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):
|
||||||
|
sync_version.synchronize()
|
||||||
|
|
||||||
|
def test_native_drift_is_rejected(self):
|
||||||
|
sync_version.synchronize()
|
||||||
|
(sync_version.ROOT / 'native/version.h').write_text('old version')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
sync_version.synchronize(check=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -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');
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'use strict';
|
||||||
|
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||||
|
const elements={},events={},requests=[];let active=false,sequence=0,globalResponse=false,holdStatus=false,heldStatus=null;
|
||||||
|
['calibrationVideo','cancelCalibration','calibrate','validateCalibration','wizardStatus','customProfile'].forEach(id=>elements[id]={style:{},disabled:false,currentTime:0,textContent:'',value:'',events:{},focus(){},pause(){},play(){return Promise.resolve();},addEventListener(k,f){this.events[k]=f;}});
|
||||||
|
const c={profile:'auto',profileDirty:false,targetDirty:false,showProfile(){},display(){},setInterval(){return 1;},clearInterval(){},setTimeout(){},Date,Array,JSON,
|
||||||
|
window:{addEventListener(k,f){events[k]=f;}},document:{getElementById(id){return elements[id];},addEventListener(k,f){events[k]=f;}},
|
||||||
|
call(method,done,data){
|
||||||
|
requests.push({method,data});if(method==='status' && holdStatus){heldStatus=done;return;}if(method==='calibrationSession')active=data.enabled;
|
||||||
|
const t=elements.calibrationVideo.currentTime,step=t<4?-1:Math.floor((t-4)/2);
|
||||||
|
const regions=Array.from({length:16},(_,r)=>{
|
||||||
|
const center=r*2+.5,d=Math.min(Math.abs(step-center),32-Math.abs(step-center));
|
||||||
|
const value=step<0?10:10+80*(globalResponse?1:Math.max(0,1-d/2));return [value,value,value];
|
||||||
|
});
|
||||||
|
done(method==='status'?{returnValue:true,running:true,activeSource:'HyperDRM',tvModel:'OTHER-TV',colorValid:true,calibrating:active,sampleSequence:++sequence,sampleAgeMs:10,regions}:{returnValue:true});
|
||||||
|
}
|
||||||
|
};vm.createContext(c);
|
||||||
|
vm.runInContext(fs.readFileSync('app/calibration.js','utf8'),c);vm.runInContext(fs.readFileSync('app/wizard.js','utf8'),c);
|
||||||
|
function measure(){
|
||||||
|
elements.calibrate.onclick();assert(active);assert.deepEqual(c.window.remoteNavigationScope,['cancelCalibration']);
|
||||||
|
[1,1.5,2].forEach(t=>{elements.calibrationVideo.currentTime=t;c.wizardSample();});
|
||||||
|
for(let step=0;step<32;step++)[.8,1.1,1.4].forEach(t=>{elements.calibrationVideo.currentTime=4+step*2+t;c.wizardSample();});
|
||||||
|
elements.calibrationVideo.currentTime=68;c.wizardSample();
|
||||||
|
assert(!active && c.wizard===null && c.window.remoteNavigationScope===null);
|
||||||
|
}
|
||||||
|
measure();assert.equal(c.profile,'custom');assert(c.profileDirty);
|
||||||
|
const proposal=JSON.parse(elements.customProfile.value);assert.equal(proposal.points.length,16);
|
||||||
|
assert(requests.some(r=>r.method==='calibrationDraft'));assert(!requests.some(r=>r.method==='calibration'));
|
||||||
|
c.profileDirty=false;globalResponse=true;measure();assert(elements.wizardStatus.textContent.includes('reliable spatial'));assert.equal(elements.customProfile.value,JSON.stringify(proposal,null,2));
|
||||||
|
globalResponse=false;elements.calibrate.onclick();elements.cancelCalibration.onclick();assert(!active && !c.wizard);
|
||||||
|
elements.validateCalibration.onclick();assert.equal(elements.calibrationVideo.currentTime,68);elements.calibrationVideo.events.ended();assert(!active && elements.wizardStatus.textContent.includes('Top-left red'));
|
||||||
|
elements.calibrate.onclick();let prevented=false;events.keydown({keyCode:461,preventDefault(){prevented=true;}});assert(prevented && !active);
|
||||||
|
elements.calibrate.onclick();holdStatus=true;elements.calibrationVideo.currentTime=1;c.wizardSample();assert(heldStatus);const oldReply=heldStatus;
|
||||||
|
elements.cancelCalibration.onclick();holdStatus=false;elements.calibrate.onclick();const newSession=c.wizard;oldReply({returnValue:false,errorText:'Old failure'});assert.strictEqual(c.wizard,newSession);assert(!c.wizard.busy);elements.cancelCalibration.onclick();
|
||||||
|
console.log('PASS: guided measurement, proposal without save, invalid-response preservation, validation, cancel/Back and scoped remote navigation');
|
||||||
Reference in new issue
Block a user