aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/Dockerfile3
-rw-r--r--web/index.html38
-rw-r--r--web/main.js25
-rw-r--r--web/plot.js81
-rw-r--r--web/style.css3
5 files changed, 96 insertions, 54 deletions
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 0000000..d728eb4
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,3 @@
+FROM nginx:alpine
+COPY . /usr/share/nginx/html
+RUN chmod 555 -R /usr/share/nginx/html
diff --git a/web/index.html b/web/index.html
index 5e8bd6a..34215ff 100644
--- a/web/index.html
+++ b/web/index.html
@@ -24,15 +24,32 @@
<label id="status">(disconnected)</label>
<button id="connect">connect</button>
</div>
+ <h2>temperatures</h2>
<div>
- <label>chamber temp</label>
+ <label>chamber</label>
<label id="temps_chamber">-</label>
</div>
<div>
- <label>controller temp</label>
+ <label>controller</label>
<label id="temps_controller">-</label>
</div>
<div>
+ <label>export traces</label>
+ <button id="export" disabled>download csv</button>
+ </div>
+ <h2>manual controls</h2>
+ <div>
+ <label for="heat">heat</label>
+ <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 id="cool" class="grow" type="range" min="0" max="255" value="0" />
+ <input id="cool_mode" type="checkbox" />
+ </div>
+ <h2>stored profiles</h2>
+ <div>
<select id="profile" value="" class="grow">
<option value="">none / manual</option>
<option value="0">profile 1</option>
@@ -51,19 +68,6 @@
<button id="write" class="grow" disabled>write</button>
</div>
</section>
- <section>
- <h1>manual control</h1>
- <div>
- <label for="heat">heat</label>
- <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 id="cool" class="grow" type="range" min="0" max="255" value="0" />
- <input id="cool_mode" type="checkbox" />
- </div>
- </section>
<section class="grow">
<h1>
<span class="grow">reflow profile</span>
@@ -100,6 +104,10 @@
<option value="" class="new">(new paste)</option>
</select>
</section>
+ <section>
+ help, information and source code:
+ <a href="https://git.s-ol.nu/t937-serial">git.s-ol.nu/t937-serial</a>
+ </section>
</aside>
<svg id="plot" />
<script type="module" src="main.js" ></script>
diff --git a/web/main.js b/web/main.js
index 6d29a50..e568bbc 100644
--- a/web/main.js
+++ b/web/main.js
@@ -102,7 +102,7 @@ plot.addPlot('temp3', { color: 'sienna' });
plot.addPlot('tempavg', { color: 'rebeccapurple' });
const elements = [
- 'connect', 'status', 'temps_chamber', 'temps_controller',
+ 'connect', 'status', 'temps_chamber', 'temps_controller', 'export',
'profile', 'start', 'read', 'write',
'heat', 'heat_mode', 'cool', 'cool_mode',
'profile_load', 'profile_save', 'profile_delete', 'profile_show', 'profile_edit', 'profile_clear', 'profile_stored',
@@ -118,6 +118,7 @@ const updateUI = () => {
el.start.disabled = !ready;
el.read.disabled = !ready;
el.write.disabled = !ready;
+ el.export.disabled = !plot.PLOTS.tempavg.data.length;
el.profile_show.firstElementChild.firstElementChild.href.baseVal = plot.PLOTS.profile.visible ? '#i-eye' : '#i-no-eye';
el.paste_show.firstElementChild.firstElementChild.href.baseVal = plot.PLOTS.paste.visible ? '#i-eye' : '#i-no-eye';
@@ -278,6 +279,28 @@ for (const [slider, checkbox] of pwms) {
};
}
+el.export.onclick = () => {
+ const tavg = plot.PLOTS.tempavg.data;
+ const columns = [
+ plot.PLOTS.temp1.data,
+ plot.PLOTS.temp2.data,
+ plot.PLOTS.tempavg.data,
+ plot.PLOTS.temp3.data,
+ ];
+
+ let csv = 'Seconds,Chamber Temp 1,Chamber Temp 2,Chamber Average,Controller Temp\n';
+ for (let i = 0; i < tavg.length; i++) {
+ const data = columns.map(col => col[i][1]).join(',');
+ csv += `${tavg[i][0]},${data}\n`;
+ }
+
+ const a = document.createElement('a');
+ a.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
+ a.target = '_blank';
+ a.download = `temps_${new Date().toISOString}.csv`;
+ a.click();
+};
+
el.start.onclick = () => {
if (STATUS === 'idle') {
STATUS = el.profile.value === '' ? 'manual' : 'running';
diff --git a/web/plot.js b/web/plot.js
index 98d8d88..cc40989 100644
--- a/web/plot.js
+++ b/web/plot.js
@@ -23,6 +23,46 @@ const min = d3.format(' ');
const minsec = v => `${min(Math.floor(v / 60))}:${sec(Math.floor(v % 60))}`;
const degc = v => `${Math.floor(v)}°C`;
+const crosshair = svg.append('g')
+ .attr('display', 'none')
+ .attr('color', '#00000080')
+ .attr('font-size', 10)
+ .attr('font-family', 'sans-serif')
+ .attr('stroke-width', 0.5);
+crosshair.append('text')
+ .attr('id', 'ctextx')
+ .attr('y', marginy)
+ .attr('dx', '0.32em')
+ .attr('dy', '1em')
+ .attr('fill', 'currentColor');
+crosshair.append('text')
+ .attr('id', 'ctexty')
+ .attr('x', marginx)
+ .attr('dx', '0.32em')
+ .attr('dy', '-0.32em')
+ .attr('fill', 'currentColor');;
+crosshair.append('line')
+ .attr('id', 'clinex')
+ .attr('stroke', 'currentColor');
+crosshair.append('line')
+ .attr('id', 'cliney')
+ .attr('stroke', 'currentColor');
+
+const updateCrosshair = ([px, py]) => {
+ const [time, temp] = [x.invert(px), y.invert(py)];
+
+ const width = svg.node().clientWidth;
+ const height = svg.node().clientHeight;
+ const inRange = marginx < px && px < width - marginx &&
+ marginy < py && py < height - marginy;
+
+ crosshair.attr('display', inRange ? null : 'none').lower();
+ crosshair.select('#ctextx').text(minsec(time)).attr('x', px);
+ crosshair.select('#ctexty').text(degc(temp)).attr('y', py);
+ crosshair.select('#clinex').attr('x1', px).attr('x2', px).attr('y1', marginy).attr('y2', height - marginy);
+ crosshair.select('#cliney').attr('y1', py).attr('y2', py).attr('x1', marginx).attr('x2', width - marginx);
+};
+
export const addPlot = (name, options={}) => {
const plot = Object.assign({
color: 'black',
@@ -126,7 +166,9 @@ export const update = (transition=true) => {
d3.select(this)
.attr('cx', (d) => x(d[0]))
.attr('cy', (d) => y(d[1]));
+
update(false);
+ updateCrosshair([e.x, e.y]);
})
.on('end', () => points.attr('cursor', 'grab'))
);
@@ -156,46 +198,9 @@ const onresize = () => {
};
window.addEventListener('resize', onresize);
-const crosshair = svg.append('g')
- .attr('display', 'none')
- .attr('color', '#00000080')
- .attr('font-size', 10)
- .attr('font-family', 'sans-serif')
- .attr('stroke-width', 0.5);
-crosshair.append('text')
- .attr('id', 'ctextx')
- .attr('y', marginy)
- .attr('dx', '0.32em')
- .attr('dy', '1em')
- .attr('fill', 'currentColor');
-crosshair.append('text')
- .attr('id', 'ctexty')
- .attr('x', marginx)
- .attr('dx', '0.32em')
- .attr('dy', '-0.32em')
- .attr('fill', 'currentColor');;
-crosshair.append('line')
- .attr('id', 'clinex')
- .attr('stroke', 'currentColor');
-crosshair.append('line')
- .attr('id', 'cliney')
- .attr('stroke', 'currentColor');
-
svg
.on('mousemove', (e) => {
- const [px, py] = d3.pointer(e);
- const [time, temp] = [x.invert(px), y.invert(py)];
-
- const width = svg.node().clientWidth;
- const height = svg.node().clientHeight;
- const inRange = marginx < px && px < width - marginx &&
- marginy < py && py < height - marginy;
-
- crosshair.attr('display', inRange ? null : 'none').lower();
- crosshair.select('#ctextx').text(minsec(time)).attr('x', px);
- crosshair.select('#ctexty').text(degc(temp)).attr('y', py);
- crosshair.select('#clinex').attr('x1', px).attr('x2', px).attr('y1', marginy).attr('y2', height - marginy);
- crosshair.select('#cliney').attr('y1', py).attr('y2', py).attr('x1', marginx).attr('x2', width - marginx);
+ updateCrosshair(d3.pointer(e));
})
.on('dblclick', (e) => {
const plot = ACTIVE && PLOTS[ACTIVE];
diff --git a/web/style.css b/web/style.css
index 2f0db1a..c0fcf8e 100644
--- a/web/style.css
+++ b/web/style.css
@@ -29,6 +29,9 @@ aside h1 {
aside h1 span {
display: flex;
}
+aside h2 {
+ font-size: 1em;
+}
aside section {
display: flex;
flex-direction: column;