๐ซ Phygital Laser Tag
B2B system for arenas: tagger and headband firmware, hub and instructor panel
Not a client project but our own product: proof that the team can build a whole system, from ESP32 firmware to the server side and the instructor's panel. The current stage is a virtual test bench: physical prototypes have not been built yet.
The task
A laser tag arena that rents equipment to corporate clients needs a system of its own: taggers, headbands, a hub and an instructor's panel. The game must not stop when the connection drops behind concrete walls. Damage and hit markers must not depend on the link to the hub.
The second problem is the cost of a mistake. A frozen kit in the middle of a paid match means one player less for the whole game, and lost match statistics mean a complaint from the client. There are no physical prototypes yet, so the entire backend and the game logic have to be debugged before the boards go into production.
The solution
A set of programs and firmware for ESP32 and Raspberry Pi 5. What is implemented and confirmed by the repository's code and tests:
- A tagger on ESP32-S3 and a headband on ESP32-C3; the MilesTag II infrared protocol with a codec in Python and in C++.
- Damage is computed at the edge of the network: the headband subtracts HP itself and itself sends the shooter a hit marker over ESP-NOW, while the hub only collects statistics.
- A hub on Raspberry Pi 5: event intake over MQTT, PostgreSQL, Redis, WebSocket, statistics.
- An instructor panel and a hall display in React and TypeScript, password sign-in, two roles.
- A single
protocol.yamlfile as the one source of truth, with code generation of constants for Python and C++. - Golden vectors: the same reference examples check both Python and C++, and the check runs in CI on every commit.
- A Mosquitto broker with no anonymous connections, with topic permissions generated from the contract.
- One firmware for the whole fleet: the kit number is bound in the panel by MAC address and stored in NVS.
- Over-the-air firmware updates, with images distributed by the hub.
- A kit simulator, a load test and demo scenarios.
- A watchdog on the controllers, Cyrillic text on the tagger's screen, a wiring diagram and a procurement budget.
How it works
Damage computed at the edge of the network
The โedge of the networkโ is the device itself, not a server. The tagger sends an IR packet, the headband receives it, subtracts HP and answers the tagger with a hit marker over ESP-NOW (a direct radio link between ESP32 chips, with no router). The hub is not part of the combat loop: it aggregates events and computes statistics. So the game doesn't break when the link to the hub is lost: events accumulate in the device's log and are sent later.
protocol.yaml: one source of truth and code generation
The entire protocol is described in one file, protocol/protocol.yaml, and all pin assignments in hardware/pinout.yaml. From these the constants for Python and a header for C++ are generated, along with the wiring diagram and assembly drawings. Neither the simulator, the backend nor the firmware keeps protocol constants of its own. The pinout generator also refuses to build an obviously broken map, such as an analog input on ADC2, which doesn't work while Wi-Fi is on.
Golden vectors: Python and C++ bit for bit
A golden vector is a reference example, an input and its expected output, recorded once. The bit-packing logic is written by hand, but the Python code and the C++ are checked against the same vectors. Python is the reference; C++ must match it bit for bit and microsecond for microsecond. The first of the four CI runs makes sure the generated files don't drift away from protocol.yaml and pinout.yaml.
Infrared MilesTag II on a 56 kHz carrier
MilesTag II is a laser tag infrared protocol: 14 bits per burst on a 56 kHz carrier. A burst lasts from 19.8 to 28.2 ms, which gives a physical ceiling of about 35 shots per second, and a test locks it in. A match holds up to 128 players and up to 4 teams. The CHANGELOG explains the choice this way: version v1 (MilesTag II) is active for compatibility with Laserwar and Forpost equipment, while the project's own 32-bit v2 is designed but switched off. Compatibility with real equipment has not been verified yet: the parity-bit convention is waiting to be checked on a headband.
An MQTT broker with topic permissions from the contract
Mosquitto starts with passwords and permissions, and there are no anonymous connections. There are two accounts: hub writes commands and device only reports about itself. Topic permissions are generated from the contract and never edited by hand. Separately, the broker has the Nagle algorithm turned off: with it on, two IR beams 50 ms apart reached the headband as one chunk.
One firmware for the fleet and a kit number by MAC
The kit number is not baked into the firmware. A blank board reaches the hub with its MAC address, the instructor binds the address to a slot in the panel, and the number goes into NVS (the controller's non-volatile memory). A burnt-out board is replaced without a rebuild. A board without a number neither shoots nor counts hits, and the number can only be changed in the first 10 minutes after power-up, so that a forged reply can't send working kits into a reboot in the middle of a match.
Over-the-air firmware updates
An image is uploaded to the hub and announced to the fleet as a separate action: you can put the firmware on the hub ahead of time and distribute it when nobody is in the hall. A kit accepts the image only if the role matches, the version differs from its own, no match is running and the charge is above 40 percent. The hash is verified before the boot partition switches, so an interrupted download leaves the working old firmware in place.
LoRa 433 MHz for emergency commands
The emergency channel is part of the architecture: LoRa at 433 MHz, commands only, no more than 10 mW, bypassing Wi-Fi. The modules for it have not been purchased yet, and signing of commands and replay protection have not been started.
The arena simulator and the load test
The whole fleet lives in the simulator: tagger, headband, log and an NVS emulation. The load test on 82 kits checks convergence rather than throughput: events in the database, plus those left in buffers, plus those lost, must equal what the devices generated. A mismatch would mean lost statistics for a match the client paid for.
Reports for the instructor and for the client
Reports are separated by audience. The instructor's report shows everything, including gaps in the logs, and the client's report shows only figures. This is a deliberate decision: a caveat in a corporate client's report costs more than lost events.
Results
- All the game logic, the protocol and the backend were debugged on the simulator before any physical prototype was built.
- The full test run on a live test bench passed five times in a row: 284 to 285 tests, per the development log entry of September 22, 2026.
- The โmerged beamsโ regression was found and closed: in 40 repeats of the scenario after the fixes there were zero losses.
- The load test on 82 kits checks what the devices count against what reached PostgreSQL.
- The procurement budget and the wiring diagram were assembled and cross-checked against each other.
Technologies and why
- ESP32-S3 and ESP32-C3, C++17 (PlatformIO): the tagger and the headband; portable logic is tested on the host rather than with debug prints.
- ESP-NOW: the headband-to-tagger radio link with no router.
- MQTT, Mosquitto 2: communication between devices and the hub.
- Python, FastAPI, PostgreSQL, Redis: the hub: event intake, statistics, match state.
- React, TypeScript, Vite: the instructor panel and the hall display.
- LoRa 433 MHz: emergency commands that bypass Wi-Fi.
- Docker Compose, GitHub Actions: the test bench and four CI runs on every commit.
Status
In development, our own product. The stage is a virtual test bench and simulator; the hub and simulator packages are at version 0.1.0. The latest development log entry is dated September 22, 2026. There are no physical prototypes: IR range, the TSOP4856 receiver tolerance, the energy budget and the clock correction are waiting for measurements.
Open questions per the README: the LoRa emergency channel has not been purchased; TLS is not configured for the venue; there is no log of instructor actions or backup of match history; the headband is not updated over the air; there are no database migrations yet. Next come building prototypes and taking measurements against the checklist.
Interested for your own arena?
Tell us about the venue and equipment โ we'll figure out what's needed to launch.