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.



Friday, October 28, 2016

Cayenn Protocol for ChiliPeppr

Revision Jan 15, 2017

The Cayenn protocol is a new method of attaching add-on devices to ChiliPeppr to extend it so that you can add new toolheads such as lasers, dispensers, tool changers, pick-and-place heads, etc. It is also about getting data into ChiliPeppr from sensors such as current/voltage sensors, heat sensors, distance sensors, touch probes, etc.

The Cayenn protocol consists of several components including:

  • Devices talk to SPJS over UDP and TCP and auto-announce their existence on the network so that SPJS/ChiliPeppr can see them
  • SPJS regurgitates UDP/TCP packets it sees from Cayenn devices back to ChiliPeppr
  • SPJS lets ChiliPeppr send it UDP/TCP commands that it sends out to Cayenn devices
  • Cayenn devices need to allow a list of commands to be uploaded to them via TCP with a unique counter ID on each command so that the main execution of Gcode can sync with each device
  • The coolant on/off pin on your CNC device becomes a sync pin that Cayenn devices should listen to so they know when to execute their list of pre-uploaded commands. This pin needs to be hard-wired to your Cayenn devices for reliability/speed during the execution of the main Gcode.
  • The Gcode widget in ChiliPeppr needs to have a pre-processor in it to see all Cayenn commands and generate the M7/M9 coolant pin toggle and the unique ID for that command so syncing works
  • The Gcode widget needs to allow for the pre-upload of all commands to each Cayenn device and then receipt that the upload is complete and correct
  • On each play of Gcode, the Gcode widget must send out a ResetCtr command to all Cayenn devices. If user starts Gcode at alternate position this should be taken into account.

The protocol expects that your devices can talk to Serial Port JSON Server (SPJS) over TCP and UDP. Thus you need your device on the network. The cheapest way to do this is an ESP8266, but other methods like the ESP32 or an ethernet connected device will work.

The typical flow is ChiliPeppr -> SPJS -> Cayenn device or the reverse Cayenn device -> SPJS -> ChiliPeppr.

There are new features in SPJS v1.93 to support Cayenn. Those features are:

  • From ChiliPeppr you can do the command cayenn-sendudp 10.0.0.255 {"key":"value"} so that you can send out broadcast commands to all devices in one shot.
  • From ChiliPeppr you can do the command cayenn-sendtcp 10.0.0.100 {"key":"value"} so that you can send out guaranteed packets to a specific device.
  • From your device you can send UDP to a broadcast address like 10.0.0.255 and SPJS will see it and regurgitate it to ChiliPeppr
  • From your device you can send TCP packet to a specific SPJS server so you have guaranteed communications. SPJS will see it and regurgitate it to ChiliPeppr.

Cayenn is still a work in progress, but the Cayenn widget for ChiliPeppr can be found at:
Github - https://github.com/chilipeppr/widget-cayenn
Cloud9 - http://ide.c9.io/chilipeppr/widget-cayenn

Sample Cayenn LaserUV Device

There is a Github page that describes in detail a sample latest working Cayenn device for a Laser 3A driver for running a UV BDR209 laser. It includes all of the Lua code tested and known to work.
https://github.com/chilipeppr/cayenn-laseruv



It has code on it and the README describes how to build it and some sample Gcode to drive it.

Cayenn Widget

The Cayenn widget is like a control panel for all of your devices. When a device announces itself it will show an icon in the Cayenn widget. This widget also understands the set structure of a specific Cayenn widget and loads it to see the meta-data like what commands it supports, if it has a toolhead, the 3D Three.js geometry for the toolhead, the active queue list, etc.


The Cayenn widget will show the command queue for each Cayenn device. You can reload the list from the device, add to it, wipe it, etc.



Here's an example of an Air Coolant device.



Here is a sample laser device.




If the Cayenn device is a toolhead, you can provide a Three.js JSON object by creating your toolhead in the Three.js Editor at https://threejs.org/editor/ and then embed it into your Cayenn widget so that the control panel can see the object and show a preview.


Sample Dispenser Widget in ChiliPeppr

Work in progress, but here is the start of a sample widget for a specific Cayenn device.

Sample Gcode that Runs Cayenn Devices

In your Gcode, you need to add active comments. These are similar to the active comments that TinyG defined recently to control heated beds. Active comments are JSON inside a () comment.

The LinearSlideOn and LinearSlideOff commands basically tell the Cayenn device to toggle on the enable of its DRV8825 stepper motor driver. That driver is directly hard-wired into the step/dir pins from the TinyG.

G21
G0 Z10 (move spindle to clearance height)
G0 X0 Y0 (move to home)
({CayennDevice:"DispenserPaste",Cmd:"LinearSlideOn"})
(Configure A axis to linear slide settings. )
{"1":{"sa":18,"tr":0.5," mi":1,"po":1,"p m":3,"pl":0}}
(Axis mode is std like a linear axis.)
{"a":{"am":1,"vm":800,"fr":800,"tn":0,"tm":72}}
({Cmd:"AirOn"})
G0 A-70 (Move linear slide down to -70mm)
({"Cmd":"AugerFwd","Speed":5})
(Move 10mm along x axis)
G1 X5 F100 (100mm/min)
({Cmd:"AugerOff"})
({Cmd:"AirOff"})
G0 A0 X0 (Move linear slide back to 0 while moving X)
({Cmd:"LinearSlideOff"})


Here is the converted Gcode after you drag/drop it into ChiliPeppr. You'll notice that Id's have been added to each Cayenn command. M8/M9 coolant on/off commands have also been added.

G21
G0 Z10 (move spindle to clearance height)
G0 X0 Y0 (move to home)
M8 G4 P0.1 ({"CayennDevice":"DispenserPaste","Cmd":"LinearSlideOn","Id":0})
M9 G4 P0.1 (cayenn)
(Configure A axis to linear slide settings. )
{"1":{"sa":18,"tr":0.5," mi":1,"po":1,"p m":3,"pl":0}}
(Axis mode is std like a linear axis.)
{"a":{"am":1,"vm":800,"fr":800,"tn":0,"tm":72}}
M8 G4 P0.1 ({"Cmd":"AirOn","Id":1})
M9 G4 P0.1 (cayenn)
G0 A-70 (Move linear slide down to -70mm)
M8 G4 P0.1 ({"Cmd":"AugerFwd","Speed":5,"Id":2})
M9 G4 P0.1 (cayenn)
(Move 10mm along x axis)
G1 X5 F100 (100mm/min)
M8 G4 P0.1 ({"Cmd":"AugerOff","Id":3})
M9 G4 P0.1 (cayenn)
M8 G4 P0.1 ({"Cmd":"AirOff","Id":4})
M9 G4 P0.1 (cayenn)
G0 A0 X0 (Move linear slide back to 0 while moving X)
M8 G4 P0.1 ({"Cmd":"LinearSlideOff","Id":5})
M9 G4 P0.1 (cayenn)

Notice that when the linear slide is turned on that A axis configuration information is inserted into the Gcode. This is because the A axis is being shared by multiple axes now and thus settings would have to be applied on each change to the A axis to ensure stepper motor settings are correct. This is not necessarily part of Cayenn, but it is related because Cayenn devices can and most likely will attempt to share the A axis (or other axes) and thus inline Gcode configurations could become commonplace.

Also notice that M8/M9 commands occur at each Cayenn command. The Cayenn devices consider a HIGH on the coolant pin to be a counter increment. They ignore the LOW trigger. On an ESP8266 you need about 10ms to trigger an interrupt so the G4 P0.01 gives you that 10ms pause guarantee. Future devices like the ESP32 may not need a pause because they are faster devices. This should be configurable for each Cayenn device.

Sample Pre-Upload of Commands to Cayenn Device

Based on the sample Gcode above, here are the commands that would be pre-uploaded to the Cayenn device from ChiliPeppr into SPJS. SPJS then regurgitates these commands over TCP to the Cayenn device. Notice the counter Id in each command. These correspond to the order of the commands from the sample Gcode above. This list will get auto-created by the Gcode widget so the Id's are accurate.

cayenn-sendtcp 10.0.0.154 {"Cmd":"CmdQ","Id":0,"RunCmd":{"Cmd":"LinearSlideOn"}}
cayenn-sendtcp 10.0.0.154 {"Cmd":"CmdQ","Id":1,"RunCmd":{"Cmd":"AirOn"}}
cayenn-sendtcp 10.0.0.154 {"Cmd":"CmdQ","Id":2,"RunCmd":{"Cmd":"AugerOn","Speed":10}}
cayenn-sendtcp 10.0.0.154 {"Cmd":"CmdQ","Id":3,"RunCmd":{"Cmd":"AugerOff"}}
cayenn-sendtcp 10.0.0.154 {"Cmd":"CmdQ","Id":4,"RunCmd":{"Cmd":"AirOff"}}
cayenn-sendtcp 10.0.0.154 {"Cmd":"CmdQ","Id":5,"RunCmd":{"Cmd":"LinearSlideOff"}}

This image below is an example of how ChiliPeppr shows you what it's doing when sending the above commands to SPJS, which then forwards to your Cayenn device over TCP so it's a guaranteed transmission.


Sample Lua Code for ESP8266 (Deprecated)

As of Jan 15, 2017 this sample code below for the dispenser it outdated. Please see a newer Cayenn device at https://github.com/chilipeppr/cayenn-laseruv for better, more recent code, known to work.

Here is some sample code for a Dispenser running from an ESP8266. The dispenser is shown below. It has a linear slide controlled by a DRV8825 stepper driver, an auger stepper motor, and a TTL toggle for air pressure. It runs on an ESP8266.



dispenser.lua - main entry file that loads all required libraries

-- Dispenser DMP16
-- Linear Slide with stepper driver
-- Auger for DMP16
-- Air Pump toggle

cayenn = require('cayennv2')
cnc = require('tinyg_read')
linearslide = require('drv8825v2')
auger = require('28BYJv3')

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"

-- opts = nil -- dealloc opts

-- define commands supported
cmds = {
  "ResetCtr", "GetCmds", "GetQ", "WipeQ", "CmdQ", 
  "AugerFwd", "AugerRev", "AugerOff",
  "AirOn", "AirOff",
  "LinearSlideOn", "LinearSlideOff"
  }

queue = {}

-- this is called by Cnc when the ID counter changes/increments
-- which occurs when the Coolant pin toggles up/down. each up 
-- on the pin increments the counter
function onIdChange(id)
  -- we need to execute the cmd associated with this id
  -- print("Got Id change")
  onCmd(queue[id])
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 == "AugerFwd" then
        -- send this cmd to 28BYJ module
        -- if payload.Speed == nil then
        --   print("Error turning on auger fwd without speed")
        -- end
        auger.fwd(payload.Speed)
        print("Turning auger fwd on. Speed: " .. payload.Speed)
      elseif payload.Cmd == "AugerRev" then
        -- send this cmd to 28BYJ module
        -- if payload.Speed == nil then
        --   print("Error turning on auger rev without speed")
        -- end
        auger.rev(payload.Speed)
        print("Turning auger rev on. Speed: " .. payload.Speed)
      elseif payload.Cmd == "AugerOff" then
        -- send this cmd to 28BYJ module
        auger.off()
        print("Turning auger off")
      elseif payload.Cmd == "AirOn" then
        print("Turning air on")
      elseif payload.Cmd == "AirOff" then
        print("Turning air off")
      elseif payload.Cmd == "LinearSlideOn" then
        -- tell drv8825 to enable stepper driver
        linearslide.on()
        print("Enabling linear slide stepper driver")
      elseif payload.Cmd == "LinearSlideOff" then
        -- tell drv8825 to disable stepper driver
        linearslide.off()
        print("Disabling linear slide stepper driver")
      -- elseif payload.Cmd == "LinearGoTop" then
      --   print("Moving linear slide to top")
      -- elseif payload.Cmd == "LinearGoBot" then
      --   print("Moving linear slide to bottom")
      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 Id change from CNC
cnc.addListenerOnIdChange(onIdChange)

cayenn.init(opts)
cnc.init()
linearslide.init()
auger.init()

cayennv2.lua - the Cayenn protocol library. It sets up the Wifi on ESP8266 and the UDP and TCP server. It also lets you send out Announce messages.

-- 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
  
  local bip = wifi.sta.getbroadcast()
  --print("Broadcast addr:" .. bip)
  
  local msg = cjson.encode(jsonTagTable)
  print("Sending msg: " .. msg .. " to ip: " .. bip)
  M.sock:connect(M.port, bip)
  M.sock:send(msg)
  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()

-- 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)

tinyg_read.lua - This library watches the Coolant on/off pin to generate the counter and provide a callback to the main entry Lua code so it can act on the coolant counter changes.

-- 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, "up", 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 m.pinCoolantCallback(level)
  gpio.trig(m.pinCoolant, "up")
  -- this method is called when the coolant pin has an interrupt
  m.idCounter = m.idCounter + 1
  m.onIdChange()
  print("Got coolant pin. Level: " .. level .. " idCounter: " .. m.idCounter)
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()

drv8825v2.lua - This library talks to a stepper motor driver like DRV8825 which is a very common/cheap stepper motor driver. For the dispenser this stepper driver controls the linear slide that raises the dispenser up/down.

-- Control DRV8825 Stepper Motor

local m = {}
-- m = {}

-- which timer are we using so we get no conflicts
-- not using
-- m.tmrNum = 5 
-- m.tmrWaitOnStepNum = 4 

-- m.pinDir = 1 -- not using
-- m.pinStep = 2 -- not using
-- m.pinSleep = 2  
-- m.pinReset = 3
m.pinEnable = 0

-- m.curDir = "f"
m.curEnable = false

function m.init()
  -- gpio.mode(m.pinDir, gpio.OUTPUT)
  -- gpio.mode(m.pinStep, gpio.OUTPUT)
  gpio.mode(m.pinEnable, gpio.OUTPUT)
  -- gpio.mode(motor.pinSleep, gpio.OUTPUT)
  -- gpio.mode(motor.pinReset, gpio.OUTPUT)
  -- gpio.write(m.pinDir, gpio.LOW)
  -- gpio.write(m.pinStep, gpio.LOW)
  -- LOW is enable. HIGH is off.
  m.off()
  gpio.write(m.pinEnable, gpio.HIGH)
  -- gpio.write(motor.pinSleep, gpio.LOW)
  -- gpio.write(motor.pinReset, gpio.LOW)
  print("Setup stepper driver.")
end 

function m.off()
  if m.curEnable then
    -- gpio.write(motor.pin1, gpio.LOW)
    -- gpio.write(motor.pin2, gpio.LOW)
    -- gpio.write(motor.pin3, gpio.LOW)
    -- gpio.write(motor.pin4, gpio.LOW)
    -- print("Turned off all outputs")
    print("Turning driver off.")
    -- LOW is enable
    gpio.write(m.pinEnable, gpio.HIGH)
    m.curEnable = false
  else
    print("Driver off already")
  end
end

function m.on() 
  if m.curEnable == false then
    -- enable it
    -- LOW is enable
    gpio.write(m.pinEnable, gpio.LOW)
    m.curEnable = true
    print("Turning driver on")
  else
    print("Driver on already")
  end
end 

return m
-- m.init()

28BYJv3.lua - This library rotates the small/cheap 5 phase stepper motor that rotates the auger on the dispenser.

-- 28BYJ-48 Stepper Control
-- This Motor has a Gear ratio of 64 , and Stride Angle 5.625°  so this motor has a 4096 Steps.
-- steps = Number of steps in One Revolution  * Gear ratio   .
-- steps= (360°/5.625°)*64"Gear ratio" = 64 * 64 =4096 . this value will 

local motor = {}
-- motor = {}
motor.pin1 = 1 -- this is GPIO14
motor.pin2 = 2 -- this is GPIO12
motor.pin3 = 6 -- this is GPIO13
motor.pin4 = 7 -- this is GPIO15
motor.speed = 20 -- ms between each step call
motor.abspos = 0 -- keep track of step pos with abs integer

function motor.init()

  -- Setup motor pins as out
  gpio.mode(motor.pin1, gpio.OUTPUT)
  gpio.mode(motor.pin2, gpio.OUTPUT)
  gpio.mode(motor.pin3, gpio.OUTPUT)
  gpio.mode(motor.pin4, gpio.OUTPUT)
  print("Setup all motors as output")
  motor.off()

end

function motor.off()
  tmr.stop(6) -- stop the step timer
  gpio.write(motor.pin1, gpio.LOW)
  gpio.write(motor.pin2, gpio.LOW)
  gpio.write(motor.pin3, gpio.LOW)
  gpio.write(motor.pin4, gpio.LOW)
  print("Turned all motor pins off")
end

function motor.fwd(speed)
  if speed then
    motor.speed = speed
  end
  tmr.stop(6)
  tmr.alarm(6, motor.speed, tmr.ALARM_AUTO, motor.fwdStep)
end

function motor.rev(speed)
  if speed then
    motor.speed = speed
  end
  tmr.stop(6)
  tmr.alarm(6, motor.speed, tmr.ALARM_AUTO, motor.revStep)
end

function motor.fwdStep()
 motor.step(1)
end

function motor.revStep()
 motor.step(-1)
end

-- Step
motor.loc = 1
function motor.step(val)
  motor.loc = motor.loc + val
  -- this could overflow
  motor.abspos = motor.abspos + val
  
  if motor.loc == 9 then 
    motor.loc = 1
  elseif motor.loc == 0 then 
    motor.loc = 9
  end

  --print("step " .. motor.loc)
  if motor.loc == 1 then
    gpio.write(motor.pin1, gpio.LOW)
    gpio.write(motor.pin2, gpio.LOW)
    gpio.write(motor.pin3, gpio.LOW)
    gpio.write(motor.pin4, gpio.HIGH)
  elseif motor.loc == 2 then
    gpio.write(motor.pin1, gpio.LOW)
    gpio.write(motor.pin2, gpio.LOW)
    gpio.write(motor.pin3, gpio.HIGH)
    gpio.write(motor.pin4, gpio.HIGH)
  elseif motor.loc == 3 then
    gpio.write(motor.pin1, gpio.LOW)
    gpio.write(motor.pin2, gpio.LOW)
    gpio.write(motor.pin3, gpio.HIGH)
    gpio.write(motor.pin4, gpio.LOW)
  elseif motor.loc == 4 then
    gpio.write(motor.pin1, gpio.LOW)
    gpio.write(motor.pin2, gpio.HIGH)
    gpio.write(motor.pin3, gpio.HIGH)
    gpio.write(motor.pin4, gpio.LOW)
  elseif motor.loc == 5 then
    gpio.write(motor.pin1, gpio.LOW)
    gpio.write(motor.pin2, gpio.HIGH)
    gpio.write(motor.pin3, gpio.LOW)
    gpio.write(motor.pin4, gpio.LOW)
  elseif motor.loc == 6 then
    gpio.write(motor.pin1, gpio.HIGH)
    gpio.write(motor.pin2, gpio.HIGH)
    gpio.write(motor.pin3, gpio.LOW)
    gpio.write(motor.pin4, gpio.LOW)
  elseif motor.loc == 7 then
    gpio.write(motor.pin1, gpio.HIGH)
    gpio.write(motor.pin2, gpio.LOW)
    gpio.write(motor.pin3, gpio.LOW)
    gpio.write(motor.pin4, gpio.LOW)
  elseif motor.loc == 8 then
    gpio.write(motor.pin1, gpio.HIGH)
    gpio.write(motor.pin2, gpio.LOW)
    gpio.write(motor.pin3, gpio.LOW)
    gpio.write(motor.pin4, gpio.HIGH)
  end 
end

return motor
-- motor.init()