aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2024-03-26 18:23:18 +0000
committers-ol <s+removethis@s-ol.nu>2024-03-26 18:23:58 +0000
commitdea3f32edf7ea11e7e47bbb14d9b64e856af18a5 (patch)
tree2b52e71812c4afffe5e31c9e6570fcaad5680cdf
parentweb: selectable, hideable plots (diff)
downloadt937-serial-dea3f32edf7ea11e7e47bbb14d9b64e856af18a5.tar.gz
t937-serial-dea3f32edf7ea11e7e47bbb14d9b64e856af18a5.zip
web: load/save/delete plots
-rw-r--r--web/index.html25
-rw-r--r--web/main.js156
-rw-r--r--web/style.css11
-rw-r--r--web/utils.js18
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);
+};