Private
Public Access
Initial HyperDRM 0.6.0: webOS ambient light app and native service
This commit is contained in:
commit
376798d6ce
34 files changed
+2170
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
* text=auto
|
||||||
|
*.c text eol=lf
|
||||||
|
*.h text eol=lf
|
||||||
|
*.py text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.ipk binary
|
||||||
|
*.mp4 binary
|
||||||
|
service/histogram-service binary
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Local dependencies and credentials
|
||||||
|
video-tools/
|
||||||
|
.ssh/
|
||||||
|
.gitea-ssh/
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
# Local diagnostic snapshots
|
||||||
|
serverinfo.json
|
||||||
|
calibration-samples.jsonl
|
||||||
|
ledstream-boost.jsonl
|
||||||
|
# Generated intermediate files and old packages
|
||||||
|
calibration-gradient.ppm
|
||||||
|
org.webosbrew.histogramhyperion_*.ipk
|
||||||
|
histogram-probe
|
||||||
|
test-blackbars
|
||||||
|
test-color-boost
|
||||||
|
test-histogram
|
||||||
|
test-interpolation
|
||||||
|
test-layout
|
||||||
|
service/led-mapping.txt
|
||||||
|
service/led-interpolation.txt
|
||||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
# HyperDRM
|
||||||
|
|
||||||
|
Ambient Light for webOS and Hyperion. An own webOS app and native ARM service that reads LG color histograms and sends spatial LED colors to a local Hyperion instance.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Spatial colors from 16 LG histogram regions with interpolated transitions along the screen perimeter.
|
||||||
|
- LED count, ordering and screen rectangles read from Hyperion at each service start. No fixed 200-LED layout; layouts with 50 and 500 LEDs were tested.
|
||||||
|
- Automatic detection of top/bottom black bars on the calibrated LG G4.
|
||||||
|
- Saturation and brightness boost (2.0 and 1.33 respectively).
|
||||||
|
- Start, stop, status and persistent boot autostart switch in the TV app.
|
||||||
|
- Local Hyperion JSON connection on `127.0.0.1:19444`, priority 149, up to 10 updates per second.
|
||||||
|
|
||||||
|
## Requirements and limits
|
||||||
|
|
||||||
|
Tested on a rooted LG G4 with webOS 24, Homebrew Channel and Hyperion 2.0.16 installed on the TV. The native service requires root elevation and the TV's existing `libvtcapture`, Luna, GLib and JSON-C libraries. Other TV models or firmware versions have not been validated.
|
||||||
|
|
||||||
|
DRM playback color output was confirmed on the test TV. Support depends on LG's capture implementation; this does not guarantee support on other devices or apps. HyperDRM transmits colors, not captured video frames. The video preview in Hyperion therefore does not show the TV picture.
|
||||||
|
|
||||||
|
The 16 histogram samples are independent measurements; interpolation produces smoother transitions without adding independently captured detail. Sample positions are approximate G4 calibration values. Top/bottom bar detection is supported; side bars, arbitrary bar thickness and HDR color calibration remain limited.
|
||||||
|
|
||||||
|
Only the own service is modified. No Hue, LG library, driver or firmware binaries are included. For histogram initialization, the service temporarily creates LG's developer marker and uses a guardian process to remove its own marker afterward; an existing marker is preserved.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The current package is [HyperDRM_0.6.0_arm.ipk](HyperDRM_0.6.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow.
|
||||||
|
|
||||||
|
Elevate the installed own service through Homebrew Channel:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
luna-send -n 1 luna://org.webosbrew.hbchannel.service/elevateService '{"id":"org.webosbrew.histogramhyperion.service"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Open **HyperDRM** on the TV and select **Starten**. 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.
|
||||||
|
|
||||||
|
## Autostart
|
||||||
|
|
||||||
|
The app's **Autostart: Ein/Aus** switch controls the own hook `/var/lib/webosbrew/init.d/60-histogramhyperion`. Homebrew Channel runs it during boot after service elevation. It retries start while the TV and Hyperion initialize, with up to 120 attempts separated by five seconds. The Homebrew failsafe can suppress boot hooks.
|
||||||
|
|
||||||
|
**Stoppen** ends the current capture session and leaves the saved autostart preference unchanged. Turning autostart off removes the own hook and cancels waiting boot attempts. A foreign file or symlink at that path will not be overwritten.
|
||||||
|
|
||||||
|
Boot log: `/var/lib/webosbrew/histogramhyperion-boot.log`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Use an ARM EABI cross-compiler and Python 3:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
arm-linux-gnueabi-gcc -Wall -Wextra -Werror -O2 -rdynamic native/service.c -ldl -o service/histogram-service
|
||||||
|
python3 build_package.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The package builder bundles only the own app, native executable, metadata and logo assets. A built executable and current IPK are included for the tested platform.
|
||||||
|
|
||||||
|
## Tests and calibration
|
||||||
|
|
||||||
|
The native tests cover histogram decoding, black bar classification, color boost, interpolation and dynamic layout validation. Build each `native/test_*.c` for ARM and run on the TV. The layout test needs the TV's JSON-C library and linking with `-ldl`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
arm-linux-gnueabi-gcc -Wall -Wextra -Werror -O2 native/test_layout.c -ldl -o test-layout
|
||||||
|
arm-linux-gnueabi-gcc -Wall -Wextra -Werror -O2 native/test_interpolation.c -o test-interpolation
|
||||||
|
```
|
||||||
|
|
||||||
|
The tests passed on the G4. Boot-hook invocation started a stopped service; an actual cold boot has not yet been tested. Autostart activation was tested; deactivation is implemented but has not yet been practically tested on the TV.
|
||||||
|
|
||||||
|
`calibration-gradient.mp4` is the own red/green coordinate ramp used for calibration. `spatial-calibration.json` and `spatial-interpolation.json` record the calibration and example layout. The offline mapping scripts require locally obtained `calibration-samples.jsonl` and `serverinfo.json`; these diagnostic snapshots are not distributed. Runtime mapping is implemented in `native/layout.h` and reads the actual Hyperion layout.
|
||||||
|
|
||||||
|
To regenerate the test video, install `imageio-ffmpeg` in your Python environment and run `python3 make_calibration.py`.
|
||||||
|
|
||||||
|
## Logo
|
||||||
|
|
||||||
|
The logo was generated with the built-in Imagegen tool. The master is `app/logo.png`, with 80 px and 130 px installation icons. The generation prompt is recorded in `logo-prompt.txt`.
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
'use strict';
|
||||||
|
var pending=[];
|
||||||
|
function call(method,done,data){
|
||||||
|
if(typeof PalmServiceBridge==='undefined'){done({returnValue:false,errorText:'Diese App benötigt einen LG-TV mit installiertem nativem Dienst.'});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:'Ungültige Antwort vom Dienst'});}};
|
||||||
|
pending.push(bridge);timer=setTimeout(function(){bridge.cancel();finish({returnValue:false,errorText:'Der Dienst antwortet nicht. Installation und Dienstprotokoll prüfen.'});},10000);
|
||||||
|
bridge.call('luna://org.webosbrew.histogramhyperion.service/'+method,JSON.stringify(data||{}));
|
||||||
|
}
|
||||||
|
function display(result){
|
||||||
|
if(typeof result.autostart==='boolean'){var toggle=document.getElementById('autostart');toggle.setAttribute('aria-checked',String(result.autostart));toggle.textContent='Autostart: '+(result.autostart?'Ein':'Aus');}
|
||||||
|
document.getElementById('error').textContent=result.errorText||result.error||'';
|
||||||
|
document.getElementById('status').textContent=result.returnValue===false?'Dienst nicht bereit':(result.running?'Aktiv':'Gestoppt')+' · '+(result.frames||0)+' bestätigte Farbausgaben';
|
||||||
|
if(result.colorValid)document.getElementById('status').textContent+=result.spatialOutput?' · '+result.ledCount+' LEDs aus 16 Farbbereichen':' · Gesendetes RGB: '+result.outputColor.join(', ');
|
||||||
|
else if(result.returnValue!==false)document.getElementById('status').textContent+=' · Keine aktuelle Farbausgabe';
|
||||||
|
if(result.blackBarDetection && result.running)document.getElementById('status').textContent+=result.letterboxDetected?' · Balken oben/unten erkannt':' · Vollbild-Erfassung';
|
||||||
|
if(result.spatialInterpolation)document.getElementById('status').textContent+=' · Feine Farbübergänge';
|
||||||
|
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='Histogramm '+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();
|
||||||
|
|
||||||
|
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});};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"id": "org.webosbrew.histogramhyperion", "version": "0.6.0", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient Light für webOS und Hyperion"}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<!doctype html><html lang="de"><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>
|
||||||
|
<h1><img src="logo.png" alt="" width="104" height="104">HyperDRM</h1><p>Ambient Light für webOS und Hyperion.</p><p id="status">Dienst wird geprüft …</p><p id="error" role="alert"></p>
|
||||||
|
<button id="start">Starten</button><button id="stop">Stoppen</button><button id="refresh">Status prüfen</button>
|
||||||
|
<button id="autostart" role="switch" aria-checked="false">Autostart: Aus</button><p><small>Startet den Dienst beim TV-Boot. Stoppen beendet nur die aktuelle Sitzung.</small></p>
|
||||||
|
<p><small>127.0.0.1:19444 · Priorität 149 · 10 Abfragen pro Sekunde</small></p>
|
||||||
|
<div id="regions" aria-label="Histogrammfarben"></div><p><small>Das Hyperion-Layout bestimmt Anzahl und Positionen der LEDs. Die Farben stammen aus 16 LG-Histogrammbereichen mit interpolierten Randfarben. Nach einer Layoutänderung den Dienst neu starten.</small></p>
|
||||||
|
<script src="app.js"></script></html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
"""Interpolate between calibrated neighboring samples along the screen rim."""
|
||||||
|
import bisect,json
|
||||||
|
from pathlib import Path
|
||||||
|
ROOT=Path(__file__).resolve().parent
|
||||||
|
cal=json.loads((ROOT/'spatial-calibration.json').read_text())
|
||||||
|
leds=json.loads((ROOT/'serverinfo.json').read_text())['info']['leds']
|
||||||
|
def perimeter(x,y):
|
||||||
|
edge=min(range(4),key=lambda e:[y,1-x,1-y,x][e])
|
||||||
|
return [x,1+y,3-x,4-y][edge]%4
|
||||||
|
used=set(cal['ledMapping'])
|
||||||
|
nodes=sorted((perimeter(r['x'],r['y']),r['id']) for r in cal['regions'] if r['id'] in used)
|
||||||
|
positions=[p for p,_ in nodes]
|
||||||
|
rows=[]
|
||||||
|
for led in leds:
|
||||||
|
t=perimeter((led['hmin']+led['hmax'])/2,(led['vmin']+led['vmax'])/2)
|
||||||
|
right=bisect.bisect_right(positions,t)%len(nodes);left=(right-1)%len(nodes)
|
||||||
|
p0,a=nodes[left];p1,b=nodes[right]
|
||||||
|
span=(p1-p0)%4;distance=(t-p0)%4
|
||||||
|
weight=round(distance/span*65535)
|
||||||
|
assert 0<=weight<=65535
|
||||||
|
rows.append((a,b,weight))
|
||||||
|
(ROOT/'service/led-interpolation.txt').write_text(str(len(rows))+'\n'+''.join(f'{a} {b} {w}\n' for a,b,w in rows))
|
||||||
|
(ROOT/'spatial-interpolation.json').write_text(json.dumps({'method':'linear interpolation along calibrated screen perimeter','nodes':nodes,'weights':rows},indent=2))
|
||||||
|
assert len(rows)==len(leds)
|
||||||
|
print(len(rows),'LED blends between',len(nodes),'measured rim regions; wraparound included')
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Infer approximate region centers from a measured red/green coordinate ramp."""
|
||||||
|
import collections,json,statistics
|
||||||
|
from pathlib import Path
|
||||||
|
ROOT=Path(__file__).resolve().parent
|
||||||
|
samples=[json.loads(line) for line in (ROOT/'calibration-samples.jsonl').read_text().splitlines() if line.strip()]
|
||||||
|
assert len(samples)>=3 and all(s['colorValid'] and len(s['regions'])==16 for s in samples)
|
||||||
|
regions=[]
|
||||||
|
for i in range(16):
|
||||||
|
color=[statistics.median(s['regions'][i][c] for s in samples) for c in range(3)]
|
||||||
|
assert all(max(s['regions'][i][c] for s in samples)-min(s['regions'][i][c] for s in samples)<=4 for c in range(3)), 'Calibration image is changing'
|
||||||
|
assert color[2]<50, 'Not the expected red/green calibration ramp'
|
||||||
|
regions.append({'id':i,'x':color[0]/255,'y':color[1]/255,'rgb':color})
|
||||||
|
assert max(r['x'] for r in regions)-min(r['x'] for r in regions)>.7
|
||||||
|
assert max(r['y'] for r in regions)-min(r['y'] for r in regions)>.7
|
||||||
|
leds=json.loads((ROOT/'serverinfo.json').read_text())['info']['leds']
|
||||||
|
mapping=[]
|
||||||
|
for led in leds:
|
||||||
|
x=(led['hmin']+led['hmax'])/2;y=(led['vmin']+led['vmax'])/2
|
||||||
|
assert 0<=x<=1 and 0<=y<=1
|
||||||
|
mapping.append(min(regions,key=lambda r:(x-r['x'])**2+(y-r['y'])**2)['id'])
|
||||||
|
(ROOT/'service/led-mapping.txt').write_text(str(len(mapping))+'\n'+' '.join(map(str,mapping))+'\n')
|
||||||
|
(ROOT/'spatial-calibration.json').write_text(json.dumps({'method':'nearest measured centroid; approximate, without HDR calibration','regions':regions,'ledMapping':mapping},indent=2))
|
||||||
|
assert len(mapping)>0 and len(set(mapping))>=1
|
||||||
|
print('LEDs:',len(mapping),'used regions:',dict(sorted(collections.Counter(mapping).items())))
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""Package own app and executable; no Signify or LG binaries are included."""
|
||||||
|
import io, json, struct, tarfile, zlib
|
||||||
|
from pathlib import Path
|
||||||
|
ROOT=Path(__file__).resolve().parent
|
||||||
|
ID='org.webosbrew.histogramhyperion'
|
||||||
|
VERSION=json.loads((ROOT/'app/appinfo.json').read_text())['version']
|
||||||
|
def tar_bytes(entries):
|
||||||
|
stream=io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=stream,mode='w:gz') 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()
|
||||||
|
entries=[]
|
||||||
|
for directory,target in [('app','applications/'+ID),('service','services/'+ID+'.service'),('package','packages/'+ID)]:
|
||||||
|
for path in sorted((ROOT/directory).iterdir()):
|
||||||
|
if path.is_file() and path.name not in {"led-mapping.txt", "led-interpolation.txt"}:entries.append(('usr/palm/'+target+'/'+path.name,path.read_bytes(),0o755 if path.name=='histogram-service' else 0o644))
|
||||||
|
control=f'Package: {ID}\nVersion: {VERSION}\nArchitecture: arm\nMaintainer: Local development\nDescription: HyperDRM ambient light for webOS and Hyperion\nSection: misc\nPriority: optional\n\n'.encode()
|
||||||
|
members=[('debian-binary',b'2.0\n'),('control.tar.gz',tar_bytes([('control',control,0o644)])),('data.tar.gz',tar_bytes(entries))]
|
||||||
|
out=bytearray(b'!<arch>\n')
|
||||||
|
for name,body in members:
|
||||||
|
out.extend(f'{name+"/":<16}{0:<12}{0:<6}{0:<6}{"100644":<8}{len(body):<10}`\n'.encode());out.extend(body)
|
||||||
|
if len(body)%2:out.extend(b'\n')
|
||||||
|
destination=ROOT/('HyperDRM_'+VERSION+'_arm.ipk');destination.write_bytes(out)
|
||||||
|
print(destination)
|
||||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
|||||||
|
Tool: built-in image_gen
|
||||||
|
Transparent background: false
|
||||||
|
Prompt:
|
||||||
|
Create a polished square app logo icon for HyperDRM, an ambient television lighting app for webOS and Hyperion. One bold minimalist emblem: a front-facing television rectangle with a subtle stylized H incorporated into its dark screen, surrounded by a continuous luminous multicolor halo transitioning cyan blue violet magenta orange. Dark midnight navy solid background, clean geometric contours, premium modern technology brand, balanced centered composition with generous safe margins, recognizable at 80x80 pixels. Flat crisp graphic with restrained soft light glow, no photorealistic scene, no locks, no keys, no text or wordmark, no watermarks. Single icon fills square canvas.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Spatial calibration: red increases left-to-right, green top-to-bottom."""
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
ROOT=Path(__file__).resolve().parent
|
||||||
|
import imageio_ffmpeg
|
||||||
|
w,h=1280,720
|
||||||
|
pixels=bytearray()
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
pixels.extend((round(x*255/(w-1)),round(y*255/(h-1)),32))
|
||||||
|
ppm=ROOT/'calibration-gradient.ppm'
|
||||||
|
ppm.write_bytes(f'P6\n{w} {h}\n255\n'.encode()+pixels)
|
||||||
|
video=ROOT/'calibration-gradient.mp4'
|
||||||
|
subprocess.run([imageio_ffmpeg.get_ffmpeg_exe(),'-y','-loop','1','-framerate','1','-i',str(ppm),'-t','300','-c:v','libx264','-pix_fmt','yuv420p','-r','10','-crf','12','-movflags','+faststart',str(video)],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
|
||||||
|
print(video)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef HISTOGRAM_AUTOSTART_H
|
||||||
|
#define HISTOGRAM_AUTOSTART_H
|
||||||
|
#define BOOT_HOOK "/var/lib/webosbrew/init.d/60-histogramhyperion"
|
||||||
|
static const char 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 bool boot_enabled(void){return access(BOOT_HOOK,X_OK)==0;}
|
||||||
|
static bool set_boot(bool enabled){
|
||||||
|
struct stat st;
|
||||||
|
if(lstat(BOOT_HOOK,&st)==0){
|
||||||
|
if(!S_ISREG(st.st_mode))return false;
|
||||||
|
char existing[sizeof(boot_script)];int fd=open(BOOT_HOOK,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false;
|
||||||
|
ssize_t size=read(fd,existing,sizeof(existing));close(fd);
|
||||||
|
if(size!=(ssize_t)(sizeof(boot_script)-1) || memcmp(existing,boot_script,(size_t)size)!=0)return false;
|
||||||
|
if(!enabled)return unlink(BOOT_HOOK)==0;
|
||||||
|
return chmod(BOOT_HOOK,0755)==0;
|
||||||
|
}
|
||||||
|
if(errno!=ENOENT)return false;
|
||||||
|
if(!enabled)return true;
|
||||||
|
if(mkdir("/var/lib/webosbrew",0755)!=0 && errno!=EEXIST)return false;
|
||||||
|
if(mkdir("/var/lib/webosbrew/init.d",0755)!=0 && errno!=EEXIST)return false;
|
||||||
|
int fd=open(BOOT_HOOK ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0700);if(fd<0)return false;
|
||||||
|
size_t used=0;bool ok=true;
|
||||||
|
while(used<sizeof(boot_script)-1){ssize_t n=write(fd,boot_script+used,sizeof(boot_script)-1-used);if(n<=0){ok=false;break;}used+=(size_t)n;}
|
||||||
|
if(ok)ok=fchmod(fd,0755)==0 && fsync(fd)==0;
|
||||||
|
if(close(fd)!=0)ok=false;
|
||||||
|
if(ok)ok=rename(BOOT_HOOK ".tmp",BOOT_HOOK)==0;
|
||||||
|
if(!ok)unlink(BOOT_HOOK ".tmp");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef BLACKBARS_H
|
||||||
|
#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++;
|
||||||
|
}
|
||||||
|
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
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef COLOR_BOOST_H
|
||||||
|
#define COLOR_BOOST_H
|
||||||
|
/* HSV saturation x2 and value x1.33, preserving hue and clamping value. */
|
||||||
|
static inline void boost_color(const unsigned char in[3],unsigned char out[3]){
|
||||||
|
unsigned lo=in[0],hi=in[0];
|
||||||
|
for(unsigned c=1;c<3;c++){if(in[c]<lo)lo=in[c];if(in[c]>hi)hi=in[c];}
|
||||||
|
unsigned chroma=hi-lo;
|
||||||
|
unsigned peak=(hi*133+50)/100;if(peak>255)peak=255;
|
||||||
|
if(!chroma){for(unsigned c=0;c<3;c++)out[c]=(unsigned char)peak;return;}
|
||||||
|
unsigned expanded=chroma*2;if(expanded>hi)expanded=hi;
|
||||||
|
for(unsigned c=0;c<3;c++){
|
||||||
|
unsigned value=hi-expanded+((in[c]-lo)*expanded+chroma/2)/chroma;
|
||||||
|
out[c]=(unsigned char)((value*peak+hi/2)/hi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#ifndef LG_HISTOGRAM_H
|
||||||
|
#define LG_HISTOGRAM_H
|
||||||
|
#include <stdint.h>
|
||||||
|
#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");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#ifndef HYPERION_LAYOUT_H
|
||||||
|
#define HYPERION_LAYOUT_H
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <math.h>
|
||||||
|
typedef struct {unsigned char a,b;unsigned weight;} LedBlend;
|
||||||
|
/* Measured G4 rim samples, independent of LED count and wiring order. */
|
||||||
|
static const struct {double position;unsigned char region;} 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 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){
|
||||||
|
double t=screen_perimeter(x,y);unsigned n=sizeof(rim)/sizeof(*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);
|
||||||
|
dlclose(lib);
|
||||||
|
if(!valid){free(mapping);return false;}
|
||||||
|
*result=mapping;*count=(unsigned)n;return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include "histogram.h"
|
||||||
|
/* Process-local diagnostic sink. It changes logging only, not capture policy. */
|
||||||
|
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)level;(void)message_id;(void)file;
|
||||||
|
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 bool policy(void) { fprintf(stderr,"Capture revoked by LG policy\n"); return true; }
|
||||||
|
int main(void) {
|
||||||
|
void *lib=dlopen("libvtcapture.so.1",RTLD_NOW);
|
||||||
|
if(!lib){fprintf(stderr,"dlopen: %s\n",dlerror());return 1;}
|
||||||
|
void *(*init)(bool(*)(void))=dlsym(lib,"vtCapture_initHistogram");
|
||||||
|
int (*preset)(void*,int)=dlsym(lib,"vtCapture_setPresetHistogram");
|
||||||
|
int (*get)(void*,Histogram*)=dlsym(lib,"vtCapture_getHistogram");
|
||||||
|
int (*finish)(void*)=dlsym(lib,"vtCapture_finalizeHistogram");
|
||||||
|
if(!init||!preset||!get||!finish)return 2;
|
||||||
|
void *handle=init(policy); fprintf(stderr,"handle=%p\n",handle);
|
||||||
|
if(!handle)return 3;
|
||||||
|
int rc=preset(handle,0);fprintf(stderr,"preset=%d\n",rc);
|
||||||
|
Histogram hist[16];
|
||||||
|
for(int frame=0;rc==0 && frame<3;frame++) {
|
||||||
|
usleep(100000); rc=get(handle,hist); printf("frame=%d rc=%d\n",frame,rc);
|
||||||
|
if(rc==0)for(int i=0;i<16;i++) {printf("region=%u",hist[i].id);for(int c=0;c<3;c++){uint64_t sum=0;for(int b=0;b<256;b++)sum+=hist[i].bins[c][b];printf(" ch%d_count=%llu mean=%u",c,(unsigned long long)sum,histogram_mean(hist[i].bins[c]));} puts("");}
|
||||||
|
}
|
||||||
|
fprintf(stderr,"finalize=%d\n",finish(handle)); return rc?4:0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include "histogram.h"
|
||||||
|
#include "blackbars.h"
|
||||||
|
#include "color_boost.h"
|
||||||
|
#include "interpolation.h"
|
||||||
|
#include "layout.h"
|
||||||
|
#include "autostart.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*);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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 bool color_valid;
|
||||||
|
static unsigned led_count;
|
||||||
|
static LedBlend *led_blends;
|
||||||
|
static bool interpolation_enabled;
|
||||||
|
static unsigned recovery_attempts;
|
||||||
|
static uint64_t retry_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;
|
||||||
|
#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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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 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 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;}}
|
||||||
|
}
|
||||||
|
bool ok=complete && layout_from_json(data,&led_blends,&led_count);free(data);interpolation_enabled=ok;return ok;
|
||||||
|
}
|
||||||
|
static void stop_capture(void){
|
||||||
|
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(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 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 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; endpoint is localhost:19444\"}");
|
||||||
|
if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}");
|
||||||
|
if(m){retry_at=0;recovery_attempts=0;}
|
||||||
|
frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0;
|
||||||
|
if(!enter_developer_mode()){set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Root access is required for temporary LG developer initialization\"}");}
|
||||||
|
capture=vt_init(policy);
|
||||||
|
if(!leave_developer_mode()){stop_capture();set_error("Could not restore LG developer marker; check vt_devmode before retrying");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Developer marker cleanup failed\"}");}
|
||||||
|
if(!capture){set_error("LG could not acquire the histogram capture resource. Check for another active capture source; no colors are being sent.");return respond(h,m,"{\"returnValue\":false,\"errorCode\":1,\"errorText\":\"LG refused histogram initialization. Another capture source may hold VT_CAPTURE. No colors are being sent.\"}");}
|
||||||
|
if(vt_preset(capture,0)!=0){stop_capture();set_error("LG histogram preset initialization failed");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Histogram preset failed\"}");}
|
||||||
|
capture_preset=letterbox_mode=settling=0;pending_letterbox=-1;next_border_probe=0;
|
||||||
|
output=socket(AF_INET,SOCK_STREAM,0);
|
||||||
|
struct timeval timeout={1,0};
|
||||||
|
if(output>=0){setsockopt(output,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(output,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));}
|
||||||
|
struct sockaddr_in a={.sin_family=AF_INET,.sin_port=htons(19444),.sin_addr={htonl(INADDR_LOOPBACK)}};
|
||||||
|
if(output<0||connect(output,(void*)&a,sizeof(a))!=0){stop_capture();set_error("Hyperion JSON server is unavailable on localhost:19444");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot connect to local Hyperion\"}");}
|
||||||
|
if(!read_layout()){stop_capture();set_error("Cannot read a valid LED layout from Hyperion");return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot read Hyperion LED layout\"}");}
|
||||||
|
return respond(h,m,"{\"returnValue\":true,\"running\":true}");
|
||||||
|
}
|
||||||
|
static bool stop(void *h,void *m,void *ctx){(void)ctx;retry_at=0;recovery_attempts=0;stop_capture();return respond(h,m,"{\"returnValue\":true,\"running\":false}");}
|
||||||
|
static bool autostart(void *h,void *m,void *ctx){
|
||||||
|
(void)ctx;const char *p=m?payload(m):NULL;bool enabled;
|
||||||
|
if(p && strcmp(p,"{\"enabled\":true}")==0)enabled=true;
|
||||||
|
else if(p && strcmp(p,"{\"enabled\":false}")==0)enabled=false;
|
||||||
|
else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}");
|
||||||
|
if(geteuid()!=0 || !set_boot(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Autostart could not be saved. Check own service root elevation and boot hook.\"}");
|
||||||
|
return respond(h,m,enabled?"{\"returnValue\":true,\"autostart\":true}":"{\"returnValue\":true,\"autostart\":false}");
|
||||||
|
}
|
||||||
|
static bool status(void *h,void *m,void *ctx){
|
||||||
|
(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;
|
||||||
|
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.0\",\"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]);
|
||||||
|
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\":2.0,\"brightnessGain\":1.33,\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,\"letterboxDetected\":%s}",capture_preset,interpolation_enabled?"true":"false",boot_enabled()?"true":"false",capture&&letterbox_mode?"true":"false");return respond(h,m,json);
|
||||||
|
}
|
||||||
|
static int tick(void *unused){
|
||||||
|
(void)unused;
|
||||||
|
if(!capture){
|
||||||
|
if(retry_at && milliseconds()>=retry_at){
|
||||||
|
retry_at=0;recovery_attempts++;start(NULL,NULL,NULL);
|
||||||
|
if(!capture && recovery_attempts<6)retry_at=milliseconds()+5000;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if(revoked){set_error("LG revoked capture; retrying initialization after source transition");stop_capture();recovery_attempts=0;retry_at=milliseconds()+5000;return 1;}
|
||||||
|
if(settling){settling--;return 1;}
|
||||||
|
if(letterbox_mode && capture_preset==1 && milliseconds()>=next_border_probe){
|
||||||
|
if(vt_preset(capture,0)!=0){set_error("Border probe preset failed");stop_capture();return 1;}
|
||||||
|
capture_preset=0;settling=2;return 1;
|
||||||
|
}
|
||||||
|
Histogram hist[16]={0};int rc=vt_get(capture,hist);
|
||||||
|
if(rc){set_error("LG histogram read failed; streaming stopped");stop_capture();return 1;}
|
||||||
|
unsigned aggregate[3]={0};
|
||||||
|
for(int i=0;i<16;i++){
|
||||||
|
black_percent[i]=100;
|
||||||
|
for(int c=0;c<3;c++){
|
||||||
|
uint64_t total=0,black=0;
|
||||||
|
for(int b=0;b<256;b++){total+=hist[i].bins[c][b];if(b<=12)black+=hist[i].bins[c][b];}
|
||||||
|
unsigned fraction=total?(unsigned)(100*black/total):0;
|
||||||
|
if(fraction<black_percent[i])black_percent[i]=fraction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(int i=0;i<16;i++)for(int c=0;c<3;c++){
|
||||||
|
rgb[i][c]=histogram_mean(hist[i].bins[c]);aggregate[c]+=rgb[i][c];
|
||||||
|
}
|
||||||
|
if(capture_preset==0){
|
||||||
|
int candidate=letterbox_candidate(black_percent,rgb);uint64_t now=milliseconds();
|
||||||
|
if(candidate<0)pending_letterbox=-1;
|
||||||
|
else if(candidate!=pending_letterbox){pending_letterbox=candidate;candidate_since=now;}
|
||||||
|
else if((unsigned)candidate!=letterbox_mode && now-candidate_since>=2000)letterbox_mode=(unsigned)candidate;
|
||||||
|
if(letterbox_mode){
|
||||||
|
if(vt_preset(capture,1)!=0){set_error("Letterbox preset failed");stop_capture();return 1;}
|
||||||
|
capture_preset=1;settling=2;next_border_probe=now+2000;return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(int c=0;c<3;c++)sent_rgb[c]=(unsigned char)(aggregate[c]/16);
|
||||||
|
unsigned char boosted_mean[3];boost_color(sent_rgb,boosted_mean);memcpy(sent_rgb,boosted_mean,3);
|
||||||
|
size_t capacity=256+(size_t)led_count*12;char *line=malloc(capacity);
|
||||||
|
if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;}
|
||||||
|
size_t written=(size_t)snprintf(line,capacity,"{\"command\":\"color\",\"priority\":%d,\"duration\":1500,\"origin\":\"HyperDRM\",\"color\":[",PRIORITY);
|
||||||
|
for(unsigned i=0;i<led_count;i++){
|
||||||
|
unsigned char interpolated[3],boosted[3];LedBlend blend=led_blends[i];
|
||||||
|
blend_color(rgb[blend.a],rgb[blend.b],blend.weight,interpolated);boost_color(interpolated,boosted);
|
||||||
|
written+=(size_t)snprintf(line+written,capacity-written,"%s%u,%u,%u",i?",":"",boosted[0],boosted[1],boosted[2]);
|
||||||
|
}
|
||||||
|
snprintf(line+written,capacity-written,"]}\n");
|
||||||
|
int failed=send_line(line);free(line);
|
||||||
|
if(failed){set_error("Hyperion connection lost");stop_capture();return 1;}
|
||||||
|
char answer[4096];size_t used=0;bool complete=false;
|
||||||
|
while(used+1<sizeof(answer)){ssize_t len=recv(output,answer+used,sizeof(answer)-used-1,0);if(len<=0)break;used+=(size_t)len;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
|
||||||
|
if(!complete){set_error("Hyperion did not acknowledge color data");stop_capture();return 1;}
|
||||||
|
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Hyperion rejected the color command");stop_capture();return 1;}
|
||||||
|
color_valid=true;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);
|
||||||
|
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");
|
||||||
|
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},{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;}
|
||||||
|
timer(100,tick,NULL);
|
||||||
|
while(!quitting){iterate(NULL,0);usleep(10000);}
|
||||||
|
stop_capture();unreg(handle,&e);error_free(&e);loop_unref(loop);return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "blackbars.h"
|
||||||
|
int main(void){
|
||||||
|
unsigned black[16]={0};unsigned char color[16][3];memset(color,100,sizeof(color));
|
||||||
|
assert(letterbox_candidate(black,color)==0);
|
||||||
|
black[3]=black[11]=black[12]=66;black[1]=55;black[5]=black[9]=33;
|
||||||
|
assert(letterbox_candidate(black,color)==1);
|
||||||
|
black[1]=black[5]=black[9]=0;assert(letterbox_candidate(black,color)==-1);
|
||||||
|
for(unsigned i=0;i<16;i++)black[i]=100;
|
||||||
|
memset(color,0,sizeof(color));assert(letterbox_candidate(black,color)==-1);
|
||||||
|
memset(black,0,sizeof(black));assert(letterbox_candidate(black,color)==-1);
|
||||||
|
puts("PASS: full frame, measured letterbox, one edge only, black frame, dark frame");
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "color_boost.h"
|
||||||
|
int main(void){
|
||||||
|
unsigned char out[3];
|
||||||
|
for(unsigned v=0;v<256;v++){
|
||||||
|
unsigned char gray[3]={v,v,v};boost_color(gray,out);
|
||||||
|
unsigned expected=(v*133+50)/100;if(expected>255)expected=255;
|
||||||
|
assert(out[0]==expected && out[1]==expected && out[2]==expected);
|
||||||
|
}
|
||||||
|
unsigned char cyan[3]={93,224,242};boost_color(cyan,out);
|
||||||
|
assert(out[0]<93 && out[1]>out[0] && out[1]<out[2] && out[2]==255);
|
||||||
|
for(unsigned c=0;c<3;c++){
|
||||||
|
unsigned char primary[3]={0};primary[c]=255;boost_color(primary,out);
|
||||||
|
for(unsigned j=0;j<3;j++)assert(out[j]==primary[j]);
|
||||||
|
}
|
||||||
|
unsigned char warm[3]={171,82,0};boost_color(warm,out);
|
||||||
|
assert(out[0]==227 && out[1]>82 && out[2]==0);
|
||||||
|
puts("PASS: neutral grays, stronger cyan, brighter warm color, clamped primaries");
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "histogram.h"
|
||||||
|
int main(void) {
|
||||||
|
Histogram h={0};
|
||||||
|
assert(histogram_mean(h.bins[0])==0);
|
||||||
|
for(unsigned intensity=0;intensity<256;intensity++) {
|
||||||
|
memset(&h,0,sizeof(h));h.bins[0][intensity]=65535;
|
||||||
|
assert(histogram_mean(h.bins[0])==intensity);
|
||||||
|
}
|
||||||
|
/* Adjacent halfwords must stay independent, including the final bin. */
|
||||||
|
memset(&h,0,sizeof(h));h.bins[0][254]=65535;h.bins[0][255]=1;
|
||||||
|
assert(histogram_mean(h.bins[0])==254);
|
||||||
|
/* Weighted sum exceeds uint32 for full uint16 counts in every bin. */
|
||||||
|
for(unsigned b=0;b<256;b++)h.bins[0][b]=65535;
|
||||||
|
assert(histogram_mean(h.bins[0])==128);
|
||||||
|
for(unsigned primary=0;primary<3;primary++) {
|
||||||
|
memset(&h,0,sizeof(h));
|
||||||
|
for(unsigned c=0;c<3;c++)h.bins[c][c==primary?255:0]=100;
|
||||||
|
for(unsigned c=0;c<3;c++)assert(histogram_mean(h.bins[c])==(c==primary?255:0));
|
||||||
|
}
|
||||||
|
puts("PASS: ABI, all 256 intensities, adjacent bins, overflow, RGB primaries");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "interpolation.h"
|
||||||
|
int main(void){
|
||||||
|
const unsigned char a[3]={255,0,0},b[3]={0,255,0};unsigned char out[3];
|
||||||
|
blend_color(a,b,0,out);assert(out[0]==255 && out[1]==0 && out[2]==0);
|
||||||
|
blend_color(a,b,65535,out);assert(out[0]==0 && out[1]==255 && out[2]==0);
|
||||||
|
blend_color(a,b,32768,out);assert(out[0]==127 && out[1]==128 && out[2]==0);
|
||||||
|
unsigned previous=0;
|
||||||
|
for(unsigned w=0;w<=65535;w++){
|
||||||
|
blend_color(a,b,w,out);assert(out[1]>=previous && out[2]==0);previous=out[1];
|
||||||
|
unsigned char gray[3]={173,173,173};blend_color(gray,gray,w,out);
|
||||||
|
assert(out[0]==173 && out[1]==173 && out[2]==173);
|
||||||
|
}
|
||||||
|
puts("PASS: endpoints, midpoint, all weights monotonic, constant gray preserved");
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "layout.h"
|
||||||
|
int main(void){
|
||||||
|
unsigned sizes[]={50,500};
|
||||||
|
for(unsigned k=0;k<2;k++){
|
||||||
|
unsigned n=sizes[k];size_t capacity=150*n+100;char *text=malloc(capacity);size_t used=snprintf(text,capacity,"{\"info\":{\"leds\":[");
|
||||||
|
for(unsigned i=0;i<n;i++)used+=snprintf(text+used,capacity-used,"%s{\"hmin\":%.8f,\"hmax\":%.8f,\"vmin\":0,\"vmax\":0.05}",i?",":"",(double)i/n,(double)(i+1)/n);
|
||||||
|
snprintf(text+used,capacity-used,"]}}");LedBlend *mapping=NULL;unsigned count=0;
|
||||||
|
assert(layout_from_json(text,&mapping,&count));assert(count==n);
|
||||||
|
for(unsigned i=0;i<n;i++)assert(mapping[i].a<16 && mapping[i].b<16 && mapping[i].weight<=65535);
|
||||||
|
assert(mapping[0].a!=mapping[n-1].a);free(mapping);free(text);printf("PASS: %u LEDs, positions and order preserved\n",n);
|
||||||
|
}
|
||||||
|
const char *bad[]={"{}","{\"info\":{\"leds\":[]}}","{\"info\":{\"leds\":[42]}}","{\"info\":{\"leds\":[{\"hmin\":2,\"hmax\":1,\"vmin\":0,\"vmax\":1}]}}"};
|
||||||
|
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){LedBlend *mapping=NULL;unsigned count=0;assert(!layout_from_json(bad[i],&mapping,&count));assert(!mapping && !count);}
|
||||||
|
puts("PASS: malformed layouts rejected without replacing output");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"id": "org.webosbrew.histogramhyperion", "version": "0.6.0", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"]}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"id": "org.webosbrew.histogramhyperion.service", "description": "HyperDRM ambient light bridge to local Hyperion", "engine": "native", "executable": "histogram-service", "services": [{"name": "org.webosbrew.histogramhyperion.service", "description": "HyperDRM"}]}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"method": "nearest measured centroid; approximate, without HDR calibration",
|
||||||
|
"regions": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"x": 0.5254901960784314,
|
||||||
|
"y": 0.5450980392156862,
|
||||||
|
"rgb": [
|
||||||
|
134,
|
||||||
|
139,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"x": 0.45098039215686275,
|
||||||
|
"y": 0.9882352941176471,
|
||||||
|
"rgb": [
|
||||||
|
115,
|
||||||
|
252,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"x": 0.5686274509803921,
|
||||||
|
"y": 0.5843137254901961,
|
||||||
|
"rgb": [
|
||||||
|
145,
|
||||||
|
149,
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"x": 0.3176470588235294,
|
||||||
|
"y": 0.03529411764705882,
|
||||||
|
"rgb": [
|
||||||
|
81,
|
||||||
|
9,
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"x": 0.2823529411764706,
|
||||||
|
"y": 0.5607843137254902,
|
||||||
|
"rgb": [
|
||||||
|
72,
|
||||||
|
143,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"x": 0.13333333333333333,
|
||||||
|
"y": 0.9607843137254902,
|
||||||
|
"rgb": [
|
||||||
|
34,
|
||||||
|
245,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"x": 0.12941176470588237,
|
||||||
|
"y": 0.5882352941176471,
|
||||||
|
"rgb": [
|
||||||
|
33,
|
||||||
|
150,
|
||||||
|
11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"x": 0.20784313725490197,
|
||||||
|
"y": 0.1411764705882353,
|
||||||
|
"rgb": [
|
||||||
|
53,
|
||||||
|
36,
|
||||||
|
15
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"x": 0.7490196078431373,
|
||||||
|
"y": 0.5176470588235295,
|
||||||
|
"rgb": [
|
||||||
|
191,
|
||||||
|
132,
|
||||||
|
13
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"x": 0.8156862745098039,
|
||||||
|
"y": 0.9019607843137255,
|
||||||
|
"rgb": [
|
||||||
|
208,
|
||||||
|
230,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"x": 0.8823529411764706,
|
||||||
|
"y": 0.49411764705882355,
|
||||||
|
"rgb": [
|
||||||
|
225,
|
||||||
|
126,
|
||||||
|
17
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"x": 0.5490196078431373,
|
||||||
|
"y": 0.03137254901960784,
|
||||||
|
"rgb": [
|
||||||
|
140,
|
||||||
|
8,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"x": 0.7529411764705882,
|
||||||
|
"y": 0.027450980392156862,
|
||||||
|
"rgb": [
|
||||||
|
192,
|
||||||
|
7,
|
||||||
|
9
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"x": 0.8392156862745098,
|
||||||
|
"y": 0.12156862745098039,
|
||||||
|
"rgb": [
|
||||||
|
214,
|
||||||
|
31,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"x": 0.5450980392156862,
|
||||||
|
"y": 0.0,
|
||||||
|
"rgb": [
|
||||||
|
139,
|
||||||
|
0,
|
||||||
|
11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"x": 0.09411764705882353,
|
||||||
|
"y": 0.6,
|
||||||
|
"rgb": [
|
||||||
|
24,
|
||||||
|
153,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ledMapping": [
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
13,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
9,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
15,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user