From dea3f32edf7ea11e7e47bbb14d9b64e856af18a5 Mon Sep 17 00:00:00 2001 From: s-ol Date: Tue, 26 Mar 2024 19:23:18 +0100 Subject: web: load/save/delete plots --- web/index.html | 25 +++++---- web/main.js | 156 +++++++++++++++++++++++++++++++++++++++++++++++++-------- web/style.css | 11 ++-- web/utils.js | 18 +++++++ 4 files changed, 173 insertions(+), 37 deletions(-) diff --git a/web/index.html b/web/index.html index 7e37ab7..5e8bd6a 100644 --- a/web/index.html +++ b/web/index.html @@ -14,6 +14,7 @@ + diff --git a/web/main.js b/web/main.js index b93b2da..6d29a50 100644 --- a/web/main.js +++ b/web/main.js @@ -1,5 +1,5 @@ import { ADDR, FUNC, ARG, Message } from './protocol.js'; -import { hex, concat } from './utils.js'; +import { hex, concat, hash } from './utils.js'; import * as plot from './plot.js'; import * as PROTOCOL from './protocol.js'; import * as UTILS from './utils.js'; @@ -26,6 +26,60 @@ window.send8 = (func, ...data) => { return PORT.write(msg.buf); }; +const samplePlot = (data, interval=4) => { + if (data.length < 2) return data; + data = data.slice(); + + const result = []; + + let i = 0; + let prev; + while (data.length) { + while (data.length && data[0][0] <= i) { + prev = data.shift(); + } + + const next = data[0]; + if (!next) { + result.push([ i, prev[1] ]); + break; + } + + const dtemp = next[1] - prev[1]; + const dtime = next[0] - prev[0]; + const t = (i - prev[0]) / dtime; + result.push([ i, Math.round(prev[1] + t * dtemp) ]); + + i += interval; + } + + return result; +}; + +const unsamplePlot = (data, tolerance_interval=4) => { + if (data.length < 2) return data; + + const result = []; + result.push(data[0]); + + const tolerance = 1.1 / tolerance_interval; + + for (let i = 1; i < data.length - 1; i++) { + const prev = data[i - 1]; + const curr = data[i]; + const next = data[i + 1]; + + const left = (curr[1] - prev[1]) / (curr[0] - prev[0]); + const right = (next[1] - curr[1]) / (next[0] - curr[0]); + + if (Math.abs(left - right) < tolerance) continue; + result.push(curr); + } + + result.push(data[data.length - 1]); + return result; +}; + let PORT = null; // { write(buf), disconnect() } | null let STATUS = null; // null | 'connecting' | 'idle' | 'manual' | 'running' let START_TIME = null; @@ -48,12 +102,11 @@ plot.addPlot('temp3', { color: 'sienna' }); plot.addPlot('tempavg', { color: 'rebeccapurple' }); const elements = [ - 'temps_chamber', 'temps_controller', - 'connect', 'status', + 'connect', 'status', 'temps_chamber', 'temps_controller', 'profile', 'start', 'read', 'write', - 'profile_load', 'profile_save', 'profile_create', 'profile_show', 'profile_edit', - 'paste_load', 'paste_save', 'paste_create', 'paste_show', 'paste_edit', 'heat', 'heat_mode', 'cool', 'cool_mode', + 'profile_load', 'profile_save', 'profile_delete', 'profile_show', 'profile_edit', 'profile_clear', 'profile_stored', + 'paste_load', 'paste_save', 'paste_delete', 'paste_show', 'paste_edit', 'paste_clear', 'paste_stored', ]; const el = Object.fromEntries(elements.map(id => [id, document.getElementById(id)])); @@ -101,6 +154,28 @@ const updateUI = () => { plot.update(); }; +window.onstorage = () => { + const profile = el.profile_stored.value; + const paste = el.paste_stored.value; + + el.profile_stored.innerHTML = ''; + el.paste_stored.innerHTML = ''; + + for (const key of Object.keys(window.localStorage)) { + if (key.startsWith('profile-')) { + const data = JSON.parse(window.localStorage[key]); + el.profile_stored.add(new Option(data.name, key)); + } else if (key.startsWith('paste-')) { + const data = JSON.parse(window.localStorage[key]); + el.paste_stored.add(new Option(data.name, key)); + } + } + + el.profile_stored.value = profile; + el.paste_stored.value = paste; +}; + +window.onstorage(); updateUI(); const onMessage = async (msg) => { @@ -312,20 +387,57 @@ if (window.location.hash !== '#ws') { console.log('opened websocket'); }; } -el.profile_show.onclick = () => { - plot.PLOTS.profile.visible = !plot.PLOTS.profile.visible; - updateUI(); -}; -el.paste_show.onclick = () => { - plot.PLOTS.pase.visible = !plot.PLOTS.pase.visible; - updateUI(); -}; -el.profile_edit.onclick = () => { - plot.toggleActive('profile'); - updateUI(); -}; -el.paste_edit.onclick = () => { - plot.toggleActive('paste'); - updateUI(); -}; -}; + +for (const p of ['profile', 'paste']) { + el[p + '_show'].onclick = () => { + plot.PLOTS[p].visible = !plot.PLOTS[p].visible; + updateUI(); + }; + el[p + '_edit'].onclick = () => { + plot.toggleActive(p); + updateUI(); + }; + el[p + '_clear'].onclick = () => { + plot.PLOTS[p].data = [[0, 24]]; + updateUI(); + }; + + el[p + '_load'].onclick = () => { + const key = el[p + '_stored'].value; + let data = window.localStorage[key]; + if (!data) return; + + plot.PLOTS[p].data = JSON.parse(data).data; + updateUI(); + }; + + el[p + '_save'].onclick = () => { + let key = el[p + '_stored'].value; + let name; + if (key === '') { + name = window.prompt('please enter the new profile name'); + if (!name) return; + } else { + name = JSON.parse(window.localStorage[key]).name; + delete window.localStorage[key]; + } + + const data = plot.PLOTS[p].data; + if (p === 'profile') { + const sampled = new Uint8Array(samplePlot(data).map(d => Math.round(d[1]))); + key = `profile-${hash(sampled)}`; + } else { + key = `paste-${name}`; + } + + window.localStorage[key] = JSON.stringify({ name, data }); + window.onstorage(); + el[p + '_stored'].value = key; + }; + + el[p + '_delete'].onclick = () => { + const key = el[p + '_stored'].value; + delete window.localStorage[key]; + window.onstorage(); + }; +} diff --git a/web/style.css b/web/style.css index 8bbf5cf..2f0db1a 100644 --- a/web/style.css +++ b/web/style.css @@ -39,11 +39,6 @@ aside section > div { justify-content: space-between; gap: 4px; } -aside input[type=range], -aside select { - max-width: unset; - flex-grow: 1; -} aside button, aside label { min-width: 80px; @@ -54,6 +49,8 @@ aside div label + label { .grow { flex-grow: 1; + min-width: 0; + max-width: unset; } button.icon { @@ -68,6 +65,10 @@ button.icon svg { height: 0.5lh; } +option.new { + color: #00000080; +} + #plot { flex: 1 0 400px; user-select: none; diff --git a/web/utils.js b/web/utils.js index d7cf657..53f49ab 100644 --- a/web/utils.js +++ b/web/utils.js @@ -32,3 +32,21 @@ export const range = (a, b=null) => { // return a Promise that resolves in n seconds export const sleep = (n) => new Promise(res => setTimeout(res, n*1000)); + +// hash an ArrayBuffer +export const hash = (data, seed = 0) => { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + + data = new Uint8Array(data); + for (const byte of data) { + h1 = Math.imul(h1 ^ byte, 2654435761); + h2 = Math.imul(h2 ^ byte, 1597334677); + } + + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; -- cgit v1.2.3