From 8dee507f8d5b197eaaafdb97a499debb80df2745 Mon Sep 17 00:00:00 2001 From: Nathaniel Wesley Filardo Date: Tue, 17 Jan 2017 00:56:59 -0500 Subject: [PATCH] examples/ctfws: First pass at a CtFwS display device --- README.rst | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++ ctfws-lcd.lua | 157 +++++++++++++++++++++++++++++++++++++ ctfws.lua | 81 +++++++++++++++++++ init2.lua | 122 +++++++++++++++++++++++++++++ pushall.sh | 16 ++++ 5 files changed, 589 insertions(+) create mode 100644 README.rst create mode 100644 ctfws-lcd.lua create mode 100644 ctfws.lua create mode 100644 init2.lua create mode 100755 pushall.sh diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e4a3755 --- /dev/null +++ b/README.rst @@ -0,0 +1,213 @@ +######################################## +Capture The Flag With Stuff Glyph Module +######################################## + +This is a hardware device designed to assist the `CMU KGB +`_ game of `Capture The Flag With Stuff +`_. + +MQTT +#### + +Topic Tree +========== + +All numbers herein are base-10 encoded and devoid of leading zeros for ease +of parsing. + +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 + forced game end. If this is larger than the last ``starttime`` gotten + in a ``config`` message, then the game is considered over. + +* ``ctfws/game/message`` -- Message to be displayed everywhere + +* ``ctfws/game/message/player`` -- Message to be displayed specifically + to players, if they ever come to have their own devices (e.g. apps) + +* ``ctfws/game/message/jail`` -- Message to be displayed specifically at + jail glyph units. For the moment, that's all of them, but maybe we + want to allow other things in the future. + +Messages should be set persistent so that devices that reboot or lose their +connection will display the right thing upon reconnection. + +.. todo:: Flag bitmaps? + + Do we want to publish a bitmap of captured flags or are we happy with + counts? + +Optionally, we may wish to grant read-only views of the above topics to a +guest account for a hypothetical CtFwS app. + +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) + * any additional fields are to be ignored. + + The device should publish ``alive`` at gain of MQTT connectivity and + having registered a last will and testament to set the message ``dead``. + Thereafter, it should periodically publish to ``beat`` messages. + +ACL Configuration +================= + +For example:: + + # global read permissions + pattern read ctfws/# + + # master write to all ctfws parameters + user ctfwsmaster + pattern write ctfws/game/# + + # Per-device permissions to post to their own sub-trees. + user ctfwsdev1 + pattern write ctfws/dev/%u/# + +Example Command Line Usage +========================== + +To watch what's going on in the world:: + + mosquitto_sub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/\# -v + +To send MQTT messages, try variants of these. Note that in all cases, we +set messages persistent so that devices that (re)connect mid-way into a game +get the latest messages automatically. + + * 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:: + + 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:: + + mosquitto_pub -h $MQTT_SERVER -u ctfwsmaster -P asdf -q 1 -t ctfws/game/endtime -r -m `date +%s` + +Jail Glyph Timers +################# + +At each jail glyph, we would install a device consisting of + +* an ESP8266 module +* a beeper +* a LCD (probably a small I2C graphics display or 4x20 text or similar) +* a small lipo battery (and charging circuitry, likely) + +This device is not intended to be interactive in any way; turn it on and let +it do its thing. + +The device would join CMU's wireless network, perform SNTP to get an +accurate clock, and associate with a MQTT server managed by the KGB to +receive updates about the game for display, namely: + +* game configuration (setup duration, N rounds M seconds long) +* game start time +* team scores / flag capture counts +* game over + +It's likely beneficial (or at least, not harmful) for the devices to +heartbeat into their own MQTT topics as well, and may wish to announce which +AP they're associated with. + +The device should otherwise function more or less as a glorified stopwatch +under centralized control. + +BOM +=== + +One possible instantiation, just as a baseline: + ++---+-------------------------------------------------------------+-------+ +| 1 | NodeMCU board (ESP8266+USB serial) | 3.00 | ++---+-------------------------------------------------------------+-------+ +| 1 | 2.5Ah USB power stick | 6.00 | ++---+-------------------------------------------------------------+-------+ +| 1 | 4x20 LCD display | 7.00 | ++---+-------------------------------------------------------------+-------+ +| 1 | Buzzer | 1.00 | ++---+-------------------------------------------------------------+-------+ +| 1 | Small breadboard | 1.00 | ++---+-------------------------------------------------------------+-------+ +| | Jumper wire | 1.00 | ++---+-------------------------------------------------------------+-------+ +| | TOTAL | 19.00 | ++---+-------------------------------------------------------------+-------+ + +Character Display +================= + +Setup time display:: + + 0 1 + 01234567890123456789 + SETUP : MM:SS.s + NN⚑: R=0 Y=0 + messagemessagemessag + START IN : MM:SS.s + +Steady state display:: + + 0 1 + 01234567890123456789 + ROUND r/R : MM:SS.s + NN⚑: R=NN Y=NN + messagemessagemessag + JAILBREAK : MM:SS.s + +Last round display:: + + 0 1 + 01234567890123456789 + ROUND r/R : MM:SS.s + NN⚑: R=NN Y=NN + messagemessagemessag + GAME END : MM:SS.s + +Game over:: + + 0 1 + 01234567890123456789 + GAME OVER + NN⚑: R=NN Y=NN + messagemessagemessag + GAME OVER + +Game not configured:: + + 0 1 + 01234567890123456789 + GAME NOT CONFIGURED + + messagemessagemessag + GAME NOT CONFIGURED + +Configuration Files +=================== + +* ``nwfnet.conf`` has details of how to get connectivity to the network. +* ``nwfnet.conf2`` sets the SNTP server to use +* ``nwfmqtt.conf`` sets the MQTT server and credentials diff --git a/ctfws-lcd.lua b/ctfws-lcd.lua new file mode 100644 index 0000000..1a40e3b --- /dev/null +++ b/ctfws-lcd.lua @@ -0,0 +1,157 @@ +-- fields: +-- dl_* fields are "drawn last", used to avoid writing things that are +-- already correct on screen. They can be set to nil to force refresh. +-- Certain things (flags, messages) are considered rare enough that we +-- don't cache like this. +-- +-- dl_round +-- dl_elapsed +-- dl_remain + +local function drawDScond(max, last, decisec, thresh) + return (max >= thresh) and ((last == nil) or math.floor(last / thresh) ~= math.floor(decisec / thresh)) +end + +-- Save I2C bandwidth by only drawing what we have to, to some approximation +local function drawDS(lcd, row, col, max, last, decisec) + -- If we wanted to support hour-long times, we might do something like + -- this. As we don't, at the moment... + -- if drawDScond(max, last, decisec, 36000) then -- hours and all the way down + -- lcd:put(lcd:locate(row,col), string.format("%02d:%02d:%02d.%d", + -- decisec/3600, decisec/600, (decisec/10)%60, decisec%10)) + -- else + if drawDScond(max, last, decisec, 600) then -- minutes, seconds, and deci + lcd:put(lcd:locate(row,col), string.format("%02d:%02d.%d", decisec/600, (decisec/10)%60, decisec%10)) + elseif drawDScond(max, last, decisec, 10) then -- seconds and deci + lcd:put(lcd:locate(row,col+3), string.format("%02d.%d", (decisec/10)%60, decisec%10)) + else -- just deci + lcd:put(lcd:locate(row,col+6), string.format("%d",decisec%10)) + end +end + +local function drawNoGame(lcd, msg) + local k,r; for k,r in pairs({0,3}) do + lcd:put(lcd:locate(r,0), " ") + lcd:put(lcd:locate(r,(20-#msg)/2), msg) + end +end + +local function drawSteadyTopLine(self,rix,maxt,ela) + local lcd = self.lcd + local ctfws = self.ctfws + if self.dl_elapsed == nil then + lcd:put(lcd:locate(0,0), " ") + if rix == 0 then + lcd:put(lcd:locate(0,0), "SETUP :") + else + if ctfws.rounds >= 10 + then lcd:put(lcd:locate(0,0), string.format("RND %2d/%2d :",rix,ctfws.rounds)) + else lcd:put(lcd:locate(0,0), string.format("ROUND %d/%d :",rix,ctfws.rounds)) + end + end + end + drawDS(lcd,0,13,maxt,self.dl_elapsed,ela); self.dl_elapsed = ela +end + +local function drawSteadyBotLine(self,rix,maxt,rem) + local lcd = self.lcd + if self.dl_remain == nil then + lcd:put(lcd:locate(3,0), " ") + if rix == 0 then + lcd:put(lcd:locate(3,0), "START IN :") + elseif rix < ctfws.rounds then + lcd:put(lcd:locate(3,0), "JAILBREAK :") + else + lcd:put(lcd:locate(3,0), "GAME END :") + end + end + drawDS(lcd,3,13,maxt,self.dl_remain ,rem); self.dl_remain = rem +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 + self.dl_round = rix + self.dl_elapsed = nil -- force redraws of times on round boundaries + self.dl_remain = nil + end + drawSteadyTopLine(self,rix,maxt,ela) + drawSteadyBotLine(self,rix,maxt,maxt-ela) + return true +end + +local function drawFlags(self) + local lcd = self.lcd + local ctfws = self.ctfws + lcd:put(lcd:locate(1,0)," ") + if ctfws.startT then + local str = string.format("%d\000: R=%d Y=%d", + ctfws.flagsN, ctfws.flagsR, ctfws.flagsY) + :sub(1,20) + lcd:put(lcd:locate(1,(20-#str)/2), str) + end +end + +-- Displays only when the game is not configured; useful for initial +-- boot, perhaps. +local function drawFlagsMessage(self, msg) + if self.ctfws.flagsN then return end -- NOP on game configured + lcd:put(lcd:locate(1,0),string.format("%-20s", msg:sub(1,20))) +end + +local function drawMessage(self, msg) + local lcd = self.lcd + local mlen = (msg and #msg) or 0 + -- XXX chirp to get attention + self.mtmr:unregister() + lcd:put(lcd:locate(2,0)," ") + if not msg then return end + if mlen <= 20 + then lcd:put(lcd:locate(2,(20-#msg)/2),msg) + else + -- inspired by lcd:run(), but corrected + local ix = 1 + local function scroller() + if ix <= 20 then lcd:put(lcd:locate(2,20-ix),msg:sub(1,ix)) + elseif ix > mlen then lcd:put(lcd:locate(2,0),msg:sub(ix-19)," ") + else lcd:put(lcd:locate(2,0),msg:sub(ix-19,ix)) + end + if ix >= mlen + 20 then ix = 1 else ix = ix + 1 end + end + self.mtmr:alarm(300, tmr.ALARM_AUTO, scroller) + end +end + +local function reset(self) + self.dl_elapsed = nil + self.dl_remain = nil + self.dl_round = nil +end + +return function(ctfws, lcd, tq, t) + self = {} + self.ctfws = ctfws + self.lcd = lcd + self.tq = tq + self.mtmr = t + + self.reset = reset + self.drawTimes = drawTimes + self.drawFlags = drawFlags + self.drawMessage = drawMessage + self.drawFlagsMessage = drawFlagsMessage + + -- load custom flag glyph + lcd:define_char(0,{ 0x1F, 0x15, 0x1B, 0x15, 0x1F, 0x10, 0x10, 0x0 }) + + return self +end diff --git a/ctfws.lua b/ctfws.lua new file mode 100644 index 0000000..f1e8e34 --- /dev/null +++ b/ctfws.lua @@ -0,0 +1,81 @@ +-- game logic dictionary values: +-- +-- setupD -- deciseconds for setup round +-- roundD -- deciseconds per round +-- rounds* -- number of rounds of game play +-- startT -- NTP seconds of game start +-- endT -- NTP seconds of game end (if set) +-- +-- flagsN* -- total flags +-- flagsR* -- flags captured by the red team +-- flagsY* -- flags captured by the yellow team +-- +-- *'d fields are publicly read + +-- returns round index, this round duration, elapsed time +-- round index: 0 for setup, 1-N for game play, and nil for game over / no game +-- all return times in deciseconds +local function times(self, nowf) + if self.startT == nil then + return nil, "GAME NOT CONFIGURED!" + end + + if self.endT and self.endT >= self.startT then + return nil, "GAME OVER" + end + + local now_sec, now_usec = nowf() + + if now_sec < self.startT then + return nil, "START TIME IN FUTURE" + end + + local elapsed = (now_sec - self.startT) * 10 + math.floor(now_usec / 100000) + + if elapsed < self.setupD then + return 0, self.setupD, elapsed + end + + elapsed = elapsed - self.setupD + + local rounds = math.floor(elapsed / self.roundD) + if rounds >= self.rounds + then return nil, "TIME IS UP" + else -- game still in progress + local roundElapsed = elapsed - rounds * self.roundD + return rounds + 1, self.roundD, roundElapsed + end +end + +local function config(self, st, sd, nr, rd, nf) + self.startT = st + self.setupD = sd * 10 + self.rounds = nr + self.roundD = rd * 10 + self.flagsN = nf +end + +local function deconfig(self) + self.startT = nil + self.rounds = nil + -- leave flagsN alone for end-of-game display logic +end + +local function setFlags(self, fr, fy) + self.flagsR = fr + self.flagsY = fy +end + +local function setEndTime(self,t) + self.endT = t +end + +return function() + local self = {} + self.times = times + self.config = config + self.deconfig = deconfig + self.setFlags = setFlags + self.setEndTime = setEndTime + return self +end diff --git a/init2.lua b/init2.lua new file mode 100644 index 0000000..0afe3f7 --- /dev/null +++ b/init2.lua @@ -0,0 +1,122 @@ +-- common module initialization +cron.schedule("*/5 * * * *", function(e) dofile("nwfnet-sntp.lc").dosntp(nil) end) +nwfnet = require "nwfnet" + +tq = (dofile "tq.lc")(tmr.create()) + +-- Hardware initialization +i2c.setup(0,2,1,i2c.SLOW) -- init i2c as per silk screen (GPIO4, 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 + +ctfws_lcd:drawFlagsMessage("CONNECTING...") +dofile("nwfnet-diag.lc")(true) +dofile("nwfnet-go.lc") diff --git a/pushall.sh b/pushall.sh new file mode 100755 index 0000000..81db084 --- /dev/null +++ b/pushall.sh @@ -0,0 +1,16 @@ +#!/bin/zsh + +set -e -u + +. ./host/pushcommon.sh + +#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 examples/ctfws/ctfws.lua +dopushcompile examples/ctfws/ctfws-lcd.lua +dopushcompile examples/ctfws/init2.lua + +echo "SUCCESS" -- 2.50.1