"use strict"
function ID(s) {return document.getElementById(s);}
/***********************
** USB-Gerätezugriff **
***********************/
class USB {
constructor() {
this.device=null;
this.elDisabled=[ID("bDis"),ID("bootloader")]; // disabled when disconnected
this.elHidden=document.querySelectorAll("section"); // hidden when disconnected
Object.seal(this);
}
// Der Controller liefert ausschließlich Strukturen mit Integerzahlen,
// die vom Anwendungsprogramm skaliert werden müssen.
// Dafür gibt es einen Konstanten-Report (15), der abgeholt sein muss,
// bevor die Werte aus den anderen Reports verrechnet und dargestellt werden können.
// Die beiden Interfaces (HID und WebUSB) verhalten sich identisch.
get_report(report_id,maxlen=64) {
return this.device.controlTransferIn({
requestType: 'class',
recipient: 'interface',
request: 1, // Get Report
value: report_id,
index: 1},maxlen) // Liefert ein Versprechen!
}
await_report(maxlen=16) { // am Endpoint 3 kommt zyklisch Report 1
const ep=this.device.configuration.interfaces[1].alternate.endpoints[0].endpointNumber;
// const ps=device.configuration.interfaces[1].alternate.endpoints[0].packetSize;
return this.device.transferIn(ep,maxlen); // Liefert ein Versprechen
}
set_report(report_id, report_data) {
if (report_id) report_data.setUint8(0,report_id);
return this.device.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 9, // Set Report
value: report_id,
index: 1},report_data) // Liefert ein Versprechen, nur in Konsole anzeigen
.then(result => console.log("Report "+report_id+" mit "+result.bytesWritten+" Bytes gesendet."))
.catch(e => console.log(e))
}
static onSurpriseRemoval(evt) {
if (evt.device==usb.device) { // gleiches Objekt! (funktioniert)
usb.disconnect(false);
alert("Ofen wurde abgesteckt!");
}
}
disconnect(graceful=true) {
navigator.usb.removeEventListener("disconnect",USB.onSurpriseRemoval);
if (graceful) this.device.close(); this.device=null; // Abfragezyklus beenden
this.elDisabled.forEach(el => el.disabled=true);
this.elHidden.forEach(sec => sec.style="display:none");
}
connect() {
const self=this;
if (!navigator.usb) {
alert("Dieser Browser unterstützt kein WebUSB");
return;
}
navigator.usb.requestDevice({filters:[{vendorId:0x16C0}]})
.then(dev => {
console.log(dev);
self.device=dev;
navigator.usb.addEventListener("disconnect",USB.onSurpriseRemoval);
return dev.open();
})
.then(() => self.device.selectConfiguration(1))
.then(() => self.device.claimInterface(1)) // 0 = HID, 1 = WebUSB
.then(getAllReports)
.then(() => {
this.elDisabled.forEach(el => el.disabled=false);
/*Promise.resolve().then(*/getMsgReport(); // startet zyklische Abfrage
this.elHidden.forEach(sec => sec.removeAttribute("style"));
})
.catch(e => console.log(e));
}
static jump_to_bootloader() {
if (usb.device && window.confirm("Wirklich?")) {
const dv=new DataView(new ArrayBuffer(2));
dv.setUint8(1,1); // auf „true“ setzen
usb.set_report(99,dv);
}
}
}
const konst={}; // Konstanten-Report vom USB-Gerät
function getAllReports() {
return usb.get_report(15,18) // Konstanten zuerst
.then(result => {
const v = result.data;
const k=konst;
k.fnetz_Hz= v.getUint8(1); // Doppelte Netzfrequenz in Hz
k.Tregl_s = v.getUint16(2,true)/1000; // Zeitschritt in ms für Regler
k.Tproz_s = v.getUint16(4,true)/1000; // Zeitschritt in ms für Prozess
k.Ttemp_K = v.getUint16(6,true)/1000; // Temperaturschritt in mK
k.pidDiv = v.getUint16(8,true); // PID-Divisor (256)
k.fakDiv = v.getUint16(10,true); // Reglerfaktor-Divisor (256)
k.MAXPID = v.getUint8(12); // Maximale Größe der PID-Tabelle
k.MAXSTEP = v.getUint8(13); // Maximale Größe der Rezeptliste
k.COOKIES = v.getUint16(14,true); // Cookies in Bytes (ReportID 14 dazu)
k.HISTLEN = v.getUint16(16,true); // Maximale Historienlänge
Object.seal(k);
})
.then(() => {
pwm.getReport()
.then(() => {
msg.initialize(konst);
tpids.getReport();
steps.getReport();
})
})
.catch(e => console.log(e));
}
function EnDis(inp,disable,reasonText) {
if (inp.tagName=="INPUT") inp.disabled=disable;
if (disable) inp.title=reasonText;
else inp.removeAttribute("title");
}
/********************************************************************************
* 1. Laufende Daten (vom Interrupt-Endpoint) oder Historiendaten (TODO) *
********************************************************************************/
// Zyklische Funktion, Aufruf mit jeder Regleraktivität (hier 250 ms)
// Globale Funktion zur Vermeidung von Closure-Schachtelungen, mit festem Bezug auf "msg"
function getMsgReport() {
return usb.await_report() // „blockiert“ 250 ms bis zum Eintreffen des Reports
.then(result => msg.handleReport(result))
.then(getMsgReport) // äh, droht da kein Stapelüberlauf??
.catch(e => {if (usb.device) console.log(e);}); // "DOMException" beim Abstecken, nicht bei "Trennen"
}
class Msg{
constructor(canvas) { // die übrigen ID-Elemente werden im Konstruktor geholt, naja
this.el={ // Alle in HTML definierten GUI-Elemente im Schnellzugriff
histlen: ID("histlen"),
led: ID("led"),
alarm: ID("alarm"),
rauch: ID("rauch"),
gas: ID("gas"),
tempPy: ID("tempPy"),
tempIst: [0,1,2].map(i=>ID("tempIst"+i)),
colors: document.querySelectorAll("input[type=color]")
};
if (localStorage.histlen) this.el.histlen.value=localStorage.histlen;
this.history=new HistoryBuffer(360);
this.plot=new Plot(canvas);
this.plot.addAxis(0x12,"Zeit","h:m:s"); // unten
this.plot.addAxis(0x05,"Temperatur","°C"); // links
this.plot.addAxis(0x07,"Gas-Spannung","V"); // rechts
// this.plot.axes[2].dispRange.init(0,5);
this.tv=new TimeViewer(this.history,0.25);
// digitaler Eingang
this.plot.addTrace(this.tv,
new DataViewer(this.history,e=>e.rauch,{digital:true}),
{xaxis:0,yaxis:this.plot.addAxis(0x35,"Rauch"),join:"late",visible:false});
[
{access:"gas",
auflös:0.1,
farbel:this.el.colors[2], // <input type=color>
attr:{yaxis:2}
},
{access:["tempIst",0],
auflös:0.1,
farbel:this.el.colors[4]
},
{access:"tempPy",
auflös:1,
farbel:this.el.colors[3]
}
].forEach(ti => {
const t=this.plot.addTrace(
this.tv,
new DataViewer(this.history,ti.access,{dt:ti.auflös,unit:ti.attr?"V":"°C"}),
ti.attr);
ti.farbel.onchange=e => {
t.penColor=ti.farbel.value;
t.plot.update(2);
}
ti.farbel.onchange();
});
this.el.led.addEventListener("click",e => {
const dv=new DataView(new ArrayBuffer(2));
dv.setUint8(1,e.target.checked);
if (usb.device) usb.set_report(56,dv);
});
this.el.alarm.addEventListener("click",e => {
const dv=new DataView(new ArrayBuffer(2));
dv.setUint8(1,e.target.checked);
if (usb.device) usb.set_report(57,dv);
});
// Alle Farbauswähler (und später Linienselektor usw.) mit localStorage verknüpfen,
// weil das nicht im Mikrocontroller gespeichert wird, sondern im Browser.
this.el.colors.forEach((el,i) => {
if (attrs[i] && attrs[i].color) el.value=attrs[i].color;
el.addEventListener("change",e => {
if (!attrs[i]) attrs[i]={};
attrs[i].color=e.target.value;
localStorage.rauchattrs=JSON.stringify(attrs);
});
});
}
// nach dem Abfragen der Konstanten …
initialize(k) {
const self=this;
this.el.runTime.step=k.Tproz_s;
make_Int16_input(this.el.heatPower,undefined,pwm.maxHeat_W/pwm.maxPwm);
make_Int16_input(this.el.tempSoll,undefined,k.Ttemp_K);
this.history.clear();
getHistFromµC() // liefert Versprechen!
.then(h => (console.log(h),h))
.then(h => (console.log("Spanne = "+toTime(h.tspan)+" s"),h))
.then(h => (Hist2Hist(h,self.history),self.plot.update()))
.catch(e => console.log(e));
// TODO: In <hist> verteilen
(this.el.histlen.onchange=e=> {
try{
this.el.melder.innerHTML = "";
const hl=this.el.histlen.value;
this.history.size=fromTime(hl)/konst.Tregl_s;
localStorage.histlen=hl;
}catch(e){
this.el.melder.innerHTML = e.message;
}
})();
this.el.tempIst.forEach(el=>make_Int16_input(el,undefined,k.Ttemp_K));
this.tv.dt=k.Tregl_s;
usb.get_report(1,16) // Aktueller Zustand (ohne zu warten, nicht von Interrupt-Pipe)
.then(result => self.handleReport(result))
.catch(e => console.log(e));
}
// Zyklische Funktion, Aufruf mit jeder Regleraktivität (hier 250 ms)
handleReport(result) {
const v = result.data;
const k = konst;
const m ={
flags: v.getUint8(1),
relays: v.getUint8(2),
inputs: v.getUint8(3),
runTime: v.getUint16(4,true)*k.Tproz_s, // in Sekunden
heatPower: getInt16(v,6,pwm.maxHeat_W/pwm.maxPwm), // in Watt
tempSoll: getInt16(v,8,k.Ttemp_K), // in °C
tempIst: [0,1,2].map(i => getInt16(v,10+i*2,konst.Ttemp_K))
};
this.update(m,k);
}
// Zyklische Funktion, Aufruf mit jeder Regleraktivität (hier 250 ms)
update(m,k) {
const focus=document.activeElement;
if (this.el.limpwr.selectedIndex) {
if (m.heatPower<0) {
m.heatPower=0;
this.el.heatPower.style="background:lightblue";
}else if (m.heatPower>pwm.maxHeat_W) {
m.heatPower=pwm.maxHeat_W;
this.el.heatPower.style="background:lightpink";
}else this.el.heatPower.removeAttribute("style");
}
this.flags.setState(m.flags,0,focus);
this.flags.a[0].nextElementSibling.style.color=m.flags&1?"green":"red";
this.flags.a[1].nextElementSibling.style.color=m.flags&2?"orange":"darkblue";
this.relays.setState(m.relays,0,focus);
this.inputs.setState(m.inputs,0,focus);
this.history.push(m);
this.plot.update(6);
const values=[toTime(m.runTime),
...[m.heatPower*(this.el.heatUnit.selectedIndex?100/pwm.maxHeat_W:1),
m.tempSoll,...m.tempIst].map(e=>emptyNaN(e))];
// Die <input type=number>-Elemente vertragen weder Inf noch NaN, aber einen Leerstring.
// Das <input type=time>-Element will die Form hh:mm[:ss[.zzz] haben.
[this.el.runTime,this.el.heatPower,this.el.tempSoll,...this.el.tempIst]
.forEach((el,j) => {if (el!=focus) el.value=values[j];});
}
}
function getHistFromµC() {
return usb.get_report(13,10+konst.HISTLEN)
.then(result => {
const v = result.data;
return {
max_Δt: (v.getUint8(1)||256)*konst.Tregl_s, // maximaler Sample-Abstand
Δt: (v.getUint8(4)||256)*konst.Tregl_s, // Sample-Abstand
tTrail: v.getUint8(5)*konst.Tregl_s, // zum Festlegen der (ganz genauen) Startzeit
values: (()=>{
const a=new Array(v.getUint16(2,true));
let s=getInt16(v,6); // Starttemperatur (NaN behandeln)
const e=getInt16(v,8); // Endtemperatur (sollte stimmen)
for (let i=0; i<a.length; i++) {
const d=getInt8(v,10+i); // Differenzen (NaN behandeln)
const t=isFinite(d) ? s+=d : d;
a[i]=t*konst.Ttemp_K; // ablegen
}
console.assert(s==e,"Endwert stimmt nicht: "+s+" != "+e+"!");
return a;
})(),
get tspan() {return this.values.length*this.Δt+this.tTrail;}
};
});
}
function Hist2Hist(h,hist) { // Nur für Δt=0.25
const capacity=hist.size;
let len=Math.min(h.values.length,capacity);
// let start=len-capacity;
// if (start<0) {len+=start; start=0;}
for (let i=0; i<len; i++) hist.a[i]={tempIst:[h.values[i]]};
hist.wi=hist.fill=len;
// hist.drop=-len;
}
function checkInt16(v,min,max,step,intmin=-32766,intmax=32766) {
// Die Schreibweise !(a>=b) ist auch dann true wenn v undefined oder String ist
if (!(v>=min)) return false; // Zu kleiner (unpraktischer) Wert
if (!(v<=max)) return false; // Zu großer (unpraktischer) Wert
v/=step;
if (!(v>=intmin)) return false;
if (!(v<=intmax)) return false; // Integer-Überlauf (Platz für ±Inf und NaN lassen)
return true;
}
function checkUint16(v,min,max,step,intmin=0,intmax=65535) {
return checkInt16(v,min,max,step,intmin,intmax);
}
var msg;
var usb,attrs; // Trace-Attribute für Message-Plot (erst mal nur Farben)
document.addEventListener("DOMContentLoaded",e => {
attrs=JSON.parse(localStorage.rauchattrs||"[]");
usb=new USB;
msg=new Msg(ID("plot1"));
});
| Detected encoding: UTF-8 | 0
|