A Bluetooth dial spinner about 70 mm across, built on the nRF52840. A heavy, balanced stainless flywheel ring spins around a stationary centre that holds a colour OLED and a ring of 48 addressable RGB LEDs.
Dial me spinner main is an open-source printed circuit board by tyevco, from the tyevco/dial-me-spinner repository on GitHub. It is a 4-layer board with 209 parts on 142 nets, routed with 2,444 tracks and 366 vias.
It is built around the MDBT50Q-U1MV2, DRV8837DSGR-MS and TPS61040DBVRG, with the W25Q128JVSIQ, LIS2DW12TR-MS and RT9080-33GJ5 among its other parts. Its parts include 89 capacitors, 50 diodes, 28 resistors, 14 ICs and 5 connectors. It belongs with the nRF52 and nRF5x boards of this library.
A Bluetooth dial spinner about 70 mm across, built on the nRF52840. A heavy,
balanced stainless flywheel ring spins around a stationary centre that holds a
colour OLED and a ring of 48 addressable RGB LEDs. You can press the whole top
to click. A tiny actuator switches the ring electronically between free
spin and magnetic click detents, and it holds either position with zero
power.
The same hardware serves three uses:
Game / decision spinner. You flick the ring and a prize wheel animates on
the LEDs and the OLED, with a tick on every detent and the result shown when
it stops.
PC / phone controller. It works like a Surface Dial: a BLE or USB HID
Radial Controller on Windows, and a wheel or consumer-control device on
macOS, Linux, Android and iOS.
Fidget toy. Long free spins or clicky spins, with a spin and RPM counter.
Status: rev A. Schematics and both PCBs are generated and routed, and the
fab outputs are written; nothing has been built or tested. The schematics
of both boards pass ERC. The only report is one intended pin pair that
build.py waives and lists (U21's QOD tied to its VOUT). Both boards are
placed and routed by layout.py (Freerouting first, then the grid router
finish_route.py for the connections it leaves open) and pass KiCad DRC with
schematic parity: 0 errors and 0 unconnected on both, no warnings on the main
board, two silk-to-edge warnings on the base board. release.py wrote the
fab outputs into hardware/fab/main/ and hardware/fab/base/ only after
that gate passed. The mechanical model's clash check of the routed boards
reports only the intended contacts. A SPICE suite (hardware/sim/, docs/simulation.md)
checks the analog design against the netlists; the part changes it drove
are in the schematics. No prototype has been built and no firmware has been written.
Every runtime, force and spin-time figure in the docs is a calculation or
simulation that still has to be verified on a prototype.
Features (rev A design)
Size
Ø70 × 36 mm. Stationary centre, spinning 316L stainless ring (≈ 117 g, OD 70 / ID 54 / 10 mm) on a single 608 bearing
Detents
24 clicks per turn, strength continuously variable from 0 to about 16.5 mN·m, or free spin (the detent magnet's residual cogging is 2.7 µN·m; the stepper's own cogging still has to be bench-tested, mechanical §7). Switched by a micro linear stepper with a self-locking lead screw (zero holding power)
Rotation sensing
TI TMAG5273 3D Hall sensor reading the same 48-magnet track. Interpolated angle, about 0.05–0.1° after self-calibration, plus µA-level wake on rotation
Display
1.5 in 128×128 RGB PMOLED (SSD1351) behind a Ø30 mm round window
Lights
48 × WS2812C-2020-V6 RGB LEDs under an opal diffuser ring, 2 per detent
Input
Press-down click (the whole top floats on springs over a centre switch), rotation, tap and pick-up (LIS2DW12 accelerometer)
Sound
Magnet-free piezo for detent ticks and UI beeps
Radio
nRF52840 module (Raytac MDBT50Q-U1MV2, u.FL) with an off-board flex antenna in the base. BLE HID, a custom GATT service for a companion app, and USB HID
NFC
NFC-A tag emulation (Type 2 / Type 4) on the nRF52840 NFCT, with a coil in the top cover. Phones read it; the spinner cannot read tags (the phone app reads one and sends the content over BLE)
Power
1S LiPo of about 1000 mAh, TI BQ25180 power-path charger, USB-C. Ship mode, about 38 µA sleep (25 °C typical; power budget)
Estimated runtime
About 9 h of continuous game mode, about 4 weeks of typical desk-controller use per charge (see the power budget)
System overview, how each requirement is met, block diagram, board partition and link, subsystem decisions with the alternatives that were rejected, power tree, power budget and battery life, RF strategy, risk register, open decisions, rough BOM cost
Fabrication and assembly: JLCPCB order settings, sourcing, the link harness and antenna coax, magnet track, bearing prep, rotor balancing, mechanical assembly order
Firmware architecture plan (nRF Connect SDK / Zephyr, MCUboot), including the BQ25180 register init table (§13.1). A plan only: no firmware code exists yet
docs/ design documentation (start with architecture.md;
simulation.md for the SPICE suite, display-study.md
for rev B)
hardware/
design/ the circuit as code
SPEC.md architecture of record (what the schematics implement)
nets.py pin map and the shared net names (the "capture contract")
build.py generates the schematics (.kicad_sch), project file and
netlist from the sheet modules, and runs ERC
layout.py generates the boards (.kicad_pcb) from the netlists:
SPEC §8 placement of the constrained parts, clustering
of the rest, outline, keep-outs, zones, fiducials,
optional Freerouting autoroute, then the grid router
(hardware/tools/finish_route.py) for what it leaves
open, clean-up (GND stitching vias, silkscreen
references), then DRC
release.py fab and review outputs into hardware/fab/<board>/:
gerbers, JLCPCB BOM + CPL, ORDER-NOTES.md, schematic and
board PDFs, 3D renders; refuses to write them unless DRC
and schematic parity are clean
jlc_offsets.json cached per-LCSC-part rotation/origin corrections for the
JLC CPL (written by tools/jlc_cpl.py)
sheets/ one Python module per schematic sheet
main board: main_power, mcu, display, leds, sensors, detent
The schematics in hardware/kicad/ are generated. Do not hand-edit the
.kicad_sch files. Change the sheet modules in hardware/design/sheets/ (and
SPEC.md or nets.py if the architecture changes), then rebuild.
Regenerating the KiCad projects
Requirements:
KiCad 9, providing:
kicad-cli;
the pcbnew Python module, used to write custom footprints and by
layout.py and release.py;
the stock libraries and project template in /usr/share/kicad.
KICAD9_SYMBOL_DIR overrides the symbol path (used by build.py).
KICAD9_FOOTPRINT_DIR overrides the footprint path, and only layout.py
reads it. The project template path
(/usr/share/kicad/template/kicad.kicad_pro) is fixed in schgen.py.
Python 3.10+, with numpy for layout.py, and matplotlib for the grid router.
poppler-utils (pdftoppm), only for --render.
Optionally xvfb-run, which release.py uses for headless 3D renders.
For autorouting only: Java 21 and the Freerouting 2.1.x jar, given
by FREEROUTING_JAR.
From the repository root:
sh
# Both boards: writes hardware/kicad/main/ and hardware/kicad/base/,# runs ERC and exports a netlist for each
python3 hardware/design/build.py
# One board, plus a PDF and one PNG per sheet for review
python3 hardware/design/build.py --board main --render
# Scratch build of selected sheets into another directory, without ERC
python3 hardware/design/build.py --board main --only leds sensors --out /tmp/scratch --no-erc
Each board directory receives:
dial-me-spinner-<board>.kicad_pro, the .kicad_sch sheets and a .net
netlist;
the ERC report erc.rpt;
a project-local library in lib/ (dial_spinner.kicad_sym and
dial_spinner.pretty).
Open the .kicad_pro in KiCad 9 to inspect the result. build.py exits
non-zero if its own connectivity checks fail.
Boards and fab outputs (after build.py, which writes the netlists they read):
sh
# Place both boards and run DRC; add --route to autoroute with Freerouting# (--minutes N is the Freerouting time budget of the first attempt, default 30,# and attempt k gets k x N; --attempts N, default 3, re-runs it while# connections remain; whatever is still open then goes to the grid router;# --resume continues routing the board as it is; --complete runs only the grid# router on it; --finish only cleans up an already routed board)
python3 hardware/design/layout.py [--board main|base][--route][--minutes N][--attempts N][--resume][--complete][--finish]# Gerbers, JLCPCB BOM + CPL, ORDER-NOTES.md, PDFs and 3D renders into# hardware/fab/<board>/; stops if DRC or schematic parity reports an error# (--force writes a review-only package)
python3 hardware/design/release.py [--board main|base][--force]
The CPL corrections come from hardware/design/jlc_offsets.json, so
release.py needs no network access. It stops if a placed part has no entry
there; jlc_cpl.update_offsets() adds one by fetching that part's EasyEDA
footprint, and caches its pads. Before writing the CPL, release.py puts
those EasyEDA pads down at each row's point and angle, the way JLC places the
part, and stops if any pad lands off copper or on a differently numbered pad
of the same name (a symmetric package turned 180° still lands on copper, so
the numbers are the only sign). python3 -m pytest hardware/tools/tests -q
checks that this check passes on both boards and catches a wrong rotation or
origin. The EasyEDA footprint titles and pad centres in jlc_offsets.json come
from the JLCEDA/EasyEDA Official Library (lceda.cn,
easyeda.com), whose copyright belongs to JLCEDA/EasyEDA.
Running the SPICE suite
The suite reads the netlists that build.py writes, so run it after a
schematic change. It needs ngspice 42 or newer and pytest, numpy and scipy
(docs/simulation.md):
sh
python3 -m pytest hardware/sim -q # everything
python3 -m pytest hardware/sim -q -m "not slow"# skip the long Monte Carlo sweeps
It writes hardware/sim/results/report.md and results.json. A check that
shows a real design problem is a strict xfail with the reason, so it cannot
quietly start passing.
Status and next steps
Done: subsystem research with independent verification. Architecture
(SPEC.md). Generator toolchain. Schematics of both boards, ERC-clean
apart from the one documented build.py waiver.
Both boards placed and routed (layout.py: SPEC §8 placement of the
constrained parts, clustering of the rest, Freerouting, then the grid router
finish_route.py), DRC clean with schematic parity.
The fab-output script release.py, which writes Gerbers, the JLCPCB BOM and
CPL (rotations corrected per LCSC part), ORDER-NOTES.md, PDFs and renders
behind a DRC and schematic-parity gate, and its outputs for both boards in
hardware/fab/. The placement fixes from the
mechanical model's clash check (module u.FL facing outward, link connector
J1 at r 19.5, the 2.95 mm SM04B battery connector, R42 clear of the cover
skirt), after which the check reports only the intended contacts. The SPICE
suite in hardware/sim/ and the part changes it drove (VBUS series resistor
R12, a second USB ESD array U12 at the module, the OLED load switch's QOD
tied to its output, R4 68 Ω for the LED rail discharge, 25 V VSYS caps, base
C1 and R4). Assembly, bring-up, mechanical, simulation and firmware-plan
documents.
In progress: the SPEC items in mechanical §3A. The remaining strict
xfails of the SPICE suite are firmware-dependent rules (written into
firmware.md and SPEC §5) or accepted rev-A risks, such as the USB ESD
margin with a short harness, the actuator thrust and the OLED boost's
FB-bias spread (docs/simulation.md).
Not done: no prototype has been built or tested, and no firmware code has
been written. Every item marked "to verify on prototype" in the docs is
still open. The detent actuator's thrust margin is open (architecture risk
R4).
Next:
close the pre-order checks in assembly §2.4
and §13
(LED-cavity silk, boost-inductor spacing), then order both boards with
the outputs in hardware/fab/main/ and hardware/fab/base/: two-sided assembly
for the main board, top side only for the base board (4-layer, ENIG), the
MDBT50Q through JLC Global Sourcing or consignment, the DNP list from
ORDER-NOTES.md;
a printed mechanical mock-up to check the antenna shelf, the harness and
coax path and the ring's ESD bleed path, and to measure the RF antenna
(VNA/RSSI), spin-down and detent feel, plus a bench test of the stepper's
free-mode cogging before the PCB order;
bring-up of the first prototype (docs/bringup.md) and
the items to verify on it;
Long-lead parts to order early: the SSD1351 OLED panel (0 stock at major
distributors in Oct 2026), the MDBT50Q-U1MV2 module (0 stock at JLC: use JLC
Global Sourcing or consign it) and the Molex 146153-0100 antenna (DigiKey/Mouser;
LCSC stocks only the -0050, which is too short).
A Bluetooth dial spinner about 70 mm across, built on the nRF52840. A heavy, balanced stainless flywheel ring spins around a stationary centre that holds a colour OLED and a ring of 48 addressable RGB LEDs.
The separate vNext PCB skill and orchestrator package is available alongside the reference workflow below; existing projects keep their accepted toolchain until an explicit migration.
[G-Love] est un projet de conception d’un système de vêtement électronique (wearable) porté sur les bras., intégrant des bandes LEDSs RGB réactives à l’environnement et aux mouvements du porteur.
209 parts, built around the MDBT50Q-U1MV2, DRV8837DSGR-MS, TPS61040DBVRG and W25Q128JVSIQ. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
Where can I download the Dial me spinner main KiCad files?
From the tyevco/dial-me-spinner repository on GitHub, at the commit this page was made from: the board is hardware/kicad/main/dial-me-spinner-main.kicad_pcb and its schematic hardware/kicad/base/base.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Can I use the Dial me spinner main design in my own project?
Its repository states no licence, so its author keeps all rights: ask tyevco before reusing the design.