From 008cde9d5e5639fad47ae25616abcd4a3b3cd44b Mon Sep 17 00:00:00 2001 From: Kelvin Ly <kelvin.ly1618@gmail.com> Date: Mon, 15 May 2023 12:19:40 -0400 Subject: [PATCH] Add status, adjust updater logic, add seconds/ API, fix time formatting to be more human readable --- dev/dev.htm | 79 +++++++++++++++++++++++--- shroom_internals/tcp_server.go | 100 ++++++++++++++++++--------------- shroom_server.go | 15 ++++- 3 files changed, 139 insertions(+), 55 deletions(-) diff --git a/dev/dev.htm b/dev/dev.htm index d44c198..783ff22 100644 --- a/dev/dev.htm +++ b/dev/dev.htm @@ -26,6 +26,11 @@ function initCharts() { } }, axis: { + x: { + tick: { + format: (x) => new Date(x).toISOString() + } + }, y2: { show: true } @@ -46,6 +51,13 @@ function initCharts() { voltage: 'y', } }, + axis: { + x: { + tick: { + format: (x) => new Date(x).toISOString() + } + }, + }, zoom: { enabled: true } @@ -64,6 +76,9 @@ function status(msg) { document.getElementById('status').textContent = msg } +var autoupdate = true +var chart_updater = null + async function updateCharts() { status("loading data...") const latest = await fetch("/api/latest") @@ -76,7 +91,10 @@ async function updateCharts() { diff = Math.min(diff, max_interval_millis/1000) var path = "/api/last/weeks/" var offset = diff - if (diff < 60*60) { + if (diff < 60) { + path = "/api/last/seconds/" + offset = Math.ceil(diff) + } else if (diff < 60*60) { path = "/api/last/minutes/" offset = Math.ceil(diff/60) } else if (diff < 24*60*60) { @@ -117,17 +135,13 @@ async function updateCharts() { break } } - console.log("slice idx ", slice_idx) + //console.log("slice idx ", slice_idx) temp = temp.slice(slice_idx) humd = humd.slice(slice_idx) volts = volts.slice(slice_idx) time = time.slice(slice_idx) - console.log(time) - console.log(temp) - console.log(humd) - console.log(volts) chart.load({ columns: [ ["time"].concat(time), @@ -143,25 +157,72 @@ async function updateCharts() { }) status("charts updated") } + + if (autoupdate) { + chart_updater = chartupdater() + } else { + chart_updater = null + } +} + +const sleep = (ms) => new Promise(r => setTimeout(r, ms)) + +async function chartupdater() { + if (chart_updater != null) return + + await sleep(5000) + // wait at least two seconds to avoid wasting a lot of bandwidth + const resp = await fetch("/api/update") + chart_updater = null + updateCharts() +} + +var status_updater = null +async function updateStatus() { + const status_resp = await fetch("/api/status") + const status = await status_resp.json() + document.getElementById("device-status").textContent = "connected: " + status.connected + ", humdifier: " + (status.humdifier ? "on" : "off") + + if (autoupdate) { + status_updater = waitThenUpdateStatus() + } else { + status_updater = null + } +} + +async function waitThenUpdateStatus() { + if (status_updater != null) return + await fetch("/api/status_update") + status_updater = null + updateStatus() } window.onload = () => { initCharts() updateCharts() + updateStatus() document.getElementById('update').addEventListener('click', (e) => { updateCharts() + updateStatus() + }) + document.getElementById('autoupdate').addEventListener('click', (e) => { + autoupdate = document.getElementById('autoupdate').checked }) } </script> </head> <body> + <div id="device-status"></div> <div id="humidity-temp"></div> <div id="volts"></div> <div> - <input type=button id=update value="Update"></input> - <span id=status></span> + <form> + <input type=checkbox id=autoupdate checked>Autoupdate</input> + <input type=button id=update value="Update"></input> + <!-- TODO add decimation and window size options --> + <span id=status></span> + </form> </div> - <div id=device_status></div> </body> </html> diff --git a/shroom_internals/tcp_server.go b/shroom_internals/tcp_server.go index d6efaa5..95d8e93 100644 --- a/shroom_internals/tcp_server.go +++ b/shroom_internals/tcp_server.go @@ -30,6 +30,7 @@ type ShroomStatus struct { HumidifierOn bool NumConnections int Wait chan struct{} + StatusWait chan struct{} } func (s *ShroomStatus) Update() { @@ -39,6 +40,13 @@ func (s *ShroomStatus) Update() { s.Wait = make(chan struct{}) } +func (s *ShroomStatus) StatusUpdate() { + s.Lock() + defer s.Unlock() + close(s.StatusWait) + s.StatusWait = make(chan struct{}) +} + func parseMsg(line []byte, db *sql.DB, status *ShroomStatus) { data := ShroomData{ Time: 0, @@ -84,62 +92,66 @@ func InitTcpServer(db *sql.DB, status *ShroomStatus) { conn, err := ln.Accept() if err != nil { log.Println("tcp accept error: ", err) - continue + return } - status.Lock() - status.NumConnections += 1 - status.Unlock() - - defer func() { + func() { status.Lock() - status.NumConnections -= 1 + status.NumConnections += 1 status.Unlock() - log.Println("connection disconnected") - }() + status.StatusUpdate() - log.Println("connection started") + defer func() { + status.Lock() + status.NumConnections -= 1 + status.Unlock() + status.StatusUpdate() + log.Println("connection disconnected") + }() - go func() { - // TODO deal with the write side of the connection - }() + log.Println("connection started") - // deal with the read side of the connection - buf := make([]byte, 128) - left := buf + go func() { + // TODO deal with the write side of the connection + }() - for { - num_read, err := conn.Read(left) - left = left[num_read:] - //log.Println("buf ", buf) - //log.Println("left ", left) + // deal with the read side of the connection + buf := make([]byte, 128) + left := buf - if err != nil { - log.Println("tcp read error: ", err) - _ = conn.Close() - log.Println("disconnected from client") - break - } - // parse the message to see if it's finished - unread := buf[:len(buf)-len(left)] + for { + num_read, err := conn.Read(left) + left = left[num_read:] + //log.Println("buf ", buf) + //log.Println("left ", left) - for newlinePos(unread) != -1 { - end := newlinePos(unread) - line := unread[:end] - unread = unread[end+1:] - - //log.Println("line ", line) - //log.Println("unread ", unread) - // skip empty lines - if len(line) == 0 { - continue + if err != nil { + log.Println("tcp read error: ", err) + _ = conn.Close() + log.Println("disconnected from client") + break } + // parse the message to see if it's finished + unread := buf[:len(buf)-len(left)] - parseMsg(line, db, status) + for newlinePos(unread) != -1 { + end := newlinePos(unread) + line := unread[:end] + unread = unread[end+1:] + + //log.Println("line ", line) + //log.Println("unread ", unread) + // skip empty lines + if len(line) == 0 { + continue + } + + parseMsg(line, db, status) + } + // shift the remaining data back to the start of the buffer + copy(buf[:len(unread)], unread) + left = buf[len(unread):] } - // shift the remaining data back to the start of the buffer - copy(buf[:len(unread)], unread) - left = buf[len(unread):] - } + }() } }() } diff --git a/shroom_server.go b/shroom_server.go index 88978b3..4007fd9 100644 --- a/shroom_server.go +++ b/shroom_server.go @@ -42,7 +42,7 @@ func dumpData(db *sql.DB, multiplier int64) func(http.ResponseWriter, *http.Requ } offset := int64(count) * multiplier t := now*1000 - offset - log.Println("req start ", t) + //log.Println("req start ", t) msg, err := s.GetRows(db, t) if err != nil { w.WriteHeader(500) @@ -68,7 +68,8 @@ func main() { } status := s.ShroomStatus{ - Wait: make(chan struct{}), + Wait: make(chan struct{}), + StatusWait: make(chan struct{}), } s.InitTcpServer(db, &status) @@ -81,6 +82,7 @@ func main() { dumpDay := dumpData(db, 24*60*60*1000) dumpHour := dumpData(db, 60*60*1000) dumpMinute := dumpData(db, 60*1000) + dumpSecond := dumpData(db, 1000) lastPoint := func(w http.ResponseWriter, _req *http.Request) { msg, err := s.LastTime(db) @@ -124,6 +126,13 @@ func main() { } w.Write([]byte("ok")) } + statusUpdateHandler := func(w http.ResponseWriter, req *http.Request) { + stillopen := true + for stillopen { + _, stillopen = <-status.StatusWait + } + w.Write([]byte("ok")) + } http.Handle("/d/", http.StripPrefix("/d/", http.FileServer(http.Dir("./dev")))) http.Handle("/", http.FileServer(http.FS(contentSub))) @@ -131,10 +140,12 @@ func main() { http.HandleFunc("/api/last/days/", dumpDay) http.HandleFunc("/api/last/hours/", dumpHour) http.HandleFunc("/api/last/minutes/", dumpMinute) + http.HandleFunc("/api/last/seconds/", dumpSecond) http.HandleFunc("/api/latest", lastPoint) http.HandleFunc("/api/status", getStatus) http.HandleFunc("/api/admin", adminHandler) http.HandleFunc("/api/update", updateHandler) + http.HandleFunc("/api/status_update", statusUpdateHandler) // TODO periodically clear old entries from the database