Source file: /~heha/ewa/Motor/cdesk.zip/js/rauch.js

"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-80