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