Break drawings out to their own files, chase changes around the tree.
While here, dramatically improve documentation of the project. That was
a low bar to pass. ;)
This somewhat sizable project is a re-implementation of
``http://filimin.com/`` of sorts. It speaks MQTT and uses a CAP1188 for
-local touch sensing.
+local touch sensing. Display is via a grid of so-called "NeoPixel" LEDs.
Hardware V1
###########
+------------------------+---------------------------------------------+
| Case | https://www.adafruit.com/products/903 |
+------------------------+---------------------------------------------+
-| Copper Tape | https://www.adafruit.com/products/1128 |
-+------------------------+---------------------------------------------+
| Feed Cable | https://www.adafruit.com/products/744 |
+------------------------+---------------------------------------------+
| USB female | https://www.adafruit.com/products/1833 |
+------------------------+---------------------------------------------+
| FeatherWing Doubler | https://www.adafruit.com/products/2890 |
+------------------------+---------------------------------------------+
-| CAP1188 BOB | https://www.adafruit.com/products/1602 |
+| CAP1188 breakout board | https://www.adafruit.com/products/1602 |
++------------------------+---------------------------------------------+
+| Stacking headers | https://www.adafruit.com/products/2830 |
+------------------------+---------------------------------------------+
+| Female headers | https://www.adafruit.com/products/2886 |
++------------------------+---------------------------------------------+
+| Copper Tape | https://www.adafruit.com/products/1128 |
++------------------------+---------------------------------------------+
+| M/F jumper wires | https://www.adafruit.com/products/826 |
++------------------------+---------------------------------------------+
+| F/F jumper wires | https://www.adafruit.com/products/266 |
++------------------------+---------------------------------------------+
+
+The suggested configuration of the feather stack (hardly mandatory) is
+that the FeatherWing Doubler have two sets of female headers, the RGB array
+have male-only headers (downwards, for mating with the Doubler), and that
+the ESP8266 have stacking headers in place. Hookup is then via the exposed
+female connectors on the top of the ESP8266 board. One could, instead,
+stack the RGB array atop the ESP8266 and hookup via the Doubler's female
+headers, or do away with the Doubler entirely and hook the male pins on the
+ESP8266, etc.
+
+Theory of Operation
+###################
+
+Drawings
+########
+
+Drawings are kept in the filesystem and are only loaded on demand, in an
+effort to improve exensibility and keep heap usage low.
+
+``init2.lua`` publishes a global function ``loaddrawfn(name)`` which rummages
+through the filesystem to load ``draw-${name}.lc``. These files are
+expected to *return a function* which takes, in order,
+
+* a timer, for use in animations. It is guaranteed that this timer is
+ *unregistered* at the time of entry to the drawing functions, so there is
+ no need for static images to do so. The timer will be started after the
+ drawing function returns and will be stopped when appropriate by the rest
+ of the system.
+
+* a ws2812 framebuffer object into which all drwaing should happen. This
+ framebuffer should be closed over in any timer callbacks.
+
+* a color parameter, expressed as three arguments 0-255: green, red, blue
+ (as per ws2812 byte ordering).
+
+``lamp-touch.lua`` (see below) knows the naming scheme used by ``init2.lua``
+and so walks the list of files on the system looking for files whose name
+matches the Lua regex ``draw-(%w+).lc`` and builds a table of all such
+internally.
+
+Touch UI
+########
+
+``init2.lua`` hooks the CAP1188 IRQ pin and dispatches to ``lamp-touch.lua``
+to handle the user interface. Once this file is loaded, it *replaces* the
+IRQ hook with its own function and unregisters itself (by replacing itself
+with ``init2.lua``'s ``ontouch_load`` function) after a timeout of no
+interaction from the user. See ``ontouchdone``.
+
+At present, there are four used touch channels:
+
+* A "blackout" button, which disables drawing to the display and any other
+ user interaction until touched again. This button may be toggled at most
+ once per touch interaction; once toggled, the user must allow the touch
+ handler to timeout. This provides a crude "debounce" facility and allows
+ for things like moving the lamp to a new surface (with new conductive
+ properties) by gripping it in a way that touches this sensor and holding
+ it on the new surface for some seconds to allow the CAP1188 to
+ recalibrate.
+
+* Two color-wheel controls. One advances "quickly", one reverses "slowly"
+ and, if both are active, the wheel advances "slowly".
+
+* A shape selector toggle. This advances through the collections of
+ drawings enumerated at the beginning of a touch event. Each separate
+ interaction will reload the list for ease of development.
+
+Notes
+#####
+
+The lamp has a primitive command interpreter listening on port 23. Each
+command must fit entirely within one TCP packet, a complete and utter abuse
+of the protocol, but one that can usually be reasonably achieved.
+
+Useful commands include:
+
+* ``cap calibrate`` to force the CAP1188 to go through a calibration cycle.
+ While the module is configured to recalibrate itself periodically
+ automatically, one may wish to do so sooner especially if the sensors are
+ in a new environment that is just below the threshold of triggering
+ automatic recalibration.
+
+* ``diag exec LUA`` will ``pcall(loadstring(LUA))``, providing an emergency
+ escape hatch into the Lua interpreter without needing console access
+
+* ``file list`` will enumerate files on the flash
+
+* ``file info`` will show used and free space
+
+* ``file pwrite ...`` can write to the filesystem; don't use it by hand
+ unless you're *especially* masochistic. Use ``host/pushvia.expect`` to
+ drive.
+
+* ``file compile FILE`` invokes ``node.compile(FILE)``
+
+* ``file remove FILE`` removes ``FILE``.
+
+* ``diag heap`` will display the number of free heap bytes
--- /dev/null
+return function(t,fb,g,r,b) fb:fill(g,r,b) end
--- /dev/null
+return function(t,fb,g,r,b)
+ fb:fill(0,0,0)
+ local c = string.char(g,r,b)
+ fb:set( 3,c) fb:set( 5,c)
+ fb:set(10,c) fb:set(11,c) fb:set(12,c) fb:set(13,c) fb:set(14,c)
+ fb:set(19,c) fb:set(20,c) fb:set(21,c)
+ fb:set(28,c)
+end
--- /dev/null
+return function(t,fb,g,r,b)
+ local ix = 1
+ local c = string.char(g,r,b)
+ fb:fill(0,0,0)
+ t:register(50,tmr.ALARM_AUTO,function()
+ fb:fade(2); fb:set(ix,c); dodraw()
+ ix = ix + 1; if ix > fb:size() then ix = 1 end
+ end)
+end
--- /dev/null
+return function(t,fb,g,r,b)
+ local ix = 0
+ local c = string.char(g,r,b)
+ fb:fill(0,0,0)
+ fb:set(25,c) fb:set(26,c) fb:set(19,c) fb:set(28,c) fb:set(29,c) fb:set(30,c)
+ fb:set(23,c) fb:set(15,c) fb:set(7,c) fb:set(8,c)
+ t:register(1000,tmr.ALARM_AUTO,function()
+ if ix == 1
+ then fb:set(19,c) fb:set(27,0,0,0) fb:set(20,0,0,0) fb:set(28,c)
+ else fb:set(19,0,0,0) fb:set(27,c) fb:set(20,c) fb:set(28,0,0,0)
+ end
+ ix = 1 - ix
+ dodraw()
+ end)
+end
--- /dev/null
+return function(t,fb,g,r,b)
+ fb:fill(0,0,0)
+ local c = string.char(g,r,b)
+ fb:set( 1,c) fb:set( 4,c) fb:set( 5,c) fb:set( 8,c)
+ fb:set(10,c) fb:set(11,c) fb:set(14,c) fb:set(15,c)
+ fb:set(18,c) fb:set(19,c) fb:set(22,c) fb:set(23,c)
+ fb:set(25,c) fb:set(28,c) fb:set(29,c) fb:set(32,c)
+end
local mqttHeartTopic = string.format("lamp/%s/boot",mqttUser)
cap = require "cap1188"
+local function drawfailsafe(t,fb,g,r,b) fb:fill(g,r,b) end
+function loaddrawfn(name)
+ local f = loadfile ("draw-%s.lc":format(name))
+ local fn = f and f()
+ if fn then return fn else return drawfailsafe end
+end
+
+
-- telnetd overlay
tcpserv = net.createServer(net.TCP, 120)
tcpserv:listen(23,function(k)
if e == "mqttdscn" and c == mqc then
if mqtt_beat_cancel then mqtt_beat_cancel(); mqtt_beat_cancel = nil end
if not mqtt_reconn_poller then mqtt_reconn() end
- dofile("lamp-draw.lc").xx(remotetmr,remotefb,0,5,0); doremotedraw()
+ remotetmr:unregister()
+ loaddrawfn("xx")(remotetmr,remotefb,0,5,0); doremotedraw()
elseif e == "mqttconn" and c == mqc then
if mqtt_reconn_poller then tq:dequeue(mqtt_reconn_poller); mqtt_reconn_poller = nil end
if not mqtt_beat_cancel then mqtt_beat_cancel = dofile("nwfmqtt.lc").heartbeat(mqc,mqttHeartTopic,tq,mqttHeartbeat) end
end
-- initialize display
-dofile("lamp-draw.lc").xx(remotetmr,remotefb,0,5,5); dodraw()
+loaddrawfn("xx")(remotetmr,remotefb,0,5,5); dodraw()
-- touch overlay loader
function ontouch_load() dofile("lamp-touch.lc") end
+++ /dev/null
--- a table of functions that take framebuffers and colors
-return {
- ["heart"] = function(t,fb,g,r,b)
- t:unregister()
- fb:fill(0,0,0)
- local c = string.char(g,r,b)
- fb:set( 3,c) fb:set( 5,c)
- fb:set(10,c) fb:set(11,c) fb:set(12,c) fb:set(13,c) fb:set(14,c)
- fb:set(19,c) fb:set(20,c) fb:set(21,c)
- fb:set(28,c)
- end,
- ["fill"] = function(t,fb,g,r,b) t:unregister(); fb:fill(g,r,b) end,
- ["xx"] = function(t,fb,g,r,b)
- t:unregister()
- fb:fill(0,0,0)
- local c = string.char(g,r,b)
- fb:set( 1,c) fb:set( 4,c) fb:set( 5,c) fb:set( 8,c)
- fb:set(10,c) fb:set(11,c) fb:set(14,c) fb:set(15,c)
- fb:set(18,c) fb:set(19,c) fb:set(22,c) fb:set(23,c)
- fb:set(25,c) fb:set(28,c) fb:set(29,c) fb:set(32,c)
- end,
- ["laserchase"] = function(t,fb,g,r,b)
- local ix = 1
- local c = string.char(g,r,b)
- fb:fill(0,0,0)
- t:register(50,tmr.ALARM_AUTO,function()
- fb:fade(2); fb:set(ix,c); dodraw()
- ix = ix + 1; if ix > fb:size() then ix = 1 end
- end)
- end,
- ["snake"] = function(t,fb,g,r,b)
- local ix = 0
- local c = string.char(g,r,b)
- fb:fill(0,0,0)
- fb:set(25,c) fb:set(26,c) fb:set(19,c) fb:set(28,c) fb:set(29,c) fb:set(30,c)
- fb:set(23,c) fb:set(15,c) fb:set(7,c) fb:set(8,c)
- t:register(1000,tmr.ALARM_AUTO,function()
- if ix == 1
- then fb:set(19,c) fb:set(27,0,0,0) fb:set(20,0,0,0) fb:set(28,c)
- else fb:set(19,0,0,0) fb:set(27,c) fb:set(20,c) fb:set(28,0,0,0)
- end
- ix = 1 - ix
- dodraw()
- end)
- end
-}
--- GLOBAL: tq, remotefb, leddefault, doremotedraw, mqtt_revert, remotetmr
+-- GLOBAL: tq, remotefb, leddefault, doremotedraw, mqtt_revert, remotetmr, loaddrawfn
local function ledrevert(ix)
if ix < 3 then
dodraw()
end
-return function(m)
+return function(msg)
if mqtt_revert then tq:dequeue(mqtt_revert) end
- local ix, _, d, m, r, g, b = m:find("^(%d+)%s+(%w+)%s+(%x+)%s+(%x+)%s+(%x+)%s*$")
+ local ix, _, d, m, r, g, b = msg:find("^(%d+)%s+(%w+)%s+(%x+)%s+(%x+)%s+(%x+)%s*$")
if ix then
g = tonumber(g,16); r = tonumber(r,16); b = tonumber(b,16)
- local f = loadfile "lamp-draw.lc"
- local fn = f and f()[m]
- if fn then fn(remotetmr,remotefb,g,r,b); doremotedraw()
- else remotefb:fill(g,r,b); doremotedraw() -- failsafe
- end
+
+ remotetmr:unregister()
+ loaddrawfn(m)(remotetmr,remotefb,g,r,b); doremotedraw()
+
-- if there's a duration set, register a timer to reset the display to the default
local dn = tonumber(d)
if dn and dn > 0 then tq:queue(math.min(dn,6870947),ledrevert,0) end
--- globals referenced: isblackout, dodraw, ledfb, ledfb_claimed, remotefb, remotetmr, lamp_announce, tq
+-- globals referenced: isblackout, dodraw, ledfb, ledfb_claimed, remotefb, remotetmr, lamp_announce, tq, loaddrawfn
-- assumptions: gpio.trig(6) is the right thing to do for touch IRQs
local touch_tq = (dofile "tq.lc")(5)
local touch_db_fn = nil
local touchcolor = 40
local touchfns = { }
-local touchfnsenc = { }
local touchfnix = 1
-- Whip through the drawing functions and build indexes
local k,v
-for k,v in pairs(dofile("lamp-draw.lc")) do
- touchfns [#touchfns+1 ] = v
- touchfnsenc[#touchfnsenc+1] = k
- if k == "fill" then touchfnix = #touchfns end
+for k,v in pairs(file.list()) do
+ local ix, _, meth = k:find("^draw-(%w+)\\.lc$")
+ if ix then
+ touchfns[#touchfns+1] = meth
+ if meth == "fill" then touchfnix = #touchfns end
+ end
end
local function claimfb()
cap:mr(0x72,set30) -- link
end)
- lamp_announce(touchfnsenc[touchfnix],touchcolorvec(touchcolor))
+ lamp_announce(touchfns[touchfnix],touchcolorvec(touchcolor))
end
-- leave the ledfb pointing at us; it'll get updated eventually,
-- draw if we've claimed it!
if ledfb == touchfb then
- touchfns[touchfnix](touchtmr,touchfb,touchcolorvec(touchcolor)); dodraw()
+ touchtmr:unregister()
+ loaddrawfn(touchfns[touchfnix])(touchtmr,touchfb,touchcolorvec(touchcolor)); dodraw()
touchtmr:start()
end
end
dopushcompile net/nwfmqtt.lua
dopushcompile cap1188/cap1188.lua
dopushcompile cap1188/cap1188-init.lua
-dopushcompile examples/lamp/lamp-draw.lua
dopushcompile examples/lamp/lamp-touch.lua
dopushcompile examples/lamp/lamp-remote.lua
dopushcompile examples/lamp/telnetd-cap.lua
dopush examples/lamp/conf/nwfmqtt.subs
dopushcompile examples/lamp/init2.lua
+for i in examples/lamp/draw-*.lua; do
+ dopushcompile ${i}
+done
+
echo "SUCCESS"