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).{You can see this in the SPJS widget by choosing Show / Hide Console in the upper right corner triangle menu.
"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"
}
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.