diff options
| author | s-ol <s+removethis@s-ol.nu> | 2024-03-26 18:23:18 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2024-03-26 18:23:58 +0000 |
| commit | dea3f32edf7ea11e7e47bbb14d9b64e856af18a5 (patch) | |
| tree | 2b52e71812c4afffe5e31c9e6570fcaad5680cdf | |
| parent | web: selectable, hideable plots (diff) | |
| download | t937-serial-dea3f32edf7ea11e7e47bbb14d9b64e856af18a5.tar.gz t937-serial-dea3f32edf7ea11e7e47bbb14d9b64e856af18a5.zip | |
web: load/save/delete plots
| -rw-r--r-- | web/index.html | 25 | ||||
| -rw-r--r-- | web/main.js | 156 | ||||
| -rw-r--r-- | web/style.css | 11 | ||||
| -rw-r--r-- | 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 @@ <path id="i-no-eye" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" /> <path id="i-pencil" fill="currentColor" d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" /> <path id="i-no-pencil" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" /> + <path id="i-cross" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" d="M4 20 20 4M4 4l16 16" /> </defs> </svg> <aside> @@ -32,7 +33,7 @@ <label id="temps_controller">-</label> </div> <div> - <select id="profile" value=""> + <select id="profile" value="" class="grow"> <option value="">none / manual</option> <option value="0">profile 1</option> <option value="1">profile 2</option> @@ -54,13 +55,13 @@ <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" /> + <input id="heat" class="grow" type="range" min="0" max="255" value="0" /> + <input id="heat_mode" type="checkbox" /> </div> <div> <label for="cool">cool</label> - <input type="range" min="0" max="255" id="cool" value="0" /> - <input type="checkbox" id="cool_mode" /> + <input id="cool" class="grow" type="range" min="0" max="255" value="0" /> + <input id="cool_mode" type="checkbox" /> </div> </section> <section class="grow"> @@ -69,30 +70,34 @@ <span> <button id="profile_show" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-eye" /></svg></button> <button id="profile_edit" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-pencil" /></svg></button> + <button id="profile_clear" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-cross" /></svg></button> </span> </h1> <div> <button id="profile_load" class="grow">load</button> <button id="profile_save" class="grow">save</button> - <button id="profile_create" class="grow">create</button> + <button id="profile_delete" class="grow">delete</button> </div> - <select id="storedprofile" multiple class="grow"> + <select id="profile_stored" multiple class="grow"> + <option value="" class="new">(new profile)</option> </select> </section> <section class="grow"> <h1> - <span class="grow">paste specification</span> + <span class="grow">paste spec</span> <span> <button id="paste_show" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-eye" /></svg></button> <button id="paste_edit" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-pencil" /></svg></button> + <button id="paste_clear" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-cross" /></svg></button> </span> </h1> <div> <button id="paste_load" class="grow">load</button> <button id="paste_save" class="grow">save</button> - <button id="paste_create" class="grow">create</button> + <button id="paste_delete" class="grow">delete</button> </div> - <select id="storedprofile" multiple class="grow"> + <select id="paste_stored" multiple class="grow"> + <option value="" class="new">(new paste)</option> </select> </section> </aside> 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 = '<option value="" class="new">(new profile)</option>'; + el.paste_stored.innerHTML = '<option value="" class="new">(new paste)</option>'; + + 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); +}; |
