Release 0.12.0

This commit is contained in:
matze19999 committed 2026-10-06 22:01:38 +02:00
1 parent f74a658097
commit 7f6eeab39e
48 files changed
+3617 -1266

No files matched your search

+340 -105
View File
@@ -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);
});
},{id:'org.webosbrew.histogramhyperion.service'});
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'
});
}
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});}
save();
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
View File
@@ -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"
}
+78 -32
View File
@@ -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;
// Reject regions responding equally around the screen (global histograms).
var strong=responses.filter(function(v){return v>=peak*.65;}).length;
if(strong>10)continue;
var distant=responses.some(function(v,i){var d=Math.min(Math.abs(i-best),32-Math.abs(i-best));return v>=peak*.65 && d>6;});
if(distant)continue;
var sum=0,weighted=0;
for(var offset=-3;offset<=3;offset++){
var index=(best+offset+32)%32,weight=Math.max(0,responses[index]-peak*.35);
weighted+=offset*weight;sum+=weight;
}
var position=((best+.5+weighted/sum)/8+4)%4;
candidates.push({position:position,region:region,score:peak/strong});
}
// Nested LG regions can measure the same edge position. Keep the most
// localized response rather than making duplicate interpolation anchors.
candidates.sort(function(a,b){return b.score-a.score || a.region-b.region;});
var points=[];
candidates.forEach(function(p){if(points.some(function(q){var d=Math.abs(p.position-q.position);return Math.min(d,4-d)<.06;}))return;points.push({position:p.position,region:p.region});edges[Math.floor(p.position)]++;});
points.sort(function(a,b){return a.position-b.position;});
if(points.length<4 || edges.some(function(n){return !n;}))throw new Error('No reliable spatial response on every edge. Keep the video full-screen; this TV may not expose local histogram regions. Your saved profile is unchanged.');
return {model:model,points:points};
function calibrationFit(baseline, patches, model) {
if (!model || baseline.length !== 16 || patches.length !== 32) throw new Error('Incomplete calibration measurements.');
var candidates = [],
edges = [0, 0, 0, 0];
for (var region = 0; region < 16; region++) {
var responses = patches.map(function(p) {
if (!p || p.length !== 16) throw new Error('Missing video measurements.');
return Math.max(0, p[region] - baseline[region]);
});
var peak = Math.max.apply(Math, responses),
best = responses.indexOf(peak);
if (peak < 8) continue;
// Reject regions responding equally around the screen (global histograms).
var strong = responses.filter(function(v) {
return v >= peak * .65;
}).length;
if (strong > 10) continue;
var distant = responses.some(function(v, i) {
var d = Math.min(Math.abs(i - best), 32 - Math.abs(i - best));
return v >= peak * .65 && d > 6;
});
if (distant) continue;
var sum = 0,
weighted = 0;
for (var offset = -3; offset <= 3; offset++) {
var index = (best + offset + 32) % 32,
weight = Math.max(0, responses[index] - peak * .35);
weighted += offset * weight;
sum += weight;
}
var position = ((best + .5 + weighted / sum) / 8 + 4) % 4;
candidates.push({
position: position,
region: region,
score: peak / strong
});
}
// Nested LG regions can measure the same edge position. Keep the most
// localized response rather than making duplicate interpolation anchors.
candidates.sort(function(a, b) {
return b.score - a.score || a.region - b.region;
});
var points = [];
candidates.forEach(function(p) {
if (points.some(function(q) {
var d = Math.abs(p.position - q.position);
return Math.min(d, 4 - d) < .06;
})) return;
points.push({
position: p.position,
region: p.region
});
edges[Math.floor(p.position)]++;
});
points.sort(function(a, b) {
return a.position - b.position;
});
if (points.length < 4 || edges.some(function(n) {
return !n;
})) throw new Error('No reliable spatial response on every edge. Keep the video full-screen; this TV may not expose local histogram regions. Your saved profile is unchanged.');
return {
model: model,
points: points
};
}
function calibrationLuma(regions){return regions.map(function(c){return (c[0]+c[1]+c[2])/3;});}
function calibrationAverage(samples){
if(samples.length<3)throw new Error('Not enough fresh histogram samples. Keep the app visible and repeat calibration.');
return samples[0].map(function(_,r){return samples.reduce(function(sum,s){return sum+s[r];},0)/samples.length;});
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
View File
@@ -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();
function showPanel(name, focus) {
['overview', 'receiver', 'calibration'].forEach(function(key) {
document.getElementById(key + 'Panel').style.display = key === name ? 'block' : 'none';
document.getElementById(key + 'Tab').setAttribute('aria-pressed', String(key === name));
});
if (focus !== false) document.getElementById({
overview: 'start',
receiver: 'receiver',
calibration: 'calibrate'
} [name]).focus();
}
['overview','receiver','calibration'].forEach(function(name){document.getElementById(name+'Tab').onclick=function(){showPanel(name);};});
document.getElementById('calibrationShortcut').onclick=function(){showPanel('calibration');};
function setupFormEditors(ids,doc,win){
var pairs=ids.map(function(id){return {input:doc.getElementById(id),field:doc.getElementById(id+'Field')};});
pairs.forEach(function(pair){
var input=pair.input,field=pair.field;input.readOnly=true;
field.onclick=function(){if(input.disabled || !input.readOnly)return;input.readOnly=false;input.focus();};
input.addEventListener('blur',function(){input.readOnly=true;});
input.addEventListener('mousedown',function(event){if(input.readOnly)event.preventDefault();});
});
function finishEditing(){
pairs.forEach(function(pair){if(!pair.input.readOnly){pair.input.readOnly=true;pair.input.blur();pair.field.focus();}});
}
doc.addEventListener('keyboardStateChange',function(event){var detail=event.detail||{};if(detail.visibility===false || detail.isShowing===false)finishEditing();});
doc.addEventListener('keydown',function(event){if((event.keyCode===461 || event.key==='Escape') && pairs.some(function(pair){return !pair.input.readOnly;})){event.preventDefault();finishEditing();}});
['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);
setupFormEditors(['host', 'port', 'piccapPort', 'token', 'customProfile'], document, window);
+46 -29
View File
@@ -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();});
});
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();
}
});
doc.addEventListener('keyup',function(event){if(event.key==='Enter'||event.keyCode===13||event.which===13)enterDown=false;});
win.addEventListener('blur',function(){enterDown=false;});
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();
});
});
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();
}
});
doc.addEventListener('keyup', function(event) {
if (event.key === 'Enter' || event.keyCode === 13 || event.which === 13) enterDown = false;
});
win.addEventListener('blur', function() {
enterDown = false;
});
}
+203 -82
View File
@@ -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;
document.getElementById('calibrate').focus();
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);}
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...';
});
function wizardSample() {
if (!wizard || wizard.busy) return;
var session = wizard;
if (Date.now() - wizard.started > 100000) {
wizardFinish('Calibration timed out. Saved profile unchanged.');
return;
}
if (wizard.phase !== WizardPhase.MEASURING) return;
var t = video.currentTime;
if (t >= 68) {
try {
var measured = calibrationFit(calibrationAverage(wizard.baseline), wizard.patches.map(calibrationAverage), wizard.model);
profile = 'custom';
profileDirty = true;
showProfile();
document.getElementById('customProfile').value = JSON.stringify(measured, null, 2);
document.getElementById('wizardStatus').textContent = 'Measured ' + measured.points.length + ' edge regions. Select Save profile, then Validate profile to check all four colored edges. The previous saved profile is still active.';
wizard.busy = true;
wizard.phase = WizardPhase.COMMITTING;
call('calibrationDraft', function(r) {
if (wizard === session) wizardFinish(r.returnValue === false ? r.errorText : undefined);
}, {
profile: 'custom',
model: measured.model,
points: measured.points
});
} catch (e) {
wizardFinish(e.message);
}
return;
}
var step = t < 4 ? -1 : Math.floor((t - 4) / 2),
local = t < 4 ? t : (t - 4) % 2;
if (step < 0 && (t < .8 || t > 3.2) || step >= 0 && (local < .65 || local > 1.75)) return;
wizard.busy = true;
call('status', function(s) {
if (wizard !== session) return;
wizard.busy = false;
if (s.returnValue === false || !s.calibrating) {
wizardFinish(s.errorText || 'Calibration session ended. Saved profile unchanged.');
return;
}
// Ignore duplicates, held output and replies crossing a stimulus boundary.
var current = video.currentTime,
nowStep = current < 4 ? -1 : Math.floor((current - 4) / 2);
if (nowStep !== step || s.sampleSequence === wizard.sequence || s.sampleAgeMs < 0 || s.sampleAgeMs > 350 || !s.colorValid || !Array.isArray(s.regions) || s.regions.length !== 16) return;
wizard.sequence = s.sampleSequence;
(step < 0 ? wizard.baseline : wizard.patches[step]).push(calibrationLuma(s.regions));
document.getElementById('wizardStatus').textContent = step < 0 ? 'Measuring black reference...' : 'Measuring edge patch ' + (step + 1) + ' of 32...';
});
}
function wizardStart(validation){
if(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;}
// 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';
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);
else wizardFinish('No fresh full-screen histogram capture or TV model. Saved profile unchanged.');
});
}
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() {
if (wizard !== session) return;
wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');
});
wizard.phase = WizardPhase.WAITING;
call('start', function() {
if (wizard === session) waitCapture();
});
}, {
enabled: true
});
function waitCapture() {
if (wizard !== session) return;
call('status', function(s) {
if (wizard !== session) return;
if (s.running && s.activeSource !== 'PicCap' && s.tvModel && s.colorValid) {
wizard.phase = validation ? WizardPhase.VALIDATING : WizardPhase.MEASURING;
wizard.model = s.tvModel;
video.currentTime = validation ? 68 : 0;
video.style.display = 'block';
document.getElementById('cancelCalibration').focus();
var playing = video.play();
if (playing && playing.catch) playing.catch(function() {
if (wizard !== session) return;
wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');
});
wizard.timer = setInterval(wizardSample, 250);
} else if (Date.now() - wizard.started < 15000) setTimeout(waitCapture, 500);
else wizardFinish('No fresh full-screen histogram capture or TV model. Saved profile unchanged.');
});
}
}
document.getElementById('calibrate').onclick=function(){wizardStart(false);};
document.getElementById('validateCalibration').onclick=function(){wizardStart(true);};
document.getElementById('cancelCalibration').onclick=function(){wizardFinish('Calibration cancelled. Saved profile unchanged.');};
video.addEventListener('ended',function(){if(wizard && wizard.validation)wizardFinish('Validation finished. Top-left red, top-right green, bottom-right blue, bottom-left yellow. Check matching LED positions.');else wizardSample();});
video.addEventListener('error',function(){wizardFinish('Calibration video playback failed. Saved profile unchanged.');});
document.addEventListener('keydown',function(e){if(wizard && (e.keyCode===461 || e.key==='Escape')){e.preventDefault();wizardFinish('Calibration cancelled. Saved profile unchanged.');}});
window.addEventListener('pagehide',function(){wizardFinish();});
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);
});