Add Magic Remote directional navigation and OK activation

This commit is contained in:
matze19999 committed 2026-10-06 14:01:59 +02:00
1 parent f3a9c0a0d8
commit 3160cf005b
11 files changed
+75 -12

No files matched your search

+4 -4
View File
@@ -1,15 +1,15 @@
{ {
"id": "org.webosbrew.histogramhyperion", "id": "org.webosbrew.histogramhyperion",
"version": "0.6.2", "version": "0.6.3",
"type": "web", "type": "web",
"title": "HyperDRM", "title": "HyperDRM",
"appDescription": "Ambient lighting for webOS and Hyperion", "appDescription": "Ambient lighting for webOS and Hyperion",
"iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png", "iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png",
"sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM",
"rootRequired": true, "rootRequired": true,
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.6.2_arm.ipk", "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.6.3_arm.ipk",
"ipkHash": { "ipkHash": {
"sha256": "9de20a046176168428378346872934c1fbbee79f33f99451addd880f193892ed" "sha256": "02f0ed2ff450573ad35807d6c75dc61840031826513c8cf2e71f7fb93332e0b1"
}, },
"ipkSize": 1163072 "ipkSize": 1163846
} }
Binary file not shown.
+4 -2
View File
@@ -25,7 +25,7 @@ Only the own service is modified. No Hue, LG library, driver or firmware binarie
## Installation ## Installation
The current package is [HyperDRM_0.6.2_arm.ipk](HyperDRM_0.6.2_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. The current package is [HyperDRM_0.6.3_arm.ipk](HyperDRM_0.6.3_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
Elevate the installed own service through Homebrew Channel: Elevate the installed own service through Homebrew Channel:
@@ -35,7 +35,7 @@ luna-send -n 1 luna://org.webosbrew.hbchannel.service/elevateService '{"id":"org
Perform elevation before first opening the app or calling its service. If the service was already started without root, elevation updates its launch permissions but does not restart the existing process: terminate only the own `histogram-service` process (verify its executable path first), then reopen the app or call start again. Closing the UI alone does not terminate the native service. Perform elevation before first opening the app or calling its service. If the service was already started without root, elevation updates its launch permissions but does not restart the existing process: terminate only the own `histogram-service` process (verify its executable path first), then reopen the app or call start again. Closing the UI alone does not terminate the native service.
Open **HyperDRM** on the TV and select **Start**. Hyperion's JSON server must be enabled on port 19444. The existing Hyperion LED device/output configuration remains responsible for controlling the hardware. Open **HyperDRM** on the TV and select **Start**. Use the remote arrow buttons to move between controls and OK to activate the selected control. Magic Remote pointer input is also supported. Hyperion's JSON server must be enabled on port 19444. The existing Hyperion LED device/output configuration remains responsible for controlling the hardware.
The internal app/service IDs remain `org.webosbrew.histogramhyperion` and `org.webosbrew.histogramhyperion.service` so existing installations and the autostart hook remain compatible. Hyperion displays the source as **HyperDRM**. After changing the LED layout, restart the service. The internal app/service IDs remain `org.webosbrew.histogramhyperion` and `org.webosbrew.histogramhyperion.service` so existing installations and the autostart hook remain compatible. Hyperion displays the source as **HyperDRM**. After changing the LED layout, restart the service.
@@ -83,3 +83,5 @@ The logo master is `app/logo.png`, with 80 px and 130 px installation icons.
## Publishing status ## Publishing status
See [the publishing review](docs/publishing-review.md) for the requirements audit and unresolved submission conditions. This repository is not a claim that webOSbrew has approved the app. See [the publishing review](docs/publishing-review.md) for the requirements audit and unresolved submission conditions. This repository is not a claim that webOSbrew has approved the app.
Remote navigation regression tests: `node tests/remote-navigation.test.js`.
+2
View File
@@ -26,3 +26,5 @@ call('status',display);setInterval(function(){if(!pending.length)call('status',d
document.getElementById('start').focus(); document.getElementById('start').focus();
document.getElementById('autostart').onclick=function(){var button=this;var enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;call('autostart',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:enabled});}; document.getElementById('autostart').onclick=function(){var button=this;var enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;call('autostart',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:enabled});};
setupRemoteNavigation(['start','stop','refresh','autostart'].map(function(id){return document.getElementById(id);}),document,window);
+1 -1
View File
@@ -1 +1 @@
{"id": "org.webosbrew.histogramhyperion", "version": "0.6.2", "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 and Hyperion", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} {"id": "org.webosbrew.histogramhyperion", "version": "0.6.3", "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 and Hyperion", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
+3 -3
View File
@@ -1,8 +1,8 @@
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title> <!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>HyperDRM</title>
<style>body{background:#101827;color:#f1f5ff;font:28px sans-serif;margin:70px;max-width:1400px}h1{font-size:52px;display:flex;align-items:center;gap:24px}h1 img{border-radius:20px}button{font:inherit;padding:20px 32px;margin:12px 12px 12px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:12px}button:focus{outline:5px solid #66ddff}#regions{display:flex;flex-wrap:wrap;gap:8px}.swatch{width:64px;height:64px;border:1px solid #abc}p{line-height:1.5}#error{color:#ffbc99}small{color:#c0cbe0}</style> <style>body{background:#101827;color:#f1f5ff;font:28px sans-serif;margin:70px;max-width:1400px}h1{font-size:52px;display:flex;align-items:center;gap:24px}h1 img{border-radius:20px}button{font:inherit;padding:20px 32px;margin:12px 12px 12px 0;background:#223654;color:white;border:2px solid #739be0;border-radius:12px}button:focus{outline:5px solid #66ddff}#regions{display:flex;flex-wrap:wrap;gap:8px}.swatch{width:64px;height:64px;border:1px solid #abc}p{line-height:1.5}#error{color:#ffbc99}small{color:#c0cbe0}</style>
<h1><img src="logo.png" alt="" width="104" height="104">HyperDRM</h1><p>Ambient lighting for webOS and Hyperion.</p><p id="status">Checking service &hellip;</p><p id="error" role="alert"></p> <h1><img src="logo.png" alt="" width="104" height="104">HyperDRM</h1><p>Ambient lighting for webOS and Hyperion.</p><p id="status">Checking service &hellip;</p><p id="error" role="alert"></p>
<button id="start">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button> <div role="group" aria-label="Service controls"><button id="start">Start</button><button id="stop">Stop</button><button id="refresh">Refresh status</button>
<button id="autostart" role="switch" aria-checked="false">Autostart: Off</button><p><small>Starts the service when the TV boots. Stop ends only the current session.</small></p> <button id="autostart" role="switch" aria-checked="false">Autostart: Off</button></div><p><small>Use the arrow buttons to select a control and OK to activate it.</small></p><p><small>Starts the service when the TV boots. Stop ends only the current session.</small></p>
<p><small>127.0.0.1:19444 &middot; Priority 149 &middot; 10 updates per second</small></p> <p><small>127.0.0.1:19444 &middot; Priority 149 &middot; 10 updates per second</small></p>
<div id="regions" aria-label="Histogram colors"></div><p><small>The Hyperion layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. Restart the service after changing the LED layout.</small></p> <div id="regions" aria-label="Histogram colors"></div><p><small>The Hyperion layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. Restart the service after changing the LED layout.</small></p>
<script src="app.js"></script></html> <script src="remote.js"></script><script src="app.js"></script></html>
+30
View File
@@ -0,0 +1,30 @@
/* 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;
event.preventDefault();
var enabled=buttons.filter(function(button){return !button.disabled;});
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;});
}
+1 -1
View File
@@ -150,7 +150,7 @@ static bool autostart(void *h,void *m,void *ctx){
static bool status(void *h,void *m,void *ctx){ static bool status(void *h,void *m,void *ctx){
(void)ctx;char json[4096],safe[512];size_t n=0; (void)ctx;char json[4096],safe[512];size_t n=0;
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; for(size_t i=0;last_error[i] && n+2<sizeof(safe);i++){char c=last_error[i];if(c=='"'||c=='\\')safe[n++]='\\';if((unsigned char)c>=32)safe[n++]=c;}safe[n]=0;
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.2\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",capture?"true":"false",color_valid?"true":"false",frames,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe); int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.3\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",capture?"true":"false",color_valid?"true":"false",frames,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]); 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"); 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]); for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]);
+1 -1
View File
@@ -1 +1 @@
{"id": "org.webosbrew.histogramhyperion", "version": "0.6.2", "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.6.3", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"}
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
const assert=require('assert');
const fs=require('fs');
const path=require('path');
const vm=require('vm');
const handlers={},windowHandlers={};
const doc={activeElement:null,addEventListener(type,handler){handlers[type]=handler;}};
const buttons=['start','stop','refresh','autostart'].map(id=>({
id,disabled:false,clicks:0,handlers:{},
addEventListener(type,handler){this.handlers[type]=handler;},
focus(){doc.activeElement=this;this.handlers.focus();},
click(){this.clicks++;}
}));
const context={};vm.createContext(context);
vm.runInContext(fs.readFileSync(path.join(__dirname,'../app/remote.js'),'utf8'),context);
context.setupRemoteNavigation(buttons,doc,{addEventListener(type,handler){windowHandlers[type]=handler;}});
function key(code,type='keydown',repeat=false){let prevented=false;handlers[type]({keyCode:code,repeat,preventDefault(){prevented=true;}});return prevented;}
buttons[0].focus();assert(key(39));assert.equal(doc.activeElement.id,'stop');
key(37);assert.equal(doc.activeElement.id,'start');key(37);assert.equal(doc.activeElement.id,'autostart');
key(39);assert.equal(doc.activeElement.id,'start');
buttons[1].disabled=true;key(39);assert.equal(doc.activeElement.id,'refresh');
key(13);assert.equal(buttons[2].clicks,1);key(13);key(13,'keydown',true);assert.equal(buttons[2].clicks,1);
key(13,'keyup');key(13);assert.equal(buttons[2].clicks,2);key(13,'keyup');
buttons[3].handlers.mouseenter();assert.equal(doc.activeElement.id,'autostart');key(37);assert.equal(doc.activeElement.id,'refresh');
doc.activeElement=null;key(39);assert.equal(doc.activeElement.id,'refresh');
assert(!key(461));assert(!key(65));
key(40);assert.equal(doc.activeElement.id,'autostart');key(38);assert.equal(doc.activeElement.id,'refresh');
key(13);windowHandlers.blur();key(13);assert.equal(buttons[2].clicks,4);
buttons.forEach(button=>button.disabled=true);assert(key(39));
console.log('PASS: arrow navigation, wraparound, disabled controls, OK once per press, pointer handoff, focus recovery, Back preserved');