diff options
| author | s-ol <s+removethis@s-ol.nu> | 2024-03-09 00:12:36 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2024-03-09 00:12:36 +0000 |
| commit | f9dd9fda0d0c6ba501c7e847fc07fbdd68855ab5 (patch) | |
| tree | 00f04b16eae0aa6730aaf22d4e23259f60fbc1b6 | |
| parent | simulator: implement direct control commands/loop (diff) | |
| download | t937-serial-f9dd9fda0d0c6ba501c7e847fc07fbdd68855ab5.tar.gz t937-serial-f9dd9fda0d0c6ba501c7e847fc07fbdd68855ab5.zip | |
web: add manual control
| -rw-r--r-- | web/index.html | 23 | ||||
| -rw-r--r-- | web/main.js | 306 | ||||
| -rw-r--r-- | web/protocol.js | 138 |
3 files changed, 286 insertions, 181 deletions
diff --git a/web/index.html b/web/index.html index 286d42b..9e79aa0 100644 --- a/web/index.html +++ b/web/index.html @@ -4,6 +4,29 @@ <title>T-937/M Reflow Oven</title> </head> <body> + <nav> + <label id="status"></label> + </nav> + <aside> + <section> + <h1>device</h1> + <button id="connect">connect</button> + <button id="disconnect">disconnect</button> + </section> + <section> + <h1>manual control</h1> + <div> + <label for="heat">heat</label> + <input type="range" min="0" max="255" id="heat" value="0" /> + <input type="checkbox" id="heat_mode" /> + </div> + <div> + <label for="cool">cool</label> + <input type="range" min="0" max="255" id="cool" value="0" /> + <input type="checkbox" id="cool_mode" /> + </div> + </section> + </aside> <script type="module" src="main.js" ></script> </body> </html> diff --git a/web/main.js b/web/main.js index c889eaa..7c86ffe 100644 --- a/web/main.js +++ b/web/main.js @@ -1,157 +1,132 @@ +import { ADDR, FUNC, ARG, Message } from './protocol.js'; import { hex, concat } from './utils.js'; +import * as PROTOCOL from './protocol.js'; import * as UTILS from './utils.js'; -// a lot of these are useful for fuzzing +for (const key in PROTOCOL) { + window[key] = PROTOCOL[key]; +} + for (const key in UTILS) { window[key] = UTILS[key]; } -const ADDR = { - CTRL: 0xdb96, - OVEN: 0xaa55, +// send a message (uint16 data) +window.send16 = (func, ...data) => { + const msg = Message.from16(ADDR.OVEN, func, data); + console.info(msg.toString(), msg); + return PORT.writer.write(msg.buf); }; -const FUNC = { - // used by oven - O_CONNECT: 0x01, - O_TEMPS: 0x02, - O_READ: 0x03, - O_ACK_WRITE: 0x06, - - // used by CTRL - C_PWM_HEAT: 0x08, - C_PWM_COOL: 0x0d, - C_STARTSTOP: 0x10, - C_REQ_READ: 0x11, - C_WRITE: 0x20, - C_ACK_CONNECT: 0xf1, - C_ACK_READ: 0xf2, +// send a message (uint8 data) +window.send8 = (func, ...data) => { + const msg = Message.from8(ADDR.OVEN, func, data); + console.info(msg.toString(), msg); + return PORT.writer.write(msg.buf); }; -const ARG = { - // for STARSTOP, REQUEST_READ - STARTSTOP_START: 0x11, - STARTSTOP_STOP: 0x22, - REQUEST_READ: 0x11, - - // for CONNECT / ACK_CONNECT - CONNECT_CONNECTING: 0x0100, - CONNECT_CONNECTED: 0x0200, - - // for *_PWM - PWM_ON: 0x44, - PWM_OFF: 0x88, +let PORT = null; // { port, reader, writer } | null +let STATUS = null; // null | 'connecting' | 'manual' | 'running' +let TIMEOUT; +let TEMPS = ''; + +const elements = [ + 'connect', 'disconnect', 'status', + 'heat', 'heat_mode', 'cool', 'cool_mode', +]; +const el = Object.fromEntries(elements.map(id => [id, document.getElementById(id)])); + +const updateUI = () => { + el.connect.disabled = !!PORT; + el.disconnect.disabled = !PORT; + + switch (STATUS) { + case null: + el.status.innerText = '(disconnected)'; + break; + + case 'connecting': + el.status.innerText = '(connecting)'; + break; + + case 'manual': + case 'running': + el.status.innerText = TEMPS; + break; + } }; -window.ADDR = ADDR; -window.FUNC = FUNC; -window.ARG = ARG; - -const connect = document.createElement('button'); -connect.innerText = 'connect'; -document.body.append(connect); - -const status = document.createElement('textarea'); -status.disabled = true; -status.innerText = ''; -status.rows = 1; -status.style.fontFamily = 'monospace'; -document.body.append(status); - -class Message { - constructor(buffer) { - this.buf = buffer; - this.view = new DataView(this.buf); - this.data = new DataView(this.buf, 4, this.buf.byteLength - 6); - } - - get16(i) { return this.data.getUint16(i, false); } - set16(i, v) { this.data.setUint16(i, v, false); } - - get address() { return this.view.getUint16(0, false); } - set address(val) { this.view.setUint16(0, val, false); } - - get func() { return this.view.getUint8(2); } - set func(val) { this.view.setUint8(2, val)} +updateUI(); - get data_len() { return this.view.getUint8(3); } - set data_len(val) { this.view.setUint8(3, val); } +const disconnect = async () => { + const port = PORT; + PORT = null; + STATUS = null; + updateUI(); - get length() { return this.buf.byteLength; } + await port.reader.cancel().catch(() => null); + await port.writer.close().catch(() => null); + await port.port.close(); +}; - get checksum() { return this.view.getUint16(this.buf.byteLength-2, false); } - set checksum(val) { this.view.setUint16(this.buf.byteLength-2, val, false); } +const onMessage = async (msg) => { + if (msg.func === FUNC.O_TEMPS) { + console.debug(msg.toString(), msg); + } else { + console.info(msg.toString(), msg); + } - calculateSum() { - let sum = 0; - for (let i = 0; i < this.length - 2; i++) { - sum += this.view.getUint8(i); - } - return sum & 0xffff; + if (msg.address !== ADDR.CTRL) { + console.warn(`unknown address ${hex(msg.address)}`); + return; } - static from8(addr, func, data) { - const len = 2 + 2 + data.length + 2; - - const msg = new Message(new ArrayBuffer(len)); - msg.address = addr; - msg.func = func; - msg.data_len = data.length; - - for (let i = 0; i < data.length; i++) { - msg.data.setUint8(i, data[i]); + switch (msg.func) { + case FUNC.O_CONNECT: { + switch (msg.get16(0)) { + case ARG.CONNECT_CONNECTING: + console.log('oven found'); + const res = Message.from16(ADDR.OVEN, FUNC.C_ACK_CONNECT, [ARG.CONNECT_CONNECTED]); + await PORT.writer.write(res.buf); + break; + + case ARG.CONNECT_CONNECTED: + console.log('connection established'); + STATUS = 'manual'; + break; + } + break; } - msg.checksum = msg.calculateSum(); - return msg; - } - - static from16(addr, func, data) { - const len = 2 + 2 + 2*data.length + 2; - - const msg = new Message(new ArrayBuffer(len)); - msg.address = addr; - msg.func = func; - msg.data_len = 2*data.length; - - for (let i = 0; i < data.length; i++) { - msg.data.setUint16(2*i, data[i], false); + case FUNC.O_TEMPS: { + clearTimeout(TIMEOUT); + TIMEOUT = setTimeout(() => { + if (!PORT) return; + console.error('port timed out'); + disconnect(); + }, 2500); + + const temp1 = msg.get16(0); + const temp2 = msg.get16(2); + const temp3 = msg.get16(4); + TEMPS = `${temp1}°C - ${temp2}°C // ${temp3}°C`; + STATUS = 'manual'; + break; } - msg.checksum = msg.calculateSum(); - return msg; - } - - static empty(addr, func, data_len) { - const len = 2 + 2 + 2*data_len + 2; - - const msg = new Message(new ArrayBuffer(len)); - msg.address = addr; - msg.func = func; - msg.data_len = data_len; - - return msg; + default: + console.warn(`unknown func ${hex(msg.func)}`, msg); + break; } - static tryParse(buffer) { - if (buffer.length < 4 + 2) return; - - const data_len = buffer[3]; - if (buffer.length < 4 + data_len + 2) return; - - const msg = new Message(buffer.slice(0, 4 + data_len + 2).buffer); - if (msg.calculateSum() !== msg.checksum) { - throw new Error(`checksum mismatch: expected ${hex(msg.checksum)} but calculated ${hex(msg.calculateSum())}`); - } - return msg; - }; + updateUI(); } -const readLoop = async (reader, onMessage) => { +const loop = async () => { let buffer = new Uint8Array(); while (true) { - const { value, done } = await reader.read(); + const { value, done } = await PORT.reader.read(); if (done) throw new Error("reader cancelled"); buffer = concat(buffer, value); @@ -165,61 +140,20 @@ const readLoop = async (reader, onMessage) => { } }; -const wrapLoop = async (fn, stream, ...args) => { -}; - -connect.onclick = async () => { +el.connect.onclick = async () => { const port = await navigator.serial.requestPort(); - window.PORT = port; - console.log('got port', port); - await port.open({ baudRate: 38400 }); console.log('opened port'); let reader, writer; - - const onMessage = async (msg) => { - if (msg.address !== ADDR.CTRL) { - console.warn(`unknown address ${hex(msg.address)}`); - return; - } - - switch (msg.func) { - case FUNC.O_CONNECT: { - switch (msg.get16(0)) { - case ARG.CONNECT_CONNECTING: - console.log('oven found'); - const res = Message.from16(ADDR.OVEN, FUNC.C_ACK_CONNECT, [ARG.CONNECT_CONNECTED]); - await writer.write(res.buf); - break; - - case ARG.CONNECT_CONNECTED: - console.log('connection established'); - break; - } - break; - } - - case FUNC.O_TEMPS: { - const temp1 = msg.get16(0); - const temp2 = msg.get16(2); - const temp3 = msg.get16(4); - status.innerText = `${temp1}°C - ${temp2}°C // ${temp3}°C`; - break; - } - - default: - console.warn(`unknown func ${hex(msg.func)}`, msg); - break; - } - } - try { reader = port.readable.getReader(); writer = port.writable.getWriter(); - window.writer = writer; + PORT = { port, reader, writer }; + STATUS = 'connecting'; + updateUI(); - await readLoop(reader, onMessage); + await loop(); } finally { console.log('shutting down'); reader.releaseLock(); @@ -227,14 +161,24 @@ connect.onclick = async () => { } }; -// send a message (uint16 data) -window.send16 = (func, ...data) => { - const msg = Message.from16(ADDR.OVEN, func, data); - return window.writer.write(msg.buf); -}; - -// send a message (uint8 data) -window.send8 = (func, ...data) => { - const msg = Message.from8(ADDR.OVEN, func, data); - return window.writer.write(msg.buf); -}; +el.disconnect.onclick = disconnect; + + +const pwms = [ + [el.heat, el.heat_mode], + [el.cool, el.cool_mode], +]; +for (const [slider, checkbox] of pwms) { + slider.onchange = async () => { + checkbox.checked = +slider.value; + await send8(FUNC.C_PWM_HEAT, +el.heat.value, el.heat_mode.checked ? ARG.PWM_ON : ARG.PWM_OFF); + await send8(FUNC.C_PWM_COOL, +el.cool.value, el.cool_mode.checked ? ARG.PWM_ON : ARG.PWM_OFF); + }; + checkbox.onchange = async () => { + if (checkbox.checked && +slider.value === 0) { + slider.value = 0x64; + } + await send8(FUNC.C_PWM_HEAT, +el.heat.value, el.heat_mode.checked ? ARG.PWM_ON : ARG.PWM_OFF); + await send8(FUNC.C_PWM_COOL, +el.cool.value, el.cool_mode.checked ? ARG.PWM_ON : ARG.PWM_OFF); + }; +} diff --git a/web/protocol.js b/web/protocol.js new file mode 100644 index 0000000..9c93f8e --- /dev/null +++ b/web/protocol.js @@ -0,0 +1,138 @@ +export const ADDR = { + CTRL: 0xdb96, + OVEN: 0xaa55, +}; + +export const FUNC = { + // used by oven + O_CONNECT: 0x01, + O_TEMPS: 0x02, + O_READ: 0x03, + O_ACK_WRITE: 0x06, + + // used by CTRL + C_PWM_HEAT: 0x08, + C_PWM_COOL: 0x0d, + C_STARTSTOP: 0x10, + C_REQ_READ: 0x11, + C_WRITE: 0x20, + C_ACK_CONNECT: 0xf1, + C_ACK_READ: 0xf2, +}; + +export const ARG = { + // for STARSTOP, REQUEST_READ + STARTSTOP_START: 0x11, + STARTSTOP_STOP: 0x22, + REQUEST_READ: 0x11, + + // for CONNECT / ACK_CONNECT + CONNECT_CONNECTING: 0x0100, + CONNECT_CONNECTED: 0x0200, + + // for *_PWM + PWM_ON: 0x44, + PWM_OFF: 0x88, +}; + +const lookup = (tbl, val) => Object.keys(tbl).find(key => tbl[key] === val); + +export class Message { + constructor(buffer) { + this.buf = buffer; + this.view = new DataView(this.buf); + this.data = new DataView(this.buf, 4, this.buf.byteLength - 6); + } + + get16(i) { return this.data.getUint16(i, false); } + set16(i, v) { this.data.setUint16(i, v, false); } + + get address() { return this.view.getUint16(0, false); } + set address(val) { this.view.setUint16(0, val, false); } + + get func() { return this.view.getUint8(2); } + set func(val) { this.view.setUint8(2, val)} + + get data_len() { return this.view.getUint8(3); } + set data_len(val) { this.view.setUint8(3, val); } + + get length() { return this.buf.byteLength; } + + get checksum() { return this.view.getUint16(this.buf.byteLength-2, false); } + set checksum(val) { this.view.setUint16(this.buf.byteLength-2, val, false); } + + calculateSum() { + let sum = 0; + for (let i = 0; i < this.length - 2; i++) { + sum += this.view.getUint8(i); + } + return sum & 0xffff; + } + + getDataArray() { + return new Uint8Array(this.buf, 4, this.buf.byteLength - 6); + } + + toString() { + const address = lookup(ADDR, this.address) ?? "????"; + const func = lookup(FUNC, this.func) ?? "?_??"; + const data = this.getDataArray(); + return `to ${address}: ${func.substr(2)} [${data}]`; + } + + static from8(addr, func, data) { + const len = 2 + 2 + data.length + 2; + + const msg = new Message(new ArrayBuffer(len)); + msg.address = addr; + msg.func = func; + msg.data_len = data.length; + + for (let i = 0; i < data.length; i++) { + msg.data.setUint8(i, data[i]); + } + + msg.checksum = msg.calculateSum(); + return msg; + } + + static from16(addr, func, data) { + const len = 2 + 2 + 2*data.length + 2; + + const msg = new Message(new ArrayBuffer(len)); + msg.address = addr; + msg.func = func; + msg.data_len = 2*data.length; + + for (let i = 0; i < data.length; i++) { + msg.data.setUint16(2*i, data[i], false); + } + + msg.checksum = msg.calculateSum(); + return msg; + } + + static empty(addr, func, data_len) { + const len = 2 + 2 + 2*data_len + 2; + + const msg = new Message(new ArrayBuffer(len)); + msg.address = addr; + msg.func = func; + msg.data_len = data_len; + + return msg; + } + + static tryParse(buffer) { + if (buffer.length < 4 + 2) return; + + const data_len = buffer[3]; + if (buffer.length < 4 + data_len + 2) return; + + const msg = new Message(buffer.slice(0, 4 + data_len + 2).buffer); + if (msg.calculateSum() !== msg.checksum) { + throw new Error(`checksum mismatch: expected ${hex(msg.checksum)} but calculated ${hex(msg.calculateSum())}`); + } + return msg; + }; +} |
