aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2024-03-09 00:12:36 +0000
committers-ol <s+removethis@s-ol.nu>2024-03-09 00:12:36 +0000
commitf9dd9fda0d0c6ba501c7e847fc07fbdd68855ab5 (patch)
tree00f04b16eae0aa6730aaf22d4e23259f60fbc1b6
parentsimulator: implement direct control commands/loop (diff)
downloadt937-serial-f9dd9fda0d0c6ba501c7e847fc07fbdd68855ab5.tar.gz
t937-serial-f9dd9fda0d0c6ba501c7e847fc07fbdd68855ab5.zip
web: add manual control
-rw-r--r--web/index.html23
-rw-r--r--web/main.js306
-rw-r--r--web/protocol.js138
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;
+ };
+}