Private
Public Access
Release 0.12.0
This commit is contained in:
1 parent
f74a658097
commit
7f6eeab39e
48 files changed
+3526
-1175
No files matched your search
@@ -0,0 +1,7 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 100
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
SortIncludes: Never
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"id": "org.webosbrew.histogramhyperion",
|
||||
"version": "0.11.0",
|
||||
"version": "0.12.0",
|
||||
"type": "web",
|
||||
"title": "HyperDRM",
|
||||
"appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR",
|
||||
"iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png",
|
||||
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM",
|
||||
"rootRequired": true,
|
||||
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.11.0_arm.ipk",
|
||||
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.12.0_arm.ipk",
|
||||
"ipkHash": {
|
||||
"sha256": "4e2be7de5f6e06cca4a389d17b18c770aa47ef2d862aac651c16ed02f231d711"
|
||||
"sha256": "b4fcc609d763b8e1a72d84484a5d454627416dde0ef51ef65ab705173e3c0810"
|
||||
},
|
||||
"ipkSize": 1203394
|
||||
"ipkSize": 1204332
|
||||
}
|
||||
Binary file not shown.
@@ -27,7 +27,7 @@ Only the own service is modified. Optional Auto PicCap can configure its receive
|
||||
|
||||
## Installation
|
||||
|
||||
The current package is [HyperDRM_0.11.0_arm.ipk](HyperDRM_0.11.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||
The current package is [HyperDRM_0.12.0_arm.ipk](HyperDRM_0.12.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||
|
||||
Elevate the installed own service through Homebrew Channel:
|
||||
|
||||
@@ -192,3 +192,20 @@ service, verifies UID 0, and retries the switch. Root and boot-hook failures hav
|
||||
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.
|
||||
+338
-103
@@ -1,117 +1,352 @@
|
||||
'use strict';
|
||||
var pending=[];
|
||||
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';
|
||||
var pending = [];
|
||||
var actionError = '';
|
||||
|
||||
function showActionError(message) {
|
||||
actionError = message || '';
|
||||
document.getElementById('error').textContent = actionError;
|
||||
}
|
||||
|
||||
function call(method,done,data){callUri('luna://org.webosbrew.histogramhyperion.service/'+method,done,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 clearActionError() {
|
||||
showActionError('');
|
||||
}
|
||||
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){
|
||||
profile=result.calibrationProfile;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':'');
|
||||
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'
|
||||
};
|
||||
|
||||
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('receiver').textContent='Receiver: '+receiver;
|
||||
}
|
||||
document.getElementById('token').placeholder=result.tokenSet?'Saved token (leave blank to keep)':'No token saved';
|
||||
if(result.endpoint)document.getElementById('endpoint').textContent=result.receiver+' · '+result.endpoint+' · Priority 149 · 10 updates per second';
|
||||
var t=result.connectionTest;
|
||||
if(t)document.getElementById('testResult').textContent='Connection test: '+t.state+(t.state==='Passed'?' · '+t.ledCount+' LEDs · '+t.elapsedMs+' ms':'')+(t.error?' · '+t.error:'');
|
||||
document.getElementById('testConnection').disabled=!!t && ['Connecting','Sending request','Checking authentication','Reading LED layout'].indexOf(t.state)>=0;
|
||||
if(result.version)document.getElementById('diagnostics').textContent='Version '+result.version+' · '+(result.uid===0?'Root service':'Service needs root elevation')+' · '+result.captureState+' · Playback: '+result.playbackProtection+' · Capture recoveries: '+result.captureRecoveries+' (last: '+result.lastRecoveryMs+' ms)'+(result.lastOutputAgeMs>=0?' · Last histogram output: '+result.lastOutputAgeMs+' ms ago':'')+(result.retryScheduled?' · Retry in '+result.retryInMs+' ms · Attempts: '+result.retryAttempts:'')+(result.piccapError?' · '+result.piccapError:'')+(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;
|
||||
handover.setAttribute('aria-checked',String(!!result.piccapHandover));
|
||||
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');}
|
||||
document.getElementById('error').textContent=result.errorText||result.error||'';
|
||||
document.getElementById('status').textContent=result.returnValue===false?'Service unavailable':(result.running?'Running':'Stopped')+' · '+(result.frames||0)+' acknowledged color updates';
|
||||
if(result.activeSource==='PicCap'){
|
||||
document.getElementById('status').textContent='PicCap · '+(result.running?'Running':'Switching capture source')+' · '+(result.piccapConnected?'Receiver connected':'Waiting for receiver');
|
||||
document.getElementById('regions').textContent='';return;
|
||||
}
|
||||
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.colorValid && result.effectiveCalibration==='average')document.getElementById('status').textContent+=' · Average color output';
|
||||
if(result.colorValid)document.getElementById('status').textContent+=result.spatialOutput?' · '+result.ledCount+' LEDs from 16 color regions'+(result.receiver==='HyperHDR'?' · Spatial image output':''):' · Output RGB: '+result.outputColor.join(', ');
|
||||
else if(result.returnValue!==false)document.getElementById('status').textContent+=' · No current color output';
|
||||
if(result.blackBarDetection && result.running)document.getElementById('status').textContent+=result.letterboxDetected?' · Top/bottom black bars detected':' · Full-screen capture';
|
||||
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.retryPaused)document.getElementById('status').textContent+=' · Automatic retry paused until Start';
|
||||
var box=document.getElementById('regions');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);});
|
||||
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';
|
||||
}
|
||||
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);
|
||||
|
||||
function call(method, done, data) {
|
||||
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) {
|
||||
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) {
|
||||
profile = result.calibrationProfile;
|
||||
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.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('receiver').textContent = 'Receiver: ' + receiver;
|
||||
}
|
||||
document.getElementById('token').placeholder = result.tokenSet ? 'Saved token (leave blank to keep)' : 'No token saved';
|
||||
if (result.endpoint) document.getElementById('endpoint').textContent = result.receiver + ' · ' + result.endpoint + ' · Priority 149 · 10 updates per second';
|
||||
var t = result.connectionTest;
|
||||
if (t) document.getElementById('testResult').textContent = 'Connection test: ' + t.state + (t.state === 'Passed' ? ' · ' + t.ledCount + ' LEDs · ' + t.elapsedMs + ' ms' : '') + (t.error ? ' · ' + t.error : '');
|
||||
document.getElementById('testConnection').disabled = !!t && ['Connecting', 'Sending request', 'Checking authentication', 'Reading LED layout'].indexOf(t.state) >= 0;
|
||||
if (result.version) document.getElementById('diagnostics').textContent = 'Version ' + result.version + ' · ' + (result.uid === 0 ? 'Root service' : 'Service needs root elevation') + ' · ' + result.captureState + ' · Playback: ' + result.playbackProtection + ' · Capture recoveries: ' + result.captureRecoveries + ' (last: ' + result.lastRecoveryMs + ' ms)' + (result.lastOutputAgeMs >= 0 ? ' · Last histogram output: ' + result.lastOutputAgeMs + ' ms ago' : '') + (result.retryScheduled ? ' · Retry in ' + result.retryInMs + ' ms · Attempts: ' + result.retryAttempts : '') + (result.piccapError ? ' · ' + result.piccapError : '') + (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;
|
||||
handover.setAttribute('aria-checked', String(!!result.piccapHandover));
|
||||
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');
|
||||
}
|
||||
document.getElementById('error').textContent = actionError || result.errorText || result.error || '';
|
||||
document.getElementById('status').textContent = result.returnValue === false ? 'Service unavailable' : (result.running ? 'Running' : 'Stopped') + ' · ' + (result.frames || 0) + ' acknowledged color updates';
|
||||
if (result.activeSource === 'PicCap') {
|
||||
document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver');
|
||||
document.getElementById('regions').textContent = '';
|
||||
return;
|
||||
}
|
||||
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.colorValid && result.effectiveCalibration === 'average') document.getElementById('status').textContent += ' · Average color output';
|
||||
if (result.colorValid) document.getElementById('status').textContent += result.spatialOutput ? ' · ' + result.ledCount + ' LEDs from 16 color regions' + (result.receiver === 'HyperHDR' ? ' · Spatial image output' : '') : ' · Output RGB: ' + result.outputColor.join(', ');
|
||||
else if (result.returnValue !== false) document.getElementById('status').textContent += ' · No current color output';
|
||||
if (result.blackBarDetection && result.running) document.getElementById('status').textContent += result.letterboxDetected ? ' · Top/bottom black bars detected' : ' · Full-screen capture';
|
||||
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.retryPaused) document.getElementById('status').textContent += ' · Automatic retry paused until Start';
|
||||
var box = document.getElementById('regions');
|
||||
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);
|
||||
});
|
||||
}
|
||||
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();
|
||||
|
||||
var repairingRoot=false,rootWaiters=[];
|
||||
function repairRoot(done){
|
||||
rootWaiters.push(done);if(repairingRoot)return;repairingRoot=true;document.getElementById('grantRoot').disabled=true;
|
||||
document.getElementById('error').textContent='Granting root access to HyperDRM...';
|
||||
function finish(error){repairingRoot=false;document.getElementById('grantRoot').disabled=false;if(error)document.getElementById('error').textContent=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);
|
||||
var repairingRoot = false,
|
||||
rootWaiters = [];
|
||||
|
||||
function repairRoot(done) {
|
||||
clearActionError();
|
||||
rootWaiters.push(done);
|
||||
if (repairingRoot) return;
|
||||
repairingRoot = true;
|
||||
document.getElementById('grantRoot').disabled = true;
|
||||
showActionError('Granting root access to HyperDRM...');
|
||||
|
||||
function finish(error) {
|
||||
repairingRoot = false;
|
||||
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'
|
||||
});
|
||||
},{id:'org.webosbrew.histogramhyperion.service'});
|
||||
}
|
||||
document.getElementById('grantRoot').onclick=function(){repairRoot(function(ok){if(ok)call('status',display);});};
|
||||
document.getElementById('autostart').onclick=function(){
|
||||
var button=this,enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;
|
||||
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)document.getElementById('error').textContent=r.errorText;else call('status',display);
|
||||
},{enabled:enabled});}
|
||||
document.getElementById('grantRoot').onclick = function() {
|
||||
repairRoot(function(ok) {
|
||||
if (ok) call('status', display);
|
||||
});
|
||||
};
|
||||
document.getElementById('autostart').onclick = function() {
|
||||
var button = this,
|
||||
enabled = button.getAttribute('aria-checked') !== 'true';
|
||||
button.disabled = true;
|
||||
|
||||
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)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:button.getAttribute('aria-checked')!=='true'});};
|
||||
['host','port','piccapPort','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});});
|
||||
document.getElementById('receiver').onclick=function(){receiver=receiver==='Hyperion'?'HyperHDR':'Hyperion';this.textContent='Receiver: '+receiver;targetDirty=true;};
|
||||
document.getElementById('saveTarget').onclick=function(){
|
||||
var port=Number(document.getElementById('port').value),button=this;
|
||||
if(!Number.isInteger(port)||port<1||port>65535){document.getElementById('error').textContent='JSON port must be an integer from 1 to 65535.';return;}
|
||||
var flatPort=Number(document.getElementById('piccapPort').value);
|
||||
if(!Number.isInteger(flatPort)||flatPort<1||flatPort>65535){document.getElementById('error').textContent='PicCap FlatBuffer port must be an integer from 1 to 65535.';return;}
|
||||
var settings={piccapPort:flatPort,host:document.getElementById('host').value.trim(),port:port,receiver:receiver,clearToken:document.getElementById('clearToken').checked};
|
||||
var token=document.getElementById('token').value.trim();if(token)settings.token=token;
|
||||
button.disabled=true;call('settings',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}targetDirty=false;document.getElementById('token').value='';document.getElementById('clearToken').checked=false;call('status',display);},settings);
|
||||
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'
|
||||
});
|
||||
};
|
||||
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('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){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);};
|
||||
document.getElementById('editProfile').onclick=function(){advancedProfile=!advancedProfile;showProfile();if(advancedProfile)document.getElementById('customProfileField').focus();};
|
||||
setupRemoteNavigation(['overviewTab','receiverTab','calibrationTab','grantRoot','start','stop','refresh','autostart','handover','calibrationShortcut','receiver','hostField','portField','piccapPortField','tokenField','clearToken','saveTarget','testConnection','calibrate','validateCalibration','profile','saveProfile','editProfile','customProfileField','cancelCalibration'].map(function(id){return document.getElementById(id);}),document,window);
|
||||
['host', 'port', 'piccapPort', 'token', 'clearToken'].forEach(function(id) {
|
||||
document.getElementById(id).addEventListener('input', function() {
|
||||
targetDirty = true;
|
||||
});
|
||||
});
|
||||
document.getElementById('receiver').onclick = function() {
|
||||
receiver = receiver === 'Hyperion' ? 'HyperHDR' : 'Hyperion';
|
||||
this.textContent = 'Receiver: ' + receiver;
|
||||
targetDirty = true;
|
||||
};
|
||||
document.getElementById('saveTarget').onclick = function() {
|
||||
var port = Number(document.getElementById('port').value),
|
||||
button = this;
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
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 settings = {
|
||||
piccapPort: flatPort,
|
||||
host: document.getElementById('host').value.trim(),
|
||||
port: port,
|
||||
receiver: receiver,
|
||||
clearToken: document.getElementById('clearToken').checked
|
||||
};
|
||||
var token = document.getElementById('token').value.trim();
|
||||
if (token) settings.token = token;
|
||||
button.disabled = true;
|
||||
call('settings', function(r) {
|
||||
button.disabled = false;
|
||||
if (r.returnValue === false) {
|
||||
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', 'grantRoot', 'start', 'stop', 'refresh', 'autostart', 'handover', 'calibrationShortcut', 'receiver', 'hostField', 'portField', 'piccapPortField', 'tokenField', 'clearToken', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'profile', 'saveProfile', 'editProfile', 'customProfileField', 'cancelCalibration'].map(function(id) {
|
||||
return document.getElementById(id);
|
||||
}), document, window);
|
||||
+14
-1
@@ -1 +1,14 @@
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.11.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.12.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"
|
||||
}
|
||||
+73
-27
@@ -1,35 +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;
|
||||
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 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});
|
||||
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};
|
||||
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;});
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
+65
-19
@@ -1,21 +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 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));
|
||||
});
|
||||
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();}});
|
||||
if (focus !== false) document.getElementById({
|
||||
overview: 'start',
|
||||
receiver: 'receiver',
|
||||
calibration: 'calibrate'
|
||||
} [name]).focus();
|
||||
}
|
||||
setupFormEditors(['host','port','piccapPort','token','customProfile'],document,window);
|
||||
['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', 'token', 'customProfile'], document, window);
|
||||
+42
-25
@@ -1,32 +1,49 @@
|
||||
/* Directional control for webOS remotes and standard keyboards. */
|
||||
function setupRemoteNavigation(buttons, doc, win) {
|
||||
var lastFocused=buttons[0], enterDown=false;
|
||||
buttons.forEach(function(button){
|
||||
button.addEventListener('focus',function(){lastFocused=button;});
|
||||
button.addEventListener('mouseenter',function(){if(!button.disabled)button.focus();});
|
||||
var lastFocused = buttons[0],
|
||||
enterDown = false;
|
||||
buttons.forEach(function(button) {
|
||||
button.addEventListener('focus', function() {
|
||||
lastFocused = button;
|
||||
});
|
||||
doc.addEventListener('keydown',function(event){
|
||||
var code=event.keyCode||event.which;
|
||||
var direction=event.key==='ArrowLeft'||event.key==='ArrowUp'||code===37||code===38?-1:
|
||||
event.key==='ArrowRight'||event.key==='ArrowDown'||code===39||code===40?1:0;
|
||||
var enter=event.key==='Enter'||code===13;
|
||||
if(!direction&&!enter)return;
|
||||
var editing=doc.activeElement && !doc.activeElement.readOnly && (doc.activeElement.tagName==='INPUT' && doc.activeElement.type!=='checkbox' || doc.activeElement.tagName==='TEXTAREA');
|
||||
if(editing)return;
|
||||
button.addEventListener('mouseenter', function() {
|
||||
if (!button.disabled) button.focus();
|
||||
});
|
||||
});
|
||||
doc.addEventListener('keydown', function(event) {
|
||||
var code = event.keyCode || event.which;
|
||||
var direction = event.key === 'ArrowLeft' || event.key === 'ArrowUp' || code === 37 || code === 38 ? -1 :
|
||||
event.key === 'ArrowRight' || event.key === 'ArrowDown' || code === 39 || code === 40 ? 1 : 0;
|
||||
var enter = event.key === 'Enter' || code === 13;
|
||||
if (!direction && !enter) return;
|
||||
var editing = doc.activeElement && !doc.activeElement.readOnly && (doc.activeElement.tagName === 'INPUT' && doc.activeElement.type !== 'checkbox' || doc.activeElement.tagName === 'TEXTAREA');
|
||||
if (editing) return;
|
||||
event.preventDefault();
|
||||
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;
|
||||
var focused=doc.activeElement,index=enabled.indexOf(focused);
|
||||
if(direction){
|
||||
if(index<0){index=enabled.indexOf(lastFocused);if(index<0)index=0;enabled[index].focus();}
|
||||
else enabled[(index+direction+enabled.length)%enabled.length].focus();
|
||||
}else{
|
||||
if(enterDown||event.repeat)return;
|
||||
enterDown=true;
|
||||
if(index<0){index=enabled.indexOf(lastFocused);enabled[index<0?0:index].focus();}
|
||||
else focused.click();
|
||||
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;
|
||||
var focused = doc.activeElement,
|
||||
index = enabled.indexOf(focused);
|
||||
if (direction) {
|
||||
if (index < 0) {
|
||||
index = enabled.indexOf(lastFocused);
|
||||
if (index < 0) index = 0;
|
||||
enabled[index].focus();
|
||||
} else enabled[(index + direction + enabled.length) % enabled.length].focus();
|
||||
} else {
|
||||
if (enterDown || event.repeat) return;
|
||||
enterDown = true;
|
||||
if (index < 0) {
|
||||
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;});
|
||||
win.addEventListener('blur',function(){enterDown=false;});
|
||||
doc.addEventListener('keyup', function(event) {
|
||||
if (event.key === 'Enter' || event.keyCode === 13 || event.which === 13) enterDown = false;
|
||||
});
|
||||
win.addEventListener('blur', function() {
|
||||
enterDown = false;
|
||||
});
|
||||
}
|
||||
+192
-71
@@ -1,87 +1,208 @@
|
||||
'use strict';
|
||||
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;
|
||||
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;
|
||||
if(Date.now()-wizard.started>100000){wizardFinish('Calibration timed out. Saved profile unchanged.');return;}
|
||||
if(wizard.validation)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;
|
||||
call('calibrationDraft',function(r){if(wizard)wizardFinish(r.returnValue===false?r.errorText:undefined);},{profile:'custom',model:measured.model,points:measured.points});
|
||||
}catch(e){wizardFinish(e.message);}
|
||||
|
||||
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;
|
||||
}
|
||||
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)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...';
|
||||
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(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={validation:validation,started:Date.now(),busy:false,sequence:-1,model:'',baseline:[],patches:Array.from({length:32},function(){return [];})};
|
||||
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)return;if(r.returnValue===false){wizardFinish(r.errorText);return;}
|
||||
|
||||
function wizardStart(validation) {
|
||||
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(){wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');});
|
||||
call('start',function(){waitCapture();});
|
||||
},{enabled:true});
|
||||
function waitCapture(){
|
||||
if(!wizard)return;
|
||||
call('status',function(s){
|
||||
if(!wizard)return;
|
||||
if(s.running && s.activeSource!=='PicCap' && s.tvModel && s.colorValid){
|
||||
wizard.model=s.tvModel;video.currentTime=validation?68:0;video.style.display='block';
|
||||
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(){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);
|
||||
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();});
|
||||
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);});
|
||||
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);
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Describe the exact built IPK for Homebrew Channel."""
|
||||
import hashlib, json
|
||||
from pathlib import Path
|
||||
from sync_version import synchronize
|
||||
synchronize(check=True)
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
BASE='https://gitea.adnau.de/matze19999/HyperDRM'
|
||||
RAW=BASE+'/raw/branch/main/'
|
||||
|
||||
+5
-3
@@ -1,16 +1,18 @@
|
||||
"""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 sync_version import synchronize
|
||||
synchronize(check=True)
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
ID='org.webosbrew.histogramhyperion'
|
||||
VERSION=json.loads((ROOT/'app/appinfo.json').read_text(encoding='utf-8'))['version']
|
||||
def tar_bytes(entries):
|
||||
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:
|
||||
info=tarfile.TarInfo(name);info.size=len(body);info.mode=mode;info.mtime=0
|
||||
tar.addfile(info,io.BytesIO(body))
|
||||
return stream.getvalue()
|
||||
return gzip.compress(stream.getvalue(), mtime=0)
|
||||
entries=[]
|
||||
for directory,target in [('app','applications/'+ID),('service','services/'+ID+'.service'),('package','packages/'+ID)]:
|
||||
for path in sorted((ROOT/directory).iterdir()):
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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_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.
|
||||
@@ -136,3 +136,24 @@ 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.
|
||||
@@ -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.
|
||||
+53
-21
@@ -4,34 +4,66 @@
|
||||
#define BOOT_DIRECTORY "/var/lib/webosbrew/init.d"
|
||||
#endif
|
||||
#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
|
||||
#define BOOT_HOOK BOOT_DIRECTORY "/60-histogramhyperion"
|
||||
/* 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. */
|
||||
static int boot_hook_kind(void){
|
||||
struct stat st;if(lstat(BOOT_HOOK,&st)!=0)return errno==ENOENT?0:-1;
|
||||
if(S_ISLNK(st.st_mode)){
|
||||
char target[sizeof(BOOT_SCRIPT)+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;
|
||||
static int boot_hook_kind(void) {
|
||||
struct stat st;
|
||||
if (lstat(BOOT_HOOK, &st) != 0)
|
||||
return errno == ENOENT ? 0 : -1;
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
char target[sizeof(BOOT_SCRIPT) + 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;
|
||||
char data[sizeof(legacy_boot_script)];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;
|
||||
if (!S_ISREG(st.st_mode))
|
||||
return -1;
|
||||
char data[sizeof(legacy_boot_script)];
|
||||
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 set_boot(bool enabled){
|
||||
int kind=boot_hook_kind();if(kind<0)return false;
|
||||
if(!enabled)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;
|
||||
static bool boot_enabled(void) {
|
||||
return boot_hook_kind() > 0 && access(BOOT_HOOK, X_OK) == 0;
|
||||
}
|
||||
static bool set_boot(bool enabled) {
|
||||
int kind = boot_hook_kind();
|
||||
if (kind < 0)
|
||||
return false;
|
||||
if (!enabled)
|
||||
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. */
|
||||
if(symlink(BOOT_SCRIPT,BOOT_HOOK ".tmp")!=0)return false;
|
||||
bool ok=rename(BOOT_HOOK ".tmp",BOOT_HOOK)==0;
|
||||
if(!ok)unlink(BOOT_HOOK ".tmp");
|
||||
if (symlink(BOOT_SCRIPT, BOOT_HOOK ".tmp") != 0)
|
||||
return false;
|
||||
bool ok = rename(BOOT_HOOK ".tmp", BOOT_HOOK) == 0;
|
||||
if (!ok)
|
||||
unlink(BOOT_HOOK ".tmp");
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
+18
-11
@@ -2,18 +2,25 @@
|
||||
#define BLACKBARS_H
|
||||
/* 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. */
|
||||
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;
|
||||
for(unsigned i=0;i<4;i++){
|
||||
unsigned r=interior[i],peak=color[r][0];
|
||||
for(unsigned c=1;c<3;c++)if(color[r][c]>peak)peak=color[r][c];
|
||||
if(black[r]<20 && peak>35)lit++;
|
||||
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;
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
unsigned r = interior[i], peak = color[r][0];
|
||||
for (unsigned c = 1; c < 3; c++)
|
||||
if (color[r][c] > peak)
|
||||
peak = color[r][c];
|
||||
if (black[r] < 20 && peak > 35)
|
||||
lit++;
|
||||
}
|
||||
if(lit<2)return -1;
|
||||
unsigned top=(black[3]+black[11]+black[12])/3;
|
||||
unsigned bottom=(black[1]+black[5]+black[9])/3;
|
||||
if(top>=55 && bottom>=25)return 1;
|
||||
if(top<20 && bottom<20)return 0;
|
||||
if (lit < 2)
|
||||
return -1;
|
||||
unsigned top = (black[3] + black[11] + black[12]) / 3;
|
||||
unsigned bottom = (black[1] + black[5] + black[9]) / 3;
|
||||
if (top >= 55 && bottom >= 25)
|
||||
return 1;
|
||||
if (top < 20 && bottom < 20)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
+134
-62
@@ -2,83 +2,155 @@
|
||||
#define HYPERDRM_CALIBRATION_H
|
||||
#include <math.h>
|
||||
#include "playback.h"
|
||||
#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 struct {CalibrationKind kind;char model[64];unsigned count;RimPoint points[16];} Calibration;
|
||||
#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 struct {
|
||||
CalibrationKind kind;
|
||||
char model[64];
|
||||
unsigned count;
|
||||
RimPoint points[16];
|
||||
} Calibration;
|
||||
static Calibration calibration;
|
||||
static char tv_model[64];
|
||||
static unsigned calibration_generation=1;
|
||||
static inline unsigned calibration_revision(void){return calibration_generation;}
|
||||
static const RimPoint g4_rim[]={
|
||||
{0.2078431373,7},{0.3176470588,3},{0.5490196078,11},{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){
|
||||
const char *keys[]={"auto","g4","average","custom"};return keys[kind];
|
||||
static unsigned calibration_generation = 1;
|
||||
static inline unsigned calibration_revision(void) {
|
||||
return calibration_generation;
|
||||
}
|
||||
static inline bool calibration_model_valid(const char *model){
|
||||
if(!model[0] || strlen(model)>63)return false;
|
||||
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;
|
||||
static const RimPoint g4_rim[] = {{0.2078431373, 7}, {0.3176470588, 3}, {0.5490196078, 11},
|
||||
{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) {
|
||||
const char *keys[] = {"auto", "g4", "average", "custom"};
|
||||
return keys[kind];
|
||||
}
|
||||
static inline bool calibration_model_valid(const char *model) {
|
||||
if (!model[0] || strlen(model) > 63)
|
||||
return false;
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
/* LG retail model identifiers: OLED + two-digit panel size + G4 series. */
|
||||
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';
|
||||
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';
|
||||
}
|
||||
static inline CalibrationKind calibration_effective(void){
|
||||
if(calibration.kind!=CAL_AUTO)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;
|
||||
static inline CalibrationKind calibration_effective(void) {
|
||||
if (calibration.kind != CAL_AUTO)
|
||||
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;
|
||||
}
|
||||
static inline bool calibration_parse(PlaybackJson *j,const char *text,Calibration *result){
|
||||
void *root=j->parse(text);if(!root)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;
|
||||
if(j->type(root)==4 && j->get(root,"points",&points)){
|
||||
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));
|
||||
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++){
|
||||
void *point=j->at(points,i),*position=playback_field(j,point,"position"),*region=playback_field(j,point,"region");
|
||||
valid=position && (j->type(position)==2 || j->type(position)==3) && region && j->type(region)==3;
|
||||
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;}
|
||||
static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibration *result) {
|
||||
void *root = j->parse(text);
|
||||
if (!root)
|
||||
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;
|
||||
if (j->type(root) == 4 && j->get(root, "points", &points)) {
|
||||
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));
|
||||
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++) {
|
||||
void *point = j->at(points, i), *position = playback_field(j, point, "position"),
|
||||
*region = playback_field(j, point, "region");
|
||||
valid = position && (j->type(position) == 2 || j->type(position) == 3) && region &&
|
||||
j->type(region) == 3;
|
||||
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){
|
||||
char temp[512],data[2048];int used=snprintf(data,sizeof(data),"{\"profile\":\"%s\"",calibration_key(value->kind));
|
||||
if(value->count){
|
||||
used+=snprintf(data+used,sizeof(data)-used,",\"model\":\"%s\",\"points\":[",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,"]");
|
||||
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->count) {
|
||||
used += snprintf(data + used, sizeof(data) - used, ",\"model\":\"%s\",\"points\":[",
|
||||
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,"}\n");
|
||||
if(snprintf(temp,sizeof(temp),"%s.tmp",path)>=(int)sizeof(temp))return false;
|
||||
int fd=open(temp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
||||
bool ok=write(fd,data,used)==used && fsync(fd)==0;if(close(fd)!=0)ok=false;
|
||||
if(ok)ok=rename(temp,path)==0;
|
||||
if(!ok)unlink(temp);
|
||||
used += snprintf(data + used, sizeof(data) - used, "}\n");
|
||||
if (snprintf(temp, sizeof(temp), "%s.tmp", path) >= (int)sizeof(temp))
|
||||
return false;
|
||||
int fd = open(temp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
bool ok = write(fd, data, used) == used && fsync(fd) == 0;
|
||||
if (close(fd) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = rename(temp, path) == 0;
|
||||
if (!ok)
|
||||
unlink(temp);
|
||||
return ok;
|
||||
}
|
||||
static inline void calibration_load(PlaybackJson *j,const char *path){
|
||||
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return;
|
||||
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_load(PlaybackJson *j, const char *path) {
|
||||
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return;
|
||||
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]){
|
||||
memcpy(output,input,48);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;
|
||||
static inline void calibration_colors(const unsigned char input[16][3],
|
||||
unsigned char output[16][3]) {
|
||||
memcpy(output, input, 48);
|
||||
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
|
||||
@@ -0,0 +1,150 @@
|
||||
/* Capture scheduler and histogram processing. */
|
||||
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);
|
||||
stream_tick(&playback_json, now, handover_enabled && !retries.paused && !retries.blocked);
|
||||
bool owned = handover_tick();
|
||||
bridge_tick(now);
|
||||
if (owned)
|
||||
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 (!CALIBRATION_ACTIVE && 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);
|
||||
bool populated = !rc && histogram_populated(hist);
|
||||
if (!populated) {
|
||||
unsigned char held[16][3];
|
||||
color_valid = false;
|
||||
if (!continuity_frame(&continuity, false, rgb, now, held)) {
|
||||
set_error("Histogram data unavailable beyond hold window; recovering capture");
|
||||
release_capture(false);
|
||||
retry_source_transition(&retries, now);
|
||||
return 1;
|
||||
}
|
||||
char *line = region_command(held, led_blends, led_count,
|
||||
(unsigned)(1500 - (now - continuity.missing_at)));
|
||||
if (!line || send_line(line)) {
|
||||
free(line);
|
||||
set_error("Receiver connection lost while holding colors");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
free(line);
|
||||
if (!receiver_ack()) {
|
||||
set_error("Receiver did not acknowledge held colors");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
last_output_at = now;
|
||||
return 1;
|
||||
}
|
||||
sample_at = now;
|
||||
sample_sequence++;
|
||||
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 (!CALIBRATION_ACTIVE && 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], stable_rgb[16][3];
|
||||
calibration_colors(rgb, mapped_rgb);
|
||||
continuity_frame(&continuity, true, mapped_rgb, now, stable_rgb);
|
||||
interpolation_enabled = led_count && calibration_effective() != CAL_AVERAGE;
|
||||
char *line = region_command(stable_rgb, led_blends, led_count, 1500);
|
||||
if (!line) {
|
||||
set_error("Color buffer allocation failed");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
int failed = send_line(line);
|
||||
free(line);
|
||||
if (failed) {
|
||||
set_error("Receiver connection lost");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
if (!receiver_ack()) {
|
||||
set_error("Receiver did not acknowledge color data");
|
||||
stop_capture();
|
||||
return 1;
|
||||
}
|
||||
last_error[0] = 0;
|
||||
color_valid = true;
|
||||
last_output_at = milliseconds();
|
||||
frames++;
|
||||
return 1;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
#ifndef COLOR_BOOST_H
|
||||
#define COLOR_BOOST_H
|
||||
/* Preserve the original saturation and brightness. */
|
||||
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];
|
||||
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];
|
||||
}
|
||||
#endif
|
||||
+63
-22
@@ -1,35 +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;
|
||||
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;
|
||||
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 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));
|
||||
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;
|
||||
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;
|
||||
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
|
||||
+168
-61
@@ -2,77 +2,184 @@
|
||||
#define HYPERDRM_DIAGNOSTICS_H
|
||||
/* An independent nonblocking connection. Tests never send colors or acquire
|
||||
* capture. Work per timer iteration is bounded to keep output responsive. */
|
||||
typedef enum {TEST_IDLE,TEST_CONNECT,TEST_SEND,TEST_LOGIN,TEST_LAYOUT,TEST_DONE,TEST_FAILED} TestStage;
|
||||
typedef enum {
|
||||
TEST_IDLE,
|
||||
TEST_CONNECT,
|
||||
TEST_SEND,
|
||||
TEST_LOGIN,
|
||||
TEST_LAYOUT,
|
||||
TEST_DONE,
|
||||
TEST_FAILED
|
||||
} TestStage;
|
||||
typedef struct {
|
||||
int fd;TestStage stage,after_send;TargetConfig target;
|
||||
uint64_t started,deadline,elapsed;unsigned leds;
|
||||
char request[384],error[128];size_t sent,used;char *response;
|
||||
int fd;
|
||||
TestStage stage, after_send;
|
||||
TargetConfig target;
|
||||
uint64_t started, deadline, elapsed;
|
||||
unsigned leds;
|
||||
char request[384], error[128];
|
||||
size_t sent, used;
|
||||
char *response;
|
||||
LedBlend *mapping;
|
||||
} ReceiverProbe;
|
||||
static ReceiverProbe connection_test={.fd=-1};
|
||||
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 ReceiverProbe connection_test = {.fd = -1};
|
||||
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"};
|
||||
return names[probe->stage];
|
||||
}
|
||||
static bool probe_busy(const ReceiverProbe *probe){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){
|
||||
probe_close(probe);probe->elapsed=now-probe->started;
|
||||
probe->stage=error?TEST_FAILED:TEST_DONE;
|
||||
snprintf(probe->error,sizeof(probe->error),"%s",error?error:"");
|
||||
static bool probe_busy(const ReceiverProbe *probe) {
|
||||
return probe->stage >= TEST_CONNECT && probe->stage <= TEST_LAYOUT;
|
||||
}
|
||||
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);
|
||||
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 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 bool probe_begin(ReceiverProbe *probe,const TargetConfig *target,uint64_t now){
|
||||
if(probe_busy(probe))return false;
|
||||
probe_close(probe);memset(probe,0,sizeof(*probe));probe->fd=-1;
|
||||
probe->target=*target;probe->started=now;
|
||||
probe->response=malloc(2*1024*1024+1);
|
||||
if(!probe->response){probe_finish(probe,"Cannot allocate connection test buffer",now);return true;}
|
||||
probe->fd=target_open(target);
|
||||
if(probe->fd<0){probe_finish(probe,"Cannot open receiver connection",now);return true;}
|
||||
probe->stage=TEST_CONNECT;probe->deadline=now+3000;return true;
|
||||
static void probe_finish(ReceiverProbe *probe, const char *error, uint64_t now) {
|
||||
probe_close(probe);
|
||||
probe->elapsed = now - probe->started;
|
||||
probe->stage = error ? TEST_FAILED : TEST_DONE;
|
||||
snprintf(probe->error, sizeof(probe->error), "%s", error ? error : "");
|
||||
}
|
||||
static void probe_tick(ReceiverProbe *probe,PlaybackJson *json,uint64_t now){
|
||||
if(!probe_busy(probe))return;
|
||||
if(now>=probe->deadline){probe_finish(probe,"Receiver timed out",now);return;}
|
||||
int fd=probe->fd;
|
||||
if(probe->stage==TEST_CONNECT){
|
||||
struct pollfd p={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&p,1,0)<=0)return;
|
||||
if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){probe_finish(probe,"Receiver connection refused or unreachable",now);return;}
|
||||
probe_request(probe,probe->target.token[0]!=0,now);return;
|
||||
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);
|
||||
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) {
|
||||
if (probe_busy(probe))
|
||||
return false;
|
||||
probe_close(probe);
|
||||
memset(probe, 0, sizeof(*probe));
|
||||
probe->fd = -1;
|
||||
probe->target = *target;
|
||||
probe->started = now;
|
||||
probe->response = malloc(2 * 1024 * 1024 + 1);
|
||||
if (!probe->response) {
|
||||
probe_finish(probe, "Cannot allocate connection test buffer", now);
|
||||
return true;
|
||||
}
|
||||
if(probe->stage==TEST_SEND){
|
||||
size_t length=strlen(probe->request);
|
||||
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){probe_finish(probe,"Receiver closed the test connection",now);return;}
|
||||
probe->sent+=(size_t)n;
|
||||
if(probe->sent==length){probe->stage=probe->after_send;probe->deadline=now+3000;memset(probe->request,0,sizeof(probe->request));}
|
||||
probe->fd = target_open(target);
|
||||
if (probe->fd < 0) {
|
||||
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) {
|
||||
if (!probe_busy(probe))
|
||||
return;
|
||||
if (now >= probe->deadline) {
|
||||
probe_finish(probe, "Receiver timed out", now);
|
||||
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);
|
||||
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
|
||||
if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;}
|
||||
probe->used+=(size_t)n;probe->response[probe->used]=0;
|
||||
char *end=strchr(probe->response,'\n');if(!end)return;*end=0;
|
||||
void *root=json->parse(probe->response);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;
|
||||
int fd = probe->fd;
|
||||
if (probe->stage == TEST_CONNECT) {
|
||||
struct pollfd p = {fd, POLLOUT, 0};
|
||||
int error = 0;
|
||||
socklen_t size = sizeof(error);
|
||||
if (poll(&p, 1, 0) <= 0)
|
||||
return;
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || error) {
|
||||
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) {
|
||||
size_t length = strlen(probe->request);
|
||||
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) {
|
||||
probe_finish(probe, "Receiver closed the test connection", now);
|
||||
return;
|
||||
}
|
||||
probe->sent += (size_t)n;
|
||||
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);
|
||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||
return;
|
||||
if (n <= 0) {
|
||||
probe_finish(probe, "Receiver closed the test connection", now);
|
||||
return;
|
||||
}
|
||||
probe->used += (size_t)n;
|
||||
probe->response[probe->used] = 0;
|
||||
char *end = strchr(probe->response, '\n');
|
||||
if (!end)
|
||||
return;
|
||||
*end = 0;
|
||||
void *root = json->parse(probe->response);
|
||||
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
|
||||
+244
-94
@@ -5,22 +5,32 @@
|
||||
#define CALIBRATION_ACTIVE false
|
||||
#endif
|
||||
#ifndef PICCAP_BINARY
|
||||
#define PICCAP_BINARY "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos"
|
||||
#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"
|
||||
#define HANDOVER_PREF \
|
||||
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \
|
||||
"handover.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 uint64_t protection_at,next_media_probe,next_piccap_probe,handover_cooldown,piccap_action_at;
|
||||
typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource;
|
||||
static uint64_t protection_at, next_media_probe, next_piccap_probe, handover_cooldown,
|
||||
piccap_action_at;
|
||||
typedef enum {
|
||||
SOURCE_HISTOGRAM,
|
||||
SOURCE_PICCAP_STARTING,
|
||||
SOURCE_PICCAP,
|
||||
SOURCE_PICCAP_STOPPING
|
||||
} CaptureSource;
|
||||
static CaptureSource capture_source;
|
||||
static void *service_handle;
|
||||
static bool (*call_once)(void*,const char*,const char*,Callback,void*,unsigned long*,LSError*);
|
||||
static bool (*cancel_call)(void*,unsigned long,LSError*);
|
||||
static unsigned long piccap_token,media_token;
|
||||
static uint64_t piccap_deadline,media_deadline;
|
||||
static unsigned clear_samples,piccap_fallbacks;
|
||||
static uint64_t clear_since,unknown_since,piccap_disconnected_since;
|
||||
static bool (*call_once)(void *, const char *, const char *, Callback, void *, unsigned long *,
|
||||
LSError *);
|
||||
static bool (*cancel_call)(void *, unsigned long, LSError *);
|
||||
static unsigned long piccap_token, media_token;
|
||||
static uint64_t piccap_deadline, media_deadline;
|
||||
static unsigned clear_samples, piccap_fallbacks;
|
||||
static uint64_t clear_since, unknown_since, piccap_disconnected_since;
|
||||
#define CLEAR_STABLE_MS 2000
|
||||
#define UNKNOWN_GRACE_MS 1500
|
||||
#define PICCAP_LOSS_MS 5000
|
||||
@@ -28,104 +38,244 @@ static uint64_t clear_since,unknown_since,piccap_disconnected_since;
|
||||
static TargetConfig piccap_target;
|
||||
static char piccap_error[128];
|
||||
static bool piccap_request(int command);
|
||||
static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;}
|
||||
static bool handover_save(bool enabled){
|
||||
const char *data=enabled?"{\"piccapHandover\":true}\n":"{\"piccapHandover\":false}\n";
|
||||
int fd=open(HANDOVER_PREF ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
||||
ssize_t n=write(fd,data,strlen(data));bool ok=n==(ssize_t)strlen(data) && fsync(fd)==0;
|
||||
if(close(fd)!=0)ok=false;
|
||||
if(ok)ok=rename(HANDOVER_PREF ".tmp",HANDOVER_PREF)==0;
|
||||
if(!ok)unlink(HANDOVER_PREF ".tmp");
|
||||
static inline bool piccap_installed(void) {
|
||||
return access(PICCAP_BINARY, X_OK) == 0;
|
||||
}
|
||||
static bool handover_save(bool enabled) {
|
||||
const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n";
|
||||
int fd = open(HANDOVER_PREF ".tmp", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
ssize_t n = write(fd, data, strlen(data));
|
||||
bool ok = n == (ssize_t)strlen(data) && fsync(fd) == 0;
|
||||
if (close(fd) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = rename(HANDOVER_PREF ".tmp", HANDOVER_PREF) == 0;
|
||||
if (!ok)
|
||||
unlink(HANDOVER_PREF ".tmp");
|
||||
return ok;
|
||||
}
|
||||
static void handover_load(void){
|
||||
int fd=open(HANDOVER_PREF,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)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 void handover_load(void) {
|
||||
int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
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){
|
||||
(void)h;(void)ctx;media_busy=false;media_token=0;
|
||||
protection=playback_classify(&playback_json,payload(m));protection_at=milliseconds();
|
||||
if(protection==PLAYBACK_CLEAR){
|
||||
if(!clear_samples)clear_since=protection_at;
|
||||
if(clear_samples<3)clear_samples++;
|
||||
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;}
|
||||
static bool media_reply(void *h, void *m, void *ctx) {
|
||||
(void)h;
|
||||
(void)ctx;
|
||||
media_busy = false;
|
||||
media_token = 0;
|
||||
protection = playback_classify(&playback_json, payload(m));
|
||||
protection_at = milliseconds();
|
||||
if (protection == PLAYBACK_CLEAR) {
|
||||
if (!clear_samples)
|
||||
clear_since = protection_at;
|
||||
if (clear_samples < 3)
|
||||
clear_samples++;
|
||||
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;
|
||||
}
|
||||
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before VT reuse. */
|
||||
static bool piccap_reply(void *h,void *m,void *ctx){
|
||||
(void)h;int command=(int)(intptr_t)ctx;piccap_busy=false;piccap_token=0;
|
||||
void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue");
|
||||
if(command==0){
|
||||
void *running=playback_field(&playback_json,root,"isRunning");
|
||||
if(ok && running && playback_json.type(running)==1){
|
||||
bool active=playback_json.boolean(running);
|
||||
piccap_connected=playback_true(&playback_json,root,"connected");
|
||||
if(active){
|
||||
if(capture_source==SOURCE_PICCAP_STARTING)capture_source=SOURCE_PICCAP;
|
||||
if(piccap_connected)piccap_disconnected_since=0;
|
||||
else if(!piccap_disconnected_since)piccap_disconnected_since=milliseconds();
|
||||
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before
|
||||
* VT reuse. */
|
||||
static bool piccap_reply(void *h, void *m, void *ctx) {
|
||||
(void)h;
|
||||
int command = (int)(intptr_t)ctx;
|
||||
piccap_busy = false;
|
||||
piccap_token = 0;
|
||||
void *root = playback_json.parse(payload(m));
|
||||
bool ok = playback_true(&playback_json, root, "returnValue");
|
||||
if (command == 0) {
|
||||
void *running = playback_field(&playback_json, root, "isRunning");
|
||||
if (ok && running && playback_json.type(running) == 1) {
|
||||
bool active = playback_json.boolean(running);
|
||||
piccap_connected = playback_true(&playback_json, root, "connected");
|
||||
if (active) {
|
||||
if (capture_source == SOURCE_PICCAP_STARTING)
|
||||
capture_source = SOURCE_PICCAP;
|
||||
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){
|
||||
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 (!piccap_installed()) {
|
||||
piccap_owned = false;
|
||||
capture_source = SOURCE_HISTOGRAM;
|
||||
piccap_connected = false;
|
||||
}
|
||||
} else if (command == 3 && capture_source == SOURCE_PICCAP_STARTING) {
|
||||
void *port = playback_field(&playback_json, root, "port"),
|
||||
*socket = playback_field(&playback_json, root, "unix-socket");
|
||||
bool same =
|
||||
ok &&
|
||||
playback_is(&playback_json, root, "address", target_piccap_host(&piccap_target)) &&
|
||||
port && playback_json.type(port) == 3 &&
|
||||
strtoul(playback_json.string(port), NULL, 10) == piccap_target.piccap_port && socket &&
|
||||
playback_json.type(socket) == 1 && !playback_json.boolean(socket);
|
||||
if (ok) {
|
||||
if (!piccap_request(same ? 1 : 4)) {
|
||||
capture_source = SOURCE_PICCAP_STOPPING;
|
||||
snprintf(piccap_error, sizeof(piccap_error), "PicCap endpoint request failed");
|
||||
}
|
||||
} else {
|
||||
capture_source = SOURCE_PICCAP_STOPPING;
|
||||
snprintf(piccap_error, sizeof(piccap_error), "Cannot read PicCap settings");
|
||||
}
|
||||
} else if (command == 4) {
|
||||
if (!ok) {
|
||||
capture_source = SOURCE_PICCAP_STOPPING;
|
||||
snprintf(piccap_error, sizeof(piccap_error), "PicCap rejected receiver settings");
|
||||
}
|
||||
}else if(!piccap_installed()){piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;}
|
||||
}else if(command==3 && capture_source==SOURCE_PICCAP_STARTING){
|
||||
void *port=playback_field(&playback_json,root,"port"),*socket=playback_field(&playback_json,root,"unix-socket");
|
||||
bool same=ok && playback_is(&playback_json,root,"address",target_piccap_host(&piccap_target)) && port && playback_json.type(port)==3 && strtoul(playback_json.string(port),NULL,10)==piccap_target.piccap_port && socket && playback_json.type(socket)==1 && !playback_json.boolean(socket);
|
||||
if(ok){if(!piccap_request(same?1:4)){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap endpoint request failed");}}
|
||||
else {capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"Cannot read PicCap settings");}
|
||||
}else if(command==4){
|
||||
if(!ok){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap rejected receiver settings");}
|
||||
/* setSettings restarts PicCap itself; status confirms ownership. */
|
||||
}else if(command==1 && !ok){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=milliseconds()+10000;}
|
||||
if(command>=3 && capture_source==SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
|
||||
if(root)playback_json.put(root);
|
||||
next_piccap_probe=milliseconds()+500;
|
||||
if(command==2)piccap_request(0);
|
||||
} else if (command == 1 && !ok) {
|
||||
capture_source = SOURCE_PICCAP_STOPPING;
|
||||
handover_cooldown = milliseconds() + 10000;
|
||||
}
|
||||
if (command >= 3 && capture_source == SOURCE_PICCAP_STOPPING)
|
||||
handover_cooldown = milliseconds() + 10000;
|
||||
if (root)
|
||||
playback_json.put(root);
|
||||
next_piccap_probe = milliseconds() + 500;
|
||||
if (command == 2)
|
||||
piccap_request(0);
|
||||
return true;
|
||||
}
|
||||
static bool piccap_request(int command){
|
||||
if(piccap_busy)return false;
|
||||
const char *uri=command==3?"luna://org.webosbrew.piccap.service/getSettings":command==4?"luna://org.webosbrew.piccap.service/setSettings":command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
|
||||
LSError e;error_init(&e);
|
||||
char data[160];snprintf(data,sizeof(data),"{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",target_piccap_host(&piccap_target),piccap_target.piccap_port);
|
||||
piccap_busy=call_once(service_handle,uri,command==4?data:"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
|
||||
piccap_deadline=milliseconds()+3000;
|
||||
error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy;
|
||||
static bool piccap_request(int command) {
|
||||
if (piccap_busy)
|
||||
return false;
|
||||
const char *uri = command == 3 ? "luna://org.webosbrew.piccap.service/getSettings"
|
||||
: command == 4 ? "luna://org.webosbrew.piccap.service/setSettings"
|
||||
: command == 1 ? "luna://org.webosbrew.piccap.service/start"
|
||||
: command == 2 ? "luna://org.webosbrew.piccap.service/stop"
|
||||
: "luna://org.webosbrew.piccap.service/status";
|
||||
LSError e;
|
||||
error_init(&e);
|
||||
char data[160];
|
||||
snprintf(data, sizeof(data), "{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",
|
||||
target_piccap_host(&piccap_target), piccap_target.piccap_port);
|
||||
piccap_busy = call_once(service_handle, uri, command == 4 ? data : "{}", piccap_reply,
|
||||
(void *)(intptr_t)command, &piccap_token, &e);
|
||||
piccap_deadline = milliseconds() + 3000;
|
||||
error_free(&e);
|
||||
if (command)
|
||||
piccap_action_at = milliseconds();
|
||||
return piccap_busy;
|
||||
}
|
||||
/* Return true while PicCap owns capture or a stop confirmation is pending. */
|
||||
static bool handover_tick(void){
|
||||
uint64_t now=milliseconds();
|
||||
if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;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;
|
||||
error_free(&e);next_media_probe=now+1000;
|
||||
static bool handover_tick(void) {
|
||||
uint64_t now = milliseconds();
|
||||
if (media_busy && now >= media_deadline) {
|
||||
LSError e;
|
||||
error_init(&e);
|
||||
cancel_call(service_handle, media_token, &e);
|
||||
error_free(&e);
|
||||
media_busy = false;
|
||||
media_token = 0;
|
||||
protection = PLAYBACK_UNKNOWN;
|
||||
clear_samples = 0;
|
||||
clear_since = 0;
|
||||
if (!unknown_since)
|
||||
unknown_since = now;
|
||||
}
|
||||
if(protection==PLAYBACK_CLEAR && now-protection_at>=3000){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 stable=clear && clear_samples>=3 && now-clear_since>=CLEAR_STABLE_MS && now>=handover_cooldown;
|
||||
if(capture_source==SOURCE_HISTOGRAM && stable){
|
||||
piccap_target=target;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;}
|
||||
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;
|
||||
}
|
||||
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){
|
||||
snprintf(piccap_error,sizeof(piccap_error),"PicCap receiver disconnected; histogram fallback");piccap_fallbacks++;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;
|
||||
if (handover_enabled && piccap_installed() && !retries.paused && playback_json.lib &&
|
||||
!media_busy && now >= next_media_probe) {
|
||||
LSError e;
|
||||
error_init(&e);
|
||||
media_busy = call_once(service_handle, "luna://com.webos.media/getActivePipelines", "{}",
|
||||
media_reply, NULL, &media_token, &e);
|
||||
media_deadline = now + 3000;
|
||||
error_free(&e);
|
||||
next_media_probe = now + 1000;
|
||||
}
|
||||
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(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0);
|
||||
next_piccap_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;
|
||||
}
|
||||
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 stable = clear && clear_samples >= 3 && now - clear_since >= CLEAR_STABLE_MS &&
|
||||
now >= handover_cooldown;
|
||||
if (capture_source == SOURCE_HISTOGRAM && stable) {
|
||||
piccap_target = target;
|
||||
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;
|
||||
if (lost) {
|
||||
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 && !piccap_busy && now >= next_piccap_probe) {
|
||||
if (capture_source == SOURCE_PICCAP_STOPPING)
|
||||
piccap_request(2);
|
||||
else
|
||||
piccap_request(0);
|
||||
next_piccap_probe = now + 1000;
|
||||
}
|
||||
return piccap_owned;
|
||||
}
|
||||
|
||||
+12
-6
@@ -4,12 +4,18 @@
|
||||
#include <stddef.h>
|
||||
/* LG ABI: region ID followed by three planar channels, 256 uint16 bins
|
||||
* per channel. Bin index is the 8-bit intensity (0..255). */
|
||||
typedef struct { uint32_t id; uint16_t bins[3][256]; } Histogram;
|
||||
_Static_assert(sizeof(Histogram)==0x604,"LG ABI record size");
|
||||
_Static_assert(offsetof(Histogram,bins)==4,"LG ABI channel offset");
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint16_t bins[3][256];
|
||||
} Histogram;
|
||||
_Static_assert(sizeof(Histogram) == 0x604, "LG ABI record size");
|
||||
_Static_assert(offsetof(Histogram, bins) == 4, "LG ABI channel offset");
|
||||
static inline unsigned char histogram_mean(const uint16_t bins[256]) {
|
||||
uint64_t count=0,weight=0;
|
||||
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;
|
||||
uint64_t count = 0, weight = 0;
|
||||
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;
|
||||
}
|
||||
#endif
|
||||
@@ -1,6 +1,10 @@
|
||||
#ifndef 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]){
|
||||
for(unsigned c=0;c<3;c++)out[c]=(unsigned char)(((unsigned)a[c]*(65535-weight)+(unsigned)b[c]*weight+32767)/65535);
|
||||
static inline void blend_color(const unsigned char a[3], const unsigned char b[3], unsigned weight,
|
||||
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
|
||||
+78
-40
@@ -5,54 +5,92 @@
|
||||
#include <stdlib.h>
|
||||
#include <dlfcn.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 <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include "calibration.h"
|
||||
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;
|
||||
for(unsigned i=1;i<4;i++)if(distances[i]<distances[edge])edge=i;
|
||||
return positions[edge]>=4?positions[edge]-4:positions[edge];
|
||||
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;
|
||||
for (unsigned i = 1; i < 4; i++)
|
||||
if (distances[i] < distances[edge])
|
||||
edge = i;
|
||||
return positions[edge] >= 4 ? positions[edge] - 4 : positions[edge];
|
||||
}
|
||||
static LedBlend layout_blend(double x,double y){
|
||||
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;
|
||||
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;
|
||||
if(span<=0)span+=4;
|
||||
if(distance<0)distance+=4;
|
||||
return (LedBlend){rim[left].region,rim[right].region,(unsigned)(distance/span*65535+0.5)};
|
||||
static LedBlend layout_blend(double x, double y) {
|
||||
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;
|
||||
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;
|
||||
if (span <= 0)
|
||||
span += 4;
|
||||
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
|
||||
* layout. The safety limit is independent of the installed LED 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 *(*parse)(const char*)=dlsym(lib,"json_tokener_parse");
|
||||
int (*get)(const void*,const char*,void**)=dlsym(lib,"json_object_object_get_ex");
|
||||
int (*type)(const void*)=dlsym(lib,"json_object_get_type");
|
||||
size_t (*length)(const void*)=dlsym(lib,"json_object_array_length");
|
||||
void *(*at)(const void*,size_t)=dlsym(lib,"json_object_array_get_idx");
|
||||
double (*number)(const void*)=dlsym(lib,"json_object_get_double");
|
||||
int (*put)(void*)=dlsym(lib,"json_object_put");
|
||||
if(!parse||!get||!type||!length||!at||!number||!put){dlclose(lib);return false;}
|
||||
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"};
|
||||
for(size_t i=0;valid && i<n;i++){
|
||||
void *led=at(leds,i);double coordinates[4];valid=led && type(led)==4;
|
||||
for(unsigned c=0;valid && c<4;c++){
|
||||
void *v=NULL;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);
|
||||
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 *(*parse)(const char *) = dlsym(lib, "json_tokener_parse");
|
||||
int (*get)(const void *, const char *, void **) = dlsym(lib, "json_object_object_get_ex");
|
||||
int (*type)(const void *) = dlsym(lib, "json_object_get_type");
|
||||
size_t (*length)(const void *) = dlsym(lib, "json_object_array_length");
|
||||
void *(*at)(const void *, size_t) = dlsym(lib, "json_object_array_get_idx");
|
||||
double (*number)(const void *) = dlsym(lib, "json_object_get_double");
|
||||
int (*put)(void *) = dlsym(lib, "json_object_put");
|
||||
if (!parse || !get || !type || !length || !at || !number || !put) {
|
||||
dlclose(lib);
|
||||
if(!valid){free(mapping);return false;}
|
||||
*result=mapping;*count=(unsigned)n;return true;
|
||||
return false;
|
||||
}
|
||||
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"};
|
||||
for (size_t i = 0; valid && i < n; i++) {
|
||||
void *led = at(leds, i);
|
||||
double coordinates[4];
|
||||
valid = led && type(led) == 4;
|
||||
for (unsigned c = 0; valid && c < 4; c++) {
|
||||
void *v = NULL;
|
||||
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);
|
||||
dlclose(lib);
|
||||
if (!valid) {
|
||||
free(mapping);
|
||||
return false;
|
||||
}
|
||||
*result = mapping;
|
||||
*count = (unsigned)n;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
+41
-22
@@ -1,34 +1,53 @@
|
||||
#ifndef HYPERDRM_LAYOUT_REFRESH_H
|
||||
#define HYPERDRM_LAYOUT_REFRESH_H
|
||||
#define LAYOUT_REFRESH_MS 5000
|
||||
static ReceiverProbe layout_probe={.fd=-1};
|
||||
static uint64_t next_layout_check,last_layout_check;
|
||||
static unsigned layout_updates,layout_probe_generation;
|
||||
static ReceiverProbe layout_probe = {.fd = -1};
|
||||
static uint64_t next_layout_check, last_layout_check;
|
||||
static unsigned layout_updates, layout_probe_generation;
|
||||
static char layout_error[128];
|
||||
static void layout_refresh_reset(uint64_t due){
|
||||
probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;next_layout_check=due;
|
||||
static void layout_refresh_reset(uint64_t due) {
|
||||
probe_close(&layout_probe);
|
||||
layout_probe.stage = TEST_IDLE;
|
||||
next_layout_check = due;
|
||||
}
|
||||
static bool layout_mapping_changed(const LedBlend *next,unsigned count){
|
||||
if(count!=led_count || !led_blends)return true;
|
||||
for(unsigned i=0;i<count;i++)if(next[i].a!=led_blends[i].a || next[i].b!=led_blends[i].b || next[i].weight!=led_blends[i].weight)return true;
|
||||
static bool layout_mapping_changed(const LedBlend *next, unsigned count) {
|
||||
if (count != led_count || !led_blends)
|
||||
return true;
|
||||
for (unsigned i = 0; i < count; i++)
|
||||
if (next[i].a != led_blends[i].a || next[i].b != led_blends[i].b ||
|
||||
next[i].weight != led_blends[i].weight)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
static void layout_refresh_tick(PlaybackJson *json,uint64_t now,bool running){
|
||||
if(!running){layout_refresh_reset(now+LAYOUT_REFRESH_MS);return;}
|
||||
probe_tick(&layout_probe,json,now);
|
||||
if(layout_probe.stage==TEST_DONE){
|
||||
if(layout_probe_generation==calibration_generation){
|
||||
if(layout_mapping_changed(layout_probe.mapping,layout_probe.leds)){
|
||||
free(led_blends);led_blends=layout_probe.mapping;layout_probe.mapping=NULL;led_count=layout_probe.leds;layout_updates++;
|
||||
static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running) {
|
||||
if (!running) {
|
||||
layout_refresh_reset(now + LAYOUT_REFRESH_MS);
|
||||
return;
|
||||
}
|
||||
last_layout_check=now;layout_error[0]=0;
|
||||
}else next_layout_check=now;
|
||||
probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;
|
||||
}else if(layout_probe.stage==TEST_FAILED){
|
||||
snprintf(layout_error,sizeof(layout_error),"%s",layout_probe.error);layout_probe.stage=TEST_IDLE;
|
||||
probe_tick(&layout_probe, json, now);
|
||||
if (layout_probe.stage == TEST_DONE) {
|
||||
if (layout_probe_generation == calibration_generation) {
|
||||
if (layout_mapping_changed(layout_probe.mapping, layout_probe.leds)) {
|
||||
free(led_blends);
|
||||
led_blends = layout_probe.mapping;
|
||||
layout_probe.mapping = NULL;
|
||||
led_count = layout_probe.leds;
|
||||
layout_updates++;
|
||||
}
|
||||
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;
|
||||
last_layout_check = now;
|
||||
layout_error[0] = 0;
|
||||
} else
|
||||
next_layout_check = now;
|
||||
probe_close(&layout_probe);
|
||||
layout_probe.stage = TEST_IDLE;
|
||||
} else if (layout_probe.stage == TEST_FAILED) {
|
||||
snprintf(layout_error, sizeof(layout_error), "%s", layout_probe.error);
|
||||
layout_probe.stage = TEST_IDLE;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+77
-31
@@ -5,43 +5,89 @@
|
||||
#include <string.h>
|
||||
#include <dlfcn.h>
|
||||
typedef struct {
|
||||
void *lib;void *(*parse)(const char*);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 *lib;
|
||||
void *(*parse)(const char *);
|
||||
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 *);
|
||||
} PlaybackJson;
|
||||
static inline bool playback_json_init(PlaybackJson *j){
|
||||
j->lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!j->lib)return false;
|
||||
j->parse=dlsym(j->lib,"json_tokener_parse");j->get=dlsym(j->lib,"json_object_object_get_ex");
|
||||
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");
|
||||
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;
|
||||
static inline bool playback_json_init(PlaybackJson *j) {
|
||||
j->lib = dlopen("libjson-c.so.5", RTLD_NOW);
|
||||
if (!j->lib)
|
||||
return false;
|
||||
j->parse = dlsym(j->lib, "json_tokener_parse");
|
||||
j->get = dlsym(j->lib, "json_object_object_get_ex");
|
||||
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");
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
/* G4 resource-state classification, not black-frame or app-name guessing.
|
||||
* Any active Secure Video Path wins. Only a foreground media decoder without
|
||||
* a secure resource is eligible; HDMI, idle and malformed replies are unknown. */
|
||||
static inline PlaybackProtection playback_classify(PlaybackJson *j,const char *text){
|
||||
void *root=j->parse(text);if(!root)return PLAYBACK_UNKNOWN;
|
||||
bool clear=false,invalid=j->type(root)!=5,secure=false;
|
||||
size_t n=invalid?0:j->length(root);
|
||||
for(size_t i=0;i<n;i++){
|
||||
void *p=j->at(root,i);if(!p || j->type(p)!=4){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;
|
||||
for(size_t k=0;k<j->length(resources);k++){
|
||||
void *resource=j->at(resources,k);void *name=playback_field(j,resource,"resource");
|
||||
if(!name || j->type(name)!=6){invalid=true;continue;}
|
||||
const char *s=j->string(name);
|
||||
if(strcmp(s,"SVP_CPB")==0)secure=true;
|
||||
if(strcmp(s,"VDEC")==0)decoder=true;
|
||||
static inline PlaybackProtection playback_classify(PlaybackJson *j, const char *text) {
|
||||
void *root = j->parse(text);
|
||||
if (!root)
|
||||
return PLAYBACK_UNKNOWN;
|
||||
bool clear = false, invalid = j->type(root) != 5, secure = false;
|
||||
size_t n = invalid ? 0 : j->length(root);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
void *p = j->at(root, i);
|
||||
if (!p || j->type(p) != 4) {
|
||||
invalid = true;
|
||||
continue;
|
||||
}
|
||||
if(decoder && playback_true(j,p,"is_foreground") && playback_is(j,p,"type","media"))clear=true;
|
||||
void *resources = playback_field(j, p, "resource");
|
||||
if (!resources)
|
||||
resources = playback_field(j, p, "resources");
|
||||
if (!resources || j->type(resources) != 5) {
|
||||
invalid = true;
|
||||
continue;
|
||||
}
|
||||
j->put(root);return secure?PLAYBACK_PROTECTED:clear&&!invalid?PLAYBACK_CLEAR:PLAYBACK_UNKNOWN;
|
||||
bool decoder = false;
|
||||
for (size_t k = 0; k < j->length(resources); k++) {
|
||||
void *resource = j->at(resources, k);
|
||||
void *name = playback_field(j, resource, "resource");
|
||||
if (!name || j->type(name) != 6) {
|
||||
invalid = true;
|
||||
continue;
|
||||
}
|
||||
const char *s = j->string(name);
|
||||
if (strcmp(s, "SVP_CPB") == 0)
|
||||
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;
|
||||
}
|
||||
j->put(root);
|
||||
return secure ? PLAYBACK_PROTECTED : clear && !invalid ? PLAYBACK_CLEAR : PLAYBACK_UNKNOWN;
|
||||
}
|
||||
#endif
|
||||
+211
-55
@@ -5,67 +5,223 @@
|
||||
#endif
|
||||
/* Independent raw LED subscription: bounded, nonblocking, before receiver
|
||||
* color correction. Old result and newer data envelopes are both accepted. */
|
||||
typedef struct {int fd,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;
|
||||
#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 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=1;stream.deadline=now+3000;stream.revision=calibration_revision();stream.layout_revision=RECEIVER_LAYOUT_REVISION;
|
||||
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 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];}
|
||||
static void stream_start(uint64_t now) {
|
||||
stream.fd = target_open(&target);
|
||||
if (stream.fd < 0) {
|
||||
stream.retry_at = now + 5000;
|
||||
return;
|
||||
}
|
||||
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;
|
||||
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 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<4 && now>=stream.deadline){stream_close(now);return;}
|
||||
if(stream.stage==1){
|
||||
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=2;}
|
||||
else {snprintf(line,sizeof(line),"{\"command\":\"serverinfo\"}\n");stream.stage=3;}
|
||||
if(!stream_queue(line)){stream_close(now);return;}
|
||||
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;
|
||||
}
|
||||
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;}
|
||||
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;
|
||||
}
|
||||
if(stream.used==262144){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))return;
|
||||
if(n<=0){stream_close(now);return;}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=j->parse(stream.data);
|
||||
if(stream.stage==2){if(!playback_true(j,root,"success")){if(root)j->put(root);stream_close(now);return;}stream.stage=3;stream_queue("{\"command\":\"serverinfo\"}\n");stream.deadline=now+3000;}
|
||||
else if(stream.stage==3){
|
||||
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=4;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;
|
||||
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)
|
||||
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 = j->parse(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
|
||||
+41
-17
@@ -5,26 +5,50 @@
|
||||
#include <limits.h>
|
||||
#define RETRY_DELAY_MS 5000
|
||||
/* Continuous autostart retries and bounded manual source recovery. */
|
||||
typedef struct {uint64_t due;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){
|
||||
bool previous=state->automatic;
|
||||
state->automatic=enabled && !state->paused && !state->blocked;
|
||||
if(previous && !state->automatic){state->due=0;state->attempts=0;}
|
||||
if(state->automatic && !running && !state->due)state->due=now+RETRY_DELAY_MS;
|
||||
typedef struct {
|
||||
uint64_t due;
|
||||
unsigned attempts, quick_retries;
|
||||
bool automatic, paused, blocked;
|
||||
} RetryState;
|
||||
static void retry_reset(RetryState *state) {
|
||||
*state = (RetryState){0};
|
||||
}
|
||||
static bool retry_take(RetryState *state,uint64_t now){
|
||||
if(!state->due || now<state->due || state->paused || state->blocked)return false;
|
||||
state->due=0;if(state->attempts<UINT_MAX)state->attempts++;return true;
|
||||
static void retry_pause(RetryState *state) {
|
||||
retry_reset(state);
|
||||
state->paused = true;
|
||||
}
|
||||
static void retry_failure(RetryState *state,uint64_t now){
|
||||
if(!state->paused && !state->blocked && (state->automatic || state->attempts<6)){
|
||||
if(state->quick_retries){state->quick_retries--;state->due=now+400;}
|
||||
else state->due=now+RETRY_DELAY_MS;
|
||||
static void retry_update(RetryState *state, bool enabled, bool running, uint64_t now) {
|
||||
bool previous = state->automatic;
|
||||
state->automatic = enabled && !state->paused && !state->blocked;
|
||||
if (previous && !state->automatic) {
|
||||
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) {
|
||||
if (!state->due || now < state->due || state->paused || state->blocked)
|
||||
return false;
|
||||
state->due = 0;
|
||||
if (state->attempts < UINT_MAX)
|
||||
state->attempts++;
|
||||
return true;
|
||||
}
|
||||
static void retry_failure(RetryState *state, uint64_t now) {
|
||||
if (!state->paused && !state->blocked && (state->automatic || state->attempts < 6)) {
|
||||
if (state->quick_retries) {
|
||||
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){
|
||||
if(!state->paused && !state->blocked){state->attempts=0;state->quick_retries=2;state->due=now+200;}
|
||||
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;
|
||||
}
|
||||
}
|
||||
#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
|
||||
+334
-329
@@ -17,6 +17,7 @@
|
||||
#include <sys/time.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
#include "version.h"
|
||||
#include "histogram.h"
|
||||
#include "blackbars.h"
|
||||
#include "color_boost.h"
|
||||
@@ -30,379 +31,383 @@
|
||||
#include "continuity.h"
|
||||
#define SERVICE "org.webosbrew.histogramhyperion.service"
|
||||
#define PRIORITY 149
|
||||
typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError;
|
||||
typedef bool (*Callback)(void*,void*,void*);
|
||||
typedef struct {const char *name;Callback function;int flags;} Method;
|
||||
static bool (*reply)(void*,void*,const char*,LSError*);
|
||||
static const char *(*payload)(void*);
|
||||
static bool (*error_init)(LSError*);
|
||||
static void (*error_free)(LSError*);
|
||||
static void *(*vt_init)(bool(*)(void));
|
||||
static int (*vt_preset)(void*,int),(*vt_get)(void*,Histogram*),(*vt_finish)(void*);
|
||||
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 struct {
|
||||
const char *name;
|
||||
Callback function;
|
||||
int flags;
|
||||
} Method;
|
||||
static bool (*reply)(void *, void *, const char *, LSError *);
|
||||
static const char *(*payload)(void *);
|
||||
static bool (*error_init)(LSError *);
|
||||
static void (*error_free)(LSError *);
|
||||
static void *(*vt_init)(bool (*)(void));
|
||||
static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *);
|
||||
static void *capture;
|
||||
static char lg_diagnostic[256];
|
||||
/* Process-local LG diagnostics; no changes to the loaded LG library. */
|
||||
void printLog(const char *context,int level,const char *message_id,
|
||||
const char *file,const char *function,int line,const char *format,...) {
|
||||
(void)context;(void)message_id;(void)file;
|
||||
if(capture && level>3)return;
|
||||
if(format){va_list args;va_start(args,format);vsnprintf(lg_diagnostic,sizeof(lg_diagnostic),format,args);va_end(args);}
|
||||
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);
|
||||
void printLog(const char *context, int level, const char *message_id, const char *file,
|
||||
const char *function, int line, const char *format, ...) {
|
||||
(void)context;
|
||||
(void)message_id;
|
||||
(void)file;
|
||||
if (capture && level > 3)
|
||||
return;
|
||||
if (format) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(lg_diagnostic, sizeof(lg_diagnostic), format, args);
|
||||
va_end(args);
|
||||
}
|
||||
fprintf(stderr, "LG %s:%d: ", function ? function : "?", line);
|
||||
if (format) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(stderr, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
static int output=-1;
|
||||
static int output = -1;
|
||||
static unsigned long frames;
|
||||
static unsigned char rgb[16][3];
|
||||
static unsigned char sent_rgb[3];
|
||||
static unsigned black_percent[16];
|
||||
static unsigned capture_preset;
|
||||
static unsigned letterbox_mode,settling;
|
||||
static int pending_letterbox=-1;
|
||||
static uint64_t candidate_since,next_border_probe;
|
||||
static unsigned letterbox_mode, settling;
|
||||
static int pending_letterbox = -1;
|
||||
static uint64_t candidate_since, next_border_probe;
|
||||
static bool color_valid;
|
||||
static Continuity continuity;
|
||||
static uint64_t sample_at,calibration_until;
|
||||
static uint64_t sample_at, calibration_until;
|
||||
static unsigned sample_sequence;
|
||||
#define CALIBRATION_ACTIVE (calibration_until>milliseconds())
|
||||
#define CALIBRATION_ACTIVE (calibration_until > milliseconds())
|
||||
static unsigned led_count;
|
||||
static LedBlend *led_blends;
|
||||
static bool interpolation_enabled;
|
||||
static RetryState retries;
|
||||
static unsigned capture_recoveries;
|
||||
static uint64_t recovery_started,last_recovery_ms,last_output_at;
|
||||
static uint64_t milliseconds(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000+t.tv_nsec/1000000;}
|
||||
static uint64_t recovery_started, last_recovery_ms, last_output_at;
|
||||
static uint64_t milliseconds(void) {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||
}
|
||||
static char last_error[256];
|
||||
static volatile sig_atomic_t revoked,quitting;
|
||||
static int guard_pipe=-1;
|
||||
static pid_t guard_pid=-1;
|
||||
static volatile sig_atomic_t revoked, quitting;
|
||||
static int guard_pipe = -1;
|
||||
static pid_t guard_pid = -1;
|
||||
#define DEV_MODE "/var/luna/preferences/vt_devmode"
|
||||
/* LG's developer gate is enabled only during initialization. A separate
|
||||
* guardian removes our own marker even if this process aborts or is killed.
|
||||
* An existing marker is preserved, as are files replaced by another owner. */
|
||||
static bool enter_developer_mode(void){
|
||||
int marker=open(DEV_MODE,O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC,0600);
|
||||
if(marker<0)return errno==EEXIST;
|
||||
static bool enter_developer_mode(void) {
|
||||
int marker = open(DEV_MODE, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
|
||||
if (marker < 0)
|
||||
return errno == EEXIST;
|
||||
struct stat owned;
|
||||
if(fstat(marker,&owned)!=0){close(marker);unlink(DEV_MODE);return false;}
|
||||
close(marker);int p[2];
|
||||
if(pipe(p)!=0){unlink(DEV_MODE);return false;}
|
||||
pid_t child=fork();
|
||||
if(child<0){close(p[0]);close(p[1]);unlink(DEV_MODE);return false;}
|
||||
if(child==0){
|
||||
close(p[1]);struct pollfd fd={p[0],POLLIN|POLLHUP,0};
|
||||
/* Bounded cleanup also covers an indefinitely blocked initialization. */
|
||||
poll(&fd,1,10000);close(p[0]);struct stat current;
|
||||
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);
|
||||
_exit(rc==0?0:1);
|
||||
if (fstat(marker, &owned) != 0) {
|
||||
close(marker);
|
||||
unlink(DEV_MODE);
|
||||
return false;
|
||||
}
|
||||
close(p[0]);guard_pipe=p[1];guard_pid=child;return true;
|
||||
close(marker);
|
||||
int p[2];
|
||||
if (pipe(p) != 0) {
|
||||
unlink(DEV_MODE);
|
||||
return false;
|
||||
}
|
||||
pid_t child = fork();
|
||||
if (child < 0) {
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
unlink(DEV_MODE);
|
||||
return false;
|
||||
}
|
||||
if (child == 0) {
|
||||
close(p[1]);
|
||||
struct pollfd fd = {p[0], POLLIN | POLLHUP, 0};
|
||||
/* Bounded cleanup also covers an indefinitely blocked initialization. */
|
||||
poll(&fd, 1, 10000);
|
||||
close(p[0]);
|
||||
struct stat current;
|
||||
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);
|
||||
_exit(rc == 0 ? 0 : 1);
|
||||
}
|
||||
close(p[0]);
|
||||
guard_pipe = p[1];
|
||||
guard_pid = child;
|
||||
return true;
|
||||
}
|
||||
static bool leave_developer_mode(void){
|
||||
if(guard_pipe<0)return true;
|
||||
close(guard_pipe);guard_pipe=-1;int status;
|
||||
while(waitpid(guard_pid,&status,0)<0){if(errno!=EINTR)return false;}
|
||||
guard_pid=-1;return WIFEXITED(status)&&WEXITSTATUS(status)==0;
|
||||
static bool leave_developer_mode(void) {
|
||||
if (guard_pipe < 0)
|
||||
return true;
|
||||
close(guard_pipe);
|
||||
guard_pipe = -1;
|
||||
int status;
|
||||
while (waitpid(guard_pid, &status, 0) < 0) {
|
||||
if (errno != EINTR)
|
||||
return false;
|
||||
}
|
||||
guard_pid = -1;
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
static void quit_signal(int s){(void)s;quitting=1;}
|
||||
static bool policy(void){revoked=1;return true;}
|
||||
static void set_error(const char *s){snprintf(last_error,sizeof(last_error),"%s",s);fprintf(stderr,"%s\n",s);}
|
||||
static int send_line(const char *line){
|
||||
size_t left=strlen(line);const char *p=line;
|
||||
while(left){ssize_t n=send(output,p,left,MSG_NOSIGNAL);if(n<=0)return -1;p+=n;left-=n;}
|
||||
static void quit_signal(int s) {
|
||||
(void)s;
|
||||
quitting = 1;
|
||||
}
|
||||
static bool policy(void) {
|
||||
revoked = 1;
|
||||
return true;
|
||||
}
|
||||
static void set_error(const char *s) {
|
||||
snprintf(last_error, sizeof(last_error), "%s", s);
|
||||
fprintf(stderr, "%s\n", s);
|
||||
}
|
||||
static int send_line(const char *line) {
|
||||
size_t left = strlen(line);
|
||||
const char *p = line;
|
||||
while (left) {
|
||||
ssize_t n = send(output, p, left, MSG_NOSIGNAL);
|
||||
if (n <= 0)
|
||||
return -1;
|
||||
p += n;
|
||||
left -= n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static bool receiver_ack(void){
|
||||
char answer[4096];size_t used=0;bool complete=false;
|
||||
while(used+1<sizeof(answer)){ssize_t n=recv(output,answer+used,sizeof(answer)-used-1,0);if(n<=0)break;used+=(size_t)n;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
|
||||
PlaybackJson json={0};if(!complete || !playback_json_init(&json))return false;
|
||||
void *root=json.parse(answer);bool ok=playback_true(&json,root,"success");if(root)json.put(root);dlclose(json.lib);return ok;
|
||||
}
|
||||
static bool read_layout(void){
|
||||
const size_t capacity=2*1024*1024;char *data=malloc(capacity+1);if(!data)return false;
|
||||
bool complete=false;size_t used=0;
|
||||
if(send_line("{\"command\":\"serverinfo\"}\n")==0){
|
||||
while(used<capacity){ssize_t n=recv(output,data+used,capacity-used,0);if(n<=0)break;used+=(size_t)n;data[used]=0;char *end=strchr(data,'\n');if(end){*end=0;complete=true;break;}}
|
||||
static bool receiver_ack(void) {
|
||||
char answer[4096];
|
||||
size_t used = 0;
|
||||
bool complete = false;
|
||||
while (used + 1 < sizeof(answer)) {
|
||||
ssize_t n = recv(output, answer + used, sizeof(answer) - used - 1, 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
used += (size_t)n;
|
||||
answer[used] = 0;
|
||||
if (strchr(answer, '\n')) {
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
LedBlend *mapping=NULL;unsigned count=0;bool ok=complete && layout_from_json(data,&mapping,&count);
|
||||
if(ok){free(led_blends);led_blends=mapping;led_count=count;}free(data);interpolation_enabled=ok && calibration_effective()!=CAL_AVERAGE;return ok;
|
||||
}
|
||||
static bool target_login(void){
|
||||
if(!target.token[0])return true;
|
||||
char request[384],answer[4096];size_t used=0;
|
||||
snprintf(request,sizeof(request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);
|
||||
if(send_line(request)!=0)return false;
|
||||
while(used+1<sizeof(answer)){
|
||||
ssize_t n=recv(output,answer+used,sizeof(answer)-used-1,0);if(n<=0)return false;
|
||||
used+=(size_t)n;answer[used]=0;if(strchr(answer,'\n'))break;
|
||||
}
|
||||
PlaybackJson json={0};if(!playback_json_init(&json))return false;
|
||||
void *root=json.parse(answer);bool ok=playback_true(&json,root,"success");if(root)json.put(root);dlclose(json.lib);return ok;
|
||||
PlaybackJson json = {0};
|
||||
if (!complete || !playback_json_init(&json))
|
||||
return false;
|
||||
void *root = json.parse(answer);
|
||||
bool ok = playback_true(&json, root, "success");
|
||||
if (root)
|
||||
json.put(root);
|
||||
dlclose(json.lib);
|
||||
return ok;
|
||||
}
|
||||
static bool read_layout(void) {
|
||||
const size_t capacity = 2 * 1024 * 1024;
|
||||
char *data = malloc(capacity + 1);
|
||||
if (!data)
|
||||
return false;
|
||||
bool complete = false;
|
||||
size_t used = 0;
|
||||
if (send_line("{\"command\":\"serverinfo\"}\n") == 0) {
|
||||
while (used < capacity) {
|
||||
ssize_t n = recv(output, data + used, capacity - used, 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
used += (size_t)n;
|
||||
data[used] = 0;
|
||||
char *end = strchr(data, '\n');
|
||||
if (end) {
|
||||
*end = 0;
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LedBlend *mapping = NULL;
|
||||
unsigned count = 0;
|
||||
bool ok = complete && layout_from_json(data, &mapping, &count);
|
||||
if (ok) {
|
||||
free(led_blends);
|
||||
led_blends = mapping;
|
||||
led_count = count;
|
||||
}
|
||||
free(data);
|
||||
interpolation_enabled = ok && calibration_effective() != CAL_AVERAGE;
|
||||
return ok;
|
||||
}
|
||||
static bool target_login(void) {
|
||||
if (!target.token[0])
|
||||
return true;
|
||||
char request[384], answer[4096];
|
||||
size_t used = 0;
|
||||
snprintf(request, sizeof(request),
|
||||
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
|
||||
target.token);
|
||||
if (send_line(request) != 0)
|
||||
return false;
|
||||
while (used + 1 < sizeof(answer)) {
|
||||
ssize_t n = recv(output, answer + used, sizeof(answer) - used - 1, 0);
|
||||
if (n <= 0)
|
||||
return false;
|
||||
used += (size_t)n;
|
||||
answer[used] = 0;
|
||||
if (strchr(answer, '\n'))
|
||||
break;
|
||||
}
|
||||
PlaybackJson json = {0};
|
||||
if (!playback_json_init(&json))
|
||||
return false;
|
||||
void *root = json.parse(answer);
|
||||
bool ok = playback_true(&json, root, "success");
|
||||
if (root)
|
||||
json.put(root);
|
||||
dlclose(json.lib);
|
||||
return ok;
|
||||
}
|
||||
static void layout_refresh_reset(uint64_t due);
|
||||
static void release_capture(bool clear_output){
|
||||
layout_refresh_reset(milliseconds()+5000);
|
||||
free(led_blends);led_blends=NULL;led_count=0;interpolation_enabled=false;
|
||||
color_valid=false;memset(rgb,0,sizeof(rgb));memset(sent_rgb,0,sizeof(sent_rgb));
|
||||
if(output>=0){if(clear_output && frames){char line[80];snprintf(line,sizeof(line),"{\"command\":\"clear\",\"priority\":%d}\n",PRIORITY);send_line(line);}close(output);output=-1;}
|
||||
if(capture){vt_finish(capture);capture=NULL;}
|
||||
static void release_capture(bool clear_output) {
|
||||
layout_refresh_reset(milliseconds() + 5000);
|
||||
free(led_blends);
|
||||
led_blends = NULL;
|
||||
led_count = 0;
|
||||
interpolation_enabled = false;
|
||||
color_valid = false;
|
||||
memset(rgb, 0, sizeof(rgb));
|
||||
memset(sent_rgb, 0, sizeof(sent_rgb));
|
||||
if (output >= 0) {
|
||||
if (clear_output && frames) {
|
||||
char line[80];
|
||||
snprintf(line, sizeof(line), "{\"command\":\"clear\",\"priority\":%d}\n", PRIORITY);
|
||||
send_line(line);
|
||||
}
|
||||
close(output);
|
||||
output = -1;
|
||||
}
|
||||
if (capture) {
|
||||
vt_finish(capture);
|
||||
capture = NULL;
|
||||
}
|
||||
}
|
||||
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 "layout_refresh.h"
|
||||
#define RECEIVER_LAYOUT_REVISION layout_updates
|
||||
#include "receiver_stream.h"
|
||||
#include "source_bridge.h"
|
||||
static bool calibration_session(void *h,void *m,void *ctx){
|
||||
(void)ctx;const char *p=payload(m);
|
||||
if(!strcmp(p,"{\"enabled\":true}")){
|
||||
calibration_until=milliseconds()+110000;
|
||||
if(capture && vt_preset(capture,0)!=0){calibration_until=0;return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot select full-screen calibration capture\"}");}
|
||||
capture_preset=letterbox_mode=0;settling=2;pending_letterbox=-1;
|
||||
}else if(!strcmp(p,"{\"enabled\":false}"))calibration_until=0;
|
||||
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static Calibration calibration_draft;
|
||||
static char calibration_message[256];
|
||||
static bool report_calibration(void *h,void *m,void *ctx){
|
||||
if(!playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}");
|
||||
(void)ctx;void *root=playback_json.parse(payload(m)),*message=playback_field(&playback_json,root,"message");
|
||||
bool valid=message && playback_json.type(message)==6 && strlen(playback_json.string(message))<sizeof(calibration_message);
|
||||
if(valid)snprintf(calibration_message,sizeof(calibration_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_draft=next;return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static bool model_busy;
|
||||
static unsigned long model_token;
|
||||
static uint64_t model_deadline,next_model_probe;
|
||||
static bool model_reply(void *h,void *m,void *ctx){
|
||||
(void)h;(void)ctx;model_busy=false;model_token=0;
|
||||
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());
|
||||
#include "service_api.h"
|
||||
#include "capture_loop.h"
|
||||
static void *symbol(void *lib, const char *name) {
|
||||
void *s = dlsym(lib, name);
|
||||
if (!s) {
|
||||
fprintf(stderr, "Missing symbol %s\n", name);
|
||||
exit(2);
|
||||
}
|
||||
if(root)playback_json.put(root);
|
||||
return true;
|
||||
return s;
|
||||
}
|
||||
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;
|
||||
int main(void) {
|
||||
signal(SIGTERM, quit_signal);
|
||||
signal(SIGINT, quit_signal);
|
||||
if (boot_enabled() && !set_boot(true))
|
||||
fprintf(stderr, "Own legacy boot hook migration failed\n");
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;calibration_until=0;retry_pause(&retries);
|
||||
bool bridging=bridge_phase!=0;stream_close(milliseconds());bridge_phase=0;
|
||||
memset(&continuity,0,sizeof(continuity));stop_capture();
|
||||
if(bridging){
|
||||
output=target_connect(&target);
|
||||
if(output>=0){if(target_login())send_line("{\"command\":\"clear\",\"priority\":149}\n");close(output);output=-1;}
|
||||
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");
|
||||
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);
|
||||
}
|
||||
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!=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 || piccap_owned || retries.due;
|
||||
stop_capture();stream_close(milliseconds());bridge_phase=0;memset(&continuity,0,sizeof(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}");
|
||||
}
|
||||
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_message[i] && k+2<sizeof(report);i++){char c=calibration_message[i];if(c=='"'||c=='\\')report[k++]='\\';if((unsigned char)c>=32)report[k++]=c;}report[k]=0;
|
||||
int used=snprintf(json,sizeof(json),"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\",\"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,"\"calibrationMessage\":\"%s\",",report);
|
||||
used+=snprintf(json+used,sizeof(json)-used,"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[",calibration_draft.model);
|
||||
for(unsigned i=0;i<calibration_draft.count;i++)used+=snprintf(json+used,sizeof(json)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",calibration_draft.points[i].position,calibration_draft.points[i].region);
|
||||
used+=snprintf(json+used,sizeof(json)-used,"]},\"receiverStreamConnected\":%s,",stream.stage==4?"true":"false");
|
||||
used+=snprintf(json+used,sizeof(json)-used,"\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s,\"sourceFade\":%s,\"calibrating\":%s,",sample_sequence,sample_at?(long long)(now-sample_at):-1,continuity.holding?"true":"false",(continuity.fading || bridge_phase==3)?"true":"false",CALIBRATION_ACTIVE?"true":"false");
|
||||
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.11.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);
|
||||
stream_tick(&playback_json,now,handover_enabled && !retries.paused && !retries.blocked);
|
||||
bool owned=handover_tick();bridge_tick(now);
|
||||
if(owned)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());
|
||||
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 (*category)(void *, const char *, Method *, void *, void *, LSError *) =
|
||||
symbol(ls, "LSRegisterCategory");
|
||||
bool (*attach)(void *, void *, LSError *) = symbol(ls, "LSGmainAttach");
|
||||
bool (*unreg)(void *, LSError *) = symbol(ls, "LSUnregister");
|
||||
void *(*loop_new)(void *, int) = symbol(glib, "g_main_loop_new");
|
||||
int (*iterate)(void *, int) = symbol(glib, "g_main_context_iteration");
|
||||
unsigned (*timer)(unsigned, int (*)(void *), void *) = symbol(glib, "g_timeout_add");
|
||||
void (*loop_unref)(void *) = symbol(glib, "g_main_loop_unref");
|
||||
void *handle = NULL, *loop = loop_new(NULL, 0);
|
||||
LSError e;
|
||||
error_init(&e);
|
||||
Method methods[] = {{"start", start, 0},
|
||||
{"stop", stop, 0},
|
||||
{"status", status, 0},
|
||||
{"autostart", autostart, 0},
|
||||
{"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;
|
||||
}
|
||||
return 1;
|
||||
service_handle = handle;
|
||||
if (!boot_enabled())
|
||||
retry_pause(&retries);
|
||||
timer(100, tick, NULL);
|
||||
while (!quitting) {
|
||||
iterate(NULL, 0);
|
||||
usleep(10000);
|
||||
}
|
||||
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(!CALIBRATION_ACTIVE && 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);
|
||||
bool populated=!rc && histogram_populated(hist);
|
||||
if(!populated){
|
||||
unsigned char held[16][3];
|
||||
color_valid=false;
|
||||
if(!continuity_frame(&continuity,false,rgb,now,held)){set_error("Histogram data unavailable beyond hold window; recovering capture");release_capture(false);retry_source_transition(&retries,now);return 1;}
|
||||
char *line=region_command(held,led_blends,led_count,(unsigned)(1500-(now-continuity.missing_at)));
|
||||
if(!line || send_line(line)){free(line);set_error("Receiver connection lost while holding colors");stop_capture();return 1;}free(line);
|
||||
if(!receiver_ack()){set_error("Receiver did not acknowledge held colors");stop_capture();return 1;}
|
||||
last_output_at=now;return 1;
|
||||
}
|
||||
sample_at=now;sample_sequence++;
|
||||
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;
|
||||
stream_close(milliseconds());
|
||||
test_close();
|
||||
stop_capture();
|
||||
if (piccap_owned) {
|
||||
handover_enabled = false;
|
||||
retry_pause(&retries);
|
||||
uint64_t deadline = milliseconds() + 4000;
|
||||
while (piccap_owned && milliseconds() < deadline) {
|
||||
handover_tick();
|
||||
iterate(NULL, 0);
|
||||
usleep(10000);
|
||||
}
|
||||
}
|
||||
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(!CALIBRATION_ACTIVE && 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],stable_rgb[16][3];calibration_colors(rgb,mapped_rgb);
|
||||
continuity_frame(&continuity,true,mapped_rgb,now,stable_rgb);
|
||||
interpolation_enabled=led_count && calibration_effective()!=CAL_AVERAGE;
|
||||
char *line=region_command(stable_rgb,led_blends,led_count,1500);
|
||||
if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;}
|
||||
int failed=send_line(line);free(line);
|
||||
if(failed){set_error("Receiver connection lost");stop_capture();return 1;}
|
||||
if(!receiver_ack()){set_error("Receiver did not acknowledge color data");stop_capture();return 1;}
|
||||
last_error[0]=0;
|
||||
color_valid=true;last_output_at=milliseconds();frames++;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){
|
||||
signal(SIGTERM,quit_signal);signal(SIGINT,quit_signal);
|
||||
if(boot_enabled() && !set_boot(true))fprintf(stderr,"Own legacy boot hook migration failed\n");
|
||||
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");
|
||||
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);}
|
||||
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 (*category)(void*,const char*,Method*,void*,void*,LSError*)=symbol(ls,"LSRegisterCategory");
|
||||
bool (*attach)(void*,void*,LSError*)=symbol(ls,"LSGmainAttach");
|
||||
bool (*unreg)(void*,LSError*)=symbol(ls,"LSUnregister");
|
||||
void *(*loop_new)(void*,int)=symbol(glib,"g_main_loop_new");
|
||||
int (*iterate)(void*,int)=symbol(glib,"g_main_context_iteration");
|
||||
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
|
||||
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
|
||||
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
|
||||
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"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;
|
||||
if(!boot_enabled())retry_pause(&retries);
|
||||
timer(100,tick,NULL);
|
||||
while(!quitting){iterate(NULL,0);usleep(10000);}
|
||||
stream_close(milliseconds());test_close();stop_capture();
|
||||
if(piccap_owned){
|
||||
handover_enabled=false;retry_pause(&retries);
|
||||
uint64_t deadline=milliseconds()+4000;
|
||||
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,202 @@
|
||||
/* 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)
|
||||
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;
|
||||
calibration_until = 0;
|
||||
retry_pause(&retries);
|
||||
bool bridging = bridge_phase != BRIDGE_IDLE;
|
||||
stream_close(milliseconds());
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
memset(&continuity, 0, sizeof(continuity));
|
||||
stop_capture();
|
||||
if (bridging) {
|
||||
output = target_connect(&target);
|
||||
if (output >= 0) {
|
||||
if (target_login())
|
||||
send_line("{\"command\":\"clear\",\"priority\":149}\n");
|
||||
close(output);
|
||||
output = -1;
|
||||
}
|
||||
}
|
||||
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 != 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 || piccap_owned || retries.due;
|
||||
stop_capture();
|
||||
stream_close(milliseconds());
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
memset(&continuity, 0, sizeof(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,107 @@
|
||||
/* 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_until = milliseconds() + 110000;
|
||||
if (capture && vt_preset(capture, 0) != 0) {
|
||||
calibration_until = 0;
|
||||
return respond(h, m,
|
||||
"{\"returnValue\":false,\"errorText\":\"Cannot select full-screen "
|
||||
"calibration capture\"}");
|
||||
}
|
||||
capture_preset = letterbox_mode = 0;
|
||||
settling = 2;
|
||||
pending_letterbox = -1;
|
||||
} else if (!strcmp(p, "{\"enabled\":false}"))
|
||||
calibration_until = 0;
|
||||
else
|
||||
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||
return respond(h, m, "{\"returnValue\":true}");
|
||||
}
|
||||
static Calibration calibration_draft;
|
||||
static char calibration_message[256];
|
||||
static bool report_calibration(void *h, void *m, void *ctx) {
|
||||
if (!playback_json.lib)
|
||||
return respond(h, m, "{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}");
|
||||
(void)ctx;
|
||||
void *root = playback_json.parse(payload(m)),
|
||||
*message = playback_field(&playback_json, root, "message");
|
||||
bool valid = message && playback_json.type(message) == 6 &&
|
||||
strlen(playback_json.string(message)) < sizeof(calibration_message);
|
||||
if (valid)
|
||||
snprintf(calibration_message, sizeof(calibration_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_draft = next;
|
||||
return respond(h, m, "{\"returnValue\":true}");
|
||||
}
|
||||
static bool model_busy;
|
||||
static unsigned long model_token;
|
||||
static uint64_t model_deadline, next_model_probe;
|
||||
static bool model_reply(void *h, void *m, void *ctx) {
|
||||
(void)h;
|
||||
(void)ctx;
|
||||
model_busy = false;
|
||||
model_token = 0;
|
||||
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 (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}");
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/* 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_message[i] && k + 2 < sizeof(report); i++) {
|
||||
char c = calibration_message[i];
|
||||
if (c == '"' || c == '\\')
|
||||
report[k++] = '\\';
|
||||
if ((unsigned char)c >= 32)
|
||||
report[k++] = c;
|
||||
}
|
||||
report[k] = 0;
|
||||
int used = snprintf(json, sizeof(json),
|
||||
"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\","
|
||||
"\"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, "\"calibrationMessage\":\"%s\",", report);
|
||||
used +=
|
||||
snprintf(json + used, sizeof(json) - used,
|
||||
"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[", calibration_draft.model);
|
||||
for (unsigned i = 0; i < calibration_draft.count; i++)
|
||||
used += snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}",
|
||||
i ? "," : "", calibration_draft.points[i].position,
|
||||
calibration_draft.points[i].region);
|
||||
used += snprintf(json + used, sizeof(json) - used, "]},\"receiverStreamConnected\":%s,",
|
||||
stream.stage == STREAM_SUBSCRIBED ? "true" : "false");
|
||||
used += snprintf(json + used, sizeof(json) - used,
|
||||
"\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s,"
|
||||
"\"sourceFade\":%s,\"calibrating\":%s,",
|
||||
sample_sequence, sample_at ? (long long)(now - sample_at) : -1,
|
||||
continuity.holding ? "true" : "false",
|
||||
(continuity.fading || bridge_phase == BRIDGE_FADING) ? "true" : "false",
|
||||
CALIBRATION_ACTIVE ? "true" : "false");
|
||||
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\":\"" HYPERDRM_VERSION
|
||||
"\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,"
|
||||
"\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,"
|
||||
"\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,"
|
||||
"\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,"
|
||||
"\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",
|
||||
(capture || capture_source == SOURCE_PICCAP) ? "true" : "false",
|
||||
color_valid ? "true" : "false", frames, capture_recoveries,
|
||||
(unsigned long long)last_recovery_ms, endpoint, target.host, target.port,
|
||||
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);
|
||||
}
|
||||
+101
-36
@@ -1,47 +1,112 @@
|
||||
#ifndef HYPERDRM_SOURCE_BRIDGE_H
|
||||
#define HYPERDRM_SOURCE_BRIDGE_H
|
||||
static unsigned bridge_phase,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];
|
||||
#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 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==4 && stream_queue("{\"command\":\"clear\",\"priority\":149}\n");}
|
||||
static bool bridge_send(const unsigned char colors[16][3]){
|
||||
if(stream.stage!=4)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 bool bridge_clear(void) {
|
||||
return stream.stage == STREAM_SUBSCRIBED &&
|
||||
stream_queue("{\"command\":\"clear\",\"priority\":149}\n");
|
||||
}
|
||||
static void bridge_tick(uint64_t now){
|
||||
if(retries.paused || retries.blocked){if(bridge_phase)bridge_clear();bridge_phase=0;bridge_source=capture_source;return;}
|
||||
if(capture_source!=bridge_source){
|
||||
if(capture_source==SOURCE_PICCAP_STARTING){
|
||||
bridge_phase=continuity.available && now-continuity.valid_at<2000?1:0;bridge_started=now;memcpy(bridge_from,continuity.last,sizeof(bridge_from));
|
||||
}else if(capture_source==SOURCE_PICCAP_STOPPING || (bridge_source==SOURCE_PICCAP && capture_source==SOURCE_HISTOGRAM)){
|
||||
bridge_phase=0;
|
||||
if(stream.valid && now-stream.updated<1000){continuity_seed(&continuity,stream.colors,now);bridge_send(stream.colors);}
|
||||
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) {
|
||||
if (retries.paused || retries.blocked) {
|
||||
if (bridge_phase)
|
||||
bridge_clear();
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
bridge_source = capture_source;
|
||||
return;
|
||||
}
|
||||
bridge_source=capture_source;
|
||||
if (capture_source != bridge_source) {
|
||||
if (capture_source == SOURCE_PICCAP_STARTING) {
|
||||
bridge_phase = continuity.available && now - continuity.valid_at < 2000 ? BRIDGE_HOLDING
|
||||
: BRIDGE_IDLE;
|
||||
bridge_started = now;
|
||||
memcpy(bridge_from, continuity.last, sizeof(bridge_from));
|
||||
} else if (capture_source == SOURCE_PICCAP_STOPPING ||
|
||||
(bridge_source == SOURCE_PICCAP && capture_source == SOURCE_HISTOGRAM)) {
|
||||
bridge_phase = BRIDGE_IDLE;
|
||||
if (stream.valid && now - stream.updated < 1000) {
|
||||
continuity_seed(&continuity, stream.colors, now);
|
||||
bridge_send(stream.colors);
|
||||
}
|
||||
if(bridge_phase && now-bridge_started>=3000){if(bridge_clear())bridge_phase=0;return;}
|
||||
if(bridge_phase==1 && capture_source==SOURCE_PICCAP && piccap_connected && stream.stage==4){
|
||||
if(bridge_clear()){bridge_sequence=stream.sequence;bridge_clear_at=now;bridge_phase=2;}
|
||||
}else if(bridge_phase==1 && now-bridge_last_sent>=100 && stream.stage==4){
|
||||
if(bridge_send(bridge_from))bridge_last_sent=now;
|
||||
}else if(bridge_phase==2 && 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=3;
|
||||
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=0;
|
||||
}
|
||||
if(bridge_phase==3 && now-bridge_last_sent>=100){
|
||||
if(now-bridge_fade_at>=500){if(bridge_clear())bridge_phase=0;}
|
||||
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;}
|
||||
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
|
||||
+38
-16
@@ -5,26 +5,48 @@
|
||||
#include "layout.h"
|
||||
#include "interpolation.h"
|
||||
#include "color_boost.h"
|
||||
enum {SPATIAL_WIDTH=160,SPATIAL_HEIGHT=90,SPATIAL_PIXELS=SPATIAL_WIDTH*SPATIAL_HEIGHT};
|
||||
enum { SPATIAL_WIDTH = 160, SPATIAL_HEIGHT = 90, SPATIAL_PIXELS = SPATIAL_WIDTH * SPATIAL_HEIGHT };
|
||||
/* Synthetic color plane from histogram samples, never captured video pixels.
|
||||
* HyperHDR 22 treats JSON color as a solid color; image input retains space.
|
||||
* The receiver samples this plane using its configured LED rectangles. */
|
||||
static char *spatial_image_command_duration(const unsigned char regions[16][3],unsigned priority,unsigned duration){
|
||||
static LedBlend blends[SPATIAL_PIXELS];static unsigned generation;
|
||||
if(generation!=calibration_generation){
|
||||
for(unsigned y=0;y<SPATIAL_HEIGHT;y++)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;
|
||||
static char *spatial_image_command_duration(const unsigned char regions[16][3], unsigned priority,
|
||||
unsigned duration) {
|
||||
static LedBlend blends[SPATIAL_PIXELS];
|
||||
static unsigned generation;
|
||||
if (generation != calibration_generation) {
|
||||
for (unsigned y = 0; y < SPATIAL_HEIGHT; y++)
|
||||
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;
|
||||
}
|
||||
const char alphabet[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t capacity=SPATIAL_PIXELS*4+256;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++){
|
||||
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);
|
||||
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];
|
||||
const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t capacity = SPATIAL_PIXELS * 4 + 256;
|
||||
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++) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
static inline char *spatial_image_command(const unsigned char regions[16][3],unsigned priority){return spatial_image_command_duration(regions,priority,1500);}
|
||||
#endif
|
||||
+176
-62
@@ -10,80 +10,194 @@
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
#include "playback.h"
|
||||
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
||||
typedef struct {char host[46],receiver[16],token[257];unsigned port,piccap_port;} TargetConfig;
|
||||
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444,.piccap_port=19400};
|
||||
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
|
||||
static inline bool target_piccap_compatible(const TargetConfig *t){unsigned char address[4];return !strcmp(t->host,"::1") || inet_pton(AF_INET,t->host,address)==1;}
|
||||
static inline const char *target_piccap_host(const TargetConfig *t){return !strcmp(t->host,"::1")?"127.0.0.1":t->host;}
|
||||
static bool target_valid(const TargetConfig *t){
|
||||
#define TARGET_PREF \
|
||||
"/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
||||
typedef struct {
|
||||
char host[46], receiver[16], token[257];
|
||||
unsigned port, piccap_port;
|
||||
} TargetConfig;
|
||||
static TargetConfig target = {
|
||||
.host = "127.0.0.1", .receiver = "Hyperion", .port = 19444, .piccap_port = 19400};
|
||||
static inline bool target_piccap_local(const TargetConfig *t) {
|
||||
return t->port == 19444 && (!strcmp(t->host, "127.0.0.1") || !strcmp(t->host, "::1"));
|
||||
}
|
||||
static inline bool target_piccap_compatible(const TargetConfig *t) {
|
||||
unsigned char address[4];
|
||||
return !strcmp(t->host, "::1") || inet_pton(AF_INET, t->host, address) == 1;
|
||||
}
|
||||
static inline const char *target_piccap_host(const TargetConfig *t) {
|
||||
return !strcmp(t->host, "::1") ? "127.0.0.1" : t->host;
|
||||
}
|
||||
static bool target_valid(const TargetConfig *t) {
|
||||
unsigned char address[16];
|
||||
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
|
||||
if(!t->piccap_port || t->piccap_port>65535)return false;
|
||||
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
|
||||
for(const unsigned char *p=(const unsigned char*)t->token;*p;p++)
|
||||
if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false;
|
||||
if (inet_pton(AF_INET, t->host, address) != 1 && inet_pton(AF_INET6, t->host, address) != 1)
|
||||
return false;
|
||||
if (!t->piccap_port || t->piccap_port > 65535)
|
||||
return false;
|
||||
if (!t->port || t->port > 65535 ||
|
||||
(strcmp(t->receiver, "Hyperion") && strcmp(t->receiver, "HyperHDR")))
|
||||
return false;
|
||||
for (const unsigned char *p = (const unsigned char *)t->token; *p; p++)
|
||||
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
|
||||
*p == '-' || *p == '_' || *p == '.'))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
|
||||
void *root=j->parse(text);if(!root)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)};
|
||||
for(unsigned i=0;valid && i<3;i++){
|
||||
void *v=playback_field(j,root,names[i]);
|
||||
if(!v){if(i<2)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);
|
||||
static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
|
||||
void *root = j->parse(text);
|
||||
if (!root)
|
||||
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)};
|
||||
for (unsigned i = 0; valid && i < 3; i++) {
|
||||
void *v = playback_field(j, root, names[i]);
|
||||
if (!v) {
|
||||
if (i < 2)
|
||||
valid = false;
|
||||
continue;
|
||||
}
|
||||
void *port=playback_field(j,root,"port");
|
||||
if(!port || j->type(port)!=3)valid=false;
|
||||
else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;}
|
||||
void *flat=NULL;
|
||||
if(j->type(root)==4 && j->get(root,"piccapPort",&flat)){
|
||||
if(j->type(flat)!=3)valid=false;
|
||||
else {char *end=NULL;errno=0;long n=strtol(j->string(flat),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.piccap_port=(unsigned)n;}
|
||||
if (j->type(v) != 6) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
void *clear=playback_field(j,root,"clearToken");
|
||||
if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;}
|
||||
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
|
||||
const char *s = j->string(v);
|
||||
if (strlen(s) >= sizes[i]) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
strcpy(fields[i], s);
|
||||
}
|
||||
void *port = playback_field(j, root, "port");
|
||||
if (!port || j->type(port) != 3)
|
||||
valid = false;
|
||||
else {
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
long n = strtol(j->string(port), &end, 10);
|
||||
if (errno || !end || *end || n < 1 || n > 65535)
|
||||
valid = false;
|
||||
else
|
||||
next.port = (unsigned)n;
|
||||
}
|
||||
void *flat = NULL;
|
||||
if (j->type(root) == 4 && j->get(root, "piccapPort", &flat)) {
|
||||
if (j->type(flat) != 3)
|
||||
valid = false;
|
||||
else {
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
long n = strtol(j->string(flat), &end, 10);
|
||||
if (errno || !end || *end || n < 1 || n > 65535)
|
||||
valid = false;
|
||||
else
|
||||
next.piccap_port = (unsigned)n;
|
||||
}
|
||||
}
|
||||
void *clear = playback_field(j, root, "clearToken");
|
||||
if (clear) {
|
||||
if (j->type(clear) != 1)
|
||||
valid = false;
|
||||
else if (j->boolean(clear))
|
||||
next.token[0] = 0;
|
||||
}
|
||||
valid = valid && target_valid(&next);
|
||||
j->put(root);
|
||||
if (valid)
|
||||
*out = next;
|
||||
return valid;
|
||||
}
|
||||
static bool target_save(const char *path,const TargetConfig *t){
|
||||
if(!target_valid(t))return false;
|
||||
char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false;
|
||||
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->piccap_port,t->receiver,t->token);
|
||||
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
||||
bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false;
|
||||
if(ok)ok=rename(tmp,path)==0;
|
||||
if(!ok)unlink(tmp);
|
||||
static bool target_save(const char *path, const TargetConfig *t) {
|
||||
if (!target_valid(t))
|
||||
return false;
|
||||
char tmp[512], data[512];
|
||||
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
||||
return false;
|
||||
int n = snprintf(
|
||||
data, sizeof(data),
|
||||
"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",
|
||||
t->host, t->port, t->piccap_port, t->receiver, t->token);
|
||||
int fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
bool ok = write(fd, data, n) == n && fsync(fd) == 0;
|
||||
if (close(fd) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = rename(tmp, path) == 0;
|
||||
if (!ok)
|
||||
unlink(tmp);
|
||||
return ok;
|
||||
}
|
||||
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;
|
||||
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 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;
|
||||
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){
|
||||
if(!target_valid(t))return -1;
|
||||
struct sockaddr_storage storage={0};socklen_t length;int family;
|
||||
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
|
||||
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);}
|
||||
else {family=AF_INET6;v6->sin6_family=family;v6->sin6_port=htons(t->port);length=sizeof(*v6);}
|
||||
int fd=socket(family,SOCK_STREAM,0);if(fd<0)return -1;
|
||||
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
|
||||
if(connect(fd,(void*)&storage,length)!=0){
|
||||
if(errno!=EINPROGRESS){close(fd);return -1;}
|
||||
static int target_open(const TargetConfig *t) {
|
||||
if (!target_valid(t))
|
||||
return -1;
|
||||
struct sockaddr_storage storage = {0};
|
||||
socklen_t length;
|
||||
int family;
|
||||
struct sockaddr_in *v4 = (void *)&storage;
|
||||
struct sockaddr_in6 *v6 = (void *)&storage;
|
||||
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);
|
||||
} else {
|
||||
family = AF_INET6;
|
||||
v6->sin6_family = family;
|
||||
v6->sin6_port = htons(t->port);
|
||||
length = sizeof(*v6);
|
||||
}
|
||||
int fd = socket(family, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
if (connect(fd, (void *)&storage, length) != 0) {
|
||||
if (errno != EINPROGRESS) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
static int target_connect(const TargetConfig *t){
|
||||
int fd=target_open(t);if(fd<0)return -1;
|
||||
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
|
||||
int flags=fcntl(fd,F_GETFL,0);
|
||||
if(flags<0 || fcntl(fd,F_SETFL,flags & ~O_NONBLOCK)<0){close(fd);return -1;}
|
||||
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
|
||||
static int target_connect(const TargetConfig *t) {
|
||||
int fd = target_open(t);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
struct pollfd p = {.fd = fd, .events = POLLOUT};
|
||||
int error = 0;
|
||||
socklen_t size = sizeof(error);
|
||||
if (poll(&p, 1, 1000) <= 0 || getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 ||
|
||||
error) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
struct timeval timeout = {1, 0};
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
+104
-29
@@ -5,35 +5,110 @@
|
||||
#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);
|
||||
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);
|
||||
}
|
||||
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);}
|
||||
void *bad=json->parse("{\"result\":{\"leds\":[0,0,999]}}");assert(!stream_colors(json,bad,2000));json->put(bad);assert(stream.colors[0][2]==40);
|
||||
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);
|
||||
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_connect(©);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;
|
||||
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_connect(©);
|
||||
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,4 @@
|
||||
#ifndef HYPERDRM_VERSION_H
|
||||
#define HYPERDRM_VERSION_H
|
||||
#define HYPERDRM_VERSION "0.12.0"
|
||||
#endif
|
||||
@@ -1 +1,10 @@
|
||||
{"id": "org.webosbrew.histogramhyperion", "version": "0.11.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.12.0",
|
||||
"vendor": "Local development",
|
||||
"app": "org.webosbrew.histogramhyperion",
|
||||
"services": [
|
||||
"org.webosbrew.histogramhyperion.service"
|
||||
],
|
||||
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
"""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
|
||||
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')
|
||||
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)
|
||||
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,33 @@
|
||||
"""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
|
||||
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)
|
||||
@@ -22,6 +22,9 @@ const grant=requests.find(r=>r.uri.includes('hbchannel'));assert.deepEqual(grant
|
||||
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,50 @@
|
||||
"""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_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()
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||
const elements={},events={},requests=[];let active=false,sequence=0,globalResponse=false;
|
||||
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==='calibrationSession')active=data.enabled;
|
||||
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));
|
||||
@@ -29,4 +29,6 @@ c.profileDirty=false;globalResponse=true;measure();assert(elements.wizardStatus.
|
||||
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