aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Schodet2018-11-18 00:15:36 +0100
committerNicolas Schodet2019-07-05 23:01:59 +0200
commit52f07a70503a46cc13d2e6f92577a553a037ca9c (patch)
treed9a2872ee7fcb11fbf71ffb69dcab7fae61844e1
Initial commit
-rw-r--r--.gitignore2
-rw-r--r--Makefile19
-rw-r--r--alarm.lua74
-rw-r--r--button.lua18
-rw-r--r--chambre/Makefile19
l---------chambre/display.lua1
-rw-r--r--clock.lua101
-rw-r--r--config.lua28
-rw-r--r--digit.lua49
-rw-r--r--display.digit.lua14
-rw-r--r--display.oled.lua25
-rw-r--r--httpd.lua136
-rw-r--r--hue.lua67
-rw-r--r--init.lua12
-rw-r--r--main.lua2
-rw-r--r--oled.lua33
-rw-r--r--salon/Makefile16
l---------salon/display.lua1
-rw-r--r--utils.lua27
19 files changed, 644 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba0528c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.up
+saved_config.lua
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..acb3ad8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+all: $(SOURCES:%.lua=%.up)
+
+ifeq ($H,)
+MYDIR = $(dir $(lastword $(MAKEFILE_LIST)))
+UPLOAD = python $(MYDIR)/../nodemcu-uploader/nodemcu-uploader.py upload $<:$(notdir $<)
+else
+UPLOAD = curl http://$H/upload/$(notdir $<) --data-binary @$<
+endif
+
+%.up: %.lua
+ $(UPLOAD)
+ touch $@
+
+.PHONY: salon chambre
+salon chambre: %:
+ $(MAKE) -C $@
+
+serial:
+ rlwrap serial -c /dev/ttyUSB0
diff --git a/alarm.lua b/alarm.lua
new file mode 100644
index 0000000..afbd406
--- /dev/null
+++ b/alarm.lua
@@ -0,0 +1,74 @@
+local httpd = require 'httpd'
+local hue = require 'hue'
+local config = require 'config'
+
+local alarm = { }
+
+local days = {
+ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
+ 'Saturday'
+}
+
+function alarm.httpd_process(method, path, headers, content)
+ local alarms = config.loaded.alarms
+ if path == '/alarm' then
+ if method == 'GET' then
+ local page = [[
+<html>
+<body>
+<form method="post">
+<table>
+%s</table>
+<input type="submit">
+</form>
+</body>
+</html>
+]]
+ local row = [[
+ <tr><td>%s</td>
+ <td><input type="checkbox" name="en%d"%s></td>
+ <td><input type="text" name="time%d" value="%02d:%02d"></td>
+ </tr>
+]]
+ local rows = { }
+ for di, d in ipairs(days) do
+ local checked = alarms[di] and alarms[di].active and ' checked'
+ or ''
+ local h = alarms[di] and alarms[di].hour or 0
+ local m = alarms[di] and alarms[di].min or 0
+ rows[#rows + 1] = string.format(row, d, di, checked, di, h, m)
+ end
+ page = string.format(page, table.concat(rows))
+ return 200, 'OK', { ['Content-Type'] = 'text/html' }, page
+ elseif method == 'POST' then
+ local form = httpd.parse_form(content)
+ for di, d in ipairs(days) do
+ active = form['en'..di] and true or false
+ h, m = form['time'..di]:match('^(%d+):(%d+)$')
+ h = h and tonumber(h)
+ m = m and tonumber(m)
+ if h and m then
+ alarms[di] = { active = active, hour = h, min = m }
+ else
+ return 200, 'OK', {}, 'Bad parameters\n'
+ end
+ end
+ config.save()
+ return 200, 'OK', {}, 'OK!\n'
+ end
+ end
+end
+
+function alarm.test(tm)
+ local di = tm.wday
+ local a = config.loaded.alarms[di]
+ if a and a.active and a.hour == tm.hour and a.min == tm.min then
+ alarm.ring()
+ end
+end
+
+function alarm.ring()
+ hue.on(nil, 64)
+end
+
+return alarm
diff --git a/button.lua b/button.lua
new file mode 100644
index 0000000..90699ff
--- /dev/null
+++ b/button.lua
@@ -0,0 +1,18 @@
+local hue = require 'hue'
+local button = { }
+local butA = 3
+local butB = 0
+local butC = 4
+
+function button.init()
+ gpio.mode(butA, gpio.INT, gpio.PULLUP)
+ gpio.mode(butC, gpio.INT, gpio.PULLUP)
+ gpio.trig(butA, "down", function()
+ hue.on(nil, 128)
+ end)
+ gpio.trig(butC, "down", function()
+ hue.off()
+ end)
+end
+
+return button
diff --git a/chambre/Makefile b/chambre/Makefile
new file mode 100644
index 0000000..4406f9a
--- /dev/null
+++ b/chambre/Makefile
@@ -0,0 +1,19 @@
+SOURCES = \
+ alarm.lua \
+ button.lua \
+ clock.lua \
+ config.lua \
+ display.lua \
+ httpd.lua \
+ hue.lua \
+ init.lua \
+ main.lua \
+ oled.lua \
+ saved_config.lua \
+ utils.lua
+
+H = clock-chambre.home
+
+vpath %.lua ..
+
+include ../Makefile
diff --git a/chambre/display.lua b/chambre/display.lua
new file mode 120000
index 0000000..a934bca
--- /dev/null
+++ b/chambre/display.lua
@@ -0,0 +1 @@
+../display.oled.lua \ No newline at end of file
diff --git a/clock.lua b/clock.lua
new file mode 100644
index 0000000..a14e5fa
--- /dev/null
+++ b/clock.lua
@@ -0,0 +1,101 @@
+local httpd = require 'httpd'
+local config = require 'config'
+local button = config.loaded.button and require 'button'
+local hue = config.loaded.button and require 'hue'
+local alarm = config.loaded.alarm and require 'alarm'
+local display = require 'display'
+
+local clock = {}
+
+local last_output
+
+local ntp_server = nil
+local ntp_counter = 0
+local ntp_counter_max = 24
+
+function clock.output()
+ local time = rtctime.get()
+ local output
+ local tm
+ if time ~= 0 then
+ tm = rtctime.epoch2cal(time + config.loaded.tz_shift * 3600)
+ output = string.format('%2d:%02d', tm.hour, tm.min)
+ else
+ output = '--:--'
+ if wifi.sta.getip() ~= nil then
+ clock.ntp_sync()
+ end
+ end
+ if last_output ~= output then
+ display.output(output)
+ last_output = output
+ if time ~= 0 and alarm then alarm.test(tm) end
+ end
+end
+
+function clock.ntp_sync()
+ if ntp_counter == 0 then
+ sntp.sync(ntp_server,
+ function(sec, usec, server)
+ local tm = rtctime.epoch2cal(sec + config.loaded.tz_shift * 3600)
+ print(string.format('ntp sync %04d-%02d-%02d %02d:%02d:%02d',
+ tm.year, tm.mon, tm.day, tm.hour, tm.min, tm.sec))
+ clock.output()
+ end,
+ function(code, err)
+ print('NTP sync fail: ' .. tostring(err) .. ' ' .. tostring(code))
+ end)
+ ntp_counter = ntp_counter_max - 1
+ else
+ ntp_counter = ntp_counter - 1
+ end
+end
+
+local function httpd_process(method, path, headers, content)
+ if method == 'GET' and path == '/' then
+ local page = [[
+<html>
+<body>
+<form method="post">
+Change time zone shift :
+<input type="text" name="tz_shift" value="%d">
+<input type="submit">
+</form>
+</body>
+</html>
+]]
+ page = string.format(page, config.loaded.tz_shift)
+ return 200, 'OK', { ['Content-Type'] = 'text/html' }, page
+ elseif method == 'POST' and path == '/' then
+ local v = content:match('tz_shift=(%d*)')
+ config.loaded.tz_shift = tonumber(v)
+ config.save()
+ clock.output()
+ return 200, 'OK', {}, 'OK!\n'
+ elseif alarm and path == '/alarm' then
+ return alarm.httpd_process(method, path, headers, content)
+ elseif hue and path == '/hue' then
+ return hue.httpd_process(method, path, headers, content)
+ else
+ return httpd.upload(method, path, headers, content)
+ end
+end
+
+function clock.start()
+ display.init()
+ wifi.setmode(wifi.STATION)
+ wifi.sta.config{
+ ssid = config.loaded.ssid,
+ pwd = config.loaded.psk,
+ save = false,
+ }
+ if config.loaded.hostname then
+ wifi.sta.sethostname(config.loaded.hostname)
+ end
+ tmr.alarm(0, 3600 * 1000, 1, clock.ntp_sync)
+ tmr.alarm(1, 1000, 1, clock.output)
+ httpd.init(httpd_process)
+ if button then button.init() end
+end
+
+return clock
diff --git a/config.lua b/config.lua
new file mode 100644
index 0000000..7b6da99
--- /dev/null
+++ b/config.lua
@@ -0,0 +1,28 @@
+local utils = require 'utils'
+local config = {}
+
+local config_file = 'saved_config.lua'
+
+config.loaded = {
+ tz_shift = 0,
+}
+
+function config.load()
+ local r, c = pcall(function() return dofile(config_file) end)
+ if r then
+ config.loaded = c
+ end
+end
+
+function config.save()
+ local cs = 'return '..utils.dump(config.loaded)
+ local fd = file.open(config_file, 'w+')
+ if fd then
+ fd:write(cs)
+ fd:close()
+ end
+end
+
+config.load()
+
+return config
diff --git a/digit.lua b/digit.lua
new file mode 100644
index 0000000..932ee57
--- /dev/null
+++ b/digit.lua
@@ -0,0 +1,49 @@
+local digit = {}
+
+local function write(data)
+ i2c.start(0)
+ i2c.address(0, 0x70, i2c.TRANSMITTER)
+ i2c.write(0, data)
+ i2c.stop(0)
+end
+
+-- -a-
+-- f b
+-- -g-
+-- e c
+-- -d- p
+local function chr(x)
+ local c = {
+ [' '] = 0, [':'] = 2, ['-'] = 0x40,
+ ['0'] = 0x3f, ['1'] = 0x06, ['2'] = 0x5b, ['3'] = 0x4f, ['4'] = 0x66,
+ ['5'] = 0x6d, ['6'] = 0x7d, ['7'] = 0x07, ['8'] = 0x7f, ['9'] = 0x6f,
+ }
+ return c[x] or c[' ']
+end
+
+function digit.init()
+ i2c.setup(0, 2, 1, i2c.SLOW)
+ write(0x21)
+ digit.on(true)
+ digit.brightness(1)
+ digit.output('00:00')
+end
+
+function digit.on(flag)
+ write(flag and 0x81 or 0x80)
+end
+
+function digit.brightness(val)
+ write(0xe0 + val)
+end
+
+function digit.blink(flag)
+ write(flag and 0x85 or 0x81)
+end
+
+function digit.output(txt)
+ write{ 0, chr(txt:sub(1, 1)), 0, chr(txt:sub(2, 2)), 0,
+ chr(txt:sub(3, 3)), 0, chr(txt:sub(4, 4)), 0, chr(txt:sub(5, 5)) }
+end
+
+return digit
diff --git a/display.digit.lua b/display.digit.lua
new file mode 100644
index 0000000..6d80ec7
--- /dev/null
+++ b/display.digit.lua
@@ -0,0 +1,14 @@
+local digit = require 'digit'
+
+local display = {}
+
+function display.init()
+ digit.init()
+ digit.output('-- --')
+end
+
+function display.output(txt)
+ digit.output(txt)
+end
+
+return display
diff --git a/display.oled.lua b/display.oled.lua
new file mode 100644
index 0000000..d68c214
--- /dev/null
+++ b/display.oled.lua
@@ -0,0 +1,25 @@
+local oled = require 'oled'
+
+local display = {}
+
+function display.init()
+ oled.init()
+ oled.update(function(disp)
+ local output = 'Hello!'
+ disp:setFont(u8g.font_gdr12)
+ disp:setFontPosCenter()
+ local w = disp:getStrWidth(output)
+ disp:drawStr(64 - w / 2, 16, output)
+ end)
+end
+
+function display.output(txt)
+ oled.update(function(disp)
+ disp:setFont(u8g.font_gdr30n)
+ disp:setFontPosCenter()
+ local w = disp:getStrWidth(txt)
+ disp:drawStr(64 - w / 2, 16, txt)
+ end)
+end
+
+return display
diff --git a/httpd.lua b/httpd.lua
new file mode 100644
index 0000000..de578c5
--- /dev/null
+++ b/httpd.lua
@@ -0,0 +1,136 @@
+local httpd = { }
+
+local socket
+local process
+
+local function send_and_close(sock, data)
+ sock:send(data, function(sock) sock:close() end)
+end
+
+local function default(method, path, headers)
+ if method == 'GET' and path == '/' then
+ return 200, 'OK', { }, 'OK, good!\n'
+ else
+ return 404, 'Not Found', { }, 'Not found!\n'
+ end
+end
+
+local function response(sock, code, status, headers, content)
+ local resp = { string.format('HTTP/1.1 %d %s', code, status) }
+ headers['Connection'] = 'close'
+ for k, v in pairs(headers) do
+ table.insert(resp, string.format('%s: %s', k, v))
+ end
+ table.insert(resp, '')
+ table.insert(resp, content)
+ resp = table.concat(resp, '\r\n')
+ send_and_close(sock, resp)
+ return true
+end
+
+local function listenFunc(sock)
+ local buf = ''
+ local method
+ local path
+ local headers = { }
+ local header_done
+ local content_length = 0
+ local function recv(sock, c)
+ local function bad_request()
+ send_and_close(sock, 'HTTP/1.1 400 Bad request\r\n')
+ end
+ buf = buf .. c
+ while true do
+ if not header_done then
+ local s = buf:find('\r\n', 1, true)
+ if s then
+ local line = buf:sub(1, s - 1)
+ buf = buf:sub(s + 2)
+ if not method then
+ method, path = line:match('^(%u+) (%S+) HTTP/1%.1$')
+ if not method then
+ bad_request()
+ break
+ end
+ elseif not header_done then
+ if line == '' then
+ header_done = true
+ else
+ local h, v = line:match('^([-a-zA-Z]+): (.*)$')
+ if h then
+ h = h:lower()
+ headers[h] = v
+ if h == 'content-length' then
+ content_length = tonumber(v)
+ end
+ else
+ bad_request()
+ break
+ end
+ end
+ end
+ else
+ break
+ end
+ elseif content_length == #buf then
+ local code, status, headers, content
+ = process(method, path, headers, buf)
+ if not code then
+ code, status, headers, content
+ = default(method, path, headers, buf)
+ end
+ response(sock, code, status, headers, content)
+ break
+ else
+ break
+ end
+ end
+ end
+ sock:on('receive', recv)
+end
+
+function httpd.init(processFunc)
+ socket = net.createServer()
+ socket:listen(80, listenFunc)
+ process = processFunc
+end
+
+function httpd.dump(method, path, headers, content)
+ local c = { method, path }
+ for k, v in pairs(headers) do
+ table.insert(c, string.format('%s: %s', k, v))
+ end
+ table.insert(c, content)
+ return 200, 'OK', { }, table.concat(c, '\n')
+end
+
+function httpd.upload(method, path, headers, content)
+ if method == 'POST' and path:sub(1, 8) == '/upload/' then
+ local fname = path:sub(9)
+ file.open(fname, "w")
+ file.write(content)
+ file.close()
+ return 200, 'OK', { }, 'OK, file uploaded.\n'
+ end
+end
+
+function httpd.parse_form(form)
+ local r = { }
+ local function a(p)
+ local k, v = p:match('(.-)=(.*)')
+ if k and v then
+ v = v:gsub('%%3A', ':')
+ r[k] = v
+ end
+ end
+ local s = form:find('&')
+ while s do
+ a(form:sub(1, s - 1))
+ form = form:sub(s + 1)
+ s = form:find('&')
+ end
+ a(form)
+ return r
+end
+
+return httpd
diff --git a/hue.lua b/hue.lua
new file mode 100644
index 0000000..54a00e4
--- /dev/null
+++ b/hue.lua
@@ -0,0 +1,67 @@
+local config = require 'config'
+local httpd = require 'httpd'
+
+local hue = { }
+
+function hue.req(conf, t, cb)
+ conf = conf or config.loaded.hue
+ local conn = net.createConnection()
+ conn:on('connection', function(conn)
+ conn:on('receive', function(conn, pl)
+ if cb then
+ cb(pl:find('200') and true)
+ end
+ end)
+ req = {
+ string.format('PUT /api/%s/lights/%d/state HTTP/1.1',
+ conf.user, conf.light),
+ string.format('Host: %s', conf.addr),
+ 'Connection: close',
+ string.format('Content-Length: %d', #t + 2),
+ '',
+ t,
+ ''
+ }
+ conn:send(table.concat(req, '\r\n'))
+ end)
+ conn:connect(80, conf.addr)
+end
+
+function hue.on(conf, bri, cb)
+ hue.req(conf, string.format(
+ '{"on":true,"bri":%d}',
+ bri or 255), cb)
+end
+
+function hue.off(conf, cb)
+ hue.req(conf, '{"on":false}', cb)
+end
+
+function hue.httpd_process(method, path, headers, content)
+ if path == '/hue' then
+ if method == 'GET' then
+ local page = [[
+<html>
+<body>
+<form method="post">
+<input type="checkbox" name="on">
+<input type="submit">
+</form>
+</body>
+</html>
+]]
+ return 200, 'OK', { ['Content-Type'] = 'text/html' }, page
+ elseif method == 'POST' then
+ local form = httpd.parse_form(content)
+ local on = form['on'] and true or false
+ if form['on'] then
+ hue.on(nil, 128)
+ else
+ hue.off()
+ end
+ return 200, 'OK', {}, 'OK!\n'
+ end
+ end
+end
+
+return hue
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..f7e8859
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,12 @@
+local function startup()
+ if file.open("main.lua") == nil then
+ print("main.lua not present")
+ else
+ file.close("main.lua")
+ print("running...")
+ dofile("main.lua")
+ end
+end
+
+print("running main.lua in 3 seconds...")
+tmr.alarm(0, 3000, 0, startup)
diff --git a/main.lua b/main.lua
new file mode 100644
index 0000000..7ca35e8
--- /dev/null
+++ b/main.lua
@@ -0,0 +1,2 @@
+local clock = require 'clock'
+clock.start()
diff --git a/oled.lua b/oled.lua
new file mode 100644
index 0000000..90bcf47
--- /dev/null
+++ b/oled.lua
@@ -0,0 +1,33 @@
+local oled = {}
+
+local disp
+
+function oled.init()
+ i2c.setup(0, 2, 1, i2c.SLOW)
+ disp = u8g.ssd1306_128x32_i2c(0x3c)
+ disp:setContrast(0)
+ oled.cmd{0xd9, 0x21}
+end
+
+function oled.update(func)
+ local function drawPages()
+ func(disp)
+ if disp:nextPage() then
+ node.task.post(drawPages)
+ end
+ end
+ disp:firstPage()
+ node.task.post(drawPages)
+end
+
+function oled.cmd(t)
+ i2c.start(0)
+ i2c.address(0, 0x3c, i2c.TRANSMITTER)
+ i2c.write(0, 0)
+ for i, v in ipairs(t) do
+ i2c.write(0, v)
+ end
+ i2c.stop(0)
+end
+
+return oled
diff --git a/salon/Makefile b/salon/Makefile
new file mode 100644
index 0000000..3c77d8e
--- /dev/null
+++ b/salon/Makefile
@@ -0,0 +1,16 @@
+SOURCES = \
+ clock.lua \
+ config.lua \
+ digit.lua \
+ display.lua \
+ httpd.lua \
+ init.lua \
+ main.lua \
+ saved_config.lua \
+ utils.lua
+
+H = clock-salon.home
+
+vpath %.lua ..
+
+include ../Makefile
diff --git a/salon/display.lua b/salon/display.lua
new file mode 120000
index 0000000..3218f7b
--- /dev/null
+++ b/salon/display.lua
@@ -0,0 +1 @@
+../display.digit.lua \ No newline at end of file
diff --git a/utils.lua b/utils.lua
new file mode 100644
index 0000000..7580387
--- /dev/null
+++ b/utils.lua
@@ -0,0 +1,27 @@
+local utils = {}
+
+function utils.dumpt(tt, result)
+ local r = result or {}
+ if type(tt) == 'table' then
+ table.insert(r, '{')
+ for k, v in pairs(tt) do
+ table.insert(r, '[')
+ utils.dumpt(k, r)
+ table.insert(r, ']=')
+ utils.dumpt(v, r)
+ table.insert(r, ',')
+ end
+ table.insert(r, '}')
+ elseif type(tt) == 'string' then
+ table.insert(r, string.format('%q', tt))
+ else
+ table.insert(r, tostring(tt))
+ end
+ return r
+end
+
+function utils.dump(tt)
+ return table.concat(utils.dumpt(tt))
+end
+
+return utils