-
Notifications
You must be signed in to change notification settings - Fork 3
/
brewbot.lua
404 lines (317 loc) · 12.3 KB
/
brewbot.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
-- Brew Bot, a CO2 detecting fermentation monitor with the ESP8266
-- Copyright (c) 2016 Albert Santoni
-- Licensed under the MIT License
function setupWifi()
-- Plain old connect-to-an-AP WiFi mode.
wifi.setmode(wifi.STATION)
-- Event handlers
wifi.sta.eventMonReg(wifi.STA_CONNECTING, function() printCorner("Connecting to \n" .. ssid) end)
wifi.sta.eventMonReg(wifi.STA_FAIL, function() printCorner("Wifi failed :(") end)
-- IP address assigned event handler
wifi.sta.eventMonReg(wifi.STA_GOTIP, function()
print("Got an IP. Starting inet services...")
printCorner(wifi.sta.getip())
-- Telnet disabled for security by default:
--startTelnetServer()
startWebServer()
-- startInfluxClient()
-- Make brewbot.local work in your web browser!
mdns.register("brewbot", {description='Kombucha CO2 Sensor',
service="http", port=80})
print("Wifi/internet online and ready!")
end)
-- Configure which AP we're connecting to.
wifi.sta.config(ssid,ssid_password)
-- Start monitoring events
wifi.sta.eventMonStart()
-- Connect and auto-connect if we disconnect for any reason
wifi.sta.autoconnect(1)
-- Power savings
wifi.sleeptype(wifi.DEEP_SLEEP)
end
-- Don't use the local keyword if you want to check
-- these values from an interactive prompt like telnet or serial!
gas_conc = 0
raw_data = ""
-- Put these in secrets.lua. They're read in from readConfiguration().
ssid = ""
ssid_password = ""
influxdb_post_url = ""
influxdb_14day_get_url = ""
influxdb_7day_get_url = ""
influxdb_24hour_get_url = ""
influxdb_auth_header = ""
-- Telnet server for debugging since
-- the ESP8266 only has one UART and we need
-- it for the CO2 sensor!
function startTelnetServer()
srv = net.createServer(net.TCP,180)
srv:listen(23,function(c)
c:on("receive",function(c,d)
-- Switch to telnet service
node.output(function(s)
if c ~= nil then c:send(s) end
end,0)
c:on("receive",function(c,d)
if d:byte(1) == 4 then c:close() -- ctrl-d to exit
else node.input(d) end
end)
c:on("disconnection",function(c)
node.output(nil)
end)
print("Welcome to Brew Bot")
node.input("\r\n")
return
end)
end)
end
-- Starts a webserver that provides a super basic read-only interface
-- displaying the CO2 concentration.
function startWebServer()
local srv = net.createServer(net.TCP, 30)
srv:listen(80,function(conn)
conn:on("receive",function(conn,payload)
-- print(payload)
httpResponse = ""
httpResponse = httpResponse .. "HTTP/1.1 200 OK\r\n"
httpResponse = httpResponse .. "Content-type: text/html\r\n"
httpResponse = httpResponse .. "Connection: close\r\n\r\n"
httpResponse = httpResponse .. "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\r\n"
httpResponse = httpResponse .. gas_conc .. " PPM<br>"
conn:send(httpResponse)
end)
conn:on("sent", function(conn)
conn:close()
end)
end)
end
-- Receive data on the UART, from the CO2 sensor
function initializeMHZ19UART()
-- Note: The 5V output from the Weemos can't supply
-- enough current to the MH-Z19 - so you'll need to provide
-- external power to it.
-- uart.alt(1) -- Use GPIO13 and GPIO15 (pins D7 and D8 on Weemos)
uart.setup(0, 9600, 8, uart.PARITY_NONE, uart.STOPBITS_1, 0)
-- Write a test string to the CO2 sensor
-- MH-Z19 strings are always 9 bytes
--uart.write(0, 0xFF) -- Start byte
--uart.write(0, 0x01) -- Sensor number
--uart.write(0, 0x86) -- Command
--uart.write(0, 0x00) -- Byte 3 - unused
--uart.write(0, 0x00) -- Byte 4 - unused
--uart.write(0, 0x00) -- Byte 5 - unused
--uart.write(0, 0x00) -- Byte 6 - unused
--uart.write(0, 0x00) -- Byte 7 - unused
--uart.write(0, 0x79) -- "Check value" checksum?
-- NOTE: If you're using the serial console while your MH-Z19 is hooked
-- up to the UART, anything the MH-Z19 sends back to you will be dumped
-- to your serial console, which probably isn't what you want.
-- (There's only 1 UART in the ESP8266.)
-- Read any buffered UART data the first time, so that we're sure it's flushed.
uart.on("data", 0, function(data) -- 9, function(data)
-- Receive data on the UART, from the CO2 sensor (9 bytes = 1 message)
uart.on("data", 9, function(data)
raw_data = data
if string.byte(data, 1) == 0xFF and
string.byte(data, 2) == 0x86 then
high_level_conc = string.byte(data, 3)
low_level_conc = string.byte(data, 4)
gas_conc = high_level_conc * 256 + low_level_conc
end
end, 0)
end, 0)
-- Request a CO2 concentration reading:
uart.write(0, 0xFF, 0x01, 0x86,0x00,0x00,0x00,0x00,0x00, 0x79)
-- Warning: Just running uart.on("data") to unregister causes a crash?
end
-- Poll the CO2 sensor via the UART interface.
function startCO2PollTimer()
tmr.alarm(1, 1000, tmr.ALARM_AUTO, function()
-- Ask the MH-Z19 for the CO2 concentration
uart.write(0, 0xFF, 0x01, 0x86,0x00,0x00,0x00,0x00,0x00, 0x79)
end)
end
-- Log our last CO2 concentration reading to InfluxDB every few seconds.
function logDataToInfluxDB(callback)
-- This should not be executed cooperatively with any other HTTP requests. Only one TCP client allowed at a time maybe?
http.post(influxdb_post_url, influxdb_auth_header, 'co2_concentration,room=livingroom value=' .. gas_conc,
function(code, data)
if code ~= 204 and code ~= 200 then
printBottom("Influx: " .. code) -- data
-- print(code, data)
end
if callback ~= nil then
node.task.post(node.task.LOW_PRIORITY, function()
callback()
end)
end
end)
end
-- Initialize the OLED display
function init_spi_display()
-- Hardware SPI CLK = GPIO14 (Huzzah) - IO 5 (ESP/D1 Mini)
-- Hardware SPI MOSI = GPIO13 (Huzzah) - IO 7 (ESP/D1 Mini)
-- Hardware SPI MISO = GPIO12 (not used)
local cs = 1 -- GPIO5 (Huzzah) - IO 1 (ESP/D1 Mini)
local dc = 2 -- GPIO4 (Huzzah) - IO 2 (ESP/D1 Mini)
local res = 0 -- GPIO16 - IO 0 (ESP/D1 Mini)
spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 8)
disp = ucg.ssd1331_18x96x64_uvis_hw_spi(cs, dc, res)
-- disp:begin(ucg.FONT_MODE_TRANSPARENT)
disp:begin(ucg.FONT_MODE_SOLID)
disp:clearScreen()
-- Fonts compiled in by default are:
-- (run the test() function to see the list!)
-- font_7x13B_tr
-- font_helvB08_hr
-- font_helvB10_hr
-- font_helvB12_hr
-- font_helvB18_hr
-- font_ncenB24_tr
-- font_ncenR12_tr
-- font_ncenR14_hr
disp:setFont(ucg.font_helvB08_hr);
disp:setColor(255, 255, 255);
disp:setColor(1, 0, 0,0);
-- disp:setPrintPos(0, 64)
-- disp:print("Hello!")
printCorner("Brew Bot 1.0")
end
-- Prints a string to the center of the screen.
-- @param string String to draw on the screen.
function printCentered(string)
local strWidth = disp:getStrWidth(string)
local fontAscent = disp:getFontAscent()
disp:setPrintPos(disp:getWidth()/2 - strWidth/2,
disp:getHeight()/2 + fontAscent/2)
disp:print(string)
end
-- Prints to the upper-left corner of the screen.
-- @param string String to draw on the screen.
function printCorner(string)
local strWidth = disp:getStrWidth(string)
local fontAscent = disp:getFontAscent()
disp:setPrintPos(0, fontAscent)
disp:print(string)
end
-- Prints to the bottom-left corner of the screen.
-- @param string String to draw on the screen.
function printBottom(string)
local strWidth = disp:getStrWidth(string)
local fontAscent = disp:getFontAscent()
disp:setPrintPos(0, disp:getHeight())
disp:print(string)
end
-- Reads your configuration from secrets.lua
function readConfiguration()
require("secrets")
end
-- Draws a 14 day graph of CO2 levels
function draw14DayGraph()
disp:clearScreen()
fetchHistoricalData(influxdb_14day_get_url, 0, function()
printCentered("14 day")
end)
end
-- Draws a 7 day graph of CO2 levels
function draw7DayGraph()
disp:clearScreen()
fetchHistoricalData(influxdb_7day_get_url, 0, function()
printCentered("7 day")
end)
end
-- Draws a 24 hour graph of CO2 levels
function draw24HourGraph()
disp:clearScreen()
fetchHistoricalData(influxdb_24hour_get_url, 0, function()
printCentered("24 hour")
end)
end
-- Main loop
function mainLoop()
local NUM_MODES = 4
local mode = 0
disp:clearScreen()
printCentered(gas_conc .. " PPM")
tmr.alarm(3, 15000, tmr.ALARM_AUTO, function()
if mode == 0 then
disp:clearScreen()
printCentered(gas_conc .. " PPM")
logDataToInfluxDB(function()
draw24HourGraph()
end)
elseif mode == 1 then
draw24HourGraph()
elseif mode == 2 then
draw7DayGraph()
elseif mode == 3 then
draw14DayGraph()
end
printCorner(gas_conc .. " PPM")
mode = (mode + 1) % NUM_MODES
end)
end
-- Fetch historical data from InfluxDB and graph it on the OLED screen.
-- @param url The InfluxDB URL to fetch data from. Includes the query! (string)
-- @param offset Pagination offset (int)
-- @param finishedCallback A callback function to call once complete (function object)
function fetchHistoricalData(url, offset, finishedCallback)
local QUERY_LIMIT = 16
local SCREEN_HEIGHT = 64
local SCREEN_WIDTH = 96
local MAX_DATA_POINTS = 84
local CO2_MAX_CONCENTRATION = 2000.0
-- 14 days * 24 hours / 4 hours per data point = 84
http.get(url .. "%20OFFSET%20" .. offset, influxdb_auth_header,
function(code, data)
if code == 200 then
local data = cjson.decode(data)
-- for k,v in pairs(data["results"][1]["series"][1]["values"]) do print(k,v) end
local lastPixelY = SCREEN_HEIGHT-1
-- Print the date range at the top?
local dataCount = 0
for k,v in pairs(data["results"][1]["series"][1]["values"]) do
-- print(k, v[1], v[2])
local pixelY = SCREEN_HEIGHT-math.floor(v[2] / CO2_MAX_CONCENTRATION * SCREEN_HEIGHT)
-- print(offset+k, pixelY)
--disp:drawPixel(offset+k, pixelY)
disp:setColor(30, 30, 128)
disp:drawLine(offset+k, SCREEN_HEIGHT,
offset+k, pixelY)
disp:setColor(80, 80, 256)
disp:drawPixel(offset+k, pixelY)
lastPixelY = pixelY
dataCount = dataCount + 1
end
if offset + dataCount < MAX_DATA_POINTS then
node.task.post(node.task.LOW_PRIORITY, function()
fetchHistoricalData(url, offset + QUERY_LIMIT, finishedCallback)
end)
else
finishedCallback()
end
else
-- print(code, data)
-- printCentered("Influx query error\n" .. code)
finishedCallback()
end
end)
end
-- Initialize all the peripherals and start the main loop.
function startup()
-- To inhibit startup, in the 5 second waiting period either run:
-- file.remove("init.lua")
-- or abort=true
print('In startup')
if abort == true then
print("Startup aborted")
return
end
readConfiguration()
initializeMHZ19UART()
startCO2PollTimer()
init_spi_display()
-- Webserver + Influx client are started when we get an IP. See this function:
setupWifi()
mainLoop()
end