1 Commits
Author SHA1 Message Date
matze19999 fcdfc4b431 Release 0.13.4 2026-10-12 18:31:34 +02:00
21 changed files with 71 additions and 27 deletions

No files matched your search

+4 -4
View File
@@ -1,15 +1,15 @@
{
"id": "org.webosbrew.histogramhyperion",
"version": "0.13.3",
"version": "0.13.4",
"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.13.3_arm.ipk",
"ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.13.4_arm.ipk",
"ipkHash": {
"sha256": "acb0978100796e8765e586a91183599ef4eb783631d8b68205d716621790e232"
"sha256": "3028ea164e05ff6b9c601aa7f3ff0f05b17781ba603eaebe3395abae503f7bb7"
},
"ipkSize": 1208120
"ipkSize": 1208554
}
Binary file not shown.
+3 -1
View File
@@ -27,7 +27,7 @@ Only the own service is modified. Optional Auto PicCap can configure its receive
## Installation
The current package is [HyperDRM_0.13.3_arm.ipk](HyperDRM_0.13.3_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
The current package is [HyperDRM_0.13.4_arm.ipk](HyperDRM_0.13.4_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
Elevate the installed own service through Homebrew Channel:
@@ -237,3 +237,5 @@ tests do not replace real LG capture and remote-control validation on a TV.
Histogram near-black handling suppresses colors with a maximum RGB channel of 16 or less, gradually restores brightness between 16 and 32, and preserves brighter colors. This applies only to histogram output; PicCap, receiver calibration and raw calibration samples are unchanged.
Thin letterbox detection compares both edges against lit interior histogram samples and requires a consistently dark upper edge for at least two seconds. Histogram probes cannot determine the exact bar height or guarantee detection on every scene.
Histogram capture defaults to 50 ms. The Receiver panel provides a saved Capture interval (ms) setting from 25 to 1000 in steps of 25. Existing settings without this field use 50 ms. PicCap capture timing is unchanged.
+1 -1
View File
@@ -1 +1 @@
0.13.3
0.13.4
+9 -2
View File
@@ -125,6 +125,7 @@ function display(result) {
document.getElementById('host').value = result.host;
document.getElementById('port').value = result.port;
document.getElementById('piccapPort').value = result.piccapPort || 19400;
document.getElementById('captureInterval').value = result.captureIntervalMs || 50;
receiver = result.receiver;
document.getElementById('receiver').textContent = 'Receiver: ' + receiver;
}
@@ -298,7 +299,7 @@ document.getElementById('handover').onclick = function() {
enabled: button.getAttribute('aria-checked') !== 'true'
});
};
['host', 'port', 'piccapPort', 'token', 'clearToken'].forEach(function(id) {
['host', 'port', 'piccapPort', 'captureInterval', 'token', 'clearToken'].forEach(function(id) {
document.getElementById(id).addEventListener('input', function() {
targetDirty = true;
});
@@ -320,7 +321,13 @@ document.getElementById('saveTarget').onclick = function() {
showActionError('PicCap FlatBuffer port must be an integer from 1 to 65535.');
return;
}
var interval = Number(document.getElementById('captureInterval').value);
if (!Number.isInteger(interval) || interval < 25 || interval > 1000 || interval % 25) {
showActionError('Capture interval must be 25–1000 ms in steps of 25.');
return;
}
var settings = {
captureIntervalMs: interval,
piccapPort: flatPort,
host: document.getElementById('host').value.trim(),
port: port,
@@ -393,6 +400,6 @@ document.getElementById('editProfile').onclick = function() {
showProfile();
if (advancedProfile) document.getElementById('customProfileField').focus();
};
setupRemoteNavigation(['overviewTab', 'receiverTab', 'calibrationTab', 'reloadService', '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) {
setupRemoteNavigation(['overviewTab', 'receiverTab', 'calibrationTab', 'reloadService', 'grantRoot', 'start', 'stop', 'refresh', 'autostart', 'handover', 'calibrationShortcut', 'receiver', 'hostField', 'portField', 'piccapPortField', 'captureIntervalField', 'tokenField', 'clearToken', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'profile', 'saveProfile', 'editProfile', 'customProfileField', 'cancelCalibration'].map(function(id) {
return document.getElementById(id);
}), document, window);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"id": "org.webosbrew.histogramhyperion",
"version": "0.13.3",
"version": "0.13.4",
"vendor": "Local development",
"type": "web",
"main": "index.html",
+2 -1
View File
@@ -38,11 +38,12 @@ nav{border-bottom:1px solid #456185;padding-bottom:10px;margin-bottom:18px}nav b
<div id="hostField" class="field" tabindex="0" role="button" aria-label="IP address, press OK to edit"><label>IP address<input id="host" tabindex="-1" readonly type="text" maxlength="45" value="127.0.0.1" spellcheck="false"></label></div>
<div id="portField" class="field narrow" tabindex="0" role="button" aria-label="JSON port, press OK to edit"><label>JSON port<input id="port" tabindex="-1" readonly type="number" min="1" max="65535" value="19444"></label></div>
<div id="piccapPortField" class="field narrow" tabindex="0" role="button" aria-label="FlatBuffer port, press OK to edit"><label>PicCap FlatBuffer port<input id="piccapPort" tabindex="-1" readonly type="number" min="1" max="65535" value="19400"></label></div>
<div id="captureIntervalField" class="field narrow" tabindex="0" role="button" aria-label="Capture interval in milliseconds, press OK to edit"><label>Capture interval (ms)<input id="captureInterval" tabindex="-1" readonly type="number" min="25" max="1000" step="25" value="50"></label></div>
<div id="tokenField" class="field" tabindex="0" role="button" aria-label="API token, press OK to edit"><label>API token (optional)<input id="token" tabindex="-1" readonly type="password" maxlength="256" autocomplete="off"></label></div>
</div>
<label><input id="clearToken" type="checkbox"> Remove saved token</label>
<div><button id="saveTarget" class="primary">Save receiver</button><button id="testConnection">Test connection</button></div><p id="testResult" role="status">Connection test: Not tested</p>
<p><small>The JSON TCP port is usually 19444. Leave the token blank to keep the saved token. Saving updates the current session.</small></p>
<p><small>The JSON TCP port is usually 19444. Leave the token blank to keep the saved token. Saving updates the current session. Histogram capture defaults to 50 ms; choose 25–1000 ms in steps of 25. Lower values reduce latency and increase TV load. PicCap uses its own capture rate.</small></p>
<p id="piccapTargetHint"><small>Auto PicCap uses the same IP and its FlatBuffer port, usually 19400. Enable the receiver's FlatBuffer server. Remote IPv6 uses HyperDRM.</small></p></fieldset>
</section>
<section id="calibrationPanel" aria-label="TV calibration" style="display:none">
+1 -1
View File
@@ -64,4 +64,4 @@ function setupFormEditors(ids, doc, win) {
}
});
}
setupFormEditors(['host', 'port', 'piccapPort', 'token', 'customProfile'], document, window);
setupFormEditors(['host', 'port', 'piccapPort', 'captureInterval', 'token', 'customProfile'], document, window);
+1 -1
View File
@@ -1,2 +1,2 @@
'use strict';
var HYPERDRM_APP_VERSION = '0.13.3';
var HYPERDRM_APP_VERSION = '0.13.4';
+3
View File
@@ -0,0 +1,3 @@
# HyperDRM 0.13.4
Default histogram capture to 50 ms and expose a persisted Capture interval (ms) in the Receiver panel (25–1000, steps of 25). Remote highlighting leaves the keyboard closed; OK opens editing. A 25 ms scheduler keeps network processing responsive independently of slower capture settings. Missing legacy values default to 50 ms. PicCap timing is unchanged. Native tests check persistence, legacy defaults and invalid values.
+2
View File
@@ -83,6 +83,8 @@ static int tick(void *unused) {
capture_ctx.settling = 2;
return 1;
}
if (capture_ctx.sample_at && now - capture_ctx.sample_at < target_capture_interval(&target))
return 1;
Histogram hist[16] = {0};
int rc = vt_get(capture_ctx.handle, hist);
bool populated = !rc && histogram_populated(hist);
+1 -1
View File
@@ -270,7 +270,7 @@ int main(void) {
service_handle = handle;
if (!boot_enabled())
retry_pause(&retries);
timer(100, tick, NULL);
timer(25, tick, NULL);
while (!quitting) {
iterate(NULL, 0);
usleep(10000);
+2 -2
View File
@@ -61,10 +61,10 @@ static bool status(void *h, void *m, void *ctx) {
CALIBRATION_ACTIVE ? "true" : "false");
used +=
snprintf(json + used, sizeof(json) - used,
"\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\","
"\"returnValue\":true,\"captureIntervalMs\":%u,\"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,
target_capture_interval(&target), target.piccap_port, test_stage_name(), connection_test.leds,
(unsigned long long)connection_test.elapsed, connection_test.error,
capture_ctx.last_output_at ? (long long)(now - capture_ctx.last_output_at) : -1,
(unsigned long long)(retries.due > now ? retries.due - now : 0),
+25 -4
View File
@@ -15,10 +15,14 @@
"/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;
unsigned port, piccap_port, capture_interval;
} TargetConfig;
static TargetConfig target = {
.host = "127.0.0.1", .receiver = "Hyperion", .port = 19444, .piccap_port = 19400};
.host = "127.0.0.1", .receiver = "Hyperion", .port = 19444, .piccap_port = 19400,
.capture_interval = 50};
static inline unsigned target_capture_interval(const TargetConfig *t) {
return t->capture_interval ? t->capture_interval : 50;
}
static inline bool target_piccap_local(const TargetConfig *t) {
return t->port == 19444 && (!strcmp(t->host, "127.0.0.1") || !strcmp(t->host, "::1"));
}
@@ -31,6 +35,9 @@ static inline const char *target_piccap_host(const TargetConfig *t) {
}
static bool target_valid(const TargetConfig *t) {
unsigned char address[16];
unsigned interval = target_capture_interval(t);
if (interval < 25 || interval > 1000 || interval % 25)
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)
@@ -97,6 +104,20 @@ static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) {
next.piccap_port = (unsigned)n;
}
}
void *interval = NULL;
if (j->type(root) == 4 && j->get(root, "captureIntervalMs", &interval)) {
if (j->type(interval) != 3)
valid = false;
else {
char *end = NULL;
errno = 0;
long n = strtol(j->string(interval), &end, 10);
if (errno || !end || *end || n < 25 || n > 1000 || n % 25)
valid = false;
else
next.capture_interval = (unsigned)n;
}
}
void *clear = playback_field(j, root, "clearToken");
if (clear) {
if (j->type(clear) != 1)
@@ -116,8 +137,8 @@ static bool target_save(const char *path, const TargetConfig *t) {
char data[512];
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);
"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\",\"captureIntervalMs\":%u}\n",
t->host, t->port, t->piccap_port, t->receiver, t->token, target_capture_interval(t));
return n > 0 && (size_t)n < sizeof(data) &&
atomic_replace(path, data, (size_t)n) != SAVE_UNCHANGED;
}
+1 -1
View File
@@ -26,7 +26,7 @@ int main(void){
assert(letterbox_candidate(thin_black,thin_color)==-1);
memset(thin_color,100,sizeof(thin_color));
assert(letterbox_candidate(thin_black,thin_color)!=1);
memset(thin_black,15,sizeof(thin_black));
for(unsigned i=0;i<16;i++)thin_black[i]=15;
assert(letterbox_candidate(thin_black,thin_color)!=1);
puts("PASS: measured wide/thin bars, full frame, one-sided/isolated edges, dark frame and low contrast");
}
+9 -1
View File
@@ -24,8 +24,16 @@ int main(void){
assert(!target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":null,\"receiver\":\"HyperHDR\"}",&t));
TargetConfig v6=t;strcpy(v6.host,"2001:db8::1");assert(!target_piccap_compatible(&v6));strcpy(v6.host,"::1");assert(target_piccap_compatible(&v6) && !strcmp(target_piccap_host(&v6),"127.0.0.1"));
const char *file="/tmp/hyperdrm-target-test.json";assert(access(file,F_OK)!=0);assert(access("/tmp/hyperdrm-target-test.json.tmp",F_OK)!=0);
assert(target_capture_interval(&t)==50);
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"HyperHDR\",\"captureIntervalMs\":100}",&t));
assert(t.capture_interval==100);
const char *invalid_intervals[]={"0","24","26","1001","50.5","null","\"50\""};
for(unsigned i=0;i<sizeof(invalid_intervals)/sizeof(*invalid_intervals);i++){
char request[256];snprintf(request,sizeof(request),"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"HyperHDR\",\"captureIntervalMs\":%s}",invalid_intervals[i]);
TargetConfig before=t;assert(!target_parse(&j,request,&t));assert(!memcmp(&before,&t,sizeof(t)));
}
assert(target_save(file,&t));struct stat st;assert(stat(file,&st)==0 && (st.st_mode&0777)==0600);
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(restored.capture_interval==100);assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
+1 -1
View File
@@ -1,4 +1,4 @@
#ifndef HYPERDRM_VERSION_H
#define HYPERDRM_VERSION_H
#define HYPERDRM_VERSION "0.13.3"
#define HYPERDRM_VERSION "0.13.4"
#endif
+1 -1
View File
@@ -1,6 +1,6 @@
{
"id": "org.webosbrew.histogramhyperion",
"version": "0.13.3",
"version": "0.13.4",
"vendor": "Local development",
"app": "org.webosbrew.histogramhyperion",
"services": [
Binary file not shown.
+1 -1
View File
@@ -7,7 +7,7 @@ function element(id,input){return {id,tagName:input?'INPUT':'DIV',type:'text',re
focus(){if(doc.activeElement && doc.activeElement!==this)doc.activeElement.blur();doc.activeElement=this;if(input)inputFocuses++;},
blur(){if(this.handlers.blur)this.handlers.blur();if(doc.activeElement===this)doc.activeElement=null;}
};}
['host','port','piccapPort','token','customProfile'].forEach(id=>{elements[id]=element(id,true);elements[id+'Field']=element(id+'Field',false);});
['host','port','piccapPort','captureInterval','token','customProfile'].forEach(id=>{elements[id]=element(id,true);elements[id+'Field']=element(id+'Field',false);});
['overview','receiver','calibration'].forEach(key=>{elements[key+'Panel']=element(key+'Panel');elements[key+'Tab']=element(key+'Tab');});
['start','receiver','calibrate','calibrationShortcut'].forEach(id=>elements[id]=element(id));
const win={addEventListener(k,f){windowEvents[k]=f;}};
+3 -3
View File
@@ -1,6 +1,6 @@
'use strict';
const assert=require('assert'),fs=require('fs'),vm=require('vm');
const ids=['reloadService','versionWarning','host','port','piccapPort','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics','profile','customEditor','customProfile','saveProfile','profileStatus','calibrate','validateCalibration','cancelCalibration','editProfile','customProfileField','overviewTab','receiverTab','calibrationTab','calibrationShortcut','hostField','portField','piccapPortField','tokenField','grantRoot','rootHint'];
const ids=['reloadService','versionWarning','host','port','piccapPort','captureInterval','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics','profile','customEditor','customProfile','saveProfile','profileStatus','calibrate','validateCalibration','cancelCalibration','editProfile','customProfileField','overviewTab','receiverTab','calibrationTab','calibrationShortcut','hostField','portField','piccapPortField','captureIntervalField','tokenField','grantRoot','rootHint'];
const elements={};
ids.forEach(id=>elements[id]={value:'',textContent:'',disabled:false,checked:false,attributes:{},style:{},addEventListener(){},focus(){},appendChild(){},setAttribute(k,v){this.attributes[k]=v;},getAttribute(k){return this.attributes[k];}});
const requests=[];
@@ -17,8 +17,8 @@ assert(elements.diagnostics.textContent.includes('Root service'));
assert(controls.includes(elements.piccapPortField)&&controls.includes(elements.testConnection));
context.targetDirty=true;elements.testConnection.onclick();assert(elements.error.textContent.includes('Save receiver'));
elements.piccapPort.value='0';elements.saveTarget.onclick();assert(elements.error.textContent.includes('FlatBuffer port'));
elements.piccapPort.value='19446';elements.host.value='10.1.0.100';elements.token.value='';elements.saveTarget.onclick();
const saved=requests.find(r=>r.uri.endsWith('/settings'));assert.strictEqual(saved.data.piccapPort,19446);assert.strictEqual(saved.data.host,'10.1.0.100');assert(!('token' in saved.data));
elements.piccapPort.value='19446';elements.captureInterval.value='26';elements.saveTarget.onclick();assert(elements.error.textContent.includes('Capture interval'));elements.captureInterval.value='100';elements.piccapPort.value='19446';elements.host.value='10.1.0.100';elements.token.value='';elements.saveTarget.onclick();
const saved=requests.find(r=>r.uri.endsWith('/settings'));assert.strictEqual(saved.data.piccapPort,19446);assert.strictEqual(saved.data.captureIntervalMs,100);assert.strictEqual(saved.data.host,'10.1.0.100');assert(!('token' in saved.data));
elements.testConnection.onclick();assert(requests.some(r=>r.uri.endsWith('/testConnection')));
assert.strictEqual(elements.token.value,'');assert(!elements.diagnostics.textContent.includes('fixture-token'));
elements.profile.onclick();assert(elements.profile.textContent.includes('LG G4'));elements.saveProfile.onclick();assert(requests.some(r=>r.uri.endsWith('/calibration')&&r.data.profile==='g4'));