Created
March 13, 2017 22:17
-
-
Save anonymous/3f9294813fe3a0feff49e16d1377de21 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ################################### | |
| -- Configuration and UI | |
| -- ################################### | |
| local Config, WifiConfig = dofile("Config.lua") | |
| dofile("TelnetSrv.lua") | |
| -- ################################### | |
| -- System variables | |
| -- ################################### | |
| Tokens = nil | |
| BillingToken = nil | |
| BillingLatch = false | |
| OperatorToken = nil | |
| TokenAuthenticated = false | |
| SpindleRunning = false | |
| TemperatureOkay = false | |
| FeedHold = true | |
| EmergencyStop = true | |
| Temperatures = {} | |
| local MQTT, TokenTimer, TickTimer | |
| local CNC_ESTOP, CNC_FEEDHOLD, CNC_SPINDLE, CNC_BILLTOKEN = "CNC/EmergencyStop", "CNC/FeedHold", "CNC/Spindle", "CNC/BillingToken" | |
| local CNC_OPERTOKEN, CNC_TEMPOKAY, CNC_AUTHED, CNC_BILL = "CNC/OperatorToken", "CNC/TemperatureOkay", "CNC/Authenticated", "CNC/BillToken" | |
| local CNC_TRUE, CNC_FALSE = "true", "false" | |
| -- ################################### | |
| -- Event Handlers | |
| -- ################################### | |
| function ReadSerial(a) | |
| a = a:sub(2, 13) | |
| ValidateTokenCode(a, HandleToken) | |
| end | |
| local function Tick() | |
| -- Bill the user if the spindle is running, or -has- been running since they auth'd | |
| if BillingToken and (SpindleRunning or BillingLatch) then | |
| Tokens[BillingToken] = Tokens[BillingToken] - 1 | |
| if Tokens[BillingToken] % 60 == 0 then | |
| Notify(CNC_BILL, BillingToken) --TODO this should probably be done over HTTP(s) or something instead | |
| end | |
| BillingLatch = true | |
| end | |
| if TokenAuthenticated and OperatorToken and TemperatureOkay then | |
| -- If everything is okay to go, then allow the machine to run | |
| gpio.write(4, gpio.LOW) | |
| gpio.write(5, gpio.LOW) | |
| FeedHold = false | |
| EmergencyStop = false | |
| Notify(CNC_FEEDHOLD, CNC_FALSE) | |
| Notify(CNC_ESTOP, CNC_FALSE) | |
| else | |
| if SpindleRunning and TemperatureOkay then | |
| -- If the token was removed, but the spindle is still going (and we aren't overheating) | |
| if not FeedHold then | |
| Notify(CNC_FEEDHOLD, CNC_TRUE) -- Notify MQTT | |
| end | |
| gpio.write(5, gpio.HIGH) -- Engage Feed-hold | |
| FeedHold = true | |
| else | |
| -- If the spindle is stopped, OR if the temperature went too high, | |
| -- immediately engage e-stop. | |
| if not EmergencyStop then | |
| Notify(CNC_ESTOP, CNC_TRUE) -- Notify MQTT | |
| end | |
| gpio.write(4, gpio.HIGH) -- Engage E-Stop | |
| EmergencyStop = true | |
| end | |
| end | |
| CheckTemperature() | |
| end | |
| -- When the spindle starts/stops (dry contact from VFD), this is called | |
| local function SpindleStateChanged(State, when) | |
| SpindleRunning = not State | |
| Notify(CNC_SPINDLE, tostring(State)) | |
| end | |
| -- If no token has been read for 30 seconds, this is called | |
| local function TokenTimeout() | |
| -- If the spindle is running, clear the operator token (this will engage feed-hold in Tick()) | |
| if SpindleRunning then | |
| OperatorToken = nil | |
| Notify(CNC_OPERTOKEN, "") | |
| else | |
| -- If the spindle is not running, lock out the machine | |
| DeAuthenticate() | |
| end | |
| end | |
| -- ################################### | |
| -- Token Management | |
| -- ################################### | |
| local function AuthorizedOperator(Token) | |
| Tokens[Token:lower()] > 0 | |
| end | |
| local function HandleToken(Token) | |
| -- If we're already "authenticated", then see if we have an operator change | |
| if TokenAuthenticated and AuthorizedOperator(Token)then | |
| -- First, reset the "token timeout" for feed-hold / e-stop | |
| TokenTimer:stop() | |
| TokenTimer:start() | |
| -- Now see if the operator has changed | |
| Token = Token:lower() | |
| if Token ~= OperatorToken then | |
| OperatorToken = Token | |
| Notify(CNC_OPERTOKEN, OperatorToken) | |
| end | |
| -- If we're not authenticated (that is, we're locked out), then see if we can unlock the machine for the user | |
| else | |
| Authenticate(Token) | |
| end | |
| end | |
| -- Authenticate a user and mark them as the "billed" user | |
| local function Authenticate(TokenCode) | |
| if AuthorizedOperator(TokenCode) then | |
| BillingToken = TokenCode:lower() | |
| OperatorToken = BillingToken | |
| Notify(CNC_AUTHED, CNC_TRUE) | |
| Notify(CNC_BILLTOKEN, TokenCode) | |
| Notify(CNC_OPERTOKEN, OperatorToken) | |
| TokenTimer:start() | |
| TokenAuthenticated = true | |
| end | |
| end | |
| -- Deauthenticate users and flag for machine shutdown | |
| function DeAuthenticate() | |
| BillingToken = nil | |
| BillingLatch = false | |
| TokenAuthenticated = false | |
| Notify(CNC_AUTHED, CNC_FALSE) | |
| Notify(CNC_BILLTOKEN, "") | |
| Notify(CNC_OPERTOKEN, "") | |
| end | |
| -- Load token data from CSV file in SPIFFS | |
| function LoadCSVData(Filename) | |
| local CSVFile = file.open(Filename, "r") | |
| local Line = CSVFile:readline() | |
| Tokens = {} | |
| repeat | |
| local Split = Line:find(",") | |
| if Split ~= nil then | |
| Tokens[Line:sub(1, Split - 1):lower()] = tonumber(Line:sub(Split + 1)) | |
| Line = CSVFile:readline() | |
| end | |
| until Line == nil | |
| CSVFile:close() | |
| CSVFile = nil | |
| end | |
| function ValidateTokenCode(Input, ValidCallback) | |
| local Data = {} | |
| local i = 0 | |
| if #Input ~= 12 then | |
| return false | |
| end | |
| for i = 1, 9, 2 do | |
| table.insert(Data, tonumber(Input:sub(i, i + 1), 16)) | |
| end | |
| local function Reverse(In) | |
| local Out = 0 | |
| local I | |
| for I = 0, 7 do | |
| if bit.isset(In, I) then | |
| Out = bit.set(Out, 7 - I) | |
| end | |
| end | |
| return Out | |
| end | |
| -- Now checksum | |
| local Checksum = tonumber(Input:sub(11), 16) | |
| Checksum = bit.bxor(Checksum, Data[1], Data[2], Data[3], Data[4], Data[5]) | |
| if Checksum ~= 0 then | |
| return false | |
| end | |
| -- Now convert | |
| for i = 1, 5 do | |
| Data[i] = string.format("%02x", Reverse(Data[i])) | |
| end | |
| ValidCallback(table.concat(Data, "")) | |
| return true | |
| end | |
| -- ################################### | |
| -- File Management | |
| -- ################################### | |
| local function UpdateTokens() | |
| --Download(Config.File, Config.File, Config.Server, LoadCSVData) | |
| end | |
| function SaveData(Filename) | |
| local CSVFile = file.open(Filename, "w") | |
| for Token,Time in pairs(Tokens) do | |
| CSVFile:write(Token .. "," .. tostring(Time) .. "\r\n") | |
| end | |
| CSVFile:close() | |
| end | |
| function Download(Filename, ToFile, Host, OnComplete, OnFail) | |
| local Buffer, DLFile, conn, ParsingHeaders = nil, nil, net.createConnection(net.TCP, 0), true | |
| local function StripHeaders(Data) | |
| Buffer = Buffer .. Data | |
| local T = Buffer:find("\r\n") | |
| while T ~= nil do | |
| Buffer = Buffer:sub(T + 2) | |
| T = Buffer:find("\r\n") | |
| if T == 1 then | |
| ParsingHeaders = false | |
| DLFile:write(Buffer:sub(3)) | |
| Buffer = nil | |
| return | |
| end | |
| end | |
| return nil | |
| end | |
| conn:on("receive", function(socket, Data) | |
| if ParsingHeaders then | |
| StripHeaders(Data) | |
| else | |
| DLFile:write(Data) | |
| end | |
| end) | |
| conn:on("connection", function(socket, data) | |
| ParsingHeaders = true | |
| Buffer = "" | |
| DLFile = file.open(ToFile, "w") | |
| conn:send("GET /".. Filename .. " HTTP/1.1\r\nhost: " .. Host .. "\r\nconnection: close\r\nUser-Agent: NodeMCU Lua (CNC Router)\r\n\r\n") | |
| end) | |
| conn:on("disconnection", function(socket, data) | |
| if DLFile ~= nil then | |
| DLFile:close() | |
| DLFile = nil | |
| if OnComplete then | |
| OnComplete() | |
| end | |
| elseif OnFail then | |
| OnFail() | |
| end | |
| conn = nil | |
| Buffer = nil | |
| end) | |
| conn:connect(80, Host) | |
| end | |
| -- ################################### | |
| -- Temperature Checking | |
| -- ################################### | |
| local function CheckTemperature() | |
| table.remove(Temperatures, Config.TempHist) | |
| table.insert(Temperatures, 1, adc.read(0)) | |
| local Total = 0 | |
| local Count = 0 | |
| for i = 1, #Temperatures do | |
| if Temperatures[i] > Config.ReadLowFilter and Temperatures[i] < Config.ReadHighFilter then -- Filter out invalid readings | |
| Total = Total + Temperatures[i] | |
| Count = Count + 1 | |
| end | |
| end | |
| if Count > 0 then | |
| Total = Total / Count | |
| local T = Total < Config.TempThreadhold | |
| if TemperatureOkay ~= T then | |
| Notify(CNC_TEMPOKAY, tostring(T)) | |
| end | |
| TemperatureOkay = T | |
| else | |
| if TemperatureOkay then | |
| Notify(CNC_TEMPOKAY, CNC_FALSE) | |
| end | |
| TemperatureOkay = false | |
| end | |
| end | |
| -- ################################### | |
| -- MQTT Functions | |
| -- ################################### | |
| local function Notify(Topic, Message) | |
| MQTT:publish(Topic, Message, 1, 1) | |
| end | |
| -- ################################### | |
| -- Startup and Initialization | |
| -- ################################### | |
| print("") | |
| print("Initializing CNC router access control") | |
| print("") | |
| print("ADC Init...") | |
| if adc.force_init_mode(adc.INIT_ADC) then | |
| print("Restarting to complete ADC Init") | |
| node.restart() | |
| end | |
| print("WiFi Init...") | |
| wifi.setmode(wifi.STATION) | |
| wifi.sta.config(WifiConfig) | |
| print("GPIO Init...") | |
| gpio.mode(4, gpio.OUTPUT) | |
| gpio.write(4, gpio.HIGH) | |
| gpio.mode(5, gpio.OUTPUT) | |
| gpio.write(5, gpio.HIGH) | |
| gpio.mode(1, gpio.INT, gpio.PULLUP) | |
| gpio.trig(1, "both", SpindleStateChanged) | |
| print("Timers Init...") | |
| TokenTimer = tmr.create() | |
| TokenTimer:register(Config.Timeout, tmr.ALARM_SEMI, TokenTimeout) | |
| TickTimer = tmr.create() | |
| TickTimer:register(1000, tmr.ALARM_AUTO, Tick) | |
| TickTimer:start() | |
| wifi.sta.eventMonReg(wifi.STA_GOTIP, function() | |
| print("SNTP Sync...") | |
| sntp.sync("pool.ntp.org") | |
| print("Init telnet server...") | |
| startTelnet() | |
| print("Cache token data...") | |
| UpdateTokens() | |
| print("Load token data...") | |
| LoadCSVData() | |
| print("Init MQTT...") | |
| MQTT = mqtt.Client(Config.MQTTId, 120) | |
| MQTT:lwt("CNC/Status", "Offline") | |
| MQTT:on("connect", function(c) Notify("CNC/Status", "Online") end) | |
| MQTT:connect(Config.MQTTServer, Config.MQTTPort, 0) | |
| print("Schedule automatic tasks...") | |
| cron.schedule("0 0 * * *", UpdateTokens) | |
| cron.schedule("0 */2 * * *", SaveData) | |
| wifi.sta.eventMonStop() | |
| print("UART Init - 9600 Baud...") | |
| print("") | |
| print("***APPLICATION STARTED***") | |
| print("") | |
| print("") | |
| uart.on("data", '\003', ReadSerial, 0) | |
| uart.setup(0, 9600, 8, uart.PARITY_NONE, uart.STOPBITS_1, 0) | |
| end) | |
| print("Waiting for WiFi start...") | |
| wifi.sta.eventMonStart() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment