Compare commits
No commits in common. "3a7bb603601e3eb616e23f11f1ed04a820e76f5b" and "25d37884787b88f1df03e1b007b5f2c67a7571db" have entirely different histories.
3a7bb60360
...
25d3788478
196
dev/dev.htm
196
dev/dev.htm
|
@ -14,9 +14,9 @@ function initCharts() {
|
||||||
chart = c3.generate({
|
chart = c3.generate({
|
||||||
bindto: '#humidity-temp',
|
bindto: '#humidity-temp',
|
||||||
data: {
|
data: {
|
||||||
x: 'time',
|
x: 'x',
|
||||||
columns: [
|
columns: [
|
||||||
['time', 1, 2, 3, 4],
|
['x', 1, 2, 3, 4],
|
||||||
['temp', 24, 24.5, 24.3, 24.6],
|
['temp', 24, 24.5, 24.3, 24.6],
|
||||||
['humidity', 83.1, 99.5, 74.3, 84.6],
|
['humidity', 83.1, 99.5, 74.3, 84.6],
|
||||||
],
|
],
|
||||||
|
@ -26,11 +26,6 @@ function initCharts() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
axis: {
|
axis: {
|
||||||
x: {
|
|
||||||
tick: {
|
|
||||||
format: (x) => new Date(x).toISOString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y2: {
|
y2: {
|
||||||
show: true
|
show: true
|
||||||
}
|
}
|
||||||
|
@ -42,208 +37,29 @@ function initCharts() {
|
||||||
chart2 = c3.generate({
|
chart2 = c3.generate({
|
||||||
bindto: '#volts',
|
bindto: '#volts',
|
||||||
data: {
|
data: {
|
||||||
x: 'time',
|
x: 'x',
|
||||||
columns: [
|
columns: [
|
||||||
['time', 1, 2, 3, 4],
|
['x', 1, 2, 3, 4],
|
||||||
['voltage', 24, 24.5, 24.3, 24.6],
|
['voltage', 24, 24.5, 24.3, 24.6],
|
||||||
],
|
],
|
||||||
axes: {
|
axes: {
|
||||||
voltage: 'y',
|
voltage: 'y',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
axis: {
|
|
||||||
x: {
|
|
||||||
tick: {
|
|
||||||
format: (x) => new Date(x).toISOString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
zoom: {
|
zoom: {
|
||||||
enabled: true
|
enabled: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var cur_time_millis = 0
|
|
||||||
var max_interval_millis = 5*60*1000
|
|
||||||
|
|
||||||
var time = []
|
|
||||||
var temp = []
|
|
||||||
var humd = []
|
|
||||||
var volts = []
|
|
||||||
|
|
||||||
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")
|
|
||||||
const last_time_millis = await latest.json()
|
|
||||||
if (last_time_millis > cur_time_millis) {
|
|
||||||
// we need to collect more data to catch up
|
|
||||||
// time is in milliseconds so we divide by 1000
|
|
||||||
var diff = (last_time_millis - cur_time_millis) / 1000
|
|
||||||
// worst case load only the last two weeks of data
|
|
||||||
diff = Math.min(diff, max_interval_millis/1000)
|
|
||||||
var path = "/api/last/weeks/"
|
|
||||||
var offset = diff
|
|
||||||
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) {
|
|
||||||
path = "/api/last/hours/"
|
|
||||||
offset = Math.ceil(diff/(60*60))
|
|
||||||
} else if (diff < 7*24*60*60) {
|
|
||||||
path = "/api/last/days/"
|
|
||||||
offset = Math.ceil(diff/(24*60*60))
|
|
||||||
} else {
|
|
||||||
path = "/api/last/weeks/"
|
|
||||||
offset = Math.ceil(diff/(7*24*60*60))
|
|
||||||
}
|
|
||||||
const new_data = await fetch(path + offset)
|
|
||||||
const new_data_json = await new_data.json()
|
|
||||||
// copy the data into the arrays
|
|
||||||
new_data_json.sort((a, b) => a.t - b.t)
|
|
||||||
new_data_json.forEach((v) => {
|
|
||||||
var last_time = time[time.length-1]
|
|
||||||
if (time.length == 0) {
|
|
||||||
last_time = 0
|
|
||||||
}
|
|
||||||
if (v.t > last_time) {
|
|
||||||
time.push(v.t)
|
|
||||||
temp.push(v.temp)
|
|
||||||
humd.push(v.hum)
|
|
||||||
volts.push(v.hv)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// truncate all the lists as necessary
|
|
||||||
// using the time array to determine the splicing points
|
|
||||||
cur_time_millis = time[time.length - 1]
|
|
||||||
const min_time = cur_time_millis - max_interval_millis
|
|
||||||
|
|
||||||
var slice_idx = 0
|
|
||||||
for (var i = 0; i < time.length; i++) {
|
|
||||||
if (time[i] >= min_time) {
|
|
||||||
slice_idx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//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)
|
|
||||||
|
|
||||||
chart.load({
|
|
||||||
columns: [
|
|
||||||
["time"].concat(time),
|
|
||||||
["temp"].concat(temp),
|
|
||||||
["humidity"].concat(humd),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
chart2.load({
|
|
||||||
columns: [
|
|
||||||
["time"].concat(time),
|
|
||||||
["voltage"].concat(volts),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
status("charts updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoupdate) {
|
|
||||||
chart_updater = chartupdater()
|
|
||||||
} else {
|
|
||||||
chart_updater = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
||||||
var chart_update_millis = 2000
|
|
||||||
|
|
||||||
async function chartupdater() {
|
|
||||||
if (chart_updater != null) return
|
|
||||||
|
|
||||||
await sleep(chart_update_millis)
|
|
||||||
// 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()
|
|
||||||
//console.log(status)
|
|
||||||
const humidifier_state = (status.humidifier ? "on" : "off")
|
|
||||||
//console.log(humidifier_state)
|
|
||||||
document.getElementById("device-status").textContent = "connected: " + status.connected + ", humidifier: " + humidifier_state
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testAdminMode() {
|
|
||||||
const msg = JSON.stringify({
|
|
||||||
auth: "password",
|
|
||||||
data: {
|
|
||||||
manual_mode: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await fetch("/api/admin", {
|
|
||||||
method: "POST",
|
|
||||||
body: msg
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
initCharts()
|
initCharts()
|
||||||
updateCharts()
|
const xhr = new XmlHttpRequest()
|
||||||
updateStatus()
|
|
||||||
|
|
||||||
document.getElementById('update').addEventListener('click', (e) => {
|
|
||||||
updateCharts()
|
|
||||||
updateStatus()
|
|
||||||
})
|
|
||||||
document.getElementById('autoupdate').addEventListener('click', (e) => {
|
|
||||||
autoupdate = document.getElementById('autoupdate').checked
|
|
||||||
})
|
|
||||||
document.getElementById('test-admin').addEventListener('click', (e) => {
|
|
||||||
testAdminMode()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="device-status"></div>
|
|
||||||
<div id="humidity-temp"></div>
|
<div id="humidity-temp"></div>
|
||||||
<div id="volts"></div>
|
<div id="volts"></div>
|
||||||
<div>
|
<p>Test!</p>
|
||||||
<form>
|
|
||||||
<input type=checkbox id=autoupdate checked>Autoupdate</input>
|
|
||||||
<input type=button id=update value="Update"></input>
|
|
||||||
<input type=button id=test-admin value="Test admin mode"></input>
|
|
||||||
<!-- TODO add decimation and window size options -->
|
|
||||||
<span id=status></span>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import serial
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
|
|
||||||
s = serial.Serial("/dev/ttyUSB0", 115200, timeout=10)
|
|
||||||
q = queue.Queue()
|
|
||||||
|
|
||||||
process = subprocess.Popen(["ssh", "shrooms@threefortiethofonehamster.com", "python", "/home/shrooms/go/src/shroom-server/shroom-pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
|
|
||||||
# TODO run thread to process data from process's stdout
|
|
||||||
|
|
||||||
def reset_serial():
|
|
||||||
s.close()
|
|
||||||
s = serial.Serial("/dev/ttyUSB0", 115200, timeout = 10)
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
class Humidifier:
|
|
||||||
def __init__(self):
|
|
||||||
self.on = False
|
|
||||||
self.history = np.array(30)
|
|
||||||
self.switch_timeout = 0
|
|
||||||
|
|
||||||
def update(self, volts):
|
|
||||||
self.history[1:] = self.history[:-1]
|
|
||||||
self.history[0] = volts
|
|
||||||
avg = np.sum(self.history)/self.history.shape[0]
|
|
||||||
if self.state:
|
|
||||||
if avg < 0.2:
|
|
||||||
self.on = False
|
|
||||||
else:
|
|
||||||
if avg > 2.6:
|
|
||||||
self.on = True
|
|
||||||
|
|
||||||
def toggle(self, s):
|
|
||||||
if time.time() > self.switch_timeout:
|
|
||||||
s.write(b"h")
|
|
||||||
self.switch_timeout = time.time() + 7
|
|
||||||
|
|
||||||
humidifier = Humidifier()
|
|
||||||
target_lower = 0.85
|
|
||||||
target_higher = 0.90
|
|
||||||
try:
|
|
||||||
last_sample = 0
|
|
||||||
while True:
|
|
||||||
now = time.time()
|
|
||||||
if now - last_sample < 0.5:
|
|
||||||
s.write(b"s")
|
|
||||||
resp = s.read(120)
|
|
||||||
if len(resp) == 0:
|
|
||||||
reset_serial()
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
parts = resp.split(b",")
|
|
||||||
humidity = float(parts[0])
|
|
||||||
temp = float(parts[1])
|
|
||||||
volts = float(parts[2])
|
|
||||||
print(humidity, temp, volts)
|
|
||||||
|
|
||||||
humidifier.update(volts)
|
|
||||||
if humidity < target_lower and humidifier.off:
|
|
||||||
humidifier.toggle(s)
|
|
||||||
elif humidity > target_upper and humidifier.on:
|
|
||||||
humidifier.toggle(s)
|
|
||||||
# TODO check on the process
|
|
||||||
else:
|
|
||||||
time.sleep(0.5-(now - last_sample))
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# TODO kill ssh connection
|
|
||||||
process.
|
|
||||||
pass
|
|
|
@ -1,170 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import json
|
|
||||||
import serial
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
#process = subprocess.Popen(["ssh", "shrooms@localhost", "/usr/bin/env", "python", "/home/shrooms/go/src/shroom-server/shroom-pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
process = subprocess.Popen(["/usr/bin/env", "python", "/home/kelvin/src/shroom-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
|
|
||||||
def send_update(msg):
|
|
||||||
global process
|
|
||||||
process.stdin.write(bytes(json.dumps(msg) + "\n", "utf8"))
|
|
||||||
process.stdin.flush()
|
|
||||||
|
|
||||||
exiting = False
|
|
||||||
# run thread to process data from process's stdout
|
|
||||||
def stdout_loop():
|
|
||||||
while not exiting:
|
|
||||||
msg = process.stdout.readline()
|
|
||||||
print("got message ", msg)
|
|
||||||
stdout_thread = threading.Thread(target=stdout_loop)
|
|
||||||
stdout_thread.start()
|
|
||||||
|
|
||||||
class MockSerial:
|
|
||||||
def __init__(self):
|
|
||||||
self.humidity = np.zeros(100)
|
|
||||||
self.humidifier_on = False
|
|
||||||
self.humidity[:] = 0.80
|
|
||||||
self.humidity[-1] = 0.20
|
|
||||||
self.humidity[0] = 0.20
|
|
||||||
|
|
||||||
def write(self, msg):
|
|
||||||
if msg == b'h':
|
|
||||||
print("mock hum toggle")
|
|
||||||
self.humidifier_on = not self.humidifier_on
|
|
||||||
|
|
||||||
def read(self, _):
|
|
||||||
t = time.time()
|
|
||||||
temp = 25 + np.sin(0.01*2*np.pi*t) + 0.5*np.sin(0.0001*2*np.pi*t + 7)
|
|
||||||
|
|
||||||
# very janky model of humidity diffusion
|
|
||||||
# fix end conditions
|
|
||||||
for _ in range(20):
|
|
||||||
self.humidity[-1] = 0.2*0.20 + 0.8*self.humidity[-2]
|
|
||||||
self.humidity[0] = 0.20
|
|
||||||
if self.humidifier_on:
|
|
||||||
self.humidity[20] = 2
|
|
||||||
# use the gradient to determine the change in humidity
|
|
||||||
avg = 0.5*(self.humidity[:-2] + self.humidity[2:])
|
|
||||||
self.humidity[1:-1] += 0.2*(avg - self.humidity[1:-1])
|
|
||||||
#print(self.humidity)
|
|
||||||
|
|
||||||
humidity = self.humidity[60] + np.random.random()*0.003
|
|
||||||
if self.humidifier_on:
|
|
||||||
hv = 3.3
|
|
||||||
else:
|
|
||||||
hv = 0.0
|
|
||||||
return bytes("{},{},{}\n".format(humidity, temp, hv), "utf8")
|
|
||||||
|
|
||||||
s = MockSerial()
|
|
||||||
|
|
||||||
def reset_serial():
|
|
||||||
pass
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
class Humidifier:
|
|
||||||
def __init__(self):
|
|
||||||
self.on = False
|
|
||||||
self.history = np.zeros(10)
|
|
||||||
self.switch_timeout = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def off(self):
|
|
||||||
return not self.on
|
|
||||||
|
|
||||||
def update(self, volts):
|
|
||||||
self.history[1:] = self.history[:-1]
|
|
||||||
self.history[0] = volts
|
|
||||||
#print(self.history)
|
|
||||||
avg = np.sum(self.history)/self.history.shape[0]
|
|
||||||
if self.on:
|
|
||||||
if avg < 0.2:
|
|
||||||
self.on = False
|
|
||||||
print("send status off")
|
|
||||||
send_update({"status": 0})
|
|
||||||
self.switch_timeout = time.time() + 1
|
|
||||||
else:
|
|
||||||
if avg > 2.6:
|
|
||||||
self.on = True
|
|
||||||
print("send status on")
|
|
||||||
send_update({"status": 1})
|
|
||||||
self.switch_timeout = time.time() + 1
|
|
||||||
|
|
||||||
def toggle(self, s):
|
|
||||||
if time.time() > self.switch_timeout:
|
|
||||||
s.write(b"h")
|
|
||||||
self.switch_timeout = time.time() + 7
|
|
||||||
|
|
||||||
humidifier = Humidifier()
|
|
||||||
target_lower = 0.85
|
|
||||||
target_upper = 0.90
|
|
||||||
|
|
||||||
humidifier_history = np.zeros(30)
|
|
||||||
first_sample = False
|
|
||||||
try:
|
|
||||||
last_sample = 0
|
|
||||||
while True:
|
|
||||||
now = time.time()
|
|
||||||
if now - last_sample < 0.5:
|
|
||||||
time.sleep(0.5 - (now - last_sample))
|
|
||||||
continue
|
|
||||||
last_sample = now
|
|
||||||
|
|
||||||
s.write(b"s")
|
|
||||||
resp = s.read(120)
|
|
||||||
if len(resp) == 0:
|
|
||||||
reset_serial()
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
parts = resp.split(b",")
|
|
||||||
humidity = float(parts[0])
|
|
||||||
temp = float(parts[1])
|
|
||||||
volts = float(parts[2])
|
|
||||||
print(humidity, temp, volts)
|
|
||||||
if first_sample:
|
|
||||||
humidifier_history[:] = humidity
|
|
||||||
first_sample = False
|
|
||||||
else:
|
|
||||||
humidifier_history[:-1] = humidifier_history[1:]
|
|
||||||
humidifier_history[-1] = humidity
|
|
||||||
|
|
||||||
# compensate for the slow response time
|
|
||||||
slope = (humidifier_history[-1] - humidifier_history[0])/humidifier_history.shape[0]
|
|
||||||
comp_humidity = humidity + 50*slope
|
|
||||||
|
|
||||||
try:
|
|
||||||
humidifier.update(volts)
|
|
||||||
if comp_humidity < target_lower and humidifier.off:
|
|
||||||
humidifier.toggle(s)
|
|
||||||
elif comp_humidity > target_upper and humidifier.on:
|
|
||||||
humidifier.toggle(s)
|
|
||||||
|
|
||||||
update = {
|
|
||||||
"time": int(now*1000),
|
|
||||||
"temp": temp,
|
|
||||||
"hum": humidity,
|
|
||||||
"hv": volts
|
|
||||||
}
|
|
||||||
send_update(update)
|
|
||||||
except Exception as e:
|
|
||||||
print("pipe errored out, restarting: ", e)
|
|
||||||
# restart the process I guess
|
|
||||||
exiting = True
|
|
||||||
process.kill()
|
|
||||||
time.sleep(0.1)
|
|
||||||
stdout_thread.join()
|
|
||||||
exiting = False
|
|
||||||
|
|
||||||
process = subprocess.Popen(["/usr/bin/env", "python", "/home/kelvin/src/shroom-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout_thread = threading.Thread(target=stdout_loop)
|
|
||||||
stdout_thread.start()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# kill ssh connection
|
|
||||||
exiting = True
|
|
||||||
process.kill()
|
|
||||||
stdout_thread.join()
|
|
|
@ -22,10 +22,10 @@ func CreateTable(db *sql.DB) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Datapoint struct {
|
type Datapoint struct {
|
||||||
Time uint64 `json:"t"`
|
Time uint64
|
||||||
Temperature float32 `json:"temp"`
|
Temperature float32
|
||||||
Humidity float32 `json:"hum"`
|
Humidity float32
|
||||||
HumidifierVolts float32 `json:"hv"`
|
HumidifierVolts float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryHistory(db *sql.DB, start int64) ([]Datapoint, error) {
|
func QueryHistory(db *sql.DB, start int64) ([]Datapoint, error) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ type ShroomData struct {
|
||||||
Temperature float32 `json:"temp"`
|
Temperature float32 `json:"temp"`
|
||||||
Humidity float32 `json:"hum"`
|
Humidity float32 `json:"hum"`
|
||||||
HumidifierVolts float32 `json:"hv"`
|
HumidifierVolts float32 `json:"hv"`
|
||||||
Status int32 `json:"status"`
|
Status uint32 `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShroomStatus struct {
|
type ShroomStatus struct {
|
||||||
|
@ -30,9 +30,6 @@ type ShroomStatus struct {
|
||||||
HumidifierOn bool
|
HumidifierOn bool
|
||||||
NumConnections int
|
NumConnections int
|
||||||
Wait chan struct{}
|
Wait chan struct{}
|
||||||
StatusWait chan struct{}
|
|
||||||
|
|
||||||
Commands chan []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShroomStatus) Update() {
|
func (s *ShroomStatus) Update() {
|
||||||
|
@ -42,20 +39,13 @@ func (s *ShroomStatus) Update() {
|
||||||
s.Wait = make(chan struct{})
|
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) {
|
func parseMsg(line []byte, db *sql.DB, status *ShroomStatus) {
|
||||||
data := ShroomData{
|
data := ShroomData{
|
||||||
Time: 0,
|
Time: 0,
|
||||||
Temperature: -274,
|
Temperature: -1,
|
||||||
Humidity: -1,
|
Humidity: -1,
|
||||||
HumidifierVolts: -1,
|
HumidifierVolts: -1,
|
||||||
Status: -1,
|
Status: 0,
|
||||||
}
|
}
|
||||||
err := json.Unmarshal(line, &data)
|
err := json.Unmarshal(line, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -64,23 +54,21 @@ func parseMsg(line []byte, db *sql.DB, status *ShroomStatus) {
|
||||||
log.Println(line)
|
log.Println(line)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//log.Println("received data ", data)
|
if data.Time > 0 && data.Temperature > 0 && data.Humidity > 0 && data.HumidifierVolts > 0 {
|
||||||
if data.Time > 0 && data.Temperature > -275 && data.Humidity > -1 && data.HumidifierVolts > -1 {
|
|
||||||
err = InsertRow(db, &data)
|
err = InsertRow(db, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("unable to write to database: ", err)
|
log.Println("unable to write to database: ", err)
|
||||||
}
|
}
|
||||||
// we got a data packet
|
// we got a data packet
|
||||||
status.Update()
|
status.Update()
|
||||||
} else if data.Status != -1 {
|
} else if data.Status > 0 {
|
||||||
//log.Println("received status ", data.Status)
|
|
||||||
status.Lock()
|
status.Lock()
|
||||||
// TODO change to have more detailed data
|
// TODO change to have more detailed data
|
||||||
status.HumidifierOn = (data.Status & 1) == 1
|
status.HumidifierOn = (data.Status & 1) == 1
|
||||||
status.Unlock()
|
status.Unlock()
|
||||||
status.StatusUpdate()
|
status.Update()
|
||||||
} else {
|
} else {
|
||||||
log.Println("unknown packet: ", line, string(line))
|
log.Println("unknown packet: ", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -96,92 +84,62 @@ func InitTcpServer(db *sql.DB, status *ShroomStatus) {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("tcp accept error: ", err)
|
log.Println("tcp accept error: ", err)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
// not spawning a goroutine here
|
status.Lock()
|
||||||
// should limit the number of connections to
|
status.NumConnections += 1
|
||||||
// one hopefully
|
status.Unlock()
|
||||||
|
|
||||||
// wrapping in a func() so that I can use defer
|
defer func() {
|
||||||
// to automatically decrement the number of connections
|
|
||||||
func() {
|
|
||||||
status.Lock()
|
status.Lock()
|
||||||
status.NumConnections += 1
|
status.NumConnections -= 1
|
||||||
status.Unlock()
|
status.Unlock()
|
||||||
status.StatusUpdate()
|
log.Println("connection disconnected")
|
||||||
|
|
||||||
defer func() {
|
|
||||||
status.Lock()
|
|
||||||
status.NumConnections -= 1
|
|
||||||
status.Unlock()
|
|
||||||
status.StatusUpdate()
|
|
||||||
log.Println("connection disconnected")
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Println("connection started")
|
|
||||||
|
|
||||||
// write loop; waits for commands and forwards them
|
|
||||||
exiting := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case v, ok := <-status.Commands:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v = append(v, []byte("\n")...)
|
|
||||||
_, err := conn.Write(v)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("tcp write err: ", err)
|
|
||||||
}
|
|
||||||
case <-exiting:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
defer func() {
|
|
||||||
close(exiting)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// deal with the read side of the connection
|
|
||||||
buf := make([]byte, 128)
|
|
||||||
left := buf
|
|
||||||
|
|
||||||
for {
|
|
||||||
num_read, err := conn.Read(left)
|
|
||||||
//log.Println("received: ", string(left[:num_read]))
|
|
||||||
left = left[num_read:]
|
|
||||||
//log.Println("buf ", buf)
|
|
||||||
//log.Println("left ", left)
|
|
||||||
|
|
||||||
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 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):]
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
log.Println("connection started")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// TODO deal with the write side of the connection
|
||||||
|
}()
|
||||||
|
|
||||||
|
// deal with the read side of the connection
|
||||||
|
buf := make([]byte, 128)
|
||||||
|
left := buf
|
||||||
|
|
||||||
|
for {
|
||||||
|
num_read, err := conn.Read(left)
|
||||||
|
left = left[num_read:]
|
||||||
|
//log.Println("buf ", buf)
|
||||||
|
//log.Println("left ", left)
|
||||||
|
|
||||||
|
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 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):]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
101
shroom_server.go
101
shroom_server.go
|
@ -16,8 +16,6 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,25 +27,10 @@ type statusJson struct {
|
||||||
Humidifier bool `json:"humidifier"`
|
Humidifier bool `json:"humidifier"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type adminMsg struct {
|
func dumpData(db *sql.DB, offset int64) func(http.ResponseWriter, *http.Request) {
|
||||||
Auth string `json:"auth"`
|
return func(w http.ResponseWriter, _req *http.Request) {
|
||||||
Msg map[string]interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpData(db *sql.DB, multiplier int64) func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
path := strings.Split(req.URL.Path, "/")
|
t := now + offset
|
||||||
last := path[len(path)-1]
|
|
||||||
count, err := strconv.Atoi(last)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
w.Write([]byte("could not read integer in path: " + err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offset := int64(count) * multiplier
|
|
||||||
t := now*1000 - offset
|
|
||||||
//log.Println("req start ", t)
|
|
||||||
msg, err := s.GetRows(db, t)
|
msg, err := s.GetRows(db, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
|
@ -73,9 +56,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
status := s.ShroomStatus{
|
status := s.ShroomStatus{
|
||||||
Wait: make(chan struct{}),
|
Wait: make(chan struct{}),
|
||||||
StatusWait: make(chan struct{}),
|
|
||||||
Commands: make(chan []byte),
|
|
||||||
}
|
}
|
||||||
s.InitTcpServer(db, &status)
|
s.InitTcpServer(db, &status)
|
||||||
|
|
||||||
|
@ -84,11 +65,10 @@ func main() {
|
||||||
log.Fatal("unable to use subdirectory of embedded fs: ", err)
|
log.Fatal("unable to use subdirectory of embedded fs: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dumpWeek := dumpData(db, 7*24*60*60*1000)
|
dumpWeek := dumpData(db, -7*24*60*60)
|
||||||
dumpDay := dumpData(db, 24*60*60*1000)
|
dumpDay := dumpData(db, -24*60*60)
|
||||||
dumpHour := dumpData(db, 60*60*1000)
|
dumpHour := dumpData(db, -60*60)
|
||||||
dumpMinute := dumpData(db, 60*1000)
|
dumpMinute := dumpData(db, -60)
|
||||||
dumpSecond := dumpData(db, 1000)
|
|
||||||
|
|
||||||
lastPoint := func(w http.ResponseWriter, _req *http.Request) {
|
lastPoint := func(w http.ResponseWriter, _req *http.Request) {
|
||||||
msg, err := s.LastTime(db)
|
msg, err := s.LastTime(db)
|
||||||
|
@ -112,7 +92,7 @@ func main() {
|
||||||
msg, err := json.Marshal(s)
|
msg, err := json.Marshal(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("unable to marshal json: %w", err)
|
err = fmt.Errorf("unable to marshal json: %w", err)
|
||||||
w.WriteHeader(400)
|
w.WriteHeader(500)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
} else {
|
} else {
|
||||||
w.Write(msg)
|
w.Write(msg)
|
||||||
|
@ -120,49 +100,9 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
adminHandler := func(w http.ResponseWriter, req *http.Request) {
|
adminHandler := func(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != "POST" {
|
w.WriteHeader(500)
|
||||||
w.WriteHeader(405)
|
w.Write([]byte("unimplemented"))
|
||||||
w.Write([]byte("method must be POST"))
|
// TODO
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := make([]byte, 256)
|
|
||||||
l, err := req.Body.Read(msg)
|
|
||||||
if err != nil && l == 0 {
|
|
||||||
err = fmt.Errorf("unable to read body: %w", err)
|
|
||||||
w.WriteHeader(400)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
adminReq := adminMsg{}
|
|
||||||
err = json.Unmarshal(msg[:l], &adminReq)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("unable to unmarshal body as json: %w", err)
|
|
||||||
w.WriteHeader(400)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO switch to embedded secret
|
|
||||||
if adminReq.Auth != "password" {
|
|
||||||
w.WriteHeader(401)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
inner_msg, err := json.Marshal(adminReq.Msg)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("unable to marshal inner message: %w", err)
|
|
||||||
w.WriteHeader(400)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case status.Commands <- inner_msg:
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
default:
|
|
||||||
w.WriteHeader(503)
|
|
||||||
w.Write([]byte("unable to forward request; controller may not be connected"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHandler := func(w http.ResponseWriter, req *http.Request) {
|
updateHandler := func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -172,26 +112,17 @@ func main() {
|
||||||
}
|
}
|
||||||
w.Write([]byte("ok"))
|
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("/d/", http.StripPrefix("/d/", http.FileServer(http.Dir("./dev"))))
|
||||||
http.Handle("/", http.FileServer(http.FS(contentSub)))
|
http.Handle("/", http.FileServer(http.FS(contentSub)))
|
||||||
http.HandleFunc("/api/last/weeks/", dumpWeek)
|
http.HandleFunc("/api/last/week", dumpWeek)
|
||||||
http.HandleFunc("/api/last/days/", dumpDay)
|
http.HandleFunc("/api/last/day", dumpDay)
|
||||||
http.HandleFunc("/api/last/hours/", dumpHour)
|
http.HandleFunc("/api/last/hour", dumpHour)
|
||||||
http.HandleFunc("/api/last/minutes/", dumpMinute)
|
http.HandleFunc("/api/last/minute", dumpMinute)
|
||||||
http.HandleFunc("/api/last/seconds/", dumpSecond)
|
|
||||||
http.HandleFunc("/api/latest", lastPoint)
|
http.HandleFunc("/api/latest", lastPoint)
|
||||||
http.HandleFunc("/api/status", getStatus)
|
http.HandleFunc("/api/status", getStatus)
|
||||||
http.HandleFunc("/api/admin", adminHandler)
|
http.HandleFunc("/api/admin", adminHandler)
|
||||||
http.HandleFunc("/api/update", updateHandler)
|
http.HandleFunc("/api/update", updateHandler)
|
||||||
http.HandleFunc("/api/status_update", statusUpdateHandler)
|
|
||||||
|
|
||||||
// TODO periodically clear old entries from the database
|
// TODO periodically clear old entries from the database
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue