Wednesday, September 6, 2017

Auto Starting Cloud9 on a Raspberry Pi (Linux)

This is an /etc/init.d/ script you can use on your Raspberry Pi, or other Linux machine, to auto start cloud9 at boot time.

#!/bin/bash

### BEGIN INIT INFO
# Provides:          cloud9
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Simple script to start cloud9 at boot
# Description:       A simple script which will start / stop cloud9 at boot / shutdown.
### END INIT INFO

# If you want a command to always run, put it here

# Carry out specific functions when asked to by the system
case "$1" in
  start)
    # echo "Starting noip"
    # run application you want to start
    cd /home/pi/c9sdk
    sudo -u pi ./server.js -s standalone-homedir -l 0.0.0.0 -a : &
    echo "Launching cloud9 with workspace root set to /home/pi"
    ;;
  stop)
    echo "Stopping cloud9"
    # kill application you want to stop
    pkill -f "node ./server.js"
    ;;
  *)
    echo "Usage: /etc/init.d/cloud9 {start|stop}"
    exit 1
    ;;
esac

exit 0

Tuesday, January 3, 2017

Latest Lua Code for Laser Cayenn Device

Please see new Github repo for latest code, as the code below is now outdated, but the description of how this project works is still valid.
https://github.com/chilipeppr/cayenn-laseruv

Here is the latest working code for a ChiliPeppr Cayenn device. The code consists of a main entry point called main_laseruv.lua. That file then loads all the supporting Lua files to create a complete Cayenn device.

The code lets you setup a NodeMCU to talk back to ChiliPeppr via the Cayenn protocol. The code does a few things:

  • Announces the existence of the device to your network so ChiliPeppr can see it
  • Lets ChiliPeppr send commands to the device to control it
  • Lets ChiliPeppr upload a set of commands with ID's from your pre-processed Gcode
  • Sync your NodeMCU to your main CNC controller via the coolant on/off pin so as your Gcode executes, the NodeMCU will stay in sync and execute any relevant commands that were pre-uploaded at the exact time
  • Send data back to ChiliPeppr during execution
The image below has 3 Cayenn devices in it, but this code is for the Laser 3A device.



main_laseruv.lua

(Please see Github project for latest version of this code, as code below is no longer current.)
https://github.com/chilipeppr/cayenn-laseruv

-- Main entry point for Laser UV Cayenn device

-- set freq to highest possible
node.setcpufreq(node.CPU160MHZ)

cayenn = require('cayenn_v3')
laser = require('laser_3amp_driver_v1')
cnc = require('tinyg_read_v3')
queue = require("queue")
led = require("led")

opts = {}
opts.Name = "LaserUV"
opts.Desc = "Control the BDR209 UV laser"
opts.Icon = "https://raw.githubusercontent.com/chilipeppr/widget-cayenn/master/laser.png"
opts.Widget = "com-chilipeppr-widget-laser"
-- opts.WidgetUrl = "https://github.com/chilipeppr/widget-laser/auto-generated.html"

-- define commands supported
cmds = {
  "ResetCtr", "GetCtr", "GetCmds", "GetQ", "WipeQ", "CmdQ", "Mem",
  "LaserBoot", "LaserShutdown",
  'PwmOn {Hz,Duty} (Max Hz:1000, Max Duty:1023)', "PwmOff",
  'PulseFor {ms}',
  'MaxDuty {Duty}'
}

-- this is called by the cnc library when the coolant pin changes
function onCncCounter(counter)
  print("Got CNC pin change. counter:" .. counter)
  local cmd = queue.getId(counter)
  onCmd(cmd)
end

-- this is called by Cayenn when an incoming cmd comes in
-- from the network, i.e. from SPJS. These are TCP commands so
-- they are guaranteed to come in (vs UDP which could drop)
function onCmd(payload)

  if (type(payload) == "table") then
      -- print("is json")
      -- print("Got incoming Cayenn cmd JSON: " .. cjson.encode(payload))
   
      -- See what cmd
      if payload.Cmd == "LaserBoot" then
        laser.relayOn()
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd})
        led.blink(4)
        -- print("Turned on relay to laser driver. Takes 3 secs to boot.")
      elseif payload.Cmd == "LaserShutdown" then
        laser.relayOff()
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd})
        led.blink(2)
        -- print("Turned off relay to laser driver")
      elseif payload.Cmd == "MaxDuty" then
        -- force a max duty to control laser power
        laser.pwmSetMaxDuty(payload.Duty)
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["MaxDuty"] = payload.Duty})
        led.blink(2)
      elseif payload.Cmd == "PulseFor" then
        -- should have been given milliseconds to pulse for
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd})
        led.blink(1)
        -- run pulse after sending resp so no cpu is being used
        -- so we get precise timing
        laser.pulseFor(payload.ms)
        -- print("Turned off relay to laser driver")
      elseif payload.Cmd == "PwmOn" then
        -- should have been given milliseconds to pulse for
     
        led.blink(1)
        if payload.Hz == nil or payload.Duty == nil then
          cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Err"] = "Hz or Duty not specified"})
        elseif 'number' ~= type(payload.Hz) or 'number' ~= type(payload.Duty) then
          cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Err"] = "Hz or Duty not a number"})
        elseif payload.Hz > 1000 then
          cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Err"] = "Hz " .. payload.Hz .. " too high", ["Hz"] = payload.Hz})
        elseif payload.Hz <= 0 then
          cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Err"] = "Hz " .. payload.Hz .. " too low", ["Hz"] = payload.Hz})
        elseif payload.Duty > 1023 then
          cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Err"] = "Duty " .. payload.Duty .. " too high"})
        else
          local actualDuty = laser.pwmOn(payload.Hz, payload.Duty)
          cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = "PwmOn", ["Hz"] = payload.Hz, ["Duty"] = actualDuty})
       
        end
      elseif payload.Cmd == "PwmOff" then
        -- should have been given milliseconds to pulse for
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = "PwmOff"})
        led.blink(1)
        laser.pwmOff()
      elseif payload.Cmd == "ResetCtr" then
        cnc.resetIdCounter()
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Ctr"] = cnc.getIdCounter()})
        led.blink(1)
      elseif payload.Cmd == "GetCtr" then
        -- cnc.resetIdCounter()
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Ctr"] = cnc.getIdCounter()})
        led.blink(1)
      elseif payload.Cmd == "GetCmds" then
        local resp = {}
        resp.Resp = "GetCmds"
        resp.Cmds = cmds
        resp.TransId = payload.TransId
        -- resp.CmdsMeta = cmdsMeta
        cayenn.sendBroadcast(resp)
        led.blink(2)
      elseif payload.Cmd == "GetQ" then
        -- this method will send slowly as not to overwhelm
        queue.send(cayenn.sendBroadcast, payload.TransId)
        -- print("Sending queue back to network")
        -- cayenn.sendBroadcast({["Resp"] = payload.Cmd, ["Start"] = 0})
        -- local resp = {}
        -- local count = 0
        -- resp.Resp = "GetQ"
        -- -- loop and send multiple packets so we don't run out of mem
        -- cmd = queue.getId(count)
        -- while cmd ~= nil do
          -- cayenn.sendBroadcast({["Resp"] = payload.Cmd, ["Cmd"] = cmd.Cmd, ["Id"] = cmd.Id})
          -- print(cmd)
        --   resp.Queue = cmd
        --   cayenn.sendBroadcast(resp)
          -- count = count + 1
          -- cmd = queue.getId(count)
          -- led.blink(1)
        -- end
        -- cayenn.sendBroadcast({["Resp"] = payload.Cmd, ["Q"] = queue.getTxt()})
        led.blink(2)
      elseif payload.Cmd == "WipeQ" then
        -- queue = {}
        -- print("Wiped queue: " .. cjson.encode(queue))
        queue.wipe()
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd})
        led.blink(1)
      elseif payload.Cmd == "CmdQ" then
        -- queuing cmd. we must have ID.
        if payload.Id == nil then
          -- print("Error queuing command. It must have an ID")
          return
        end
        if payload.RunCmd == nil then
          -- print("Error queuing command. It must have a RunCmd like RunCmd:{Cmd:AugerOn,Speed:10}.")
          return
        end
        -- wipe the peerIp cuz don't need it
        payload.peerIp = nil
        -- print("Queing command")
        --queue[payload.Id] = payload.RunCmd
        payload.RunCmd.Id = payload.Id
        queue.add(payload.RunCmd)
        -- print("New queue: " .. cjson.encode(queue))
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Id"] = payload.Id, ["MemRemain"] = node.heap()})
        led.blink(1)
      elseif payload.Cmd == "Mem" then
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["MemRemain"] = node.heap()})
        led.blink(2)
      elseif payload["Announce"] ~= nil then
        -- do nothing.
        if payload.Announce == "i-am-your-server" then
          -- perhaps store this ip in future
          -- so we know what our server is
        end
      else
        cayenn.sendBroadcast({["TransId"] = payload.TransId, ["Resp"] = payload.Cmd, ["Err"] = "Unsupported cmd"})
        -- print("Got cmd we do not understand. Huh?")
        led.blink(1)
      end
  else
      -- print("is string")
      -- print("Got incoming Cayenn cmd. str: ", payload)
  end
end

-- this callback is called when an incoming UDP broadcast
-- comes in to this device. typically this is just for
-- Cayenn Discover requests to figure out what devices are on
-- the network
function onIncomingBroadcast(cmd)
  -- print("Got incoming UDP cmd: ", cmd)
  if (type(cmd) == "table") then
    if cmd["Cayenn"] ~= nil then
      if cmd.Cayenn == "Discover" then
        -- somebody is asking me to announce myself
        cayenn.sendAnnounceBroadcast()
      else
        -- print("Do not understand incoming Cayenn cmd")
      end
    elseif cmd["Announce"] ~= nil then
      if cmd.Announce == "i-am-your-server" then
        -- we should store the server address so we can send
        -- back TCP
        -- print("Got a server announcement. Cool. Store it. TODO")
      else
        -- print("Got announce but not from a server. Huh?")
      end
    else
      -- print("Do not understand incoming UDP cmd")
    end
 
  else
    -- print("Got incoming UDP as string")
  end
end

-- add listener to incoming cayenn commands
cayenn.addListenerOnIncomingCmd(onCmd)
cayenn.addListenerOnIncomingUdpCmd(onIncomingBroadcast)

cayenn.init(opts)
laser.init()
-- laser.pwmSetMaxDuty(100)

-- listen to coolant pin changes
cnc.addListenerOnIdChange(onCncCounter)
cnc.init()

led.blink(6)
print("Mem left:" .. node.heap())

cayenn_v3.lua


-- Cayenn Protocol for ChiliPeppr
-- This module does udp/tcp sending/receiving to talk with SPJS
-- or have the browser talk direct to this ESP8266 device.
-- This module has methods for connecting to wifi, then initting
-- UDP servers, TCP servers, and sending a broadcast announce out.
-- The broadcast announce let's any listening SPJS know we're alive.
-- SPJS then reflects that message to any listening browsers like
-- ChiliPeppr so they know a device is available on the network.
-- Then the browser can send commands back to this device.
local M = {}
-- M = {}

M.port = 8988
M.myip = nil
M.sock = nil
M.jsonTagTable = nil

--M.announce =

M.isInitted = false

-- When you are initting you can pass in tags to describe your device
-- You should use a format like this:
-- cayenn.init({
--  widget: "com-chilipeppr-widget-ina219",
--  icon: "http://chilipeppr.com/img/ina219.jpg",
--  widgetUrl: "https://github.com/chilipeppr/widget-ina219/auto-generated.html",
-- })
function M.init(jsonTagTable)

  -- save the jsonTagTable
  if jsonTagTable ~= nil then
    M.jsonTagTable = jsonTagTable
  end

  if M.isInitted then
    print("Already initted")
    return
  end

  print("Init...")

  -- figure out if i have an IP
  M.myip = wifi.sta.getip()
  if M.myip == nil then
    print("Connecting to wifi.")
    M.setupWifi()
  else
    print("My IP: " .. M.myip)
    M.isInitted = true

    -- create socket for outbound UDP sending
    M.sock = net.createConnection(net.UDP, 0)

    -- create server to listen to incoming udp
    M.initUdpServer()
 
    -- create server to listen to incoming tcp
    M.initTcpServer()
 
    -- send our announce
    M.sendAnnounceBroadcast(M.jsonTagTable)

  end

end

function M.createAnnounce(jsonTagTable)

  local a = {}
  a.Announce = "i-am-a-client"
  -- a.MyDeviceId = "chip:" .. node.chipid() .. "-flash:" .. node.flashid() .. "-mac:" .. wifi.sta.getmac()
  a.MyDeviceId = "chip:" .. node.chipid() .. "-mac:" .. wifi.sta.getmac()

  -- if jsonTagTable.Name then
  --   a.Name = jsonTagTable.Name
  --   -- jsonTagTable.Name = nil -- erase from table so not in jsonTags
  -- elseif M.jsonTagTable.Name then
  --   a.Name = M.jsonTagTable.Name
  -- else
  --   a.Name = "Unnamed"
  -- end

  -- if jsonTagTable.Desc then
  --   a.Desc = jsonTagTable.Desc
  --   -- jsonTagTable.Desc = nil -- erase from table so not in jsonTags
  -- else
  --   a.Desc = "(no desc provided)"
  -- end

  -- if jsonTagTable.Icon then
  --   a.Icon = jsonTagTable.Icon
  --   -- jsonTagTable.Icon = nil -- erase from table so not in jsonTags
  -- else
  --   a.Icon = "(no icon provided)"
  -- end

  if jsonTagTable.Widget then
    a.Widget = jsonTagTable.Widget
    -- jsonTagTable.Widget = nil
  elseif M.jsonTagTable.Widget then
    a.Widget = M.jsonTagTable.Widget
  else
    a.Widget = "com-chilipeppr-widget-undefined"
  end

  -- if jsonTagTable.WidgetUrl then
  --   a.WidgetUrl = jsonTagTable.WidgetUrl
  --   -- jsonTagTable.WidgetUrl = nil
  -- else
  --   a.WidgetUrl = "(no url specified)"
  -- end

  -- see if there is a jsontagtable passed in as extra meta
  local jsontag = ""
  if jsonTagTable then
    ok, jsontag = pcall(cjson.encode, jsonTagTable)
    if ok then
      -- print("Adding jsontagtable" .. jsontag)
    else
      print("failed to encode jsontag!")
    end
  end

  a.JsonTag = jsontag

  local ok, json = pcall(cjson.encode, a)
  if ok then
    --print("Encoded json for announce: " .. json)
  else
    print("failed to encode json!")
  end
  print("Announce msg: " .. json)
  return json
end

-- send announce to broadcast addr so spjs
-- knows of our existence
function M.sendAnnounceBroadcast(jsonTagTable)
  if M.isInitted == false then
    print("You must init first.")
    return
  end

  local bip = wifi.sta.getbroadcast()
  --print("Broadcast addr:" .. bip)

  -- if there was no jsonTagTable passed in, then used
  -- stored one
  if not jsonTagTable then
    jsonTagTable = M.jsonTagTable
  end

  print("Sending announce to ip: " .. bip)
  M.sock:connect(M.port, bip)
  M.sock:send(M.createAnnounce(jsonTagTable))
  M.sock:close()

end

function M.sendBroadcast(jsonTagTable)

  if M.isInitted == false then
    print("You must init first.")
    return
  end

  -- we need to attach deviceid
  local a = {}
  a.MyDeviceId = "chip:" .. node.chipid() .. "-mac:" .. wifi.sta.getmac()

  -- see if there is a jsontagtable passed in as extra meta
  local jsontag = ""
  if jsonTagTable then
    ok, jsontag = pcall(cjson.encode, jsonTagTable)
    if ok then
      -- print("Adding jsontagtable" .. jsontag)
    else
      print("failed to encode jsontag!")
    end
  end

  a.JsonTag = jsontag

  local ok, json = pcall(cjson.encode, a)
  if ok then
    --print("Encoded json for announce: " .. json)
  else
    print("failed to encode json!")
  end

  local bip = wifi.sta.getbroadcast()
  --print("Broadcast addr:" .. bip)

  -- local msg = cjson.encode(jsonTagTable)
  -- local msg = cjson.encode(a)
  print("Sending UDP msg: " .. json .. " to ip: " .. bip)
  M.sock:connect(M.port, bip)
  M.sock:send(json)
  M.sock:close()

end

function M.setupWifi()
  -- setwifi
  wifi.setmode(wifi.STATION)
  -- longest range is b
  wifi.setphymode(wifi.PHYMODE_N)
  --Connect to access point automatically when in range

  -- for some reason digits in password seem to get mangled
  -- so splitting them seems to solve problem
  wifi.sta.config("NETGEAR-main", "(your password")
  wifi.sta.connect()

  --register callback
  wifi.sta.eventMonReg(wifi.STA_IDLE, function() print("STATION_IDLE") end)
  --wifi.sta.eventMonReg(wifi.STA_CONNECTING, function() print("STATION_CONNECTING") end)
  wifi.sta.eventMonReg(wifi.STA_WRONGPWD, function() print("STATION_WRONG_PASSWORD") end)
  wifi.sta.eventMonReg(wifi.STA_APNOTFOUND, function() print("STATION_NO_AP_FOUND") end)
  wifi.sta.eventMonReg(wifi.STA_FAIL, function() print("STATION_CONNECT_FAIL") end)
  wifi.sta.eventMonReg(wifi.STA_GOTIP, M.gotip)

  --register callback: use previous state
  wifi.sta.eventMonReg(wifi.STA_CONNECTING, function(previous_State)
      if(previous_State==wifi.STA_GOTIP) then
        print ("Reconnecting")
          -- print("Station lost connection with access point\n\tAttempting to reconnect...")
      else
        print("STATION_CONNECTING")
      end
  end)

  --start WiFi event monitor with default interval
  wifi.sta.eventMonStart(1000)

end

function M.gotip()
  print("STATION_GOT_IP")
  M.myip = wifi.sta.getip()
  print("My IP: " .. M.myip)
  -- stop monitoring now since we're connected
  wifi.sta.eventMonStop()
  print("Stopped monitoring") -- wifi events since connected.")
  -- make sure we are initted
  M.init()
end

function M.initUdpServer()
  M.udpServer = net.createServer(net.UDP)
  --M.udpServer:on("connection", M.onUdpConnection)
  M.udpServer:on("receive", M.onUdpRecv)
  M.udpServer:listen(8988)
  print("UDP Server started on port 8988")
end

function M.onUdpConnection(sck)
  print("UDP connection.")
  --ip, port = sck:getpeer()
  --print("UDP connection. from: " .. ip)
end

function M.onUdpRecv(sck, data)
  print("UDP Recvd. data: " .. data)
  if (M.listenerOnIncomingUdpCmd) then
    -- see if json
    if string.sub(data,1,1) == "{" then
      -- catch json errors
      local succ, results = pcall(function()
      return cjson.decode(data)
      end)
   
      -- see if we could parse
      if succ then
      data = results --cjson.decode(data)
        -- data.peerIp = peer
      else
      print("Error parsing JSON")
      return
      end
   
    end
    M.listenerOnIncomingUdpCmd(data)
  end
end

-- this property and method let an external object attach a
-- listener to the incoming UDP cmd
M.listenerOnIncomingUdpCmd = nil
function M.addListenerOnIncomingUdpCmd(listenerCallback)
  M.listenerOnIncomingUdpCmd = listenerCallback
  -- print("Attached listener to incoming UDP cmd")
end

function M.removeListenerOnIncomingUdpCmd(listenerCallback)
  M.listenerOnIncomingUdpCmd = nil
  -- print("Removed listener on incoming UDP cmd")
end

function M.initTcpServer()
  M.tcpServer = net.createServer(net.TCP)
  M.tcpServer:listen(8988, M.onTcpListen)

  print("TCP Server started on port 8988")
end

function M.onTcpListen(conn)
  conn:on("receive", M.onTcpRecv)
end

function M.onTcpConnection(sck)
  print("TCP connection.")
  --ip, port = sck:getpeer()
  --print("UDP connection. from: " .. ip)
end

function M.onTcpRecv(sck, data)
  local peer = sck:getpeer()
  print("TCP Recvd. data: " .. data .. ", Peer:" .. peer)
  if (M.listenerOnIncomingCmd) then
    -- see if json
    if string.sub(data,1,1) == "{" then
      -- catch json errors
      local succ, results = pcall(function()
      return cjson.decode(data)
      end)
   
      -- see if we could parse
      if succ then
      data = results --cjson.decode(data)
        data.peerIp = peer
      else
      print("Error parsing JSON")
      return
      end
   
    end
    M.listenerOnIncomingCmd(data)
  end
end

-- this property and method let an external object attach a
-- listener to the incoming TCP command
M.listenerOnIncomingCmd = nil
function M.addListenerOnIncomingCmd(listenerCallback)
  M.listenerOnIncomingCmd = listenerCallback
  -- print("Attached listener to incoming TCP cmd")
end

function M.removeListenerOnIncomingCmd(listenerCallback)
  M.listenerOnIncomingCmd = nil
  -- print("Removed listener on incoming TCP cmd")
end

return M

--M.init()

-- cayenn = M

-- cayenn.init()

-- GOOD INIT
-- opts = {}
-- opts.Name = "Dispenser DMP16"
-- opts.Desc = "Techcon DMP16 auger with stepper and linear slide"
-- opts.Icon = "http://gds-storage-prd.s3.amazonaws.com/fusion-360/161021/1972/70f0aa71/thumbnails/raasrendering-bbf3b3d5-01c5-48e4-bbdb-f81727a520ab-160-160.jpg"
-- opts.Widget = "com-chilipeppr-widget-dispenser"
-- opts.WidgetUrl = "https://github.com/chilipeppr/widget-dispenser/auto-generated.html"

-- cayenn.init(opts)

laser_3amp_driver_v1.lua

-- Laser module. Uses ACS714 for current reading.
-- Allows toggling/pulsing of laser
-- Has global relay for main power shutdown

node.setcpufreq(node.CPU160MHZ)

local m = {}
-- m = {}
m.pin = 2 -- laser TTL
m.pinRelay = 3 -- relay to main power supply
m.tmrReading = 0
m.tmrOutput = 4
m.tmrPulse = 3
m.isOn = false
m.isInitted = false

-- user can set this to ensure power is not above max
m.maxDuty = 1023

function m.init()
  
  if m.isInitted then
    return
  end
  
  -- TODO setup the i2c current sensor
  
  -- setup TTL
  gpio.mode(m.pin, gpio.OUTPUT)
  gpio.write(m.pin, gpio.LOW)
  
  -- setup relay
  -- relay requires high or low (not float) to turn on
  -- so set to float to turn off main power relay
  gpio.mode(m.pinRelay, gpio.INPUT)
  
  m.isOn = false
  m.isInitted = true
  
end

function m.relayOn()
  m.init()
  -- relay requires high or low (not float) to turn on
  gpio.mode(m.pinRelay, gpio.OUTPUT)
  gpio.write(m.pinRelay, gpio.LOW)
  -- gpio.write(m.pinRelay, gpio.HIGH)
  print("Relay On")
end

function m.relayOff()
  m.init()
  -- relay requires float to turn off
  gpio.mode(m.pinRelay, gpio.INPUT)
  -- gpio.write(m.pinRelay, gpio.LOW)
  print("Relay Off")
end

function m.on()
  m.init()
  gpio.write(m.pin, gpio.HIGH)
  print("Laser On")
end

function m.off()
  m.init()
  gpio.write(m.pin, gpio.LOW)
  print("Laser Off")
end

function m.read()
  
  -- read 30 samples and average
  local val = 0
  for i=0,9,1 
  do 
     val = val + adc.read(0)
    -- print("val: " .. val)
  end
  -- val = val / 1
  local pct = (val * 100) / 1024
  local millivolts = ((3300 * pct)) -- / 1000)
  -- subtract 2500 mV cuz that means 0 amps (2560 mV actually)
  millivolts = millivolts - 2570000
  if millivolts < 0 then millivolts = 0 end
  -- divide by 185 to figure out amps
  local ma = (millivolts) / 185
  -- print("ADC: " .. val .. " Pct: " .. pct .. " mv: " .. millivolts .. " mA: " .. ma)
  
  return ma
end

m.samples = {} --{0,0,0,0,0,0,0,0,0,0}
m.lastSampleIndex = 0
function m.readAvgStart()
  m.init()
  -- let's do a reading each 10ms
  -- create 10 samples, but always let the samples fall
  -- off the queue
  tmr.alarm(m.tmrReading, 20, tmr.ALARM_AUTO, function()
    local ma = m.read()
    m.samples[m.lastSampleIndex] = ma
    m.lastSampleIndex = m.lastSampleIndex + 1
    if m.lastSampleIndex > 9 then m.lastSampleIndex = 0 end
  end)
  
  tmr.alarm(m.tmrOutput, 500, tmr.ALARM_AUTO, function()
    -- figure out avg
    local s = 0
    for i2=0,9,1 do 
       s = s + tonumber(m.samples[i2])
      -- print("s: " .. s .. " i2: " .. i2 .. " m.samples[]: " .. m.samples[i2])
    end
    s = s / 10
    print("mA: " .. s)
  end)
end

function m.readAvgStop()
  tmr.stop(m.tmrReading)
  tmr.stop(m.tmrOutput)
  -- print("Stopped average reading.")
end

function m.readStart()
  m.init()
  tmr.alarm(m.tmrReading, 500, tmr.ALARM_AUTO, m.onRead)
end

function m.onRead()
  m.read()
end

function m.readStop()
  tmr.stop(m.tmrReading)
end

-- pulse the laser for a delay of ms
function m.pulseFor(delay)
  m.init()
  local d = 100
  if delay ~= nil then
    d = delay
  end
  tmr.alarm(m.tmrPulse, d, tmr.ALARM_AUTO, m.pulseStop)
  m.on()
end

function m.pulseStop()
  tmr.stop(m.tmrPulse)
  m.off()
end

-- frequency in hertz. max 1000hz or 1khz
-- duty cycle. 0 is 0% duty. 1023 is 100% duty. 512 is 50%.
function m.pwmOn(freqHz, duty)
  if (duty > m.maxDuty) then duty = m.maxDuty end
  print("Laser pwmOn hz:", freqHz, "duty:", duty)
  pwm.setup(m.pin, freqHz, duty)
  pwm.start(m.pin)
  return duty
end

function m.pwmOff()
  print("Laser pwmOff")
  pwm.stop(m.pin)  
end

function m.pwmSetMaxDuty(duty)
  m.maxDuty = duty
end

-- m.init()
-- m.readStart()
-- m.readAvgStart()
return m

tinyg_read_v3.lua

-- Read the inputs from TinyG
-- We need to watch Coolant Pin
-- We also need to watch the A axis step/dir pins.

local m = {}
-- m = {}

m.pinCoolant = 5
m.pinADir = 6
m.pinAStep = 7

-- global ID counter. we increment this each time we see 
-- a signal on the coolant pin 
m.idCounter = -1

function m.init()
  -- gpio.mode(m.pinCoolant, gpio.INPUT)
  gpio.mode(m.pinCoolant, gpio.INT) --, gpio.PULLUP)
  gpio.mode(m.pinADir, gpio.INT)
  gpio.mode(m.pinAStep, gpio.INT)
  gpio.trig(m.pinCoolant, "both", m.pinCoolantCallback)
  -- gpio.trig(m.pinCoolant, "up", debounce(m.pinCoolantCallback))
  -- gpio.trig(m.pinADir, "both", m.pinADirCallback)
  -- gpio.trig(m.pinAStep, "both", m.pinAStepCallback)
  -- print("Setup pin watchers for Coolant, ADir, AStep")
  print("Coolant: " .. tostring(gpio.read(m.pinCoolant)) ..
    ", ADir: " .. tostring(gpio.read(m.pinADir)) ..
    ", AStep: " .. tostring(gpio.read(m.pinAStep))
    )
end

function m.status()
  print("Coolant: " .. tostring(gpio.read(m.pinCoolant)) ..
    ", ADir: " .. tostring(gpio.read(m.pinADir)) ..
    ", AStep: " .. tostring(gpio.read(m.pinAStep))
    )
end

-- function debounce (func)
--     local last = 0
--     local delay = 50000 -- 50ms * 1000 as tmr.now() has μs resolution

--     return function (...)
--         local now = tmr.now()
--         local delta = now - last
--         if delta < 0 then delta = delta + 214748364 end; -- proposed because of delta rolling over, https://github.com/hackhitchin/esp8266-co-uk/issues/2
--         if delta < delay then return end;

--         last = now
--         return func(...)
--     end
-- end

-- function onChange ()
--     print('The pin value has changed to '..gpio.read(pin))
-- end

-- gpio.mode(pin, gpio.INT, gpio.PULLUP) -- see https://github.com/hackhitchin/esp8266-co-uk/pull/1
-- gpio.trig(pin, 'both', debounce(onChange))

m.lastTime = 0
m.lookingFor = gpio.HIGH
m.lookingCtr = 0
function m.pinCoolantCallback(level)
  -- we get called here on rising and falling edge
  if m.lookingFor == gpio.HIGH then
    -- read 10 more times, and if we get good reads, trust it
    local readCtr = 0
    for i = 0, 5 do
      if gpio.read(m.pinCoolant) == gpio.HIGH then
        readCtr = readCtr + 1
      end
    end
    
    if readCtr > 3 then
      -- treat that as good avg
      m.idCounter = m.idCounter + 1
      -- print("Got coolant pin. idCounter: " .. m.idCounter)
      m.onIdChange()
      m.lookingFor = gpio.LOW
    else
      -- print("Failed avg")
    end
    
  else
    -- looking for gpio.LOW
    -- read 10 more times, and if we get good reads, trust it
    local readCtr = 0
    for i = 0, 5 do
      if gpio.read(m.pinCoolant) == gpio.LOW then
        readCtr = readCtr + 1
      end
    end
    
    if readCtr > 3 then
      -- treat that as good avg
      -- print("Trusting low")
      m.lookingFor = gpio.HIGH
    else
      -- print("Failed avg")
    end
    
  end
  
  gpio.trig(m.pinCoolant, "both")

end

-- function m.pinCoolantCallbackOld(level)
--   -- this method is called when the coolant pin has an interrupt
  
--   if m.lookingFor == gpio.HIGH then
--     -- we are waiting for coolant to go high
    
--     if level == gpio.HIGH then
--       -- increment our ctr. if we get 10 in a row trust it.
--       m.lookingCtr = m.lookingCtr + 1
      
--       if m.lookingCtr > 5 then
--         -- we have hit our target. trust it.
--         m.idCounter = m.idCounter + 1
--         print("Got coolant pin. Level: " .. level .. " idCounter: " .. m.idCounter .. " tmr:" .. tmr.now())
--         m.onIdChange()
--         m.lookingFor = gpio.LOW
--         m.lookingCtr = 0
--         -- now look for falling edge
--         gpio.trig(m.pinCoolant, "down")
--       else
--         -- keep counting highs
--         gpio.trig(m.pinCoolant, "high")
--       end
--     else
--       -- we just saw a low, so reset our ctr to start again
--       -- so we get 10 clean reads
--       m.lookingCtr = 0
--       -- print("reset lookingCtr to 0")
--       gpio.trig(m.pinCoolant, "high")
--     end
--   else
--     -- we are waiting for coolant to go low
--     if level == gpio.LOW then
--       -- we got the low we are looking for
--       -- increment our ctr. if we get 10 in a row trust it.
--       m.lookingCtr = m.lookingCtr + 1
      
--       if m.lookingCtr > 5 then
--         -- print("Trusting we are low now")
--         -- now look for high
--         m.lookingFor = gpio.HIGH
--         m.lookingCtr = 0
--         -- now look for rising edge
--         gpio.trig(m.pinCoolant, "up")
--       else
--         -- keep counting lows
--         gpio.trig(m.pinCoolant, "low")
--       end
--     else
--       -- we just saw a high, so reset our ctr to start again
--       -- so we get 10 clean reads
--       m.lookingCtr = 0
--       -- print("reset lookingCtr to 0")
--       gpio.trig(m.pinCoolant, "low")
--     end
--   end
    -- -- if we got a low, toss it
    -- if level == gpio.LOW then 
    --   -- just trigger next callback
    --   gpio.trig(m.pinCoolant, "up")
    --   print("crapped bad lvl")
    --   return
    -- end
    
    -- we have a high. now make sure we see high for 200 more reads.
    -- if we do, we're safe
    -- for i = 0, 200 do
    --   if gpio.read(m.pinCoolant) == gpio.LOW then
    --     -- if we got a low, consider this bad data
    --     -- just trigger next callback
    --     gpio.trig(m.pinCoolant, "up")
    --     print("crapped on read " .. i .. " of 1000 reads")
    --     return
    --   end
    -- end
  
  --   -- if we got here, then we can be sure it's a good HIGH read
  --   m.idCounter = m.idCounter + 1
  --   m.onIdChange()
  --   print("Got coolant pin. Level: " .. level .. " idCounter: " .. m.idCounter .. " tmr:" .. tmr.now())
  --   gpio.trig(m.pinCoolant, "up")
    
  -- end
  
-- end

function m.resetIdCounter()
  m.idCounter = -1
  m.onIdChange()
  print("Reset idCounter: " .. m.idCounter)
end

function m.getIdCounter()
  return m.idCounter
end 

-- this property and method let an external object attach a
-- listener to the counter change
m.listenerOnIdChange = null
function m.addListenerOnIdChange(listenerCallback)
  m.listenerOnIdChange = listenerCallback
  -- print("Attached listener to Id Change")
end

function m.removeListenerOnIdChange(listenerCallback)
  m.listenerOnIdChange = null
  -- print("Removed listener on Id Change")
end

function m.onIdChange()
  if m.listenerOnIdChange then
    m.listenerOnIdChange(m.idCounter)
  end
end

-- this property and method let an external object attach a
-- listener to the ADir pin 
m.listenerOnADir = null
function m.addListenerOnADir(listenerCallback)
  m.listenerOnADir = listenerCallback
  -- print("Attached listener to ADir pin")
end

function m.removeListenerOnADir(listenerCallback)
  m.listenerOnADir = null
  -- print("Removed listener on ADir pin")
end

-- this property and method let an external object attach a
-- listener to the AStep pin 
m.listenerOnAStep = null
function m.addListenerOnAStep(listenerCallback)
  m.listenerOnAStep = listenerCallback
  -- print("Attached listener to AStep pin")
end

function m.removeListenerOnAStep(listenerCallback)
  m.listenerOnAStep = null
  -- print("Removed listener on AStep pin")
end

function m.pinADirCallback(level)
  gpio.trig(m.pinADir, "both")
  -- this method is called when the ADir pin has an interrupt
  -- we need to simply regurgitate it to appropriate listener
  print("ADir: " .. level)
  -- call listener 
  if m.listenerOnADir then 
    m.listenerOnADir()
  end 
  -- print("Got coolant pin. idCounter: " .. m.idCounter)
end

function m.pinAStepCallback(level)
  gpio.trig(m.pinAStep, "both")
  -- this method is called when the AStep pin has an interrupt
  -- we need to simply regurgitate it to appropriate listener
  print("AStep: " .. level)
  -- call listener 
  if m.listenerOnAStep then 
    m.listenerOnAStep()
  end 
end



return m
-- m.init()

queue.lua

-- Cayenn Queue by using a file
local m = {}
-- m = {}
m.filename = "queue.txt"
m.tmrSend = 1

function m.wipe()
  file.remove(m.filename)
  file.open(m.filename, "w+")
  -- file.writeline("")
  file.close()
end

function m.add(payload)
  -- open 'queue.txt' in 'a+' mode to append
  file.open(m.filename, "a+")
  local ok, jsontag = pcall(cjson.encode, payload)
  if ok then
    -- write to the end of the file
    file.writeline(jsontag)
    -- print("Wrote line to file:" .. jsontag)
  else
    -- print("failed to encode jsontag for queue file!")
  end
  file.close()
end

function m.getId(id)

  -- if file.exists(m.filename) == false then
  --   return nil
  -- end

  file.open(m.filename, "r")
  -- read the # of lines by the id
  local line
  for ctr = 0, id do
    line = file.readline()
    -- print("Line:", id, ctr, line)
  end
  file.close()
  -- print("Line:", line)

  if line == nil then
    return nil
  end

  -- parse json to table
  local succ, results = pcall(function()
  return cjson.decode(line)
  end)

  -- see if we could parse
  if succ then
  --data = results
  return results
  else
  -- print("Error parsing JSON")
  return nil
  end

end

function m.getTxt()
  file.open(m.filename, "r")
  local txt = file.read()
  file.close()
  return txt
end

-- this method sends back data slowly
m.lastSendId = 0
m.callback = nil
m.transId = nil
function m.send(callback, transId)
  -- reset ctr
  m.lastSendId = 0
  m.callback = callback
  m.transId = transId
  -- say we are starting
  m.callback({["TransId"] = m.transId, ["Resp"] = "GetQ", ["Start"] = 0})
  -- callback slowly and send q each time
  tmr.alarm(m.tmrSend, 2, tmr.ALARM_SEMI, m.onSend)
end

function m.onSend()
  -- get next line
  local cmd = queue.getId(m.lastSendId)
  if cmd ~= nil then
    m.callback({["TransId"] = m.transId, ["Resp"] = "GetQ", ["Q"] = cmd})
    m.lastSendId = m.lastSendId + 1
    tmr.start(m.tmrSend)
  else
    -- we are done, cuz hit null
    m.callback({["TransId"] = m.transId, ["Resp"] = "GetQ", ["Finish"] = m.lastSendId})
  end

end

return m

led.lua

-- control led on esp8266
-- use this module like
-- led = require("led")
-- led.on() -- turn on led
-- led.off() -- turn off led
-- led.blink(2) -- blink twice w/ 200ms delay
-- led.blink(6,100) -- blink 6 times w/ 100ms delay
-- led.blink(10,20) -- blink 10 times w/ 20ms delay

local led = {}
-- led = {}

led.pin = 4 -- this is GPIO2
led.timerId = 6  -- 0 thru 6 allowed. change to not conflict.
led.isOn = false
led.isInitted = false

function led.init()
  -- setup led as output and turn on
  gpio.mode(led.pin, gpio.OUTPUT)
  gpio.write(led.pin, gpio.HIGH)
  led.isOn = false
  led.isInitted = true
end

function led.on()
  if led.isInitted ~= true then
    led.init()
  end

  gpio.write(4, gpio.LOW) --on
  -- print("Led on")
end

function led.off()
  if led.isInitted ~= true then
    led.init()
  end

  gpio.write(4, gpio.HIGH) --off
  -- print("Led off")
end

led.blinkTimes = 0
led.delay = 200
function led.blink(val, delay)
  led.blinkTimes = val * 2
  if delay ~= nil and delay > 0 then
    led.delay = delay
  end
  tmr.alarm(led.timerId, led.delay, tmr.ALARM_AUTO, led.onTimer)
end

led.ctr = 0
function led.onTimer()

  led.ctr = led.ctr + 1

  if led.ctr > led.blinkTimes then
    tmr.stop(led.timerId)
    led.ctr = 0
    led.off()
    -- print("Done")
    led.delay = 200 --reset delay to default
    return
  end

  if gpio.read(4) == gpio.HIGH then
    led.on()
  else
    led.off()
  end
end

return led

init.lua


dofile("main_laseruv.lc")


Saturday, November 5, 2016

Cayenn RPM Sensor for ChiliPeppr Using Nodemcu ESP8266 / Hall Effect Sensor

This blog post describe the creation of an RPM hall effect sensor for the Cayenn protocol for ChiliPeppr.

It runs from a NodeMCU ESP8266.



A Hall Effect Sensor is connected to the ESP8266 to port 2. The actual sensor will eventually be mounted correctly from a plastic part.

Back side of hall effect sensor. Only 3.3v power and the digital out port are used.



The spindle head has a magnet attached on the inside of it.
Video of the sensor in action.


Oscilloscope Output

When the spindle head is running, pulses are generated by the hall effect sensor. The pulses are sent to pin 2 of the ESP8266 so it can count the rotations of the spindle.

1K RPM

When running spindle at 1K RPM. The yellow lines are the direct pulses from the hall effect sensor. The voltage is 3.3v and is low for only about 2% of the time because the magnet is very small compared to the overall circumference of the spindle plastic head. This makes detection somewhat difficult because the pulse is only low when the magnet is directly in front of the hall effect sensor.



The blue pulses are debug output on port 3 of the ESP8266. Each high/low indicates 2 counts of the spindle rotating. The high/low output made it much easier to debug on the oscilloscope to ensure counts were accurate. As seen in the screen grab above it is 100% accurate.

5K RPM

When running spindle at 5K RPM. The blips on the blue line are when the ESP8266 transmits the UDP packets. The RPM is not counted during that period. Per the screenshot below, this is a 100% exact capture of RPM count.


10K RPM

When running spindle at 10K RPM. Keep in mind the yellow pulses are just not all showing due to anti-aliasing in the oscilloscope software.


Asking RPM Sensor to Start Sending

Once the ESP8266 is powered on, it connects to Wifi and announces its existence on the network. Per the Cayenn protocol, it sends the following announcement to the broadcast network address, thus reaching SPJS, thus subsequently reaching ChiliPeppr (if you're connected to SPJS).

{
  "Addr":{
    "IP":"10.0.0.169",
    "Port":3739,
    "Network":"udp"
  },
  "Announce":"i-am-a-client",
  "Widget":"com-chilipeppr-widget-rpmsensor",
  "JsonTag":"{\"Icon\":\"http:\\/\\/gds-storage-prd.s3.amazonaws.com\\/fusion-360\\/161021\\/1972\\/70f0aa71\\/thumbnails\\/raasrendering-bbf3b3d5-01c5-48e4-bbdb-f81727a520ab-160-160.jpg\",\"Name\":\"Spindle RPM\",\"WidgetUrl\":\"https:\\/\\/github.com\\/chilipeppr\\/widget-dispenser\\/auto-generated.html\",\"Widget\":\"com-chilipeppr-widget-rpmsensor\",\"Desc\":\"Counts spindle RPM using LED tachometer\"}",
  "DeviceId":"chip:8395871-flash:1458400-mac:5c:cf:7f:80:1c:5f"
}
You can see this in the SPJS widget by choosing Show / Hide Console in the upper right corner triangle menu.

The JSON announce packet is highlighted in yellow below to show you where it shows up in the console.

Request Announcement

You can also request to discover all Cayenn devices if you want them to announce themselves again. Send the following command to SPJS at any time. Take note this is a UDP command.
cayenn-sendudp 10.0.0.255 {"Cayenn":"Discover"}
 

Start RPM Sensing

You will get back all discovered devices. When you find your RPM sensor, and thus its IP address, you can ask it to start sending RPM readings. You will need to send the following command to SPJS. Take note this is a TCP command. Ensure the IP address is the address of your device.
cayenn-sendtcp 10.0.0.169 {"Cmd":"RpmSenseStart"}

UDP Packets Sent from ESP8266 to SPJS to ChiliPeppr

When the RPM sensor starts to provide data, it will come in packets like those below. Notice that the ESP8266 sends the RPM update packets via UDP out to the network broadcast address. This makes the sending efficient because TCP may have to retransmit packets. Since RPM data is very fleeting, it doesn't matter if a packet gets dropped occasionally by the Wifi network.

The actual RPM value comes in as a JsonTag which is the part of the Cayenn protocol that allows random tags of any type, and it is simply up to the final widget to interpret the data. Notice that a unique DeviceID is also transmitted each time to assist ChiliPeppr with uniquely recognizing each Cayenn device.

{
  "Addr":{
    "IP":"10.0.0.169",
    "Port":7366,
    "Network":"udp"
  },
  "Announce":"",
  "Widget":"",
  "JsonTag":"{\"Rpm\":1020}",
  "DeviceId":"chip:8395871-flash:1458400-mac:5c:cf:7f:80:1c:5f"
}

RPM Widget in ChiliPeppr

Once ChiliPeppr recognizes the existence of the hall effect sensor, it will dynamically load the Javascript for the widget into memory. The widget will be initted and auto appear inside ChiliPeppr. This makes configuring your workspace easy.

Here is what the widget looks like in the TinyG workspace in ChiliPeppr.


Lua Code

The Lua code consists of several modules.
  • init.lua
  • main_rpmsensor.lua
  • rpmsensorv2.lua
  • cayennv2.lua
  • led.lua

init.lua - auto loading file when ESP8266 boots

-- Init file
-- Simply run the main compiled file for this Cayenn device
dofile("main_rpmsensor.lc")

main_rpmsensor.lua - main entry point

-- RPM Sensor
-- Supports Cayenn announcement and UDP data sending of RPM data

-- run fast to gather rpm counters
-- node.setcpufreq(node.CPU160MHZ)

cayenn = require('cayennv2')
rpmsensor = require('rpmsensorv2')
led = require("led")

opts = {}
opts.Name = "Spindle RPM"
opts.Desc = "Counts spindle RPM using LED tachometer"
opts.Icon = "http://gds-storage-prd.s3.amazonaws.com/fusion-360/161021/1972/70f0aa71/thumbnails/raasrendering-bbf3b3d5-01c5-48e4-bbdb-f81727a520ab-160-160.jpg"
opts.Widget = "com-chilipeppr-widget-rpmsensor"
opts.WidgetUrl = "https://github.com/chilipeppr/widget-dispenser/auto-generated.html"

-- opts = nil -- dealloc opts

-- define commands supported
cmds = {
  "ResetCtr", "GetCmds", "GetQ", "WipeQ", "CmdQ", 
  "RpmSenseStart", "RpmSenseStop"
  }

queue = {}

-- this is called by RpmSensor each second when the new RPM count 
-- is known.
function onRpmRead(ctr)
  -- print("Got RPM value from RPM sensor: " .. ctr)

  -- led inidcator we sent
  led.on()
  
  -- stop rpm watching to try to get more accurate count
  -- because sending UDP chews up cpu, so interrupts get weird
  -- during that time
  rpmsensor.stopRpmWatch()

  -- send UDP msg to SPJS
  local rpm = {}
  rpm.Rpm = ctr
  cayenn.sendBroadcast(rpm)
  
  led.off()
  
  -- start watching interrupts again
  rpmsensor.startRpmWatch()
end

-- this is called by Cayenn when an incoming cmd comes in
-- from the network, i.e. from SPJS. These are TCP commands so
-- they are guaranteed to come in (vs UDP which could drop)
function onCmd(payload)
  
  if (type(payload) == "table") then
      -- print("is json")
      -- print("Got incoming Cayenn cmd JSON: " .. cjson.encode(payload))
      
      -- See what cmd
      if payload.Cmd == "ResetCtr" then
        cnc.resetIdCounter()
      elseif payload.Cmd == "GetCmds" then
        cayenn.sendAnnounceBroadcast(cmds)
      elseif payload.Cmd == "GetQ" then
        -- print("Sending queue back to network")
        cayenn.sendAnnounceBroadcast(queue)
      elseif payload.Cmd == "WipeQ" then
        queue = nil
        -- print("Wiped queue: " .. cjson.encode(queue))
      elseif payload.Cmd == "CmdQ" then
        -- queuing cmd. we must have ID.
        if payload.Id == nil then
          print("Error queuing command. It must have an ID")
          return
        end
        if payload.RunCmd == nil then
          print("Error queuing command. It must have a RunCmd like RunCmd:{Cmd:AugerOn,Speed:10}.")
          return
        end
        -- print("Queing command")
        queue[payload.Id] = payload.RunCmd
        -- print("New queue: " .. cjson.encode(queue))
        
      elseif payload.Cmd == "RpmSenseStart" then
        rpmsensor.startRpmWatch()
        print("Start sensing RPM and sending updates each second")
      elseif payload.Cmd == "RpmSenseStop" then
        rpmsensor.stopRpmWatch()
        print("Stop sensing RPM and sending updates each second")
      else
        print("Got cmd we do not understand. Huh?")
      end
  else
      -- print("is string")
      -- print("Got incoming Cayenn cmd. str: ", payload)
  end
end

-- this callback is called when an incoming UDP broadcast 
-- comes in to this device. typically this is just for 
-- Cayenn Discover requests to figure out what devices are on 
-- the network
function onIncomingBroadcast(cmd)
  -- print("Got incoming UDP cmd: ", cmd)
  if (type(cmd) == "table") then
    if cmd["Cayenn"] ~= nil then
      if cmd.Cayenn == "Discover" then
        -- somebody is asking me to announce myself
        cayenn.sendAnnounceBroadcast()
      else
        print("Do not understand incoming Cayenn cmd")
      end
    elseif cmd["Announce"] ~= nil then
      if cmd.Announce == "i-am-your-server" then
        print("Got a server announcement. Cool. Store it. TODO")
      else 
        print("Got announce but not from a server. Huh?")
      end 
    else 
      print("Do not understand incoming UDP cmd")
    end
    
  else 
    print("Got incoming UDP as string")
  end
end

-- add listener to incoming cayenn commands
cayenn.addListenerOnIncomingCmd(onCmd)
cayenn.addListenerOnIncomingUdpCmd(onIncomingBroadcast)
-- add listener to RPM Known
rpmsensor.addListenerOnRpmKnown(onRpmRead)

cayenn.init(opts)
rpmsensor.init()

led.blink(6)

rpmsensorv2.lua - module that reads the interrupts from the hall sensor

-- Read the inputs from RPM Sensor
-- We need to watch D2 Pin

local m = {}
-- m = {}

node.setcpufreq(node.CPU160MHZ)
-- node.setcpufreq(node.CPU80MHZ)

m.pinRpm = 2

-- global  counter. we increment this each time we see 
-- a signal on the rpm sensor pin 
m.counter = 0

function m.init()
  -- gpio.mode(m.pinCoolant, gpio.INPUT)
  gpio.mode(m.pinRpm, gpio.INT) --, gpio.PULLUP)
  gpio.mode(3, gpio.OUTPUT)
  -- gpio.trig(m.pinRpm, "up", m.pinRpmCallback)
  gpio.trig(m.pinRpm, "up", m.pinRpmCallback)
  print("Setup pin watcher for RPM sensor. pin: " .. m.pinRpm)
  -- m.startRpmWatch()
  m.status()
end

function m.startRpmWatch()
  m.counter = 0
  tmr.alarm(0, 1000, tmr.ALARM_AUTO, m.onTimer)
end

function m.stopRpmWatch()
  tmr.stop(0)
end

function m.onTimer()
  tmr.stop(0)
  -- print("RPM: " .. m.counter * 60)
  m.onRpmKnown(m.counter * 60)
  m.counter = 0
  tmr.start(0)
end

function m.status()
  print("RPM Pin: " .. tostring(gpio.read(m.pinRpm)))
end

m.lastTime = 0
function m.pinRpmCallback(level)
  
  -- make sure we got a callback later than our min time
  if tmr.now() - m.lastTime > 3000 then
    if m.isHigh then
      gpio.write(3, gpio.LOW)
      m.isHigh = false
    else
      gpio.write(3, gpio.HIGH)
      m.isHigh = true
    end
    m.counter = m.counter + 1
  end

  -- if they don't start the counter then just reset when too high
  if m.counter > 1000 then
    m.counter = 0
  end

  m.lastTime = tmr.now()
  
  gpio.trig(m.pinRpm, "up")
end

m.isHigh = false
function m.pinRpmCallbackOrig(level)
  if level == 1 then
    m.counter = m.counter + 1
    gpio.trig(m.pinRpm, "up")
    if m.isHigh then 
      gpio.write(3, gpio.LOW)
      m.isHigh = false
    else
      gpio.write(3, gpio.HIGH)
      m.isHigh = true
    end
  end
end

function m.resetCounter()
  m.counter = 0
  -- m.onIdChange()
  print("Reset counter: " .. m.counter)
end

function m.getCounter()
  return m.counter
end 

-- this property and method let an external object attach a
-- listener to the counter change
m.listenerOnRpmKnown = null
function m.addListenerOnRpmKnown(listenerCallback)
  m.listenerOnRpmKnown = listenerCallback
  print("Attached listener to RPM Known")
end

function m.removeListenerOnRpmKnown(listenerCallback)
  m.listenerOnRpmKnown = null
  print("Removed listener on RPM Known")
end

function m.onRpmKnown(ctr)
  if m.listenerOnRpmKnown then
    m.listenerOnRpmKnown(ctr)
  end
end

return m
-- m.init()

cayennv2.lua - standard Cayenn module providing UDP/TCP communications

-- Cayenn Protocol for ChiliPeppr
-- This module does udp/tcp sending/receiving to talk with SPJS
-- or have the browser talk direct to this ESP8266 device.
-- This module has methods for connecting to wifi, then initting
-- UDP servers, TCP servers, and sending a broadcast announce out.
-- The broadcast announce let's any listening SPJS know we're alive.
-- SPJS then reflects that message to any listening browsers like
-- ChiliPeppr so they know a device is available on the network.
-- Then the browser can send commands back to this device.
local M = {}
-- M = {}

M.port = 8988
M.myip = nil
M.sock = nil
M.jsonTagTable = nil

--M.announce = 

M.isInitted = false

-- When you are initting you can pass in tags to describe your device
-- You should use a format like this:
-- cayenn.init({
--  widget: "com-chilipeppr-widget-ina219",
--  icon: "http://chilipeppr.com/img/ina219.jpg",
--  widgetUrl: "https://github.com/chilipeppr/widget-ina219/auto-generated.html",
-- })
function M.init(jsonTagTable)
  
  -- save the jsonTagTable
  if jsonTagTable ~= nil then
    M.jsonTagTable = jsonTagTable
  end
  
  if M.isInitted then
    print("Already initted, but got called again")
    return
  end
  
  print("Initting...")

  -- figure out if i have an IP
  M.myip = wifi.sta.getip()
  if M.myip == nil then
    print("You need to connect to wifi. Connecting for you.")
    M.setupWifi()
  else 
    print("My IP: " .. M.myip)
    M.isInitted = true

    -- create socket for outbound UDP sending
    M.sock = net.createConnection(net.UDP, 0)

    -- create server to listen to incoming udp
    M.initUdpServer()
    
    -- create server to listen to incoming tcp
    M.initTcpServer()
    
    -- send our announce
    M.sendAnnounceBroadcast(M.jsonTagTable)
  
  end 
  
end

function M.createAnnounce(jsonTagTable)
  
  local a = {}
  a.Announce = "i-am-a-client"
  a.MyDeviceId = "chip:" .. node.chipid() .. "-flash:" .. node.flashid() .. "-mac:" .. wifi.sta.getmac()
  
  if jsonTagTable.Name then
    a.Name = jsonTagTable.Name
    -- jsonTagTable.Name = nil -- erase from table so not in jsonTags
  elseif M.jsonTagTable.Name then
    a.Name = M.jsonTagTable.Name
  else
    a.Name = "Unnamed"
  end
  
  if jsonTagTable.Desc then
    a.Desc = jsonTagTable.Desc
    -- jsonTagTable.Desc = nil -- erase from table so not in jsonTags
  else
    a.Desc = "(no desc provided)"
  end
  
  if jsonTagTable.Icon then
    a.Icon = jsonTagTable.Icon
    -- jsonTagTable.Icon = nil -- erase from table so not in jsonTags
  else
    a.Icon = "(no icon provided)"
  end
  
  if jsonTagTable.Widget then
    a.Widget = jsonTagTable.Widget
    -- jsonTagTable.Widget = nil
  elseif M.jsonTagTable.Widget then
    a.Widget = M.jsonTagTable.Widget
  else
    a.Widget = "com-chilipeppr-widget-undefined"
  end
  
  if jsonTagTable.WidgetUrl then
    a.WidgetUrl = jsonTagTable.WidgetUrl
    -- jsonTagTable.WidgetUrl = nil
  else
    a.WidgetUrl = "(no url specified)"
  end
  
  -- see if there is a jsontagtable passed in as extra meta
  local jsontag = ""
  if jsonTagTable then
    ok, jsontag = pcall(cjson.encode, jsonTagTable)
    if ok then
      -- print("Adding jsontagtable" .. jsontag)
    else
      print("failed to encode jsontag!")
    end
  end

  a.JsonTag = jsontag
  
  local ok, json = pcall(cjson.encode, a)
  if ok then
    --print("Encoded json for announce: " .. json)
  else
    print("failed to encode json!")
  end
  print("Announce msg: " .. json)
  return json
end

-- send announce to broadcast addr so spjs 
-- knows of our existence
function M.sendAnnounceBroadcast(jsonTagTable)
  if M.isInitted == false then
    print("You must init first.")
    return 
  end
  
  local bip = wifi.sta.getbroadcast()
  --print("Broadcast addr:" .. bip)
  
  -- if there was no jsonTagTable passed in, then used 
  -- stored one 
  if not jsonTagTable then 
    jsonTagTable = M.jsonTagTable
  end 
  
  print("Sending announce to ip: " .. bip)
  M.sock:connect(M.port, bip)
  M.sock:send(M.createAnnounce(jsonTagTable))
  M.sock:close()
  
end

function M.sendBroadcast(jsonTagTable)
  if M.isInitted == false then
    print("You must init first.")
    return 
  end
  
  -- we need to attach deviceid 
  local a = {}
  a.MyDeviceId = "chip:" .. node.chipid() .. "-flash:" .. node.flashid() .. "-mac:" .. wifi.sta.getmac()

  -- see if there is a jsontagtable passed in as extra meta
  local jsontag = ""
  if jsonTagTable then
    ok, jsontag = pcall(cjson.encode, jsonTagTable)
    if ok then
      -- print("Adding jsontagtable" .. jsontag)
    else
      print("failed to encode jsontag!")
    end
  end

  a.JsonTag = jsontag
  
  local ok, json = pcall(cjson.encode, a)
  if ok then
    --print("Encoded json for announce: " .. json)
  else
    print("failed to encode json!")
  end
  
  local bip = wifi.sta.getbroadcast()
  --print("Broadcast addr:" .. bip)
  
  -- local msg = cjson.encode(jsonTagTable)
  -- local msg = cjson.encode(a)
  -- print("Sending msg: " .. json .. " to ip: " .. bip)
  M.sock:connect(M.port, bip)
  M.sock:send(json)
  M.sock:close()
  
end

function M.setupWifi()
  -- setwifi
  wifi.setmode(wifi.STATION)
  -- longest range is b
  wifi.setphymode(wifi.PHYMODE_N)
  --Connect to access point automatically when in range
  
  -- for some reason digits in password seem to get mangled
  -- so splitting them seems to solve problem
  wifi.sta.config("NETGEAR-main", "yourpassword")
  wifi.sta.connect()
  
  --register callback
  wifi.sta.eventMonReg(wifi.STA_IDLE, function() print("STATION_IDLE") end)
  --wifi.sta.eventMonReg(wifi.STA_CONNECTING, function() print("STATION_CONNECTING") end)
  wifi.sta.eventMonReg(wifi.STA_WRONGPWD, function() print("STATION_WRONG_PASSWORD") end)
  wifi.sta.eventMonReg(wifi.STA_APNOTFOUND, function() print("STATION_NO_AP_FOUND") end)
  wifi.sta.eventMonReg(wifi.STA_FAIL, function() print("STATION_CONNECT_FAIL") end)
  wifi.sta.eventMonReg(wifi.STA_GOTIP, M.gotip)
  
  --register callback: use previous state
  wifi.sta.eventMonReg(wifi.STA_CONNECTING, function(previous_State)
      if(previous_State==wifi.STA_GOTIP) then 
          print("Station lost connection with access point\n\tAttempting to reconnect...")
      else
          print("STATION_CONNECTING")
      end
  end)
  
  --start WiFi event monitor with default interval
  wifi.sta.eventMonStart(1000)
  
end

function M.gotip()
  print("STATION_GOT_IP")
  M.myip = wifi.sta.getip()
  print("My IP: " .. M.myip)
  -- stop monitoring now since we're connected
  wifi.sta.eventMonStop()
  print("Stopped monitoring wifi events since connected.")
  -- make sure we are initted
  M.init()
end

function M.initUdpServer()
  M.udpServer = net.createServer(net.UDP)
  --M.udpServer:on("connection", M.onUdpConnection)
  M.udpServer:on("receive", M.onUdpRecv) 
  M.udpServer:listen(8988)
  print("UDP Server started on port 8988")
end

function M.onUdpConnection(sck)
  print("UDP connection.")
  --ip, port = sck:getpeer()
  --print("UDP connection. from: " .. ip)
end

function M.onUdpRecv(sck, data)
  print("UDP Recvd. data: " .. data)
  if (M.listenerOnIncomingUdpCmd) then
    -- see if json
    if string.sub(data,1,1) == "{" then
      -- catch json errors
      local succ, results = pcall(function()
       return cjson.decode(data)
      end)
      
      -- see if we could parse
      if succ then
       data = results --cjson.decode(data)
        -- data.peerIp = peer
      else
       print("Error parsing JSON")
       return
      end
      
    end
    M.listenerOnIncomingUdpCmd(data)
  end
end

-- this property and method let an external object attach a
-- listener to the incoming UDP cmd
M.listenerOnIncomingUdpCmd = nil
function M.addListenerOnIncomingUdpCmd(listenerCallback)
  M.listenerOnIncomingUdpCmd = listenerCallback
  print("Attached listener to incoming UDP cmd")
end

function M.removeListenerOnIncomingUdpCmd(listenerCallback)
  M.listenerOnIncomingUdpCmd = nil
  print("Removed listener on incoming UDP cmd")
end

function M.initTcpServer()
  M.tcpServer = net.createServer(net.TCP)
  M.tcpServer:listen(8988, M.onTcpListen)
  
  print("TCP Server started on port 8988")
end

function M.onTcpListen(conn)
  conn:on("receive", M.onTcpRecv)
end

function M.onTcpConnection(sck)
  print("TCP connection.")
  --ip, port = sck:getpeer()
  --print("UDP connection. from: " .. ip)
end

function M.onTcpRecv(sck, data)
  local peer = sck:getpeer()
  print("TCP Recvd. data: " .. data .. ", Peer:" .. peer)
  if (M.listenerOnIncomingCmd) then
    -- see if json
    if string.sub(data,1,1) == "{" then
      -- catch json errors
      local succ, results = pcall(function()
       return cjson.decode(data)
      end)
      
      -- see if we could parse
      if succ then
       data = results --cjson.decode(data)
        data.peerIp = peer
      else
       print("Error parsing JSON")
       return
      end
      
    end
    M.listenerOnIncomingCmd(data)
  end
end

-- this property and method let an external object attach a
-- listener to the incoming TCP command
M.listenerOnIncomingCmd = nil
function M.addListenerOnIncomingCmd(listenerCallback)
  M.listenerOnIncomingCmd = listenerCallback
  print("Attached listener to incoming TCP cmd")
end

function M.removeListenerOnIncomingCmd(listenerCallback)
  M.listenerOnIncomingCmd = nil
  print("Removed listener on incoming TCP cmd")
end

return M

--M.init()

led.lua - utility module to blink ESP8266 led easily

-- control led on esp8266
-- use this module like
-- led = require("led")
-- led.on() -- turn on led
-- led.off() -- turn off led
-- led.blink(2) -- blink twice w/ 200ms delay
-- led.blink(6,100) -- blink 6 times w/ 100ms delay
-- led.blink(10,20) -- blink 10 times w/ 20ms delay

local led = {}
led = {}

led.pin = 4 -- this is GPIO2
led.timerId = 6  -- 0 thru 6 allowed. change to not conflict.
led.isOn = false
led.isInitted = false

function led.init()
  -- setup led as output and turn on
  gpio.mode(led.pin, gpio.OUTPUT)
  gpio.write(led.pin, gpio.HIGH)
  led.isOn = false
  led.isInitted = true
end

function led.on()
  if led.isInitted ~= true then
    led.init()
  end
  
  gpio.write(4, gpio.LOW) --on
  -- print("Led on")
end

function led.off()
  if led.isInitted ~= true then
    led.init()
  end
  
  gpio.write(4, gpio.HIGH) --off
  -- print("Led off")
end

led.blinkTimes = 0
led.delay = 200
function led.blink(val, delay)
  led.blinkTimes = val * 2
  if delay ~= nil and delay > 0 then
    led.delay = delay
  end
  tmr.alarm(led.timerId, led.delay, tmr.ALARM_AUTO, led.onTimer)
end

led.ctr = 0
function led.onTimer()
  
  led.ctr = led.ctr + 1
  
  if led.ctr > led.blinkTimes then 
    tmr.stop(led.timerId)
    led.ctr = 0 
    led.off()
    -- print("Done")
    led.delay = 200 --reset delay to default
    return
  end 
  
  if gpio.read(4) == gpio.HIGH then
    led.on() 
  else 
    led.off()
  end
end 

return led

Using ChiliPeppr's Nodemcu Workspace to Edit Lua Code

I use the ChiliPeppr Nodemcu workspace at http://chilipeppr.com/nodemcu to edit all code and for uploading to the Nodemcu ESP8266 device. It's a really good workflow and I think much better than programming via the Arduino IDE because it fully embraces the flash memory on the ESP8266. This lets you develop different portions of code iteratively and just reference the module.

You can also send and receive Cayenn commands in the serial port console window to debug your code.