Private
Public Access
Add resilient handover, live LED layout refresh and TV calibration profiles
This commit is contained in:
1 parent
4ab4fa034b
commit
e55d37772a
26 files changed
+543
-134
No files matched your search
@@ -0,0 +1,63 @@
|
||||
"""Test-only JSON relay with a controlled serverinfo LED layout.
|
||||
|
||||
Only the configured TV may connect. Receiver settings remain unchanged.
|
||||
The control file can select count/shift, an invalid layout or passthrough.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import socketserver
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--bind', required=True)
|
||||
parser.add_argument('--port', type=int, default=19445)
|
||||
parser.add_argument('--upstream', required=True)
|
||||
parser.add_argument('--upstream-port', type=int, default=19444)
|
||||
parser.add_argument('--control-file', required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
class Relay(socketserver.StreamRequestHandler):
|
||||
def handle(self):
|
||||
if self.client_address[0] != args.upstream:
|
||||
return
|
||||
try:
|
||||
with socket.create_connection((args.upstream, args.upstream_port), timeout=3) as remote:
|
||||
incoming = remote.makefile('rb')
|
||||
while line := self.rfile.readline(2 * 1024 * 1024):
|
||||
command = json.loads(line)
|
||||
remote.sendall(line)
|
||||
reply = incoming.readline(2 * 1024 * 1024)
|
||||
if not reply:
|
||||
return
|
||||
if command.get('command') == 'serverinfo':
|
||||
control = json.loads(Path(args.control_file).read_text(encoding='utf-8'))
|
||||
response = json.loads(reply)
|
||||
if control.get('invalid'):
|
||||
response['info']['leds'] = []
|
||||
elif 'count' in control:
|
||||
count = control['count']
|
||||
if not isinstance(count, int) or not 1 <= count <= 16384:
|
||||
return
|
||||
shift = control.get('shift', 0)
|
||||
response['info']['leds'] = [
|
||||
{'hmin': (i + shift) / count, 'hmax': (i + 1) / count,
|
||||
'vmin': 0, 'vmax': .05}
|
||||
for i in range(count)
|
||||
]
|
||||
reply = json.dumps(response).encode() + b'\n'
|
||||
print(json.dumps({'serverinfo': control}), flush=True)
|
||||
self.request.sendall(reply[:10])
|
||||
self.request.sendall(reply[10:])
|
||||
except (OSError, ValueError, KeyError):
|
||||
pass
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingTCPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
with Server((args.bind, args.port), Relay) as server:
|
||||
print('Layout relay ready', flush=True)
|
||||
server.serve_forever()
|
||||
@@ -1,10 +1,10 @@
|
||||
'use strict';
|
||||
const assert=require('assert'),fs=require('fs'),vm=require('vm');
|
||||
const ids=['host','port','piccapPort','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics'];
|
||||
const ids=['host','port','piccapPort','receiver','token','clearToken','endpoint','handover','autostart','error','status','regions','start','stop','refresh','saveTarget','testConnection','testResult','diagnostics','profile','customEditor','customProfile','saveProfile','profileStatus'];
|
||||
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=[];
|
||||
const status={returnValue:true,version:'0.9.0',host:'127.0.0.1',port:19444,piccapPort:19400,receiver:'HyperHDR',tokenSet:true,piccapInstalled:true,autostart:true,captureState:'Histogram',playbackProtection:'protected',connectionTest:{state:'Passed',ledCount:120,elapsedMs:300,error:''},uid:0};
|
||||
const status={returnValue:true,version:'0.9.0',host:'127.0.0.1',port:19444,piccapPort:19400,receiver:'HyperHDR',tokenSet:true,piccapInstalled:true,autostart:true,captureState:'Histogram',playbackProtection:'protected',connectionTest:{state:'Passed',ledCount:120,elapsedMs:300,error:''},uid:0,calibrationProfile:'auto',effectiveCalibration:'g4',tvModel:'OLED65G42LW.DEUQLJP',customModel:'',customPoints:[]};
|
||||
class Bridge {
|
||||
call(uri,data){requests.push({uri,data:JSON.parse(data)});this.onservicecallback(JSON.stringify(uri.endsWith('/status')?status:{returnValue:true}));}
|
||||
cancel(){}
|
||||
@@ -21,4 +21,7 @@ elements.piccapPort.value='19446';elements.host.value='10.1.0.100';elements.toke
|
||||
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.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'));
|
||||
context.profile='custom';context.showProfile();elements.customProfile.value='invalid';elements.saveProfile.onclick();assert(elements.error.textContent.includes('valid JSON'));
|
||||
elements.customProfile.value=JSON.stringify({model:'OTHER-TV',points:[{position:0,region:1},{position:2,region:5}]});elements.saveProfile.onclick();assert(requests.some(r=>r.uri.endsWith('/calibration')&&r.data.model==='OTHER-TV'&&r.data.points.length===2));
|
||||
console.log('PASS: receiver fields, port validation, saved-token retention, test control and diagnostics');
|
||||
@@ -30,6 +30,10 @@ key(39);assert.equal(doc.activeElement.id,'start');key(37);assert.equal(doc.acti
|
||||
buttons[2].tagName='INPUT';buttons[2].type='text';buttons[2].focus();
|
||||
assert(!key(37));assert(!key(39));assert(!key(13));assert.equal(doc.activeElement.id,'refresh');
|
||||
key(40);assert.equal(doc.activeElement.id,'autostart');key(38);assert.equal(doc.activeElement.id,'refresh');
|
||||
buttons[2].tagName='TEXTAREA';buttons[2].focus();
|
||||
assert(!key(37));assert(!key(39));assert(!key(13));
|
||||
key(40);assert.equal(doc.activeElement.id,'autostart');
|
||||
key(38);assert.equal(doc.activeElement.id,'refresh');
|
||||
buttons[2].tagName='BUTTON';
|
||||
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');
|
||||
Reference in new issue
Block a user