From 52f07a70503a46cc13d2e6f92577a553a037ca9c Mon Sep 17 00:00:00 2001 From: Nicolas Schodet Date: Sun, 18 Nov 2018 00:15:36 +0100 Subject: Initial commit --- .gitignore | 2 + Makefile | 19 ++++++++ alarm.lua | 74 ++++++++++++++++++++++++++++ button.lua | 18 +++++++ chambre/Makefile | 19 ++++++++ chambre/display.lua | 1 + clock.lua | 101 ++++++++++++++++++++++++++++++++++++++ config.lua | 28 +++++++++++ digit.lua | 49 +++++++++++++++++++ display.digit.lua | 14 ++++++ display.oled.lua | 25 ++++++++++ httpd.lua | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++ hue.lua | 67 ++++++++++++++++++++++++++ init.lua | 12 +++++ main.lua | 2 + oled.lua | 33 +++++++++++++ salon/Makefile | 16 +++++++ salon/display.lua | 1 + utils.lua | 27 +++++++++++ 19 files changed, 644 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 alarm.lua create mode 100644 button.lua create mode 100644 chambre/Makefile create mode 120000 chambre/display.lua create mode 100644 clock.lua create mode 100644 config.lua create mode 100644 digit.lua create mode 100644 display.digit.lua create mode 100644 display.oled.lua create mode 100644 httpd.lua create mode 100644 hue.lua create mode 100644 init.lua create mode 100644 main.lua create mode 100644 oled.lua create mode 100644 salon/Makefile create mode 120000 salon/display.lua create mode 100644 utils.lua 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 = [[ + + +
+ +%s
+ +
+ + +]] + local row = [[ + %s + + + +]] + 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 = [[ + + +
+Change time zone shift : + + +
+ + +]] + 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 = [[ + + +
+ + +
+ + +]] + 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 -- cgit v1.2.3