The tablet side of a buttonless BLE smart toy.
Cross-platform Unity / C# client library that talks to the Melbits POD smart toy over Bluetooth Low Energy. Shipped inside the companion app on iOS and Android in 2020 by Melbot Studios, S.L.
This library is one of three components I built for the POD program: the firmware runs on the device itself, this comms library runs inside the Unity companion app on the player's tablet, and a separate factory desktop application was used by the contract manufacturer during assembly. All three speak the same custom Playground protocol over BLE.
Publication notice. This source is published as a portfolio archive with the express written authorization of Melbot Studios, S.L. All rights remain with Melbot Studios — see
LICENSEfor full terms. The commercial UnityBleBridge Unity plugin, on which thePodLib/uBLE/adapter depends, has been removed before publication and must be obtained separately. FOTA production endpoints have been redacted.
The POD is a BLE peripheral with no screen and no buttons. The tablet running the companion app needs to:
- Find and pair with a specific POD belonging to this player — intuitively, with a child as the operator.
- Push and pull mission state to the POD over a custom encrypted protocol so the on-device activity engine can run the mission.
- Render real-time feedback for the player while the POD is off doing its thing (LightPort animations, connection status, firmware update progress).
- Stream sensor data back from the POD for live UI (motion, light, temperature).
- Update the firmware over the air when a new release is available.
PodLib is the entire device ↔ tablet contract on the C# side. It
exposes a strongly-typed, event-driven API that the Unity game code
consumes through a handful of interfaces (IPodManager, IPort,
IDFUDevice), without ever touching the underlying BLE stack
directly.
| Runtime | Unity (game engine), targeting iOS and Android |
| Language | C# / .NET Standard 2.0 (Unity's managed runtime) |
| BLE access | Via the commercial UnityBleBridge plugin (removed from this publication) |
| JSON | Newtonsoft.Json (Json.NET), via NuGet / UPM |
| Size | ~8,500 LOC across 29 source files |
| Doc generation | Sandcastle Help File Builder (XML doc comments → HTML reference) |
┌──────────────────────────────────────────────────────────────┐
│ Unity game code (companion app) │
│ Subscribes to events on IPodManager / IPort │
├──────────────────────────────────────────────────────────────┤
│ PodLib — public API surface │
│ IPodManager · IPort · IDFUDevice · IDevice │
├──────────────────────────────────────────────────────────────┤
│ PodLib — runtime │
│ PodManager (MonoBehaviour) · Port (per-slot FSM) │
│ LinkManager · DFUAgent · FWDatabase │
│ Playground (protocol) · Command · CommandResponse │
│ EventPacket · StreamElement · BroadcastPacket │
│ FileXfer · XferQueue · AssetsPack · PodFile │
│ MLinkTX (LightPort event stream) · PGCT (AES-CTR) │
├──────────────────────────────────────────────────────────────┤
│ PodLib/uBLE — adapter to the commercial BLE plugin │
│ uBLEDevice · uBLEDeviceFinder │
├──────────────────────────────────────────────────────────────┤
│ UnityBleBridge (commercial plugin — not redistributed) │
│ iOS Core Bluetooth · Android BLE · macOS / Windows │
└──────────────────────────────────────────────────────────────┘
The shape that matters: everything that talks to the BLE plugin
lives inside PodLib/uBLE/ — exactly two files. The rest of the
library never imports UnityBleBridge types and would compile against
any other BLE backend that satisfies the BLEDevice / DeviceFinder
contracts. That's the value of an adapter layer: vendor swap is a
two-file rewrite, not a library rewrite.
The same separation applies upward: Unity game code never touches
Port, PodManager, or any concrete class. It talks through
IPodManager and IPort, subscribes to typed events, and is free
of plumbing.
See PodLib/architecture.txt for the
original FSM design notes (states, per-slot operations, event lists).
The POD pairs with the tablet by sitting on top of a circular optical pattern rendered on the screen — Magic Link (see the firmware repo for the device-side decoder and the rationale). This library is the tablet side of that protocol.
Important: this library does not render the LightPort itself.
It cannot — rendering means drawing to a Canvas / RawImage /
MeshRenderer of the game's choosing, with the visual style the game
designer wants, layered into whatever UI flow the app provides. That's
game code, not library code.
What this library does is drive the LightPort by emitting a stream of typed events that tell the game what to render, frame by frame:
public enum LightPortEventType
{
LP_BEGIN, // start rendering the circle
LP_END, // tear it down
LP_WHITE, // this frame must be white (high luminance)
LP_BLACK, // this frame must be black (low luminance)
LP_LEVEL, // legacy variable-level frame (with Level field 0..1)
LP_ERROR // optical handshake aborted / failed
}Per port (each port = one POD slot):
public event Port.MLINKHandlerDelegate MLinkHandler;
// MLINKHandlerDelegate(MLinkTX.LightPortEvent eventType)The game subscribes to MLinkHandler on each IPort and, inside its
own Update() / animation loop, paints the circle according to the
latest LP_WHITE / LP_BLACK / LP_LEVEL instruction. The library
guarantees frame timing and pairing semantics; the game owns the
visuals.
Implementation lives in PodLib/MLinkTX.cs
(event source) and PodLib/Port.cs (per-slot
integration with connection state).
The app can be connected to up to 4 PODs simultaneously, each in
its own IPort slot with an independent FSM:
IDLE → FINDING → ATMATCHING → CONNECTING → DISCCHARS →
SUBSCRIBING → HANDSHAKE → CONNECTED → [DISCONNECTED|RECONNECTING]
Each port has its own:
MLinkHandler(LightPort events)StreamHandler(sensor stream when streaming mode is on)EventHandler(async events from the device's Playground protocol)ConnectionHandler(state transitions with typed reasons)Stickyflag — when set, the port auto-reconnects on unexpected disconnects without re-displaying the LightPort
See PodLib/IPort.cs and
PodLib/Port.cs.
PodLib/Playground.cs and PodLib/Command.cs implement the C# side
of the same custom application protocol the firmware exposes over BLE
NUS. Every command is a typed C# class (with WaitForCommand async
helpers); responses and async events come back through the
CommandResponse / EventPacket parsers.
Application-layer crypto is in
PodLib/PGCT.cs — AES-128-CTR keyed during the
Magic Link handshake, mirroring the firmware's HAL/Crypt.c. The
inline doxygen comments are intentionally kept aligned with the
firmware-side documentation for cross-reference.
The Playground protocol exposes the device's storage and many of its commands as a pseudo-FTP over BLE. This library wraps that into a proper file-transfer agent:
PodLib/FileXfer.cs— single transfer engine with flow control, retries, progress reportingPodLib/XferQueue.cs— multi-transfer scheduling with cancellationPodLib/AssetsPack.cs— bundles individual asset files into a packed format for efficient batch uploadPodLib/PodFile.cs— high-level file abstraction
End-to-end firmware update orchestration on the client side:
PodLib/FWDatabase.cs— polls the production update server, validates manifests against a JSON schema, downloads signed firmware images, verifies SHA-256 hashes (production endpoints redacted in this publication).PodLib/DFUAgent.cs— orchestrates the end-to-end client-side update flow: drives the device into DFU mode, sequences the upload, reports progress to the UI, and handles failure modes (mid-transfer disconnect → resume, bad signature → roll back). The actual Nordic Secure DFU protocol transport (iOS Core Bluetooth / Android Nordic DFU Library) lives in a separate Melbot internal library (Melbot.Platform.AndroidiOS.NativeFeatures) and is invoked from here through a thinAndroidiOSMisc.*façade.
PodLib/BroadcastPacket.cs decodes the POD's custom advertising
manufacturer data — the same packet format the firmware emits with
state flags (Magic Link detected, reconnection in progress, MLLess
mode, etc.). PodLib/CandidateDevice.cs and PodLib/DeviceFinder.cs
classify scan results so the right device gets connected to the right
slot.
Every public member carries proper <summary>, <param>,
<returns>, and <seealso cref="..."> cross-references. When the
library shipped, those XML comments were compiled into a navigable
HTML reference by Sandcastle Help File Builder via a tiny
.shfbproj (omitted from publication — it was scaffolding pointing
at the Unity project, regenerable in seconds). The doc content,
which is what matters, is right there in the source.
This library shipped in 2020 inside Unity ~2019 LTS, on top of the patterns and tooling that were idiomatic at the time. With six years of distance, these are the changes I would deliberately make today.
- Replace events + coroutines with
async/awaitandIAsyncEnumerable<T>. Operations likeAttachNew,WaitForCommand,Reconnect, and the file-transfer agents are all conceptually async, and the current callback + coroutine style (PodLib/WaitForCommand.cs) predates Unity's mature async support. Modern Unity can runTasks on its main thread viaUniTaskorAwaitable, which would collapse a lot of glue code. - Adopt
System.Threading.Channelsfor the stream/event pipelines.
- Extract the protocol layer from
MonoBehaviour. TodayPodLib/PodManager.csinherits fromMonoBehaviour, which couples it to Unity's lifecycle and makes pure-C# unit tests painful. A pure POCO core plus a thin Unity bridge would unlock NUnit / xUnit testing without an editor instance. - Fake
BLEDeviceandDeviceFinderin tests. The adapter pattern already isolates UnityBleBridge — same trick can power test doubles. The fact that we never invested in this in 2020 is the single biggest gap. - Recorded BLE traffic replay. A handful of
.bincaptures from a real device session, replayed against the parser, would catch 90% of regressions inPlayground/CommandResponse/EventPacketfor free.
- Move from inheritance to composition in the per-slot FSM.
Port.csat 623 LOC is honest, but a state-pattern split with one file per state would unlock unit testing per state and remove the longswitchin the message dispatch. - ADRs (Architecture Decision Records) for the load-bearing calls: why a custom file-server abstraction over the BLE NUS service rather than GATT-native characteristics; why AES-CTR (and not AEAD) at the application layer; why we paired multi-slot with per-port FSMs rather than a global session manager.
- Replace AES-CTR with an AEAD construction end-to-end. The
current
PGCT.csprovides confidentiality but no integrity — a man-in-the-middle on the link with known plaintext can flip ciphertext bits and the device cannot detect it. AES-GCM or ChaCha20-Poly1305 closes that. This is the same finding called out in the firmware repo's "What I'd do differently" section, and the fix has to happen on both sides at once. - Make Magic Link mutually authenticated. Today the optical channel transfers a nonce in one direction. A signed challenge back over BLE would close the asymmetry.
- Ship as a UPM (Unity Package Manager) package with a proper
package.json, semver versioning, an.asmdefper assembly boundary (one for the protocol POCOs, one for the Unity-coupled runtime, one for the UnityBleBridge adapter), and explicit IL2CPP[Preserve]annotations on the JSON DTOs. - CI build matrix that compiles against the supported Unity
versions on each PR, plus a
dotnet testjob for the POCO core.
- A structured log sink that surfaces protocol-level events
(connection state changes, command latencies, BLE retries, DFU
progress) to the game's analytics layer. The current logging is
ad-hoc
Debug.Logcalls, which is fine during development and useless in production.
None of this invalidates what shipped — the library was the BLE + protocol foundation of a product that passed certification and reached store shelves. But the engineering bar for "shipped in 2020" and "engineered correctly in 2026" are different bars, and a portfolio that pretends otherwise is not useful to anyone.
This library is one piece of a larger system:
melbits-pod-firmware— The device-side firmware (C, nRF52810, BLE peripheral + custom application protocol + signed DFU bootloader + Magic Link decoder). Already published.
I joined Melbot Studios as Tech Lead for the POD program and owned the full device ↔ tablet communication path, the Melbits Pod firmware and tooling, and construction of initial prototypes end-to-end.
On the tablet side that specifically meant:
- API design — the
IPodManager,IPort,IDFUDevicesurface that the rest of the Unity app consumed. - Protocol implementation — the Playground client (commands, responses, events, streaming, file transfer, encryption).
- Multi-slot runtime — concurrent connections to up to 4 PODs with independent FSMs, candidate matching, and per-slot UX states.
- Magic Link LightPort orchestration — the typed event stream driving the game's optical rendering loop, paired with the firmware-side optical decoder I also wrote.
- OTA / DFU client — manifest fetch, image download + verification, DFU drive, progress reporting, rollback on failure.
- Adapter to the commercial BLE plugin — keeping the rest of the library vendor-agnostic.
- Generated reference docs with Sandcastle from the XML comments.
The non-comms parts of the Unity companion app (game design, game logic, art, AR, sound design) were the work of the rest of the Melbot team — I owned the embedded firmware, embedded layer, the cross-platform comms, and the contract between them and everything else -- including providing support to the game programmers using the firmware, this library, and my other tools, reporting directly to the CEO.
Beyond the POD program, during my time at Melbot Studios I also programmed and optimized other titles the studio shipped — work that lives outside this repository.
This repository is a portfolio archive, not a buildable package.
The library has hard dependencies that are intentionally absent from
this publication — the commercial UnityBleBridge Unity plugin, a
sister Melbot native-features C library used by the DFU agent, the
production FOTA service endpoints, and the surrounding Unity project
that integrated all of it. They are listed in LICENSE for
attribution, not as a build manifest.
The code is published for portfolio, preservation, and educational purposes, not compiled. If
that's what you came for, start at PodLib/IPodManager.cs
and PodLib/IPort.cs — the two public interfaces
that define the entire contract — and follow the references from
there.
This repository is published under a portfolio-only license. All
rights remain with Melbot Studios, S.L. Source-available for reading
and study; redistribution, commercial use, and derivative works are
not permitted without prior written consent. See LICENSE
for full terms and the list of third-party components.
Miguel Angel Exposito — Tech Lead, embedded systems, BLE, cross-platform comms. Find me on LinkedIn · GitHub · Blog.