]> hydra-www.ietfng.org Git - acmetensortoys-esp-lua_ctfws/commitdiff
examples/ctfws: First pass at a CtFwS display device
authorNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Tue, 17 Jan 2017 05:56:59 +0000 (00:56 -0500)
committerNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Wed, 18 Jan 2017 02:18:02 +0000 (21:18 -0500)
README.rst [new file with mode: 0644]
ctfws-lcd.lua [new file with mode: 0644]
ctfws.lua [new file with mode: 0644]
init2.lua [new file with mode: 0644]
pushall.sh [new file with mode: 0755]

diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..e4a3755
--- /dev/null
@@ -0,0 +1,213 @@
+########################################
+Capture The Flag With Stuff Glyph Module
+########################################
+
+This is a hardware device designed to assist the `CMU KGB
+<http://www.cmukgb.org/>`_ game of `Capture The Flag With Stuff
+<http://www.cmukgb.org/activities/ctfws.php>`_.
+
+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 (file)
index 0000000..1a40e3b
--- /dev/null
@@ -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 (file)
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 (file)
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 (executable)
index 0000000..81db084
--- /dev/null
@@ -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"