450 lines
16 KiB
Python
450 lines
16 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
# LPC 0140650 web UI,运行于 Ubuntu 192.168.58.7:8090
|
|||
|
|
# LPC 自治架构:Modbus 轮询+闭环在 LPC Lua(触发器 #14/#15),本程序只做监控/记录/HTTP 转发
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
import urllib.request
|
|||
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|||
|
|
from urllib.parse import urlparse, parse_qs
|
|||
|
|
|
|||
|
|
LPC = "http://192.168.58.109"
|
|||
|
|
PORT = 8090
|
|||
|
|
HIST_FILE = "/home/tonycao/rs485-lpc/history.jsonl"
|
|||
|
|
SAMPLE_INTERVAL = 30 # 秒
|
|||
|
|
HIST_KEEP = 48 * 3600 # 48 小时
|
|||
|
|
|
|||
|
|
TRIG = {"BRT": 2, "LUX": 8, "LXT": 9, "LX2": 10, "FX": 11, "SC": 12,
|
|||
|
|
"FAD": 18, "PER": 19, "DBD": 20}
|
|||
|
|
TRIG_LINK_ON = 16
|
|||
|
|
TRIG_LINK_OFF = 17
|
|||
|
|
|
|||
|
|
_hist = [] # [[t_ms, brt, out, lux], ...] lux 可为 None
|
|||
|
|
_hist_lock = threading.Lock()
|
|||
|
|
_samples_since_compact = 0
|
|||
|
|
|
|||
|
|
def lpc_json(path):
|
|||
|
|
with urllib.request.urlopen(LPC + path, timeout=5) as r:
|
|||
|
|
return json.loads(r.read().decode("utf-8", "replace"))
|
|||
|
|
|
|||
|
|
def lpc_lua(names):
|
|||
|
|
d = lpc_json("/api/lua?variables=" + ",".join(names))
|
|||
|
|
return d
|
|||
|
|
|
|||
|
|
def lua_num(v):
|
|||
|
|
try:
|
|||
|
|
return int(round(float(v)))
|
|||
|
|
except Exception:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def read_lux():
|
|||
|
|
try:
|
|||
|
|
return lua_num(lpc_lua(["lux_value"]).get("lux_value"))
|
|||
|
|
except Exception:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def link_on():
|
|||
|
|
try:
|
|||
|
|
return bool(lpc_lua(["lux_link_on"]).get("lux_link_on"))
|
|||
|
|
except Exception:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def fire_trigger(num, var=None):
|
|||
|
|
url = LPC + "/api/trigger?num=%d" % num
|
|||
|
|
if var is not None:
|
|||
|
|
url += "&var=%d" % var
|
|||
|
|
req = urllib.request.Request(url, data=b"", method="POST")
|
|||
|
|
with urllib.request.urlopen(req, timeout=5) as r:
|
|||
|
|
r.read()
|
|||
|
|
|
|||
|
|
def sample_once():
|
|||
|
|
t = int(time.time() * 1000)
|
|||
|
|
brt = None
|
|||
|
|
out = None
|
|||
|
|
try:
|
|||
|
|
g = lpc_json("/api/group")
|
|||
|
|
brt = g["groups"][0]["level"]
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
o = lpc_json("/api/output?universe=dmx:1")
|
|||
|
|
vals = []
|
|||
|
|
for c in o["channels"][:32]:
|
|||
|
|
m = re.search(r"\d+", str(c))
|
|||
|
|
vals.append(int(m.group(0)) if m else 0)
|
|||
|
|
out = round(sum(vals) * 100.0 / (255 * len(vals)), 1)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
lux = read_lux()
|
|||
|
|
row = [t, brt, out, lux]
|
|||
|
|
with _hist_lock:
|
|||
|
|
_hist.append(row)
|
|||
|
|
cutoff = t - HIST_KEEP * 1000
|
|||
|
|
while _hist and _hist[0][0] < cutoff:
|
|||
|
|
_hist.pop(0)
|
|||
|
|
try:
|
|||
|
|
with open(HIST_FILE, "a") as f:
|
|||
|
|
f.write(json.dumps(row, separators=(",", ":")) + "\n")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
global _samples_since_compact
|
|||
|
|
_samples_since_compact += 1
|
|||
|
|
if _samples_since_compact >= 120: # 每小时压实一次文件
|
|||
|
|
_samples_since_compact = 0
|
|||
|
|
compact_history()
|
|||
|
|
|
|||
|
|
def compact_history():
|
|||
|
|
with _hist_lock:
|
|||
|
|
data = list(_hist)
|
|||
|
|
try:
|
|||
|
|
with open(HIST_FILE, "w") as f:
|
|||
|
|
for r in data:
|
|||
|
|
f.write(json.dumps(r, separators=(",", ":")) + "\n")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def load_history():
|
|||
|
|
try:
|
|||
|
|
cutoff = int(time.time() * 1000) - HIST_KEEP * 1000
|
|||
|
|
with open(HIST_FILE) as f:
|
|||
|
|
for line in f:
|
|||
|
|
line = line.strip()
|
|||
|
|
if not line:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
r = json.loads(line)
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
if isinstance(r, list) and len(r) == 4 and r[0] >= cutoff:
|
|||
|
|
_hist.append(r)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def sampler_loop():
|
|||
|
|
while True:
|
|||
|
|
try:
|
|||
|
|
sample_once()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
time.sleep(SAMPLE_INTERVAL)
|
|||
|
|
|
|||
|
|
PAGE = r"""<!DOCTYPE html>
|
|||
|
|
<html lang="zh">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|||
|
|
<title>LPC 灯光控制</title>
|
|||
|
|
<style>
|
|||
|
|
body{font-family:system-ui,"Microsoft YaHei",sans-serif;background:#111;color:#eee;margin:0;padding:12px;max-width:860px;margin:auto}
|
|||
|
|
h1{font-size:20px} h2{font-size:15px;color:#9cf;margin:14px 0 6px}
|
|||
|
|
.card{background:#1d1d1d;border-radius:10px;padding:12px;margin-bottom:12px}
|
|||
|
|
button{font-size:15px;padding:8px 14px;margin:4px;border:0;border-radius:8px;background:#2d6cdf;color:#fff;cursor:pointer}
|
|||
|
|
button.sc{background:#555}
|
|||
|
|
.row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:4px 0}
|
|||
|
|
.bars{display:grid;grid-template-columns:repeat(16,1fr);gap:4px}
|
|||
|
|
.ch{background:#333;border-radius:4px;text-align:center;font-size:10px;padding:2px 0}
|
|||
|
|
.ch .v{height:38px;display:flex;align-items:flex-end;justify-content:center}
|
|||
|
|
.ch .f{width:80%;border-radius:3px 3px 0 0;background:#fc3}
|
|||
|
|
.tl{display:flex;gap:8px;flex-wrap:wrap}
|
|||
|
|
.t{padding:6px 10px;border-radius:8px;background:#333;font-size:13px}
|
|||
|
|
.t.run{background:#1e5e1e}.t.rel{background:#5e1e1e}
|
|||
|
|
input[type=range]{width:220px}
|
|||
|
|
.sw{font-size:15px;padding:8px 14px;border-radius:8px;border:0;cursor:pointer;background:#555;color:#fff}
|
|||
|
|
.sw.on{background:#1e7e1e}
|
|||
|
|
#lux{font-size:24px;color:#fc3}
|
|||
|
|
.dim{color:#888;font-size:12px}
|
|||
|
|
canvas{width:100%;height:240px;background:#161616;border-radius:8px}
|
|||
|
|
.lg{display:inline-block;width:18px;height:3px;margin:0 5px 2px 12px;vertical-align:middle}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<h1>LPC 0140650 实时状态</h1>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>照度联动</h2>
|
|||
|
|
<div class="row">
|
|||
|
|
<button id="luxsw" class="sw" onclick="luxlink()">…</button>
|
|||
|
|
<span>照度:<b id="lux">-</b> Lux</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="row">目标照度:
|
|||
|
|
<input type="number" id="lxt" min="1" max="200000" step="10" style="width:90px"> Lux
|
|||
|
|
<button onclick="setlxt()">设定</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="row">轮询周期:
|
|||
|
|
<input type="number" id="per" min="1" max="60" step="1" style="width:60px"> s
|
|||
|
|
<button onclick="setper()">设定</button>
|
|||
|
|
渐变时间:
|
|||
|
|
<input type="number" id="fad" min="0.1" max="10" step="0.1" style="width:60px"> s
|
|||
|
|
<button onclick="setfad()">设定</button>
|
|||
|
|
死区:
|
|||
|
|
<input type="number" id="dbd" min="1" max="1000" step="1" style="width:60px"> Lux
|
|||
|
|
<button onclick="setdbd()">设定</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="dim">闭环在 LPC 内运行(触发器 #14 每秒轮询传感器,#15 Lua 自动调光);轮询周期=闭环调节间隔,照度采集与显示仍每秒刷新;FAD 单位为 0.1s</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>控制</h2>
|
|||
|
|
<div class="row">效果:
|
|||
|
|
<button onclick="send('FX=01')">常亮</button>
|
|||
|
|
<button onclick="send('FX=02')">呼吸</button>
|
|||
|
|
<button onclick="send('FX=03')">闪烁</button>
|
|||
|
|
<button onclick="send('FX=04')">缓变</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="row">场景:
|
|||
|
|
<button class="sc" onclick="send('SC=01')">全亮</button>
|
|||
|
|
<button class="sc" onclick="send('SC=02')">半亮</button>
|
|||
|
|
<button class="sc" onclick="send('SC=03')">微亮</button>
|
|||
|
|
<button class="sc" onclick="send('SC=04')">全灭</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="row">总亮度:
|
|||
|
|
<input type="range" id="brt" min="0" max="100" value="100" onchange="brt()">
|
|||
|
|
<span id="brtv">100</span>%
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>历史曲线(48 小时,30s 采样)</h2>
|
|||
|
|
<canvas id="hist"></canvas>
|
|||
|
|
<div class="dim">
|
|||
|
|
<span class="lg" style="background:#fc3"></span>输出%(左轴 0-100)
|
|||
|
|
<span class="lg" style="background:#f80"></span>总调光 BRT%(左轴)
|
|||
|
|
<span class="lg" style="background:#3cf"></span>照度 Lux(右轴,对数 1-200k)
|
|||
|
|
<span id="histinfo"></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>Timeline</h2>
|
|||
|
|
<div class="tl" id="tls"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>DMX 输出(通道 1~32)</h2>
|
|||
|
|
<div class="bars" id="bars"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="dim" id="ts"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const $=id=>document.getElementById(id);
|
|||
|
|
let luxOn=false, sending=false;
|
|||
|
|
|
|||
|
|
async function j(u){const r=await fetch(u);return r.json();}
|
|||
|
|
|
|||
|
|
async function refresh(){
|
|||
|
|
try{
|
|||
|
|
const tl=await j('/api/timeline');
|
|||
|
|
$('tls').innerHTML=tl.timelines.map(t=>
|
|||
|
|
`<div class="t ${t.state==='running'?'run':(t.state==='released'?'rel':'')}">T${t.num} ${t.name}<br>${t.state} ${(t.position/1000).toFixed(1)}s</div>`).join('');
|
|||
|
|
const out=await j('/api/output?universe=dmx:1');
|
|||
|
|
$('bars').innerHTML=out.channels.slice(0,32).map((c,i)=>{
|
|||
|
|
const v=parseInt(String(c).replace(/\D/g,''))||0;
|
|||
|
|
return `<div class="ch"><div class="v"><div class="f" style="height:${v/255*100}%"></div></div>${i+1}<br>${v}</div>`;}).join('');
|
|||
|
|
const g=await j('/api/group');
|
|||
|
|
const lv=g.groups[0].level;
|
|||
|
|
if(document.activeElement!==$('brt')){$('brt').value=lv;$('brtv').textContent=lv;}
|
|||
|
|
$('ts').textContent='更新于 '+new Date().toLocaleTimeString();
|
|||
|
|
}catch(e){$('ts').textContent='连接失败: '+e;}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function luxstat(){
|
|||
|
|
try{
|
|||
|
|
const s=await j('/luxlink');
|
|||
|
|
luxOn=s.on;
|
|||
|
|
$('luxsw').textContent=s.on?'联动中(点击关闭)':'已关闭(点击开启)';
|
|||
|
|
$('luxsw').className='sw'+(s.on?' on':'');
|
|||
|
|
if(s.lux!=null)$('lux').textContent=s.lux;
|
|||
|
|
if(s.target!=null&&document.activeElement!==$('lxt'))$('lxt').value=s.target;
|
|||
|
|
if(s.period!=null&&document.activeElement!==$('per'))$('per').value=s.period;
|
|||
|
|
if(s.fade!=null&&document.activeElement!==$('fad'))$('fad').value=(Math.round(s.fade*10)/10);
|
|||
|
|
if(s.deadband!=null&&document.activeElement!==$('dbd'))$('dbd').value=s.deadband;
|
|||
|
|
}catch(e){}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function setlxt(){const v=parseInt($('lxt').value);if(v>=1&&v<=200000)send('LXT='+v);}
|
|||
|
|
async function setper(){const v=parseInt($('per').value);if(v>=1&&v<=60)send('PER='+v);}
|
|||
|
|
async function setfad(){const v=Math.round(parseFloat($('fad').value)*10);if(v>=1&&v<=100)send('FAD='+v);}
|
|||
|
|
async function setdbd(){const v=parseInt($('dbd').value);if(v>=1&&v<=1000)send('DBD='+v);}
|
|||
|
|
|
|||
|
|
async function luxlink(){
|
|||
|
|
$('luxsw').textContent='切换中…';
|
|||
|
|
await fetch('/luxlink?on='+(luxOn?'0':'1'),{method:'POST'});
|
|||
|
|
setTimeout(luxstat,1500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function send(c){
|
|||
|
|
if(sending)return; sending=true;
|
|||
|
|
await fetch('/send?cmd='+encodeURIComponent(c),{method:'POST'});
|
|||
|
|
sending=false; setTimeout(refresh,800); setTimeout(hist,5000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function brt(){
|
|||
|
|
const v=$('brt').value; $('brtv').textContent=v;
|
|||
|
|
send('BRT='+String(v).padStart(3,'0'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const LUXMIN=1, LUXMAX=200000;
|
|||
|
|
function luxY(lux,h,pad){ // 对数轴
|
|||
|
|
const v=Math.max(LUXMIN,Math.min(LUXMAX,lux));
|
|||
|
|
const f=(Math.log10(v)-Math.log10(LUXMIN))/(Math.log10(LUXMAX)-Math.log10(LUXMIN));
|
|||
|
|
return h-pad-f*(h-2*pad);
|
|||
|
|
}
|
|||
|
|
function pctY(p,h,pad){ return h-pad-(p/100)*(h-2*pad); }
|
|||
|
|
|
|||
|
|
async function hist(){
|
|||
|
|
try{
|
|||
|
|
const d=await j('/history');
|
|||
|
|
const pts=d.points;
|
|||
|
|
const cv=$('hist');
|
|||
|
|
const w=cv.clientWidth, h=cv.clientHeight;
|
|||
|
|
const dpr=window.devicePixelRatio||1;
|
|||
|
|
cv.width=w*dpr; cv.height=h*dpr;
|
|||
|
|
const ctx=cv.getContext('2d'); ctx.scale(dpr,dpr);
|
|||
|
|
ctx.clearRect(0,0,w,h);
|
|||
|
|
const pad=28, now=Date.now(), t0=now-48*3600*1000;
|
|||
|
|
const X=t=>pad+(t-t0)/(now-t0)*(w-2*pad);
|
|||
|
|
|
|||
|
|
// 网格 + 时间刻度(每 6h)
|
|||
|
|
ctx.strokeStyle='#333'; ctx.fillStyle='#888'; ctx.font='10px sans-serif'; ctx.lineWidth=1;
|
|||
|
|
for(let k=0;k<=8;k++){
|
|||
|
|
const t=t0+k*6*3600*1000, x=X(t);
|
|||
|
|
ctx.beginPath(); ctx.moveTo(x,pad); ctx.lineTo(x,h-pad); ctx.stroke();
|
|||
|
|
const dt=new Date(t);
|
|||
|
|
ctx.fillText((dt.getMonth()+1)+'/'+dt.getDate()+' '+String(dt.getHours()).padStart(2,'0')+'时', x-24, h-8);
|
|||
|
|
}
|
|||
|
|
// 左轴 0/50/100%
|
|||
|
|
ctx.fillStyle='#fc3';
|
|||
|
|
[0,50,100].forEach(p=>{
|
|||
|
|
const y=pctY(p,h,pad);
|
|||
|
|
ctx.strokeStyle='#2a2a2a'; ctx.beginPath(); ctx.moveTo(pad,y); ctx.lineTo(w-pad,y); ctx.stroke();
|
|||
|
|
ctx.fillText(p+'%', 2, y+3);
|
|||
|
|
});
|
|||
|
|
// 右轴 lux 对数刻度
|
|||
|
|
ctx.fillStyle='#3cf';
|
|||
|
|
[1,10,100,1000,10000,100000,200000].forEach(l=>{
|
|||
|
|
const y=luxY(l,h,pad);
|
|||
|
|
ctx.fillText(l>=1000?(l/1000)+'k':l, w-pad+3, y+3);
|
|||
|
|
});
|
|||
|
|
if(!pts.length){ $('histinfo').textContent=' (暂无数据,采样中…)'; return; }
|
|||
|
|
|
|||
|
|
// 输出% 实线
|
|||
|
|
ctx.strokeStyle='#fc3'; ctx.lineWidth=1.5; ctx.beginPath();
|
|||
|
|
let started=false;
|
|||
|
|
for(const p of pts){
|
|||
|
|
if(p[2]==null){started=false;continue;}
|
|||
|
|
const x=X(p[0]), y=pctY(p[2],h,pad);
|
|||
|
|
started?ctx.lineTo(x,y):ctx.moveTo(x,y); started=true;
|
|||
|
|
}
|
|||
|
|
ctx.stroke();
|
|||
|
|
// BRT% 虚线
|
|||
|
|
ctx.strokeStyle='#f80'; ctx.setLineDash([4,3]); ctx.beginPath(); started=false;
|
|||
|
|
for(const p of pts){
|
|||
|
|
if(p[1]==null){started=false;continue;}
|
|||
|
|
const x=X(p[0]), y=pctY(p[1],h,pad);
|
|||
|
|
started?ctx.lineTo(x,y):ctx.moveTo(x,y); started=true;
|
|||
|
|
}
|
|||
|
|
ctx.stroke(); ctx.setLineDash([]);
|
|||
|
|
// lux 对数
|
|||
|
|
ctx.strokeStyle='#3cf'; ctx.lineWidth=1.5; ctx.beginPath(); started=false;
|
|||
|
|
for(const p of pts){
|
|||
|
|
if(p[3]==null){started=false;continue;}
|
|||
|
|
const x=X(p[0]), y=luxY(p[3],h,pad);
|
|||
|
|
started?ctx.lineTo(x,y):ctx.moveTo(x,y); started=true;
|
|||
|
|
}
|
|||
|
|
ctx.stroke();
|
|||
|
|
|
|||
|
|
const first=new Date(pts[0][0]);
|
|||
|
|
$('histinfo').textContent=` ${pts.length} 点,自 ${first.toLocaleString()}`;
|
|||
|
|
}catch(e){}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
refresh(); luxstat(); hist();
|
|||
|
|
setInterval(refresh,1500); setInterval(luxstat,6000); setInterval(hist,60000);
|
|||
|
|
window.addEventListener('resize', hist);
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
class H(BaseHTTPRequestHandler):
|
|||
|
|
def _send(self, code, body, ctype="application/json; charset=utf-8"):
|
|||
|
|
b = body if isinstance(body, bytes) else body.encode("utf-8")
|
|||
|
|
self.send_response(code)
|
|||
|
|
self.send_header("Content-Type", ctype)
|
|||
|
|
self.send_header("Content-Length", str(len(b)))
|
|||
|
|
self.end_headers()
|
|||
|
|
self.wfile.write(b)
|
|||
|
|
|
|||
|
|
def log_message(self, *a):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _reqlog(self):
|
|||
|
|
if self.command != "POST":
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
with open("/home/tonycao/rs485-lpc/lpc_web.req.log", "a") as f:
|
|||
|
|
f.write("%s %s %s %s\n" % (time.strftime("%F %T"),
|
|||
|
|
self.client_address[0], self.command, self.path))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def do_GET(self):
|
|||
|
|
u = urlparse(self.path)
|
|||
|
|
if u.path in ("/", "/index.html"):
|
|||
|
|
self._send(200, PAGE, "text/html; charset=utf-8")
|
|||
|
|
elif u.path.startswith("/api/"):
|
|||
|
|
try:
|
|||
|
|
with urllib.request.urlopen(LPC + u.path + (("?" + u.query) if u.query else ""), timeout=5) as r:
|
|||
|
|
self._send(200, r.read(), "application/json")
|
|||
|
|
except Exception as e:
|
|||
|
|
self._send(502, json.dumps({"error": str(e)}))
|
|||
|
|
elif u.path == "/luxlink":
|
|||
|
|
cfg = {}
|
|||
|
|
try:
|
|||
|
|
d = lpc_lua(["lux_target", "lux_fade", "lux_poll_s", "lux_deadband"])
|
|||
|
|
cfg = {"target": lua_num(d.get("lux_target")),
|
|||
|
|
"fade": d.get("lux_fade"),
|
|||
|
|
"period": lua_num(d.get("lux_poll_s")),
|
|||
|
|
"deadband": lua_num(d.get("lux_deadband"))}
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._send(200, json.dumps({"on": link_on(), "lux": read_lux(), **cfg}))
|
|||
|
|
elif u.path == "/history":
|
|||
|
|
with _hist_lock:
|
|||
|
|
data = list(_hist)
|
|||
|
|
self._send(200, json.dumps({"points": data}, separators=(",", ":")))
|
|||
|
|
else:
|
|||
|
|
self._send(404, "{}")
|
|||
|
|
|
|||
|
|
def do_POST(self):
|
|||
|
|
self._reqlog()
|
|||
|
|
u = urlparse(self.path)
|
|||
|
|
q = parse_qs(u.query)
|
|||
|
|
if u.path == "/send":
|
|||
|
|
cmd = q.get("cmd", [""])[0]
|
|||
|
|
m = re.fullmatch(r"(BRT|FX|SC|LUX|LXT|LX2|FAD|PER|DBD)=(\d+)", cmd)
|
|||
|
|
if not m:
|
|||
|
|
self._send(400, json.dumps({"error": "bad cmd"}))
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
fire_trigger(TRIG[m.group(1)], int(m.group(2)))
|
|||
|
|
self._send(200, json.dumps({"ok": True}))
|
|||
|
|
except Exception as e:
|
|||
|
|
self._send(502, json.dumps({"error": str(e)}))
|
|||
|
|
elif u.path == "/luxlink":
|
|||
|
|
try:
|
|||
|
|
fire_trigger(TRIG_LINK_ON if q.get("on", ["0"])[0] == "1" else TRIG_LINK_OFF)
|
|||
|
|
self._send(200, json.dumps({"ok": True}))
|
|||
|
|
except Exception as e:
|
|||
|
|
self._send(502, json.dumps({"error": str(e)}))
|
|||
|
|
else:
|
|||
|
|
self._send(404, "{}")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
load_history()
|
|||
|
|
threading.Thread(target=sampler_loop, daemon=True).start()
|
|||
|
|
print("LPC web UI: http://0.0.0.0:%d (history %d pts)" % (PORT, len(_hist)))
|
|||
|
|
ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever()
|