Centrally-set topics:
* ``ctfws/game/config`` the string ``none`` or a whitespace-separated text field:
+
* ``starttime`` -- NTP seconds indicating start state
+
* ``setupduration`` -- setup duration, in seconds
+
* ``rounds`` -- number of rounds
+
* ``roundduration`` -- seconds per round
+
* ``nflags`` -- number of flags per team
+
* any additional fields are to be ignored.
* ``ctfws/game/flags`` -- whitespace-separated text field:
+
* ``red`` -- red team flag capture count (int)
+
* ``yel`` -- yellow team flag capture count (int)
+
* any additional fields are to be ignored.
* ``ctfws/game/endtime`` -- a single number, denoting NTP seconds of a
Device-set topics:
* ``ctfws/dev/$DEVICENAME/beat``
+
* one of ``alive``, ``beat``, or ``dead`` (LWT; no further fields)
* ``time`` (UNIX time, from local clock)
* ``ap`` (MAC addr)
set messages persistent so that devices that (re)connect mid-way into a game
get the latest messages automatically.
- * To start a game::
+* To start a game::
mosquitto_pub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/game/flags -r -m '0 0'
mosquitto_pub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/game/config -r -m `date +%s`' 900 3 900 10'
- * To post information::
+* To post information::
mosquitto_pub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/game/flags -r -m '1 2'
mosquitto_pub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/game/message -r -m 'Red team captured a flag!'
- * To end a game::
+* To end a game::
mosquitto_pub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/game/endtime -r -m `date +%s`
-
+
Jail Glyph Timers
#################
The device should otherwise function more or less as a glorified stopwatch
under centralized control.
+NodeMCU modules used
+====================
+
+Please ensure that your build of NodeMCU supports the following modules:
+
+* ``bit`` (for LCD)
+* ``cjson``
+* ``cron``
+* ``file``
+* ``i2c`` (for LCD)
+* ``mqtt``
+* ``net``
+* ``node``
+* ``rtctime``
+* ``sntp``
+* ``tmr``
+* ``wifi``
+
+Additionally,
+
+* ``mDNS`` may be a good idea, too, if you want to talk to your device over,
+ e.g. telnet, and want it to have a somewhat friendly name.
+
+* ``rtcmem`` may be useful if you wish to stash a little bit of state
+ frequently and don't want to write to flash.
+
+* ``uart`` is in most default builds but is not necessary, if you need space.
+
BOM
===
0 1
01234567890123456789
SETUP : MM:SS.s
- NN⚑: R=0 Y=0
+ NN⚑: R=0 Y=0
messagemessagemessag
START IN : MM:SS.s
0 1
01234567890123456789
ROUND r/R : MM:SS.s
- NN⚑: R=NN Y=NN
+ NN⚑: R=NN Y=NN
messagemessagemessag
JAILBREAK : MM:SS.s
drawDS(lcd,3,13,maxt,self.dl_remain ,rem); self.dl_remain = rem
end
+local function attention(self)
+ if self.attnState then return end
+
+ local tq = self.tq
+ local lcd = self.lcd
+
+ local function doBlink()
+ if self.attnState <= 0 then self.attnState = nil ; return end
+ self.attnState = self.attnState - 1
+ lcd:light(false)
+ tq:queue(250, function() lcd:light(true); tq:queue(500, doBlink) end)
+ end
+
+ self.attnState = 2
+ tq:queue(250, doBlink)
+end
+
-- returns true if timers should keep going or false if we should wait for
-- the next message or event
local function drawTimes(self)
local ctfws = self.ctfws
local rix, maxt, ela = ctfws:times(rtctime.get)
if rix == nil then
- -- XXX beep to get attention
drawNoGame(self.lcd, maxt)
return false
end
if rix ~= self.dl_round then
- if self.dl_round ~= nil then end -- XXX beep when not forcibly reset
+ if self.dl_round ~= nil then attention(self) end -- XXX beep when not forcibly reset
self.dl_round = rix
self.dl_elapsed = nil -- force redraws of times on round boundaries
self.dl_remain = nil
local function drawFlags(self)
local lcd = self.lcd
local ctfws = self.ctfws
- lcd:put(lcd:locate(1,0)," ")
+ if ctfws.flagsN then -- try not to blank a flagsmessage unless we have reason
+ lcd:put(lcd:locate(1,0)," ")
+ end
if ctfws.startT then
local str = string.format("%d\000: R=%d Y=%d",
ctfws.flagsN, ctfws.flagsR, ctfws.flagsY)
self.tq = tq
self.mtmr = t
- self.reset = reset
- self.drawTimes = drawTimes
- self.drawFlags = drawFlags
- self.drawMessage = drawMessage
+ self.attnState = nil
+
+ self.reset = reset
+ self.drawTimes = drawTimes
+ self.drawFlags = drawFlags
+ self.drawMessage = drawMessage
self.drawFlagsMessage = drawFlagsMessage
-- load custom flag glyph
--- common module initialization
-cron.schedule("*/5 * * * *", function(e) dofile("nwfnet-sntp.lc").dosntp(nil) end)
-nwfnet = require "nwfnet"
+-- It's early in boot, so we have plenty of RAM. Compile
+-- the rest of the firmware from source if it's there.
+local k,v
+for k,v in pairs(file.list()) do
+ local ix, _ = k:find("^.*%.lua$")
+ if ix and k ~= "init.lua" then
+ print("early compile",k)
+ node.compile(k); file.remove(k)
+ end
+end
-tq = (dofile "tq.lc")(tmr.create())
-- Hardware initialization
-i2c.setup(0,2,1,i2c.SLOW) -- init i2c as per silk screen (GPIO4, GPIO5)
+i2c.setup(0,2,1,i2c.SLOW) -- init i2c on GPIO4 and GPIO5
lcd = dofile("lcd1602.lc")(0x27)
--- Game logic modules
-ctfws = dofile("ctfws.lc")()
-ctfws:setFlags(0,0)
-
-msg_tmr = tmr.create()
-ctfws_lcd = dofile("ctfws-lcd.lc")(ctfws, lcd, tq, msg_tmr)
-ctfws_tmr = tmr.create()
-
--- Draw the default display
-ctfws_lcd:drawTimes()
-ctfws_lcd:drawFlagsMessage("BOOT...")
-
--- MQTT plumbing
-
-mqc, mqttUser = dofile("nwfmqtt.lc").mkclient("nwfmqtt.conf")
-local mqttBootTopic = string.format("ctfws/dev/%s/beat",mqttUser)
-mqc:lwt(mqttBootTopic,"dead",1,1)
-
--- This is not, properly speaking, OK, but it's so convenient
-ctfws_lcd:drawMessage(string.format("I am: %s", mqttUser))
-
-local myBSSID = "00:00:00:00:00:00"
-
-local mqtt_reconn_cronentry
-local function mqtt_reconn()
- mqtt_reconn_cronentry = cron.schedule("* * * * *", function(e)
- mqc:close(); dofile("nwfmqtt.lc").connect(mqc,"nwfmqtt.conf")
- end)
- dofile("nwfmqtt.lc").connect(mqc,"nwfmqtt.conf")
-end
-
-local mqtt_beat_cronentry
-local function mqtt_beat()
- mqtt_beat_cronentry = cron.schedule("*/5 * * * *", function(e)
- mqc:publish(mqttBootTopic,string.format("beat %d %s",rtctime.get(),myBSSID),1,1)
- end)
-end
-
-local function ctfws_lcd_draw_all()
- ctfws_lcd:reset()
- ctfws_lcd:drawFlags()
- ctfws_lcd:drawTimes()
-end
-
-local function ctfws_start_tmr()
- ctfws_tmr:alarm(100,tmr.ALARM_AUTO,function()
- if not ctfws_lcd:drawTimes() then ctfws_tmr:unregister() end
- end)
-end
-
-nwfnet.onmqtt["init"] = function(c,t,m)
- if t == "ctfws/game/config" then
- ctfws_tmr:unregister()
- if not m or m == "none"
- then ctfws:deconfig()
- else local st, sd, nr, rd, nf = m:match("^%s*(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+).*$")
- if st == nil
- then ctfws:deconfig()
- else -- the game's afoot!
- ctfws:config(tonumber(st), tonumber(sd), tonumber(nr),
- tonumber(rd), tonumber(nf))
- ctfws_start_tmr()
- end
- end
- ctfws_lcd_draw_all()
- elseif t == "ctfws/game/endtime" then
- ctfws:setEndTime(tonumber(m))
- ctfws_lcd_draw_all()
- ctfws_start_tmr() -- might have been unset; restart display if so
- elseif t == "ctfws/game/flags" then
- if not m then ctfws:setFlags(0,0); return end
- local fr, fy = m:match("^%s*(%d+)%s+(%d+).*$")
- if fr ~= nil then
- ctfws:setFlags(tonumber(fr),tonumber(fy))
- ctfws_lcd:drawFlags()
- end
- elseif t:match("^ctfws/game/message") then
- ctfws_lcd:drawMessage(m)
- end
-end
-nwfnet.onnet["init"] = function(e,c)
- if e == "mqttdscn" and c == mqc then
- if mqtt_beat_cronentry then mqtt_beat_cronentry:unschedule() mqtt_beat_cronentry = nil end
- if not mqtt_reconn_cronentry then mqtt_reconn() end
- ctfws_lcd:drawFlagsMessage("MQTT Disconnected")
- elseif e == "mqttconn" and c == mqc then
- if mqtt_reconn_cronentry then mqtt_reconn_cronentry:unschedule() mqtt_reconn_cronentry = nil end
- if not mqtt_beat_cronentry then mqtt_beat() end
- mqc:publish(mqttBootTopic,"alive",1,1)
- mqc:subscribe("ctfws/game/config",1)
- mqc:subscribe("ctfws/game/endtime",1)
- mqc:subscribe("ctfws/game/flags",1)
- mqc:subscribe("ctfws/game/message",1) -- broadcast messages
- mqc:subscribe("ctfws/game/message/jail",1) -- jail-specific messages
- ctfws_lcd:drawFlagsMessage("MQTT CONNECTED")
- elseif e == "wstagoip" then
- if not mqtt_reconn_cronentry then mqtt_reconn() end
- ctfws_lcd:drawFlagsMessage(string.format("DHCP %s",c.IP))
- elseif e == "wstaconn" then
- myBSSID = c.BSSID
- ctfws_lcd:drawFlagsMessage(string.format("WIFI %s",c.SSID))
- elseif e == "sntpsync" then
- -- If we have a game configuration and just got SNTP sync, it might
- -- be that we just lept far into the future, so go ahead and start
- -- the game!
- if ctfws.startT then ctfws_start_tmr() end
- end
-end
+tq = (dofile "tq.lc")(tmr.create())
-ctfws_lcd:drawFlagsMessage("CONNECTING...")
-dofile("nwfnet-diag.lc")(true)
-dofile("nwfnet-go.lc")
+-- give the LCD time to initialize properly
+tq:queue(125, function() dofile("init3.lc") end)
--- /dev/null
+-- common module initialization
+cron.schedule("*/5 * * * *", function(e) dofile("nwfnet-sntp.lc").dosntp(nil) end)
+nwfnet = require "nwfnet"
+
+-- Game logic modules
+ctfws = dofile("ctfws.lc")()
+ctfws:setFlags(0,0)
+
+msg_tmr = tmr.create()
+ctfws_lcd = dofile("ctfws-lcd.lc")(ctfws, lcd, tq, msg_tmr)
+ctfws_tmr = tmr.create()
+
+-- Draw the default display
+ctfws_lcd:drawTimes()
+ctfws_lcd:drawFlagsMessage("BOOT...")
+
+-- MQTT plumbing
+mqc, mqttUser = dofile("nwfmqtt.lc").mkclient("nwfmqtt.conf")
+local mqttBootTopic = string.format("ctfws/dev/%s/beat",mqttUser)
+mqc:lwt(mqttBootTopic,"dead",1,1)
+
+-- This is not, properly speaking, OK, but it's so convenient
+ctfws_lcd:drawMessage(string.format("I am: %s", mqttUser))
+
+local myBSSID = "00:00:00:00:00:00"
+
+local mqtt_reconn_cronentry
+local function mqtt_reconn()
+ mqtt_reconn_cronentry = cron.schedule("* * * * *", function(e)
+ mqc:close(); dofile("nwfmqtt.lc").connect(mqc,"nwfmqtt.conf")
+ end)
+ dofile("nwfmqtt.lc").connect(mqc,"nwfmqtt.conf")
+end
+
+local mqtt_beat_cronentry
+local function mqtt_beat()
+ mqtt_beat_cronentry = cron.schedule("*/5 * * * *", function(e)
+ mqc:publish(mqttBootTopic,string.format("beat %d %s",rtctime.get(),myBSSID),1,1)
+ end)
+end
+
+local function ctfws_lcd_draw_all()
+ ctfws_lcd:reset()
+ ctfws_lcd:drawFlags()
+ ctfws_lcd:drawTimes()
+end
+
+local function ctfws_start_tmr()
+ ctfws_tmr:alarm(100,tmr.ALARM_AUTO,function()
+ if not ctfws_lcd:drawTimes() then ctfws_tmr:unregister() end
+ end)
+end
+
+nwfnet.onmqtt["init"] = function(c,t,m)
+ if t == "ctfws/game/config" then
+ ctfws_tmr:unregister()
+ if not m or m == "none"
+ then ctfws:deconfig()
+ ctfws_lcd_draw_all()
+ else local st, sd, nr, rd, nf = m:match("^%s*(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+).*$")
+ if st == nil
+ then ctfws:deconfig()
+ else -- the game's afoot!
+ ctfws:config(tonumber(st), tonumber(sd), tonumber(nr),
+ tonumber(rd), tonumber(nf))
+ ctfws_start_tmr()
+ end
+ ctfws_lcd_draw_all()
+ end
+ elseif t == "ctfws/game/endtime" then
+ ctfws:setEndTime(tonumber(m))
+ ctfws_lcd_draw_all()
+ ctfws_start_tmr() -- might have been unset; restart display if so
+ elseif t == "ctfws/game/flags" then
+ if not m then ctfws:setFlags(0,0); return end
+ local fr, fy = m:match("^%s*(%d+)%s+(%d+).*$")
+ if fr ~= nil then
+ ctfws:setFlags(tonumber(fr),tonumber(fy))
+ ctfws_lcd:drawFlags()
+ end
+ elseif t:match("^ctfws/game/message") then
+ ctfws_lcd:drawMessage(m)
+ end
+end
+
+-- network callbacks
+
+nwfnet.onnet["init"] = function(e,c)
+ if e == "mqttdscn" and c == mqc then
+ if mqtt_beat_cronentry then mqtt_beat_cronentry:unschedule() mqtt_beat_cronentry = nil end
+ if not mqtt_reconn_cronentry then mqtt_reconn() end
+ ctfws_lcd:drawFlagsMessage("MQTT Disconnected")
+ elseif e == "mqttconn" and c == mqc then
+ if mqtt_reconn_cronentry then mqtt_reconn_cronentry:unschedule() mqtt_reconn_cronentry = nil end
+ if not mqtt_beat_cronentry then mqtt_beat() end
+ mqc:publish(mqttBootTopic,"alive",1,1)
+ mqc:subscribe("ctfws/game/config",1)
+ mqc:subscribe("ctfws/game/endtime",1)
+ mqc:subscribe("ctfws/game/flags",1)
+ mqc:subscribe("ctfws/game/message",1) -- broadcast messages
+ mqc:subscribe("ctfws/game/message/jail",1) -- jail-specific messages
+ ctfws_lcd:drawFlagsMessage("MQTT CONNECTED")
+ elseif e == "wstagoip" then
+ if not mqtt_reconn_cronentry then mqtt_reconn() end
+ ctfws_lcd:drawFlagsMessage(string.format("DHCP %s",c.IP))
+ elseif e == "wstaconn" then
+ myBSSID = c.BSSID
+ ctfws_lcd:drawFlagsMessage(string.format("WIFI %s",c.SSID))
+ elseif e == "sntpsync" then
+ -- If we have a game configuration and just got SNTP sync, it might
+ -- be that we just lept far into the future, so go ahead and start
+ -- the game!
+ if ctfws.startT then ctfws_start_tmr() end
+ end
+end
+
+-- hook us up to the network!
+ctfws_lcd:drawFlagsMessage("CONNECTING...")
+-- dofile("nwfnet-diag.lc")(true)
+dofile("nwfnet-go.lc")
. ./host/pushcommon.sh
-#dopushcompile net/nwfmqtt.lua
-#dopush examples/ctfws/conf/nwfnet.conf
+dopushcompile net/nwfmqtt.lua
+dopush examples/ctfws/conf/nwfnet.conf
dopush examples/ctfws/conf/nwfnet.conf2
dopush examples/ctfws/conf/nwfmqtt.conf
-#dopushcompile _external/dvv-nodemcu-thingies/lcd1602.lua
+dopushcompile _external/lcd1602.lua
dopushcompile examples/ctfws/ctfws.lua
dopushcompile examples/ctfws/ctfws-lcd.lua
+dopushcompile examples/ctfws/init3.lua
dopushcompile examples/ctfws/init2.lua
echo "SUCCESS"