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