diff --git a/firmware/2.2.0/app/include/user_config.h b/firmware/2.2.0/app/include/user_config.h index b43a794..f1303b0 100644 --- a/firmware/2.2.0/app/include/user_config.h +++ b/firmware/2.2.0/app/include/user_config.h @@ -93,7 +93,7 @@ #define CLIENT_SSL_ENABLE //#define MD2_ENABLE #define SHA2_ENABLE -#define SSL_BUFFER_SIZE 4196 +#define SSL_BUFFER_SIZE 5376 // GPIO_INTERRUPT_ENABLE needs to be defined if your application uses the diff --git a/firmware/2.2.0/app/include/user_modules.h b/firmware/2.2.0/app/include/user_modules.h index baafc21..3e9f757 100644 --- a/firmware/2.2.0/app/include/user_modules.h +++ b/firmware/2.2.0/app/include/user_modules.h @@ -12,7 +12,7 @@ //#define LUA_USE_MODULES_ADXL345 //#define LUA_USE_MODULES_AM2320 //#define LUA_USE_MODULES_APA102 -//#define LUA_USE_MODULES_BIT +#define LUA_USE_MODULES_BIT //#define LUA_USE_MODULES_BLOOM //#define LUA_USE_MODULES_BMP085 //#define LUA_USE_MODULES_BME280 @@ -20,7 +20,7 @@ //#define LUA_USE_MODULES_COAP //#define LUA_USE_MODULES_COLOR_UTILS //#define LUA_USE_MODULES_CRON -//#define LUA_USE_MODULES_CRYPTO +#define LUA_USE_MODULES_CRYPTO #define LUA_USE_MODULES_DHT #define LUA_USE_MODULES_DS18B20 //#define LUA_USE_MODULES_ENCODER @@ -37,7 +37,7 @@ //#define LUA_USE_MODULES_L3G4200D //#define LUA_USE_MODULES_MCP4725 //#define LUA_USE_MODULES_MDNS -//#define LUA_USE_MODULES_MQTT +#define LUA_USE_MODULES_MQTT #define LUA_USE_MODULES_NET #define LUA_USE_MODULES_NODE //#define LUA_USE_MODULES_OW @@ -49,11 +49,11 @@ //#define LUA_USE_MODULES_ROTARY //#define LUA_USE_MODULES_RTCFIFO //#define LUA_USE_MODULES_RTCMEM -//#define LUA_USE_MODULES_RTCTIME +#define LUA_USE_MODULES_RTCTIME //#define LUA_USE_MODULES_SI7021 //#define LUA_USE_MODULES_SIGMA_DELTA #define LUA_USE_MODULES_SJSON -//#define LUA_USE_MODULES_SNTP +#define LUA_USE_MODULES_SNTP //#define LUA_USE_MODULES_SOMFY //#define LUA_USE_MODULES_SPI //#define LUA_USE_MODULES_SQLITE3 @@ -67,7 +67,7 @@ #define LUA_USE_MODULES_UART //#define LUA_USE_MODULES_U8G2 //#define LUA_USE_MODULES_UCG -//#define LUA_USE_MODULES_WEBSOCKET +#define LUA_USE_MODULES_WEBSOCKET #define LUA_USE_MODULES_WIFI //#define LUA_USE_MODULES_WIFI_MONITOR //#define LUA_USE_MODULES_WPS diff --git a/firmware/2.2.0/app/include/user_version.h b/firmware/2.2.0/app/include/user_version.h index 13d6235..a33e207 100644 --- a/firmware/2.2.0/app/include/user_version.h +++ b/firmware/2.2.0/app/include/user_version.h @@ -11,10 +11,10 @@ #define NODE_VERSION_STR(x) #x #define NODE_VERSION_XSTR(x) NODE_VERSION_STR(x) -#define NODE_VERSION "Konnected firmware 2.2.7\r\nNodeMCU " ESP_SDK_VERSION_STRING "." NODE_VERSION_XSTR(NODE_VERSION_INTERNAL) +#define NODE_VERSION "Konnected firmware 2.3.0\r\nNodeMCU " ESP_SDK_VERSION_STRING "." NODE_VERSION_XSTR(NODE_VERSION_INTERNAL) #ifndef BUILD_DATE -#define BUILD_DATE "20190424" +#define BUILD_DATE "20190805" #endif extern char SDK_VERSION[]; diff --git a/firmware/konnected-filesystem-0x100000-2-3-0.img b/firmware/konnected-filesystem-0x100000-2-3-0.img new file mode 100644 index 0000000..057e0d7 Binary files /dev/null and b/firmware/konnected-filesystem-0x100000-2-3-0.img differ diff --git a/firmware/konnected-firmware-2-3-0.bin b/firmware/konnected-firmware-2-3-0.bin new file mode 100644 index 0000000..422ff6c Binary files /dev/null and b/firmware/konnected-firmware-2-3-0.bin differ diff --git a/scripts/build-firmware b/scripts/build-firmware index 69fcc16..4a411ff 100755 --- a/scripts/build-firmware +++ b/scripts/build-firmware @@ -1,14 +1,14 @@ #!/bin/bash FIRMWARE_PATH=${PWD}/../nodemcu-firmware -IMAGE_NAME=konnected-firmware-2-2-7 +IMAGE_NAME=konnected-firmware-2-3-0 # Copy firmware configuration from this repository to the nodemcu-firmware repo cp firmware/2.2.0/app/include/* $FIRMWARE_PATH/app/include/ rm $FIRMWARE_PATH/local/fs/* -# Build NodeMCU firmware image + Build NodeMCU firmware image docker run -e "INTEGER_ONLY=1" \ -e "IMAGE_NAME=${IMAGE_NAME}" \ --rm -ti -v $FIRMWARE_PATH:/opt/nodemcu-firmware marcelstoer/nodemcu-build build @@ -18,7 +18,8 @@ docker run -e "IMAGE_NAME=lfs" \ --rm -ti -v $FIRMWARE_PATH:/opt/nodemcu-firmware \ -v ${PWD}/src/lfs:/opt/lua marcelstoer/nodemcu-build lfs-image -mv src/lfs/LFS_float_lfs.img src/lfs/lfs.img +mv src/lfs/LFS_integer_lfs.img src/lfs/lfs.img +rm src/lfs/LFS_float_lfs.img # Build SPIFFS image docker run \ @@ -27,4 +28,4 @@ docker run \ -v ${PWD}/src:/opt/lua marcelstoer/nodemcu-build bash /scripts/build-spiffs cp ${FIRMWARE_PATH}/bin/nodemcu_integer_${IMAGE_NAME}.bin firmware/${IMAGE_NAME}.bin -cp ${FIRMWARE_PATH}/bin/konnected-filesystem-0x100000.img firmware/konnected-filesystem-0x100000-2-2-7.img +cp ${FIRMWARE_PATH}/bin/konnected-filesystem-0x100000.img firmware/konnected-filesystem-0x100000-2-3-0.img diff --git a/scripts/flash b/scripts/flash index 6ec46eb..0a1d31b 100755 --- a/scripts/flash +++ b/scripts/flash @@ -1,7 +1,7 @@ #!/bin/bash -FIRMWARE_NAME=konnected-firmware-2-2-7.bin -FILESYSTEM_NAME=konnected-filesystem-0x100000-2-2-7.img +FIRMWARE_NAME=konnected-firmware-2-3-0.bin +FILESYSTEM_NAME=konnected-filesystem-0x100000-2-3-0.img PORT=/dev/cu.wchusbserial1410 esptool.py --port=${PORT} write_flash --flash_mode dio 0x00000 firmware/${FIRMWARE_NAME} diff --git a/src/http_index.html.gz b/src/http_index.html.gz index 3f42f47..857f43c 100644 Binary files a/src/http_index.html.gz and b/src/http_index.html.gz differ diff --git a/src/lfs/application.lua b/src/lfs/application.lua index 537df36..5144491 100644 --- a/src/lfs/application.lua +++ b/src/lfs/application.lua @@ -4,14 +4,11 @@ local dht_sensors = require("dht_sensors") local ds18b20_sensors = require("ds18b20_sensors") local actuators = require("actuators") local settings = require("settings") -local sensorPut = {} -local actuatorGet = {} -local dni = wifi.sta.getmac():gsub("%:", "") -local timeout = tmr.create() local sensorTimer = tmr.create() -local sendTimer = tmr.create() -timeout:register(10000, tmr.ALARM_SEMI, node.restart) +-- globals +sensorPut = {} +actuatorGet = {} -- initialize binary sensors for i, sensor in pairs(sensors) do @@ -88,87 +85,19 @@ sensorTimer:alarm(200, tmr.ALARM_AUTO, function(t) end end) --- print HTTP status line -local printHttpResponse = function(code, data) - local a = { "Heap:", node.heap(), "HTTP Call:", code } - for k, v in pairs(data) do - table.insert(a, k) - table.insert(a, v) - end - print(unpack(a)) + +-- Support different communication methods for reporting to upstream platforms +local endpoint_type = settings.endpoint_type or 'rest' + +-- REST is the default communication method and is used by the original SmartThings, Hubitat, Home Assistant, +-- and OpenHab integrations. +if endpoint_type == 'rest' then + require("rest_endpoint")(settings) + +-- AWS IoT is used for the Konnected Cloud Connector or custom integrations build on AWS +elseif endpoint_type == 'aws_iot' then + require("aws_iot")(settings) end --- This loop makes the HTTP requests to the home automation service to get or update device state -sendTimer:alarm(200, tmr.ALARM_AUTO, function(t) - - -- gets state of actuators - if actuatorGet[1] then - t:stop() - local actuator = actuatorGet[1] - timeout:start() - - http.get(table.concat({ settings.apiUrl, "/device/", dni, '?pin=', actuator.pin }), - table.concat({ "Authorization: Bearer ", settings.token, "\r\nAccept: application/json\r\n" }), - function(code, response) - timeout:stop() - local pin, state, json_response, status - if response and code >= 200 and code < 300 then - status, json_response = pcall(function() return sjson.decode(response) end) - if status then - pin = tonumber(json_response.pin) - state = tonumber(json_response.state) - end - end - printHttpResponse(code, {pin = pin, state = state}) - - gpio.mode(actuator.pin, gpio.OUTPUT) - if pin == actuator.pin and code >= 200 and code < 300 and state then - gpio.write(actuator.pin, state) - else - state = actuator.trigger == gpio.LOW and gpio.HIGH or gpio.LOW - gpio.write(actuator.pin, state) - end - print("Heap:", node.heap(), "Initialized actuator Pin:", actuator.pin, "Trigger:", actuator.trigger, "Initial state:", state) - - table.remove(actuatorGet, 1) - blinktimer:start() - t:start() - end) - - -- update state of sensors when needed - elseif sensorPut[1] then - t:stop() - local sensor = sensorPut[1] - timeout:start() - http.put(table.concat({ settings.apiUrl, "/device/", dni }), - table.concat({ "Authorization: Bearer ", settings.token, "\r\nAccept: application/json\r\nContent-Type: application/json\r\n" }), - sjson.encode(sensor), - function(code) - timeout:stop() - printHttpResponse(code, sensor) - - -- check for success and retry if necessary - if code >= 200 and code < 300 then - table.remove(sensorPut, 1) - else - -- retry up to 10 times then reboot as a failsafe - local retry = sensor.retry or 0 - if retry == 10 then - print("Heap:", node.heap(), "Retried 10 times and failed. Rebooting in 30 seconds.") - for k,v in pairs(sensorPut) do sensorPut[k]=nil end -- remove all pending sensor updates - tmr.create():alarm(30000, tmr.ALARM_SINGLE, function() node.restart() end) -- reboot in 30 sec - else - sensor.retry = retry + 1 - sensorPut[1] = sensor - end - end - - blinktimer:start() - t:start() - end) - end - collectgarbage() -end) -print("Heap:", node.heap(), "Endpoint:", settings.apiUrl) \ No newline at end of file diff --git a/src/lfs/aws_iot.lua b/src/lfs/aws_iot.lua new file mode 100644 index 0000000..0a43eb7 --- /dev/null +++ b/src/lfs/aws_iot.lua @@ -0,0 +1,94 @@ +local module = ... + +local mqtt = require('mqtt_ws') +local device_id = wifi.sta.getmac():lower():gsub(':','') +local c = mqtt.Client() + +local function aws_sign_url(settings) + local aws_sig = require('aws_sig') + local url = aws_sig.createSignature( + settings.aws.access_key, settings.aws.secret_key, + settings.aws.region, 'iotdevicegateway', 'GET', settings.endpoint, '') + + aws_sig = nil + package.loaded.aws_sig = nil + return url +end + +local sendTimer = tmr.create() +local timeout = tmr.create() + +timeout:register(3000, tmr.ALARM_SEMI, function() + sensorPut[1].retry = (sensorPut[1].retry or 0) + 1 + sensorPut[1].message_id = nil + sendTimer:start() +end) + +sendTimer:register(200, tmr.ALARM_AUTO, function(t) + local sensor = sensorPut[1] + if sensor then + t:stop() + + if sensor.retry and sensor.retry > 0 then + print("Heap:", node.heap(), "Retry:", sensor.retry) + end + + if sensor.retry and sensor.retry > 10 then + print("Heap:", node.heap(), "Retried 10 times and failed. Rebooting in 30 seconds.") + for k, v in pairs(sensorPut) do sensorPut[k] = nil end -- remove all pending sensor updates + tmr.create():alarm(30000, tmr.ALARM_SINGLE, function() node.restart() end) -- reboot in 30 sec + else + local message_id = c.msg_id + local topic = "konnected/" .. device_id .. "/sensor/" .. sensor.pin + print("Heap:", node.heap(), "PUBLISH", "Message ID:", message_id, "Topic:", topic, "Payload:", sjson.encode(sensor)) + timeout:start() + c:publish(topic, sensor) + sensor.message_id = message_id + end + end +end) + +local function startLoop(settings) + print("Heap:", node.heap(), 'Connecting to AWS IoT Endpoint:', settings.endpoint) + + c:on('offline', function() + print("Heap:", node.heap(), "mqtt: offline") + sendTimer:stop() + c:connect(aws_sign_url(settings)) + end) + + c:connect(aws_sign_url(settings)) +end + +c:on('puback', function(_, message_id) + local sensor = sensorPut[1] + if sensor.message_id == message_id then + print("Heap:", node.heap(), 'PUBACK', 'Message ID:', message_id) + table.remove(sensorPut, 1) + blinktimer:start() + timeout:stop() + sendTimer:start() + end +end) + +c:on('message', function(_, topic, message) + print("Heap:", node.heap(), 'topic:', topic, 'msg:', message) + local payload = sjson.decode(message) + require("switch")(payload) +end) + +c:on('connect', function() + print("Heap:", node.heap(), "mqtt: connected") + for i, actuator in pairs(actuatorGet) do + local topic = "konnected/" .. device_id .. "/switch/" .. actuator.pin + print("Heap:", node.heap(), "Subscribing to topic:", topic) + c:subscribe(topic) + end + sendTimer:start() +end) + +return function(settings) + package.loaded[module] = nil + module = nil + return startLoop(settings) +end \ No newline at end of file diff --git a/src/lfs/aws_sig.lua b/src/lfs/aws_sig.lua new file mode 100644 index 0000000..f7d8a11 --- /dev/null +++ b/src/lfs/aws_sig.lua @@ -0,0 +1,56 @@ +local module = {} +local function sign(key, msg) + return crypto.hmac('SHA256', msg, key) +end + +local function getSignatureKey(key, dateStamp, regionName, serviceName) + local kDate = sign('AWS4' .. key, dateStamp) + local kRegion = sign(kDate, regionName) + local kService = sign(kRegion, serviceName) + return sign(kService, 'aws4_request') +end + +local function url_quote(text) + return string.gsub(text, "([^%w_%-~%.])", function(c) return string.format("%%%02X", string.byte(c)) end) +end + +local function createSignature(aws_access_key, aws_secret_key, region, service, method, url, payload) + local t = rtctime.epoch2cal(rtctime.get()) + local amz_date = string.format("%04d%02d%02dT%02d%02d%02dZ", t['year'], t['mon'], t['day'], t['hour'], t['min'], t['sec']) + local datestamp = amz_date:sub(1,8) + + local protocol, host, path + protocol, host, path = string.match(url, "(%w+)://([^/]+)(.*)") + path = path or '/' + + local canonical_headers = 'host:' .. host .. ':443\n' + local signed_headers = 'host' + + local credential_scope = datestamp .. '/' .. region .. '/' .. service .. '/' .. 'aws4_request' + + local canonical_querystring = ( + 'X-Amz-Algorithm=AWS4-HMAC-SHA256' .. + '&X-Amz-Credential=' .. url_quote(aws_access_key .. '/' .. credential_scope) .. + '&X-Amz-Date=' .. amz_date .. + '&X-Amz-Expires=86400' .. + '&X-Amz-SignedHeaders=' .. signed_headers + ) + local payload_hash = 'UNSIGNED-PAYLOAD' + if payload ~= nil then + payload_hash = crypto.toHex(crypto.hash('SHA256', payload)) + end + local canonical_request = ( + method .. '\n' .. path .. '\n' .. + canonical_querystring .. '\n' .. canonical_headers .. '\n' .. + signed_headers .. '\n' .. payload_hash + ) + -- print('canonical_request', canonical_request) + local string_to_sign = 'AWS4-HMAC-SHA256\n' .. amz_date .. '\n' .. credential_scope .. '\n' .. crypto.toHex(crypto.hash('SHA256', canonical_request)) + + local signing_key = getSignatureKey(aws_secret_key, datestamp, region, service) + local signature = crypto.toHex(crypto.hmac('SHA256', string_to_sign, signing_key)) + + return protocol .. '://' .. host .. path .. '?' .. canonical_querystring .. '&X-Amz-Signature=' .. signature +end +module.createSignature = createSignature +return module diff --git a/src/lfs/device.lua b/src/lfs/device.lua index 5c484b5..8ccd3c5 100644 --- a/src/lfs/device.lua +++ b/src/lfs/device.lua @@ -1,8 +1,8 @@ local me = { id = "uuid:8f655392-a778-4fee-97b9-4825918" .. string.format("%x", node.chipid()), name = "Konnected", - hwVersion = "2.2.7", - swVersion = "2.2.7", + hwVersion = "2.3.0", + swVersion = "2.3.0", http_port = math.floor(node.chipid()/1000) + 8000, urn = "urn:schemas-konnected-io:device:Security:1" } diff --git a/src/lfs/http_ota.lua b/src/lfs/http_ota.lua new file mode 100644 index 0000000..e5aad4c --- /dev/null +++ b/src/lfs/http_ota.lua @@ -0,0 +1,77 @@ +-- +-- If you have the LFS _init loaded then you invoke the provision by +-- executing LFS.HTTP_OTA('your server','directory','image name'). Note +-- that is unencrypted and unsigned. But the loader does validate that +-- the image file is a valid and complete LFS image before loading. +-- + +local host, dir, image = ... + +local doRequest, firstRec, subsRec, finalise +local n, total, size = 0, 0 + +doRequest = function(sk,hostIP) + if hostIP then + local con = net.createConnection(net.TCP,0) + con:connect(80,hostIP) + -- Note that the current dev version can only accept uncompressed LFS images + con:on("connection",function(sck) + local request = table.concat( { + "GET "..dir..image.." HTTP/1.1", + "User-Agent: ESP8266 app (linux-gnu)", + "Accept: application/octet-stream", + "Accept-Encoding: identity", + "Host: "..host, + "Connection: close", + "", "", }, "\r\n") + print(request) + sck:send(request) + sck:on("receive",firstRec) + end) + end +end + +firstRec = function (sck,rec) + -- Process the headers; only interested in content length + local i = rec:find('\r\n\r\n',1,true) or 1 + local header = rec:sub(1,i+1):lower() + size = tonumber(header:match('\ncontent%-length: *(%d+)\r') or 0) + print(rec:sub(1, i+1)) + if size > 0 then + sck:on("receive",subsRec) + file.open(image, 'w') + subsRec(sck, rec:sub(i+4)) + else + sck:on("receive", nil) + sck:close() + print("GET failed") + end +end + +subsRec = function(sck,rec) + total, n = total + #rec, n + 1 + if n % 4 == 1 then + sck:hold() + node.task.post(0, function() sck:unhold() end) + end + uart.write(0,('%u of %u, '):format(total, size)) + file.write(rec) + if total == size then finalise(sck) end +end + +finalise = function(sck) + file.close() + sck:on("receive", nil) + sck:close() + local s = file.stat(image) + if (s and size == s.size) then + wifi.setmode(wifi.NULLMODE, false) + collectgarbage();collectgarbage() + -- run as separate task to maximise RAM available + node.task.post(function() node.flashreload(image) end) + else + print"Invalid save of image file" + end +end + +net.dns.resolve(host, doRequest) \ No newline at end of file diff --git a/src/lfs/httpd_req.lua b/src/lfs/httpd_req.lua index c3ae888..39cb3f1 100644 --- a/src/lfs/httpd_req.lua +++ b/src/lfs/httpd_req.lua @@ -29,7 +29,13 @@ local function httpdRequest(data) httpdRequestHandler.body = string.sub(data, bodyPos + 4, #data) if httpdRequestHandler.contentType == "application/json" then - httpdRequestHandler.body = sjson.decode(httpdRequestHandler.body) + local status, body_obj = pcall(function() return sjson.decode(httpdRequestHandler.body) end) + if status then + httpdRequestHandler.body = body_obj + else + print("Heap:", node.heap(), "Discarding malformed JSON", httpdRequestHandler.body) + httpdRequestHandler.body = nil + end end end diff --git a/src/lfs/httpd_res.lua b/src/lfs/httpd_res.lua index 6c44401..26460e6 100644 --- a/src/lfs/httpd_res.lua +++ b/src/lfs/httpd_res.lua @@ -3,7 +3,10 @@ local module = ... local function respondWithText(sck, body, ty, st) local ty = ty or "application/json" local st = st or 200 - local sendContent = table.concat({'HTTP/1.1 ', st, '\r\nContent-Type: ', ty, '\r\nContent-Length: ', string.len(body), '\r\n\r\n', body}) + local sendContent = table.concat({ + 'HTTP/1.1 ', st, '\r\nContent-Type: ', ty, '\r\nContent-Length: ', string.len(body), + '\r\nAccess-Control-Allow-Origin: *\r\n\r\n', body + }) local function doSend(s) if sendContent == '' then s:close() @@ -28,7 +31,7 @@ local function respondWithFile(sck, filename, ty, st) return end - local header = {'HTTP/1.1 ', st, '\r\nContent-Type: ', ty, '\r\n'} + local header = {'HTTP/1.1 ', st, '\r\nContent-Type: ', ty, '\r\n', 'Access-Control-Allow-Origin: *\r\n'} if string.sub(filename, -3) == '.gz' then table.insert(header, 'Content-Encoding: gzip\r\n') end diff --git a/src/lfs/mqtt_packet.lua b/src/lfs/mqtt_packet.lua new file mode 100644 index 0000000..ac02c1a --- /dev/null +++ b/src/lfs/mqtt_packet.lua @@ -0,0 +1,71 @@ +local bit32 = bit32 or bit + +local function toHex(str) + return str:gsub("(.)", function(s) return string.format("%02x ", string.byte(s)) end) +end + +local function lengthStr(length) + local buf = "" + local digit = 0 + repeat + digit = bit32.band(length, 127) + length = bit32.rshift(length, 7) + if length > 0 then + digit = digit + 128 + end + buf = buf .. string.char(digit) + until length == 0 + return buf +end + +local function numberStr(n) + return string.char(bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) +end + +local function textStr(text) + return numberStr(text:len()) .. text +end + +local function subscribe(opts) + local topics_buf = "" + for topic, qos in pairs(opts.topics) do + topics_buf = topics_buf .. textStr(topic) .. string.char(qos) + end + local length = 2 + topics_buf:len() + return string.char(0x82) .. lengthStr(length) .. numberStr(opts.msg_id) .. topics_buf +end + +local function publish(opts) + local length = 2 + opts.topic:len() + opts.payload:len() + (opts.qos and 2 or 0) + return string.char(0x30 + (opts.qos and 2 or 0)) .. lengthStr(length) .. textStr(opts.topic) .. (opts.qos and numberStr(opts.msg_id) or "") .. opts.payload +end + +local function parse(buf) + local packet = {} + + local cmd = bit32.rshift(buf:byte(1), 4) + + packet.cmd = cmd + + if cmd == 3 then + local i = 2 + while buf:byte(i) >= 128 do + i = i + 1 + end + i = i + 1 + local topic_len = buf:byte(i) * 256 + buf:byte(i+1) + i = i + 2 + packet.topic = buf:sub(i, i + topic_len - 1) + packet.payload = buf:sub(i + topic_len) + elseif cmd == 4 then + packet.message_id = bit32.lshift(buf:byte(3),8) + buf:byte(4) + end + return packet +end + +return { + subscribe = subscribe, + publish = publish, + parse = parse, + toHex = toHex +} diff --git a/src/lfs/mqtt_ws.lua b/src/lfs/mqtt_ws.lua new file mode 100644 index 0000000..595babb --- /dev/null +++ b/src/lfs/mqtt_ws.lua @@ -0,0 +1,82 @@ +local mqtt_packet = require('mqtt_packet') + +local function emit(self, event, ...) + local cb = self.events[event] + if cb then + cb(self, ...) + end +end + +local function connect(self, url) + self.ws:connect(url) +end + +local function close(self) + self.ws:close() +end + +local function on(self, event, callback) + self.events[event] = callback +end + +local function subscribe(self, topic, qos) + if type(topic) ~= 'table' then + topic = {[topic] = qos or 0} + end + + self.ws:send(mqtt_packet.subscribe({msg_id=self.msg_id, topics=topic}), 2) + self.msg_id = self.msg_id + 1 +end + +local function publish(self, topic, message) + self.ws:send(mqtt_packet.publish({topic=topic, payload=sjson.encode(message), qos=1, msg_id=self.msg_id}), 2) + self.msg_id = self.msg_id + 1 +end + +local function Client() + local ws = websocket.createClient() + ws:config({headers={["sec-websocket-protocol"] = "mqtt"}}) + local client = { + connect = connect, + close = close, + subscribe = subscribe, + publish = publish, + on = on, + emit = emit, + events = {}, + ws = ws, + msg_id = 1, + } + + ws:on('receive', function(_, msg, opcode, x) +-- print("received", msg:len(), "msg:", msg, "bytes:", mqtt_packet.toHex(msg)) + local parsed = mqtt_packet.parse(msg) +-- for k, v in pairs(parsed) do +-- print('>', k, v) +-- end + + if parsed.cmd == 4 then + client:emit('puback', parsed.message_id) + elseif parsed.cmd == 3 then + client:emit('message', parsed.topic, parsed.payload) + elseif parsed.cmd == 2 then + client:emit('connect') + end + end) + + ws:on('close', function(_, status) + print("Heap:", node.heap(), 'websocket closed, status:', status) + client:emit('offline') + end) + + ws:on('connection', function(_) + print("Heap:", node.heap(), "websocket connected") + ws:send(string.char(0x10, 0x0c, 0x00, 0x04, 0x4d, 0x51, 0x54, 0x54, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00), 2) + end) + + return client +end + +return { + Client = Client +} diff --git a/src/lfs/ota.lua b/src/lfs/ota.lua new file mode 100644 index 0000000..d425df7 --- /dev/null +++ b/src/lfs/ota.lua @@ -0,0 +1,19 @@ +local module = ... + +local function process(request) + if request.method == "POST" and request.contentType == "application/json" then + local uri = request.body.uri + local proto, host, path, filename = string.match(uri, "(%w+)://([^/]+)(/[%w%p]+/)(.*)") + LFS.http_ota(host, path, filename) + return '{ "status":"ok", "host":"'.. host ..'", "path":"'.. path ..'", "filename":"'.. filename ..'" }' + else + return '{ "status":"bad request" }' + end +end + + +return function(request) + package.loaded[module] = nil + module = nil + return process(request) +end \ No newline at end of file diff --git a/src/lfs/rest_endpoint.lua b/src/lfs/rest_endpoint.lua new file mode 100644 index 0000000..98e8eb1 --- /dev/null +++ b/src/lfs/rest_endpoint.lua @@ -0,0 +1,101 @@ +local module = ... + +-- print HTTP status line +local function printHttpResponse(code, data) + local a = { "Heap:", node.heap(), "HTTP Call:", code } + for k, v in pairs(data) do + table.insert(a, k) + table.insert(a, v) + end + print(unpack(a)) +end + + +-- This loop makes the HTTP requests to the home automation service to get or update device state +local function startLoop(settings) + local dni = wifi.sta.getmac():gsub("%:", "") + + local timeout = tmr.create() + timeout:register(10000, tmr.ALARM_SEMI, node.restart) + + local sendTimer = tmr.create() + sendTimer:alarm(200, tmr.ALARM_AUTO, function(t) + + -- gets state of actuators + if actuatorGet[1] then + t:stop() + local actuator = actuatorGet[1] + timeout:start() + + http.get(table.concat({ settings.endpoint, "/device/", dni, '?pin=', actuator.pin }), + table.concat({ "Authorization: Bearer ", settings.token, "\r\nAccept: application/json\r\n" }), + function(code, response) + timeout:stop() + local pin, state, json_response, status + if response and code >= 200 and code < 300 then + status, json_response = pcall(function() return sjson.decode(response) end) + if status then + pin = tonumber(json_response.pin) + state = tonumber(json_response.state) + end + end + printHttpResponse(code, {pin = pin, state = state}) + + gpio.mode(actuator.pin, gpio.OUTPUT) + if pin == actuator.pin and code >= 200 and code < 300 and state then + gpio.write(actuator.pin, state) + else + state = actuator.trigger == gpio.LOW and gpio.HIGH or gpio.LOW + gpio.write(actuator.pin, state) + end + print("Heap:", node.heap(), "Initialized actuator Pin:", actuator.pin, "Trigger:", actuator.trigger, "Initial state:", state) + + table.remove(actuatorGet, 1) + blinktimer:start() + t:start() + end) + + -- update state of sensors when needed + elseif sensorPut[1] then + t:stop() + local sensor = sensorPut[1] + printHttpResponse(0, sensor) + timeout:start() + http.put(table.concat({ settings.endpoint, "/device/", dni }), + table.concat({ "Authorization: Bearer ", settings.token, "\r\nAccept: application/json\r\nContent-Type: application/json\r\n" }), + sjson.encode(sensor), + function(code) + timeout:stop() + printHttpResponse(code, sensor) + + -- check for success and retry if necessary + if code >= 200 and code < 300 then + table.remove(sensorPut, 1) + else + -- retry up to 10 times then reboot as a failsafe + local retry = sensor.retry or 0 + if retry == 10 then + print("Heap:", node.heap(), "Retried 10 times and failed. Rebooting in 30 seconds.") + for k, v in pairs(sensorPut) do sensorPut[k] = nil end -- remove all pending sensor updates + tmr.create():alarm(30000, tmr.ALARM_SINGLE, function() node.restart() end) -- reboot in 30 sec + else + sensor.retry = retry + 1 + sensorPut[1] = sensor + end + end + + blinktimer:start() + t:start() + end) + end + + collectgarbage() + end) + print("Heap:", node.heap(), "REST Endpoint:", settings.endpoint) +end + +return function(settings) + package.loaded[module] = nil + module = nil + return startLoop(settings) +end \ No newline at end of file diff --git a/src/lfs/server_device.lua b/src/lfs/server_device.lua index e856427..6b8aa43 100644 --- a/src/lfs/server_device.lua +++ b/src/lfs/server_device.lua @@ -1,31 +1,5 @@ local module = ... - -infinateLoops = {} - -local function turnOffIn(pin, on_state, delay, times, pause) - local off = on_state == 0 and 1 or 0 - times = times or -1 - - if (times == -1) then - infinateLoops[pin] = true - end - - print("Heap:", node.heap(), "Actuator Pin:", pin, "Momentary:", delay, "Repeat:", times, "Pause:", pause) - - tmr.create():alarm(delay, tmr.ALARM_SINGLE, function() - print("Heap:", node.heap(), "Actuator Pin:", pin, "State:", off) - gpio.write(pin, off) - times = times - 1 - - if (times > 0 or infinateLoops[pin]) and pause then - tmr.create():alarm(pause, tmr.ALARM_SINGLE, function() - print("Heap:", node.heap(), "Actuator Pin:", pin, "State:", on_state) - gpio.write(pin, on_state) - turnOffIn(pin, on_state, delay, times, pause) - end) - end - end) -end +local switch = require("switch") local function process(request) if request.method == "GET" then @@ -49,36 +23,14 @@ local function process(request) if request.contentType == "application/json" then if request.method == "PUT" then - local function updatePin(payload) - local pin = tonumber(payload.pin) - local state = tonumber(payload.state) - local times = tonumber(payload.times) - print("Heap:", node.heap(), "Actuator Pin:", pin, "State:", state) - - if infinateLoops[pin] then - infinateLoops[pin] = false - end - - gpio.write(pin, state) - - blinktimer:start() - if payload.momentary then - turnOffIn(pin, state, payload.momentary, times, payload.pause) - if (times == -1) then state = -1 end -- this indicates an infinate repeat - return { pin = pin, state = state } - else - return { pin = pin, state = state } - end - end - if request.body[1] then local ret = {} for i in pairs(request.body) do - table.insert(ret, updatePin(request.body[i])) + table.insert(ret, switch(request.body[i])) end return sjson.encode(ret) else - return sjson.encode(updatePin(request.body)) + return sjson.encode(switch(request.body)) end end end diff --git a/src/lfs/server_receiver.lua b/src/lfs/server_receiver.lua index 82d59d1..3251e08 100644 --- a/src/lfs/server_receiver.lua +++ b/src/lfs/server_receiver.lua @@ -49,6 +49,11 @@ local function httpReceiver(sck, payload) response.text(sck, require("server_status")()) end + if request.path == "/ota" then + print("Heap: ", node.heap(), "HTTP: ", "OTA Update") + response.text(sck, require("ota")(request)) + end + sck, request, response = nil collectgarbage() end diff --git a/src/lfs/server_settings.lua b/src/lfs/server_settings.lua index e8ea9f0..f44bf98 100644 --- a/src/lfs/server_settings.lua +++ b/src/lfs/server_settings.lua @@ -21,9 +21,11 @@ local function process(request) local setVar = require("variables_set") setVar("settings", require("variables_build")({ token = request.body.token, - apiUrl = request.body.apiUrl, + endpoint = (request.body.endpoint or request.body.apiUrl), + endpoint_type = request.body.endpoint_type, blink = request.body.blink, - discovery = request.body.discovery + discovery = request.body.discovery, + aws = request.body.aws })) setVar("sensors", require("variables_build")(request.body.sensors)) setVar("actuators", require("variables_build")(request.body.actuators)) diff --git a/src/lfs/switch.lua b/src/lfs/switch.lua new file mode 100644 index 0000000..c05e773 --- /dev/null +++ b/src/lfs/switch.lua @@ -0,0 +1,57 @@ +local module = ... + +infiniteLoops = {} + +local function turnOffIn(pin, on_state, delay, times, pause) + local off = on_state == 0 and 1 or 0 + times = times or -1 + + if (times == -1) then + infiniteLoops[pin] = true + end + + print("Heap:", node.heap(), "Actuator Pin:", pin, "Momentary:", delay, "Repeat:", times, "Pause:", pause) + + tmr.create():alarm(delay, tmr.ALARM_SINGLE, function() + print("Heap:", node.heap(), "Actuator Pin:", pin, "State:", off) + gpio.write(pin, off) + times = times - 1 + + if (times > 0 or infiniteLoops[pin]) and pause then + tmr.create():alarm(pause, tmr.ALARM_SINGLE, function() + print("Heap:", node.heap(), "Actuator Pin:", pin, "State:", on_state) + gpio.write(pin, on_state) + turnOffIn(pin, on_state, delay, times, pause) + end) + end + end) +end + +local function updatePin(payload) + local pin = tonumber(payload.pin) + local state = tonumber(payload.state) + local times = tonumber(payload.times) + print("Heap:", node.heap(), "Actuator Pin:", pin, "State:", state) + + if infiniteLoops[pin] then + infiniteLoops[pin] = false + end + + gpio.write(pin, state) + + blinktimer:start() + if payload.momentary then + turnOffIn(pin, state, payload.momentary, times, payload.pause) + if (times == -1) then state = -1 end -- this indicates an infinite repeat + return { pin = pin, state = state } + else + return { pin = pin, state = state } + end +end + + +return function(payload) + package.loaded[module] = nil + module = nil + return updatePin(payload) +end \ No newline at end of file diff --git a/src/lfs/variables_build.lua b/src/lfs/variables_build.lua index 2f104b0..d0bc3bd 100644 --- a/src/lfs/variables_build.lua +++ b/src/lfs/variables_build.lua @@ -5,6 +5,9 @@ local function build_list(objects) local out = {} for key, value in pairs(objects) do if type(value) == 'table' then + if type(key) == 'string' then + table.insert(out, key .. "=") + end table.insert(out, "{") table.insert(out, build_list(value)) table.insert(out, "},") diff --git a/src/lfs/wifi.lua b/src/lfs/wifi.lua index f47418f..e06c159 100644 --- a/src/lfs/wifi.lua +++ b/src/lfs/wifi.lua @@ -47,14 +47,26 @@ local _ = tmr.create():alarm(900, tmr.ALARM_AUTO, function(t) end failsafeTimer:unregister() failsafeTimer = nil - print("Heap: ", node.heap(), "Wifi connected with IP: ", wifi.sta.getip()) + local ip, nm, gw = wifi.sta.getip() + print("Heap: ", node.heap(), "Wifi connected with IP: ", ip, "Gateway:", gw) gpio.write(4, gpio.HIGH) enduser_setup.stop() - require("server") - print("Heap: ", node.heap(), "Loaded: ", "server") - require("application") - print("Heap: ", node.heap(), "Loaded: ", "application") + + sntp.sync({gw, 'time.google.com', 'pool.ntp.org'}, + function(sec) + tm = rtctime.epoch2cal(sec) + print("Heap: ", node.heap(), "Current Time set:", + string.format("%04d-%02d-%02d %02d:%02d:%02d UTC", + tm["year"], tm["mon"], tm["day"], tm["hour"], tm["min"], tm["sec"])) + require("server") + print("Heap: ", node.heap(), "Loaded: ", "server") + require("application") + print("Heap: ", node.heap(), "Loaded: ", "application") + end, + function() + print("Heap: ", node.heap(), "Time sync failed!") + end) end end)