Starting the Badge Server
The Badge server for the SDC6 (2016) conference badge was an Elixir application that took raw events and generated application screens, video AND a rather superb PvP verions of BattleZone/xtanks for a couple of hundred badges.
I ended up re-writing a fair amount of the server in anger at the conference so the resulting code is a spiders-web of gnarly code. So, for 2017, here’s a refactor and some discussion regarding design.
Functionality Basics
- The badges have a serial number.
- They’re dumb terminals and don’t hold any local state.
- The 128x64x1 OLED display is rendered by the server and sent as an .xpm
- They communicate over UDP.
- They have a joystick (with a push button), and a “B” button.
- We want to run multiple “Applications” on the badge.
So, let’s model.
SDC2017.UDP
As all incoming connections to the server will be coming in via UDP on port 2391 we’re going to need a process to manage that. The joy of UDP is that there’s no state to keep track of. The UDP module can be as simple as:
To receive a packet:
- Receive incoming packet (the erlang VM sends us the packet and metadata as a message)
- Extract Serial Number.
- Route payload to a process that manages the badge.
- Cache the Source IP/Port for said packet against serial number.
To send a packet:
- Receive a message from a badge process containing badge serial number and a payload (an xbm)
- Lookup the remote IP/Port as seen on the incoming packet (The source-port fixed in firmware but, if you have multiple badges behind a NAT then the NATing device will assign unique source-ports in order to keep separate the traffic for each badge).
- Create and send the UDP packet.
Creating SDC2017.UDP
require Logger
defmodule SDC2017.UDP do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, nil, [name: __MODULE__])
end
def init(nil) do
{:ok, port} = :gen_udp.open(2391, [{:active, true}, :binary])
{:ok, %{port: port, badgesources: %{}}}
end
This is a pretty standard setup for a GenServer. For the uninitated, a GenServer is a process that takes naps until it receives a message from another process. When it receives a message it does ‘stuff’, then takes another nap. GenServers are the building-blocks of most applications.
The start_link function creates a GenServer process with the name of SDC2016.UDP.
The init function does the initial setup for the process. What do you give a GenServer that has everything? You give it a listening UDP #port<> and an initial state. The state only needs to store:
- The #port<> (which is an internal reference to where the UDP listener lives)
- Our cache of Badge Serial Numbers (badgeids) mapping to Source IP/Port.
Quite simple init?
Receiving a packet!
def handle_info({:udp, _port, ip, udpp, << badgeid::bytes-size(10), payload::binary >>}, state) do
dispatch(payload, badgeid, {ip, udpp}, state)
end
Well, that was short ;-)
If you remember above I said that when packets come in they are sent to our process as messages? When a normal bog-standard message is received by a GenServer the function handle_info is called. The arguments for a UDP message are:
handle_info({:udp, erlang_port, source_ip, source_port, payload}, state)
Elixir automatically populates all the variables with all the values 1:1 with the exception of the payload which may need a brief explaination:
<< badgeid::bytes-size(10), payload::binary >>
This bitstring maps the first 10 bytes to badgeid and the remainder of the payload to payload. Then we call a function call dispatch:
def dispatch(payload, badgeid, {ip, udpp}, state) do
newbadgesources = Map.put(state.badgesources, badgeid, {ip, udpp})
{:noreply, Map.put(state, :badgesources, newbadgesources)}
end
The function ‘dispatch’ will dispatch the payload to the correct Badge Process, but since the Badge Process hasn’t been written yet we’ll skip that fundamental detail and just cache the Source IP/Port against the BadgeID.
Summary
So we’ve gone from a rough approximation as to how this thing will work to something that will receive UDP packets. More to come.