This chapter replaces the working specification of v0.6. It is the final description of the design as built, with the reasons.
Version 0.5 gave the FlashKa keychain light a Bluetooth link and a 94.4 × 20 × 16 mm body (chapter 12, kept in the v0.5 folder). Version 0.6 keeps all of that and changes what the hand touches. There are now three stainless-steel buttons on the bottom face and a glowing ring around the main one. There is also a presenter mode: the light becomes a Bluetooth keyboard that turns slides, and the laser stays the pointer. The aluminium body gets a new surface as well: engraved panel seams and laser-marked rivets instead of the diamond knurl.
The chapter was written after the PCB, housing, firmware and web-app work was finished. Numbers come from the build checks in section 13.7. A number marked calculated was worked out for this chapter from those sources.
Status. No v0.6 board exists yet. Everything below is checked in software only: netlist, design rules, 3D fit, host tests, and simulated Bluetooth and serial links. Section 13.9 lists what the first prototype must confirm.
v0.6 is v0.5 plus four additions and the changes that follow from them.
| v0.5 | v0.6 | Why | |
|---|---|---|---|
| Buttons | one side button with a printed cap and a thumb dish | three stainless-steel buttons on the bottom face: ● main, › next, ‹ previous | slides can be turned with the thumb of the hand that holds the light; ● keeps its v0.5 job |
| Indicator | one RGB LED under the lid window | two RGB LEDs around the main button, lit through a resin ring | the state is shown on the button the thumb is on |
| Bluetooth | link to the phone app only | the same, plus presenter mode: the light is a keyboard that pairs with up to 4 computers | a computer needs no dongle and no app |
| Protocol | version 1: 18-byte Status | version 2: 20-byte Status, a Presenter characteristic, commands 0x0A and 0x0B | the app sets up presenter mode and forgets computers |
| Surface | diamond knurl and two side grooves | “fighter-jet skin”: 90° V seams 0.25 mm deep, laser-marked rivets and stencils | a styling choice; it also carries functional marks (SAFE / ARM at the slide switch, LASER near the head) |
These changes follow from the four additions:
| Change | Reason |
|---|---|
| S1 (the side button), its hole, thumb dish and ledge gap are removed | the buttons moved to the floor |
| LED3 moves to the bottom side; the red LED resistors change from 1 kΩ to 330 Ω | the ring needs two LEDs that face the floor, and more red |
| Two lid bosses with bought pads hold the PCB down | pressing a button pushes the board up |
| A second RF window is cut in the floor | the antenna must be able to radiate through the face that points up while presenting |
| The class 2 laser label moves from the bottom face to the lid | the bottom face is now the button face |
What stays the same:
| Item | Value |
|---|---|
| Body | 94.4 × 20 × 16 mm; tray and lid in 6061-T6 aluminium, 29.8 g (calculated from the solids) |
| Board | 24 × 16.5 mm, 4 layers, 0.8 mm; module ESP32-C3-MINI-1-N4, its antenna beyond the board edge under the lid RF window |
| Cells | two LP601646 in parallel, about 800 mAh |
| Main LED | SST-20 on a 10 mm DTP board (a ready-made copper board with a direct thermal path) |
| Laser | the same class 2 green module (520 nm, below 1 mW), switched by the hardware interlock S2 |
| Charger and driver | TP4057 charger on USB-C; the buck converter and current loop with the filtered PWM set point |
| Radio | Bluetooth LE only. There is no USB dongle |
The body is not larger: the envelope is still 30.2 cm³, which is 88 % of the length of v0.4 and 1.56 times the length of a RovyVon Aurora A9 Pro (Figure 13.2).

Figure 13.1 — v0.6 from the control side (the USB-C side). On the lid: the class 2 laser label, the RF window insert, the FLASHKA-06 stencil, the NO STEP hatch over the cell bay, the serial plate and the lanyard slot. Panel seams and rivet rows cover all faces.

Figure 13.2 — v0.6 next to v0.4 and the RovyVon Aurora A9 Pro (outline), same scale.
All three buttons are on the bottom face (the tray floor). Hold the light with the floor up and the head, which has the LED and the laser, pointing away from you. The laser side (+Y, with the S2 slide switch) is then on your left and the USB-C side on your right, and the row reads, from the head to the tail:
› next · ● main · ‹ previous
Next is towards the screen. Previous is towards you. The chevrons laser-marked beside the two small buttons point along the body in the same way.
| Button | Part | X position | Cap | Switch orientation |
|---|---|---|---|---|
| › next | SW2 | 16.4 mm | stainless, head Ø3.4 mm | pads along X |
| ● main | SW1 | 23.6 mm | stainless, head Ø5.8 mm, inside the lit ring | pads along Y |
| ‹ previous | SW3 | 30.8 mm | stainless, head Ø3.4 mm | pads along Y |
All three sit 3.2 mm off the centre line, 7.2 mm apart. X is measured from the front (head) face.
A click is a press shorter than 0.4 s on ● and shorter than 0.8 s on › and ‹. A longer press is a hold. Every contact is debounced: a change must stay stable for 25 ms before it counts. The main button always wins: while ● is pressed, › and ‹ are ignored until all buttons are released. A › or ‹ click counts when you release the button, so pressing ‹ and › together never sends a click of the one that went down first.

Figure 13.3 — The floor seen from below, head on the right. From the tail to the head: the plug of the floor RF window (cream), ‹ previous, ● main inside its lit ring (shown cyan), › next. The chevron beside › points to the head, the one beside ‹ to the tail.
Flashlight mode is the default after power-up. ● works exactly as in v0.5. › and ‹ are new: they step the brightness. The levels are Moon (the dimmest), Low, Medium and High. Turbo is a short boost above High.
| State | Input | Result |
|---|---|---|
| Off | ● 1 click | on at the remembered level |
| Off | ● hold | on in Moon |
| Off | ● 2 clicks | Turbo (High if the profile has no turbo or the board is too warm) |
| Off | ● click, then click and hold | laser while held (at most 60 s; the slide switch S2 must be on ARM); the main LED stays off |
| Off | ● 3 clicks | opens a 120 s Bluetooth window for the app (the ring blinks blue twice) |
| Off | ● 4 clicks | lockout on (red × 3) or off (green × 3) |
| Off | › or ‹ | ignored: no light and no ring |
| Off or on | ‹ + › held 2 s | presenter mode (the main LED goes off) |
| On | ● 1 click | off |
| On | ● hold | steps Low, Medium, High, Low … every 0.6 s |
| On | ● 2 clicks | Turbo on, or back to the previous level |
| On | ● 3 clicks | battery check on the ring: 1 to 5 blinks in green, yellow or red |
| On | › click or hold | one level up: Moon, Low, Medium, High. No wrap. Never into Turbo. From Turbo: nothing |
| On | ‹ click or hold | one level down. No wrap. From Turbo: High |
| Lockout | ● 4 clicks | unlock (green × 3) |
| Lockout | anything else | ignored (● click: one short red flash) |
The reasons:
Levels, profiles, Turbo, the laser rules, protection, charging and the battery estimate work as in v0.5 (firmware.md, section 2.6).
In presenter mode the light is a Bluetooth keyboard. It uses the standard HID over GATT profile: HID (human interface device) is the class of keyboards and mice, and GATT is the way a Bluetooth LE device offers its data. A computer, phone or tablet treats the light like any keyboard. The laser stays the pointer.
| Input | Action (key sent) |
|---|---|
| › click | next slide: Page Down (key set “arrows”: Right Arrow) |
| ‹ click | previous slide: Page Up (key set “arrows”: Left Arrow) |
| › hold 0.8 s | start the show: F5 (sent once, at 0.8 s) |
| ‹ hold 0.8 s | end the show: Esc (sent once) |
| ● click | black screen on or off: B |
| ● hold | laser on while held; same rules as in flashlight mode: S2 armed, at most 60 s, not hot, not flat, never in lockout. It works with or without a computer |
| ● 4 clicks | lockout on; this also leaves presenter mode |
| ‹ + › held 2 s | leave presenter mode |
| ‹ + › held 10 s | forget all paired computers (ring white × 3) |
How it behaves:
Which keys suit which program has not been tested. PowerPoint and LibreOffice Impress take Page Up, Page Down, B and Esc, and PDF viewers take Page Up and Page Down. F5 starts the show on Windows and Linux. It does not start a show on a Mac. For Keynote and Google Slides the firmware manual and the app both recommend the arrow key set (Apple documents the arrow keys, not Page Up and Page Down, for a Keynote show); try both key sets (13.9).
Pairing works only in presenter mode. Enter it with ‹ + › held 2 s (the ring shows magenta × 2, then cyan breathing). The light uses LE Secure Connections, Just Works: there is no PIN, and the computer asks you to confirm. When the computer is connected the ring blinks cyan twice and goes off.
| System | Steps | Note |
|---|---|---|
| Windows 10 (1803 or newer), Windows 11 | Settings, Bluetooth & devices, Add device, Bluetooth, “FlashKa-XXXX” | older Windows 10 cannot pair |
| macOS | System Settings, Bluetooth, “FlashKa-XXXX”, Connect | close the Keyboard Setup Assistant if it opens |
| Android 6 or newer | Settings, Connected devices, Pair new device, “FlashKa-XXXX” | |
| iOS, iPadOS 9 or newer | Settings, Bluetooth, “FlashKa-XXXX”, Pair | the on-screen keyboard hides while a keyboard is connected |
| Linux (BlueZ, kernel 3.19 or newer) | bluetoothctl: scan on, pair, trust, connect |
XXXX is the last two bytes of the Bluetooth address. The light remembers up to 4 computers. A fifth pairing replaces the one paired longest ago. After that, entering presenter mode is enough: the computer reconnects by itself and the ring blinks cyan twice. The pairing steps are untested on all systems.
The ring is two RGB LEDs around ●, driven together by the three status-LED pins of v0.5. It shows, highest priority first:
| Pattern | Meaning |
|---|---|
| Red steady | laser on |
| White × 3 | paired computers forgotten |
| Cyan double blink, then off | presenter mode: computer connected |
| Short white blip (40 ms) | presenter key sent |
| Magenta × 2 | presenter mode entered or left |
| Red × 3 or green × 3 | lockout on or off |
| Cyan × 3 | “Identify” from the app |
| Blue × 2 | Bluetooth window opened (● 3 clicks while off) |
| 1 to 5 blinks in green, yellow or red | battery check (● 3 clicks while on) |
| One red blink | request refused (turbo too hot, laser not allowed, switch-on refused, presenter mode refused) |
| Five fast red blinks | battery empty: light switched off, switch-on refused, or presenter mode ended |
| Short red flash | ● click while locked |
| Cyan breathing (2 s) | presenter mode: waiting for the computer |
| Green, yellow or red for 2 s after switch-on | battery above 50 %, 20 to 50 %, below 20 % |
| Red slow breathing (3 s) | USB connected, charging |
| Green steady | USB connected, charge complete |
| Red blink every 4 s (light on) | battery below 10 % |
| Cyan dim, steady | flashlight mode: app connected |
| Blue double blink every 3 s | flashlight mode: Bluetooth window open, nobody connected |
One-shot patterns (the blinks) are drawn over the steady and breathing states. A presenter one-shot is not cut short by one that ranks lower in the table. Blue is the weakest colour of this LED (4.5 mcd at 2 mA, typical), so the presenter states use cyan.
The schematic and the board are generated from one table of parts, values and connections (tools/kicad/flashka_parts.py). Only the button, LED and resistor part of the v0.5 design changed. The module, charger, LED driver, laser switch and sensors are as in v0.5 (chapter 12).
| Ref | v0.6 | Note |
|---|---|---|
| S1 | removed | no side button |
| SW1 ● | Alps SKRKAEE020 (LCSC C115357): pin 1 BTN, pin 2 GND | main; shorts BTN straight to ground |
| SW2 › | SKRKAEE020: pin 1 BTN_NX, pin 2 GND | next |
| SW3 ‹ | SKRKAEE020: pin 1 BTN_PV, pin 2 GND | previous |
| R30 | 1 kΩ (C11702) between BTN and BTN_NX | ladder step for › |
| R31 | 2.2 kΩ (C25879) between BTN and BTN_PV | ladder step for ‹ |
| R9 | 10 kΩ, 1 %, between 3V3 and BTN: unchanged | pull-up of the ladder |
| LED3 | moved to the bottom side (ring); nets unchanged | |
| LED4 | NH-B1010RGBT-HF (C2874113): pin 1 3V3, pin 2 R_K2, pin 3 G_K2, pin 4 B_K2 | second ring LED |
| R23, R32 | 330 Ω (C25104): R_K and R_K2 to RGB_R | red; it was 1 kΩ in v0.5 |
| R24, R33 | 100 Ω (C25076): G_K and G_K2 to RGB_G | green |
| R25, R34 | 100 Ω: B_K and B_K2 to RGB_B | blue |
The switch is a top-push tact switch (a small switch with a click), 3.9 × 2.9 × 2.0 mm, 1.6 N operating force, 200,000 cycles. Its actuator tip is at Z 7.2 and points down. The cap stroke check uses a switch travel of 0.25 mm.
The result is 71 parts and 44 nets (v0.5: 63 parts, 39 nets). The new nets are BTN_NX, BTN_PV, R_K2, G_K2 and B_K2.
All three switches hang on one net, BTN, with the pull-up R9. Each switch adds a different resistance to ground, so each button gives a different voltage:
3V3 ──[ R9 10 kΩ ]──┬── BTN ── GPIO3 (ADC1_CH3) and GPIO9
│
├── SW1 (● main) ───────────────────────── GND
│
├── R30 1 kΩ ──── SW2 (› next) ─────────── GND
│
└── R31 2.2 kΩ ── SW3 (‹ previous) ─────── GND
With a resistance R to ground the voltage is 3.3 V × R / (10 kΩ + R) (calculated). The firmware reads it with the ADC (analog-to-digital converter) and sorts it into a class:
| Pressed | Resistance to GND | BTN voltage | ADC class | GPIO9 |
|---|---|---|---|---|
| none | – | 3.30 V (the ADC saturates near 1050 mV) | above 810 mV: none | high |
| ● (alone or with any other) | 0 Ω | 0 V | below 106 mV: main | low |
| ‹ + › together | R30 and R31 in parallel, 0.69 kΩ (calculated) | 0.212 V | 106 to 255 mV: both | low |
| › | R30, 1 kΩ | 0.300 V | 256 to 447 mV: next | low |
| ‹ | R31, 2.2 kΩ | 0.595 V | 448 to 810 mV: previous | low |
The thresholds 106, 256 and 448 mV are the midpoints between the nominal levels. The top threshold, 810 mV, sits 215 mV above the ‹ level (calculated). The ● button is a direct short, so it always reads 0 V and wins when pressed together with another button.
Worst-case margins. The table assumes 1 % resistors, an ADC error of ±10 mV (2.5 dB range, after calibration) and a 3.3 V rail within ±2 %. The margins of ›, ‹ + › and ‹ are the values of the firmware report (host test ladder_tolerance and a calculation). The margins of ● and “none”, and the worst-case readings, are calculated for this chapter.
| Class | Nominal | Worst-case reading | Band | Smallest margin |
|---|---|---|---|---|
| ● main | 0 mV | ±10 mV | below 106 mV | 96 mV (calculated) |
| ‹ + › both | 212 mV | 194 to 231 mV (calculated) | 106 to 255 mV | 25.4 mV |
| › next | 300 mV | 279 to 322 mV (calculated) | 256 to 447 mV | 22.7 mV |
| ‹ previous | 595 mV | 564 to 627 mV (calculated) | 448 to 810 mV | 115.7 mV |
| none | 3300 mV | saturated, near 1050 mV | above 810 mV | at least 240 mV (calculated) |
The tightest margin is › at 22.7 mV. Keep the 1 % resistors, and measure the levels on the first board (13.9).
The ESP32-C3 module has 15 usable GPIO pins (GPIO0 to 10 and GPIO18 to 21). All 15 were already in use in v0.5, so there is no free pin for two more buttons. The ladder reads three buttons on the one pin that v0.5 used for one.
The BTN net goes to two pins:
Every pressed level is at or below 0.617 V in the worst case (calculated). That is under the 0.825 V low-level limit (VIL) of the digital pins. So every press is a valid digital low:
The levels are computed for the external pull-up R9 (10 kΩ). The internal pull-up of GPIO9 is switched off after start-up so that it cannot shift them. GPIO9 is never driven by the firmware.
Two NH-B1010RGBT-HF LEDs (NationStar, LCSC C2874113; 1.0 × 1.0 × 0.85 mm; common anode, which means the three colours share one plus pin) sit on the bottom side. LED3 is at X 19.8 and LED4 at X 27.4, both at Y 3.2. Each is 3.8 mm from the main axis (X 23.6) and shines down into the top face of the light-pipe tube (13.4.3). Pin 1 is the common anode, connected to 3V3. Pins 2, 3 and 4 are the red, green and blue cathodes, as the datasheet text says (the drawing could not be read, see 13.9).
The two LEDs share the three pins of the v0.5 status LED: GPIO21 (red), GPIO8 (green) and GPIO2 (blue). Each cathode has its own resistor to the shared pin. A pin pulls low to light its colour.
| Colour | Resistors | Typical forward voltage | Current per LED (calculated) | Both LEDs on one pin (calculated) | Brightness, typical |
|---|---|---|---|---|---|
| Red | R23, R32: 330 Ω | 2.0 V | 3.9 mA | 7.9 mA | 18 mcd at 5 mA |
| Green | R24, R33: 100 Ω | 2.6 V | 7.0 mA | 14 mA | 25 mcd at 2 mA |
| Blue | R25, R34: 100 Ω | 2.7 V | 6.0 mA | 12 mA | 4.5 mcd at 2 mA |
The currents are (3.3 V − forward voltage) / resistance at the typical forward voltage. The values are not measured.
Red was 1 kΩ in v0.5. At 330 Ω the red current rises from about 1.3 mA to about 3.9 mA (calculated): more red comes out of the ring. The firmware’s colour balance (RGB_FULL_R, _G and _B, 255 / 70 / 110 out of 255) still holds the v0.5 values for the 1 kΩ red. It is not tuned yet (13.9). Blue is the weakest colour, so the presenter states use cyan.
The board is 24 × 16.5 mm, 0.8 mm thick, with four layers: F.Cu (top, lid side), In1 (ground plane), In2 and B.Cu (bottom). It uses the same JLCPCB rules as v0.5: 0.13 mm tracks, at least 0.10 mm space, vias 0.2 / 0.45 mm.
Positions use the housing coordinates. X is 0 at the front (head) face and grows to the tail. Y is 0 on the centre line: −Y is the control side (USB-C), +Y the laser side (S2). Z is 0 on the bottom face and grows towards the lid. The board sits on ledges at X 8.8 to 32.8 and Y ±8.25, with its bottom face at Z 9.2 and its top face at Z 10.0. The housing interface fixes the switches, the ring LEDs, J1, S2, U4, J3 and the buck cluster (L1). The PCB script places J2 and J5 and everything else. The numbers are footprint origins (Figure 13.4 shows the wire pads and the three switches):
| Part | X, Y (mm) | Side | Note |
|---|---|---|---|
| SW1 ● main | 23.6, 3.2 | bottom | pads along Y |
| SW2 › next | 16.4, 3.2 | bottom | pads along X |
| SW3 ‹ previous | 30.8, 3.2 | bottom | pads along Y |
| LED3 | 19.8, 3.2 | bottom | ring LED |
| LED4 | 27.4, 3.2 | bottom | ring LED |
| J1 USB-C | 14.6, −4.6 | bottom | at the −Y edge; its courtyard reaches Y +0.67 |
| J2 cell wires | 31.3, −4.8 | bottom | rear −Y corner, rotated 90° |
| J3 LED wires | 10.1, 1.8 | both | one pad on each side at the same spot: LED+ on top, LED− underneath |
| J5 laser wires | 15.55, 6.35 | bottom | narrow-pad footprint so that the S2 peg fits |
| S2 laser interlock | 14.05, 5.85 | top | slide switch at the +Y edge; its slot is in the +Y wall |
| U4 module | 27.2, −1.4 | top | antenna beyond the rear board edge |
| L1 | 14.35, −5.9 | top | buck inductor |

Figure 13.4 — Wire pads and switch positions. Red is plus, dark blue is minus. J3 is one pad on each side at the same spot. The lower view is the board turned over along its long edge, so the USB-C side is at the top: J5 (laser) is on the bottom side, J2 (cells) at the rear corner next to the USB-C side. SW2, SW1 and SW3 are the three buttons.
Two keep-outs protect the new mechanics:
The other parts are placed in hand-made groups and by annealing. The hand-placed groups are the front cluster (the current-sense loop RS1, R14 and RT1, the buck feedback R10, R11 and C9, and R1, R2, R17 and R19), the buck cluster (U2, L1, C5, C7, C8) and the large parts. The remaining 45 parts, all 0402, go into 30 fixed slots chosen by annealing: a random search that scores net length, the distance of decoupling capacitors to their pins, and the room to route out of each pad (seed 82, 250,000 iterations; stored in cad/pcb_placement_sa.json). A check then flips or swaps parts until no pad is walled in. The ladder resistors R30 and R31 sit at their own switches, SW2 and SW3. The module’s courtyard (the keep-clear zone around a part) now shrinks by 0.10 mm like the others, so that a column of 0402 parts fits between the USB-C shell and the module pads.
The board is fully routed with the project’s own router (tools/kicad/route.py). Figure 13.5 shows the four layers.

Figure 13.5 — The routed board, 24 × 16.5 mm: F.Cu (top, lid side), In1 (ground plane; the green tracks are the signal tracks inside the one box described below), In2 and B.Cu (bottom, seen from above).
| Check | Result |
|---|---|
KiCad DRC (production/DRC_report.txt) |
0 errors, 0 unconnected pads, 0 footprint errors |
| DRC warnings | 65, all silkscreen: 40 overlap, 10 silk to board edge, 7 silk over copper, 8 text size (v0.5 had 33) |
| Router exact checks | 0 incomplete nets, 0 ground pads off the plane, clearance audit 0 (at least 0.101 mm) |
The board in numbers, v0.5 and v0.6:
| v0.5 | v0.6 | |
|---|---|---|
| Parts (top / bottom) | 63 | 71 (39 / 32) |
| Nets | 39 | 44 |
| Track segments | 790 | 1079 |
| Track length | 375 mm | 544 mm |
| Vias | 94 | 104 |
By layer, v0.6 has F.Cu 311 segments (124.0 mm), In1 41 (27.5 mm), In2 396 (251.5 mm) and B.Cu 331 (140.8 mm). The extra length comes from the button row and the 8.8 mm keep-out circle on the bottom side. Routing from scratch takes about 35 min (stage 1 about 4 min, the finishing step about 27 min).
The routing differs from v0.5 in these points:
FlashKa:WirePad_SMD_2_P3.0mm_N) so that the S2 peg fits.Assembly files. JLCPCB assembles 68 parts (38 on the top, 30 on the bottom) from 34 BOM lines. J2, J3 and J5 are wire pads, not assembled. The BOM and CPL (the position list) agree with the board: 68 designators in both, no duplicates, sides match, every line has an LCSC number. Rotations on the bottom side are 180° minus the KiCad rotation: SW1 90°, SW2 180°, SW3 90°, LED3 180°, LED4 180°. No correction is applied for the SKRKAEE020 or the NH-B1010RGBT-HF, so check pin 1 and orientation in the JLCPCB order preview (13.9). J1 and S2 use the pad centre in the CPL, as in v0.5.
Reproducible. Rebuilding the outputs from the released board twice gives identical BOM, CPL, netlist, DRC report, schematic and placement files. The gerber files differ only in KiCad’s date lines. A full rebuild from the placement file gives the same copper. KiCad gives new tracks random IDs when it saves, so the board file itself is not byte-identical even when the copper is; use --no-route to rebuild the outputs from the released board.
The housing is still a tray and a lid of 6061-T6 aluminium, 94.4 × 20 × 16 mm. A comparison of the two parameter files shows that 94 housing parameters are identical to v0.5 (calculated). The LED window bore, the laser bore, the cell bay, the four M1.6 lid screws, the USB-C opening, the S2 slot and the lanyard slot did not move. The 20 parameters of the side button, its dish, the diamond knurl and the two side grooves are gone. What is new is the bottom face (three buttons and a light ring), the hold-down for the board, a second radio window and the surface.
Every number in this section comes from one parameter table: class P in tools/housing/flashka_housing.py. The same script writes the STEP files, the drawings and the checks of section 13.7. The coordinates are those of 13.3.5.
| Part | Drawing | Material and process | Per light | Made by |
|---|---|---|---|---|
| Tray | FK-H-01, 2 sheets | 6061-T6, CNC, bead blast, black anodise type II (8 to 12 µm), then laser marking | 1 | JLCCNC |
| Lid | FK-H-02 | the same | 1 | JLCCNC |
| Main cap | FK-H-03 | SUS304 stainless steel, turned, bead blast, no coating | 1, plus 1 spare | JLCCNC or another CNC shop (13.4.4) |
| Slide cap | FK-H-04 | the same | 2, plus 1 spare | the same |
| Print kit | FK-H-05 for the tube (reference only) | SLA resin 8001, translucent, on 1 mm sprues: light-pipe tube, RF window insert, LED spacer ring, antenna spacer with the floor plug, resin copies of the three caps | 1 kit, plus 1 spare | JLC3DP |
| Hold-down pads | lid drawing, item 8a | silicone thermal pad, 1.0 mm; silicone rubber sheet, 1.0 mm | 1 of each | bought and cut to size, not in the kit |
| Laser sleeve | – | resin, only if the laser module is 4.5 mm instead of 6 mm | optional | JLC3DP |
Masses, calculated from the solids: tray 22.68 g and lid 7.14 g (29.8 g together); tube 0.20 g, RF window insert 0.21 g, spacer ring 0.10 g, antenna spacer 1.07 g; main cap 0.86 g and each slide cap 0.32 g.
Figure 13.6 shows how the parts fit together, and Figure 13.7 shows the print kit.

Figure 13.6 — Exploded view. Top to bottom: four M1.6 countersunk screws, the lid with its RF window insert, the two cells, the antenna spacer, the routed 4-layer board, the tray with the light-pipe tube and the three stainless caps. At the head end: the LED on its 10 mm DTP board, the laser module, the resin spacer ring, the glass window and the optional laser sleeve.

Figure 13.7 — The print kit on its sprues (8001 resin). From the left: RF window insert, LED spacer ring, antenna spacer with the plug for the floor window, light-pipe tube, then the resin copies of the main cap and the two slide caps. Cut the sprues with side cutters.
The floor stays flat and 1.5 mm thick. There is no pocket. Each button is a small stainless cap in a hole of the floor. It presses the actuator (the moving part) of its switch.
| Main cap | Slide cap (2, identical) | |
|---|---|---|
| Head, visible | Ø5.8 mm, domed, 0.4 mm sag | Ø3.4 mm, domed, 0.3 mm sag |
| Fit | in the tube bore Ø6.0 mm, 0.1 mm per side | in the floor hole Ø3.6 mm, 0.1 mm per side |
| Flange (the seat) | Ø6.6 × 0.6 mm on the tube step, Z 2.4 to 3.0 | Ø5.0 × 0.6 mm on the floor inner face, Z 1.5 to 2.1 |
| Stem | Ø3.0 mm up to Z 6.9 | Ø2.0 mm up to Z 6.9 |
| Length, apex to stem end | 7.0 mm | 7.0 mm |
| Dome apex at rest | Z −0.1 mm: 0.1 mm proud of the bottom face | the same |
The heights of the main button stack, from the lid side down:
| Z (mm) | What |
|---|---|
| 10.0 and 9.2 | top and bottom face of the board (the bottom face rests on the ledges) |
| 8.35 | faces of the two ring LEDs |
| 8.2 | top of the light-pipe tube, 0.15 mm under the LED faces |
| 7.2 | tip of the switch actuator, pointing down |
| 6.9 | stem end of every cap |
| 3.0 and 2.4 | top and underside of the main cap flange; 2.4 is the step in the tube bore (the seat) |
| 2.1 and 1.5 | top and underside of the slide cap flange; 1.5 is the inner face of the floor (the seat) |
| 1.1 | underside of the tube rim, on the counterbore ledge |
| 0 | bottom face of the tray and end face of the tube |
| −0.1 | dome apex of a cap at rest |
Free play. With a cap on its seat the stem end is 0.30 mm below the actuator tip. This gap is the free play. It keeps every switch unloaded at rest. The thumb moves the cap 0.30 mm without force and then 0.25 mm against the switch (the switch travel), so the whole cap stroke is 0.55 mm. The housing script tested all three caps with the board centred and pushed 0.24 mm to either side wall. In every case the free play is 0.30 mm, the actuator is reached after at most 0.35 mm of cap travel, the cap can be pressed to 0.55 mm, and nothing else is touched.
Tolerance stack. Four tolerances add up in the free play:
| Item | Tolerance |
|---|---|
| Printed step in the tube (the seat of the main cap) | ±0.10 mm |
| Switch height (actuator tip) | ±0.10 mm |
| Ledge height of the floor and of the board | ±0.05 mm |
| Cap length (turned) | ±0.05 mm |
The free play is 0.30 mm nominal. In the worst case it is 0.00 to 0.60 mm. The root-sum-square minimum (the statistical value, calculated by the housing script) is 0.142 mm. In no case does a cap press its switch at rest: at 0.00 mm the stem end only touches the actuator. The slide caps sit on the floor face, not on the printed step, so their stack is tighter than the main cap’s.
The hold-down bows the board down by 0.08 mm at rest (plate model, 13.4.5; 0.03 mm in the soft case and 0.14 mm in the hard case). A bow towards the floor takes up free play. The stack above does not include it. The first prototype must therefore show that no button is pressed at rest (13.9).
The caps are loose parts. A cap is held only by its flange between its seat and the switch. So the direction of the light changes where the caps rest:
| Light | Cap position | Effect |
|---|---|---|
| Floor down (on a table) | on its seat, apex 0.1 mm proud | the table touches the caps. The weight goes through the seats. The switches stay unloaded |
| Floor up (presenting) | fallen onto its actuator, apex 0.2 mm below the floor face (calculated) | the cap touches the actuator but does not press it: the caps weigh 0.86 g and 0.32 g, and a switch needs 1.6 N |
The light weighs about 50 g with its cells (the figure of v0.5; the three steel caps add 1.5 g, calculated). That is about 0.49 N (calculated). Even if all of it rested on the caps, it is far below the 1.6 N of a switch. The working specification gave 0.3 N for the weight; this chapter replaces that number (13.8, item 10).
The caps stand proud, not recessed. MEC-6 prefers a shrouded or recessed button. Section 13.8 says what protects the light from accidental presses instead, and 13.9 lists the pocket test.
The two ring LEDs sit on the underside of the board, 9.2 mm above the floor face. A tube of translucent resin carries their light down to the floor and spreads it into a ring around the main button.
| Item | Value |
|---|---|
| Axis | X 23.6 mm, Y 3.2 mm |
| Height | Z 0 (flush with the bottom face) to Z 8.2 |
| Body | outside diameter 8.4 mm, in the floor hole Ø8.45 mm (0.025 mm per side) |
| Rim | Ø8.8 mm, Z 1.1 to 1.5, in the counterbore Ø8.85 mm. The seat is only 0.2 mm wide (radial): glue the tube in, do not press it |
| Bore | Ø6.0 mm from Z 0 to 2.4 (the cap head); Ø6.8 mm above, up to the top (the cap flange). The step at Z 2.4 is the seat of the main cap |
| Wall | at least 0.8 mm |
| Light input | the top face at Z 8.2: 0.15 mm under the LED faces and 1.0 mm below the underside of the board. The two LEDs sit at radius 3.8 mm, in the middle of the wall (radius 3.4 to 4.2 mm) |
| Lit ring | the end face of the tube at Z 0, between the Ø6.0 mm bore and the Ø8.4 mm outside diameter |
| Clearance | SW1 body to the tube bore 0.97 mm |
The lit ring is the end face of the tube. It is not the 0.1 mm gap between the cap head (Ø5.8 mm) and the bore (Ø6.0 mm), which is only a thin dark line (note 2 on the tube drawing).
How even and how bright the ring is has not been seen on a real part. The datasheet brightness of the LED is low (13.3.4), and the light has to pass 8.2 mm of printed resin (13.9).
The caps are turned from SUS304 stainless steel bar (1.4301), bead blasted and not coated. The drawings give the tolerances: for example head Ø5.8 mm with +0 / −0.05 mm and stem Ø3.0 mm with −0.02 / −0.06 mm; the length is 7.0 ±0.05 mm and the stem end is flat and square to the axis within 0.03 mm. After the blast, check that each cap falls through its bore by its own weight with no catch (the tube bore for the main cap, the floor hole for a slide cap). Blasting must not make the caps stick.
Size caveat. The caps are very small. The pages of JLCCNC do not agree on what it will turn:
| Page | Statement |
|---|---|
| Capabilities | minimum part size 5 × 5 × 1 mm |
| CNC turning | minimum diameter 3 mm, recommended size 6 to 230 mm; the pictures of turned materials do not show stainless steel |
| Home page, FAQ on machining sizes | metal parts on the rotary axis: length above 20 mm and diameter above 4 mm |
Our caps are 7.0 mm long. They break the rule of the FAQ. Do this before ordering the housing:
Order one spare of each cap type. The machine shop’s rules and a drawing with tolerances for every fit that matters are the content of MEC-8.
Why. The board is not screwed down. It lies on two ledges, 0.8 mm wide, and the lid holds it from above. A press on a switch pushes the switch, and the switch pushes the board up. Without a hold-down the board would lift off its ledges and the button would feel soft. Two bosses on the underside of the lid therefore press the board down through bought pads. This is new in v0.6.

Figure 13.8 — The routed board in the open tray, seen from above, head on the left. The two hold-down pads are drawn on the board: the grey one on the module can (X 22.1 to 30.9 mm) and the small salmon one next to SW2 (X 15.65 to 19.35 mm). The lid bosses press on them. The strip beyond the can is the RF-window zone over the antenna, and the cells fill the rear bay.
| Can pad | Front pad | |
|---|---|---|
| Lid boss | X 22.1 to 30.9 mm, Y −7.6 to 4.8 mm (8.8 × 12.4 mm), 1.3 mm tall | X 15.65 to 19.35 mm, Y 0.05 to 3.25 mm (3.7 × 3.2 mm), 3.7 mm tall |
| It presses on | the top of the module can (Z 12.4). The boss stays off the RF window (X 31.8 mm and more) | the component-free area on the top side next to SW2 (X 15.6 to 19.4 mm, Y 0 to 3.3 mm) |
| Pad | silicone thermal gap pad, 1.0 mm, Shore 00 70 to 80 | silicone rubber sheet, 1.0 mm, Shore A 70 |
| Young’s modulus (assumed) | 1.0 MPa | 5.3 MPa |
| Pad area | 108.3 mm² | 11.6 mm² |
| Compressed to | 0.8 mm (20 %) | 0.8 mm (20 %) |
| Spring rate | 108.3 N/mm | 61.6 N/mm |
| Preload | 21.7 N | 12.3 N |
The lid underside is at Z 14.5. The can boss (1.3 mm) and the compressed pad (0.8 mm) fill the 2.1 mm up to the can top at Z 12.4. The total preload is 34.0 N. The targets were a preload of at least 3 N and a pad compression of 15 to 25 %. Stick each pad on its boss with a drop of silicone adhesive. The pads are bought parts and not in the print kit.
The plate model. A press on a button was calculated with a plate model of the board, because the board is thin and bends. The assumptions are:
| Assumption | Value |
|---|---|
| Plate | FR-4, 0.8 mm, E = 20 GPa, ν = 0.14; a plate model that includes shear (first-order shear theory); grid 0.273 × 0.275 mm |
| Edges | free |
| Supports | the ledges, which push but never pull: +Y ledge for X ≥ 12.9 mm, −Y ledge for X ≥ 19.6 mm. There is no ledge at the USB-C opening or at the laser. J1 inside the USB-C opening is not counted as a support |
| Pads | linear springs; stiffness = E × area / free thickness. E comes from the Shore hardness with the usual relation for rubber. These are assumptions, not datasheet values |
| S2 knob | stops the board after it has risen 0.225 mm (the knob stands in a slot of the +Y wall) |
| Load | 3 N and 10 N at a button, spread over 2 × 2 mm |
| Ignored | the stiffness that parts and copper add. The real board is stiffer than this plate |
Two more cases bound the pad assumptions. In the soft case the pad modulus is multiplied by 0.7 and the gap is 0.1 mm larger (the pads are compressed only to 0.9 mm, 10 %). In the hard case the modulus is multiplied by 1.3 and the gap is 0.1 mm smaller (compressed to 0.7 mm, 30 %).
Movement of the board at the button point, towards the lid, in mm at a press of 3 N and 10 N:
| Case | SW2 › (3 N / 10 N) | SW1 ● (3 N / 10 N) | SW3 ‹ (3 N / 10 N) | Preload |
|---|---|---|---|---|
| Nominal | 0.011 / 0.051 | 0.009 / 0.030 | 0.017 / 0.121 | 34.0 N |
| Soft | 0.013 / 0.155 | 0.010 / 0.078 | 0.033 / 0.276 | 11.9 N |
| Hard | 0.010 / 0.034 | 0.008 / 0.028 | 0.016 / 0.070 | 66.3 N |
The board leaves the ledge at SW2, SW1 and SW3 at these presses: nominal 8, 6 and 2 N; soft 3, 3 and 3 N; hard above 12, 10 and 3 N.
Result against the target. The specification asked for no lift below 3 N and at most 0.05 mm of lift at 10 N.
| Target | Result | Verdict |
|---|---|---|
| Preload at least 3 N | 34.0 N (11.9 N soft, 66.3 N hard) | met |
| Movement at 3 N | at most 0.017 mm nominal, at most 0.033 mm in the soft and hard cases | met |
| At most 0.05 mm at 10 N | SW1 0.030 mm. SW2 0.051 mm, marginal. SW3 0.121 mm, 0.276 mm soft, 0.070 mm hard | not met at SW3, marginal at SW2 |
At SW3 the board leaves its ledge at 2 N in the nominal case, that is, below the 3 N of the target, although it has moved only 0.017 mm at 3 N.
Decision: accepted for v0.6. The reasons:
SW3 is the weak point because it sits at X 30.8 mm, at the end of the can pad (X 30.9 mm) and 2 mm from the free end of the board (X 32.8 mm). A harder can pad would need about 3 MPa, which is about 70 N of preload. A third boss with a rubber pad near SW3 (for example X 28.2 to 30.8 mm, Y 5.2 to 7.4 mm) was also modelled: SW3 then moves 0.061 mm at 10 N. That needs a free area on the top side of the board, so it is a v0.7 change.
To verify on the first prototype: press the previous button (‹, SW3) hard and judge the feel. If it feels soft, fit a harder can pad or plan the third pad.
The S2 stop in the model. The model stops the board at the S2 knob after 0.225 mm. In the real 3D model of the board the knob touches the tray only after 0.393 mm (13.4.8). The model was not run again with 0.393 mm. A later stop can only make the movement larger, and only if it matters at all.
The antenna of the module reaches 5.4 mm over the rear edge of the board. In v0.5 it radiated through a window in the lid. While presenting, the floor points up and the lid points into the hand. So the floor has a window, too.
| Item | Value |
|---|---|
| Floor opening | X 33.6 to 38.7 mm, Y ±7.0 mm (5.1 × 14.0 mm), corner radius 1.5 mm |
| Plug | a part of the resin antenna spacer. It fills the opening flush with the floor |
| Lid window (kept) | 6.9 × 14.0 mm, X 31.8 to 38.7 mm, with a resin insert glued in from inside |
| Walls | opening to the side wall 1.5 mm; SW3 hole to the opening 1.0 mm |
| LED3 | it is no longer under the lid window |
Resin does not shield the radio. How far the Bluetooth link reaches with a hand over the floor is not known (13.9).
The diamond knurl and the two side grooves of v0.5 are replaced by a surface like an aircraft fuselage: panel seams, rows of rivets and stencils. It is a styling choice. It also carries marks that matter: SAFE and ARM at the slide switch and LASER at the laser end.

Figure 13.9 — The lid from above, head on the left. Class 2 laser label with the warning triangle (field X 11.72 to 28.57 mm), the RF window opening over the antenna, the FLASHKA-06 stencil, NO STEP with hatching over the cell bay, the serial plate and the lanyard slot. The four screw heads are the M1.6 countersunk lid screws.
Seams. The seams are 90° V grooves, 0.25 mm deep (±0.03 mm). A V cutter with a sharp tip (tip radius at most 0.03 mm) cuts them after the final milling and before the blast. The cutter is the one of the v0.5 grooves.
| Seam | Position |
|---|---|
| Joint ring | at X 3.95 mm. Lid and tray meet in a rabbet (a small step) 0.6 mm high, with 0.05 mm clearance per side, so that frame seams can cross the joint on the flat side without a knife edge |
| Frames (rings round the body) | X 10.2, 30.1, 41, 56, 71 and 86 mm. The frames at X 10.2 and 30.1 mm do not go all the way round, because the S2 slot and the laser bore block the +Y side |
| Stringers (seams along the body) | on both sides at Z 3.0 and Z 13.0 mm (X 3.95 to 86.0 mm); on the lid and on the floor at Y ±3.6 mm (X 41 to 86 mm) |
The metal under any seam is at least 1.11 mm thick (limit 1.1 mm). Every seam is at least 0.945 mm from an opening or hole (limit 0.5 mm). The thickness under each groove is listed in cad/housing_interface.json, key skin_walls. The four long corners of the cavity carry a 0.3 mm 45° gusset, so that metal stays under the corner seams (13.8, item 13).

Figure 13.10 — The laser side (+Y), lid on top, head on the right. At the S2 slot: SAFE towards the head and ARM towards the tail. Panel seams and rivet rows cover the sides.
Rivets and stencils. A laser marks them after anodising. It removes the black anodise, so the bare aluminium shows. Rivet dots are Ø0.45 mm at 1.8 mm pitch and 0.9 mm from the seam centre. There are 151 on the lid, 148 on the floor, 117 on the −Y side and 124 on the +Y side. The marks keep at least 0.358 mm from a seam and 0.525 mm from an opening, with 0 violations on the four faces. The stencils are:
| Stencil | Where |
|---|---|
| FLASHKA-06 | lid and −Y side |
| NO STEP, with hatching | over the cell bay on the lid. The lid is a 1.5 mm plate there |
| LASER ▲ | on the lid near the head |
| SN, a serial plate | lid, X 72.72 to 84.28 mm, Y ±1.9 mm |
| SAFE (towards the head) and ARM (towards the tail) | beside the S2 slot on the +Y side. Armed means the knob towards the tail, as in v0.5 |
| Chevrons | on the floor beside the two small buttons: › next (X 16.4 mm) points to the head, ‹ previous (X 30.8 mm) points to the tail |
The marks in the renders are a simple bright layer. Real laser marks on anodise look less white.
Laser label. The class 2 label moved from the floor to the lid, because the floor is now the button face. The field is X 11.72 to 28.57 mm and Y ±6.9 mm (16.85 × 13.8 mm). The specification asked for X 9 to 30.8 mm and Y ±7.5 mm. The frames at X 10.2 and 30.1 mm and the rivet rows take the rest of that area. The capitals are 0.6 to 0.67 mm high, and the text still holds every mandatory line:
| Line | Text |
|---|---|
| 1 | LASER RADIATION |
| 2 | DO NOT STARE INTO BEAM |
| 3 | CLASS 2 LASER PRODUCT |
| 4 | 520 nm P < 1 mW EN 60825-1:2014 |
The label also shows the warning triangle, “FLASHKA v0.6”, “Li-Po 3.7 V 800 mAh” and “LuxLibre”. A class 3R version of the artwork exists (cad/FlashKa_marking_lid_class3R.dxf). The design uses class 2 only (LAS-1).
The housing script first checked the fit with simple stand-ins for the board. When the routed board existed, fit_pcb.py repeated the check with the KiCad 3D model of that board (cad/FlashKa_main_pcb.step) plus stand-ins for the parts the model does not hold: U4 (module with can and antenna overhang), J1 (USB-C socket, 8.94 × 7.35 × 3.21 mm), SW1 to SW3 with their actuators, LED3, LED4 and L1 (4.0 × 4.0 × 2.0 mm). The assembly measures X 8.80 to 38.20 mm, Y −8.25 to 8.98 mm (the S2 knob), Z 5.99 to 12.40 mm.
Collisions. The board was placed centred and pushed 0.24 mm against each side wall. A collision is an overlap of at least 0.001 mm³.
| Pose | Collisions | Result |
|---|---|---|
| Centred | 0 | – |
| Against the −Y wall | 1: board × tray, 0.0026 mm³, at X 10.15, Y −8.48, Z 9.60 | a contact-level sliver |
| Against the +Y wall | 1: board × tray, 0.0026 mm³, at X 10.15, Y +8.48, Z 9.60 | a contact-level sliver |
There are 0 collisions of 0.01 mm³ or more. The two slivers are at the front corners of the board, where it touches the wall.
Insertion. The board goes in tilted about the S2 knob (an axis through Y 8.80 mm and Z 10.70 mm, the +Y edge low), from 9° down to 0° in 12 steps. It was checked against the tray, the tube, the three resting caps and the laser stand-in. No step has a collision above the 0.002 mm³ threshold of the script. The front corner of J1 touches the tray at 9.0, 8.25, 7.5 and 6.75°, with 0.0019, 0.0018, 0.0016 and 0.0011 mm³. From 6.0° down there is no overlap. J1 stays in contact with the tray down to 4.5° and then moves away from it (0.211 mm at 0°). The S2 knob stays at least 0.215 mm from the tray.
Smallest clearances over the three poses:
| Part | Clearance | Nearest part of the board |
|---|---|---|
| Tray | 0.000 mm (touching) | J1, board against the −Y wall. Centred 0.211 mm; against the +Y wall 0.085 mm (C10) |
| Lid | 0.410 mm | R25. Centred 0.467 mm; against the +Y wall 0.620 mm |
| RF window insert | 1.900 mm | U4 |
| Antenna spacer | 0.150 mm | board, at the rear stop |
| Light-pipe tube | 0.150 mm | LED3 |
| Caps (main, next, previous) | 0.300 mm each | SW1, SW2, SW3: this is the free play |
| Laser | 0.300 mm | board |
| Cell 1 | 2.640 mm | U4 |
| Cell 2 | 1.100 mm | U4 |
| LED board (MCPCB) | 3.600 mm | board |
| Spacer ring, window, LED | more than 5 mm | – |
Free travel of the board from the centred position, until it touches something:
| Direction | Travel | Stopped by |
|---|---|---|
| +Y and −Y | 0.229 mm | tray wall |
| +X (rear) | 0.149 mm | antenna spacer |
| −X (front) | 0.261 mm | tray |
| +Z (up) | 0.393 mm | the S2 knob against the tray |
| −Z | none | the board rests on the ledges at Z 9.2 mm by design |
The complete output is in docs/checks/fit_pcb_report.txt.
Six sheets describe the CNC parts and the tube. They are A3 sheets, 2:1 with details at 10:1, printed here on A4. Every number on the sheets was compared with the parameter table: 275 distinct numbers, 0 mismatches. Thirty probes through the 3D solids against the drawing values also gave 0 mismatches. The layout check lists 8 cosmetic notes, such as a label over a line.
| Sheet | Part | Figure | Files in cad/ (each sheet also as .png, the picture printed here) |
|---|---|---|---|
| FK-H-01, sheets 1 and 2 | tray | 13.11, 13.12 | FlashKa_tray_drawing.pdf (both sheets), FlashKa_tray_drawing.dxf (sheet 1), FlashKa_tray_drawing_sheet2.dxf (sheet 2) |
| FK-H-02 | lid | 13.13 | FlashKa_lid_drawing.pdf, .dxf |
| FK-H-03 | main cap | 13.14 | FlashKa_cap_main_drawing.pdf, .dxf |
| FK-H-04 | slide cap | 13.15 | FlashKa_cap_slide_drawing.pdf, .dxf |
| FK-H-05 | light-pipe tube (reference) | 13.16 | FlashKa_tube_drawing.pdf, .dxf |
The copies for the order are in order_files/2_JLCCNC/. There each PDF has the base name of its part and sits next to the STEP file of the same name: FlashKa_tray.pdf, FlashKa_lid.pdf, FlashKa_cap_main.pdf and FlashKa_cap_slide.pdf. The tube is a resin part of the print kit, so its drawing is not part of the CNC order.

Figure 13.11 — Tray, sheet 1 of 2 (FK-H-01): top view, front view, side views, sections A-A, B-B, C-C and D-D, the feature table and the notes (material 6061-T6, anodise type II, general tolerances ISO 2768-mK).

Figure 13.12 — Tray, sheet 2 of 2 (FK-H-01): the bottom view with the three button holes and the floor RF window, the side views with the seam layout, the V-groove detail and the notes for the laser marking.

Figure 13.13 — Lid (FK-H-02): top view, underside with the two hold-down bosses, sections J-J, K-K (rabbet lip) and G-G, and the feature table. The pads and the RF window insert are separate parts.

Figure 13.14 — Main cap (FK-H-03), half-section at 10:1: SUS304, head Ø5.8 mm, flange Ø6.6 mm, stem Ø3.0 mm, 7.0 mm long. Note 2 gives the size caveat of 13.4.4 and note 3 the free-fall test.

Figure 13.15 — Slide cap (FK-H-04, two parts), half-section at 10:1: SUS304, head Ø3.4 mm, flange Ø5.0 mm, stem Ø2.0 mm, 7.0 mm long.

Figure 13.16 — Light-pipe tube (FK-H-05, reference drawing: a resin part of the print kit, not a CNC part). The lit ring is the end face of the tube between the Ø6.0 mm bore and the Ø8.4 mm outside diameter (13.4.3, note 2).
The firmware is Rust code for the ESP32-C3, in two crates. flashka-core holds every decision of the light. It has no access to hardware, so it is tested on the host computer. flashka-fw runs on the chip: it reads the pins, drives the LEDs, runs Bluetooth and writes flash. In v0.5 the crates were called aurora-core and aurora-fw. The version is 0.6.0.
The libraries are the same as in v0.5: esp-hal 1.1.2, esp-rtos 0.3.0, esp-radio 0.18.0, esp-bootloader-esp-idf 0.5.0, esp-storage 0.9.0 and trouble-host 0.6.0 (the Bluetooth host). trouble-host gets two new features. security adds LE Secure Connections pairing and bonding. It pulls in the crates p256, aes, cmac and rand_chacha. connection-event-queue-size-8 makes the event queue 8 long instead of 2. With the short queue the “pairing complete” event can get lost, and then the bond is never stored.
New files in v0.6:
| File | Content |
|---|---|
flashka-core/src/buttons.rs |
ladder classes and the gestures of the two slide buttons |
flashka-core/src/presenter.rs |
presenter settings and the key map |
flashka-core/src/hid.rs |
HID report map, key codes, PnP ID and the byte logic of the OOB guard |
flashka-core/src/bonds.rs |
the table of paired computers (a bond is the pairing key a computer and the light store) |
flashka-fw/src/gatt.rs |
the GATT database, one layout for both modes |
flashka-fw/src/hci.rs |
the wrapper around the radio’s HCI transport that holds the OOB guard |
The other files changed where the new features needed it. Section 13.5.8 lists what a builder or user may notice. The numbered list of deviations is in 13.8.
The control loop runs every 10 ms. Each round it makes one ADC conversion of the BTN net on GPIO3 (ADC1_CH3, 2.5 dB range, 0 to 1050 mV, factory curve calibration) and sorts the result into a class (13.3.2). A class counts after it has been stable for 25 ms. ● wins: while it is pressed, the other classes are ignored until every button is released. If the ADC fails, GPIO9 decides (low is ●, high is no button), and the light behaves as the single-button light of v0.5.
| Button | Gesture | Rule |
|---|---|---|
| ● | clicks, hold, click then click-and-hold | as v0.5: a click is a press shorter than 0.4 s, and the pause between the clicks of one gesture is at most 0.4 s |
| › and ‹ | click | a press shorter than 0.8 s, counted when the button is released |
| › and ‹ | hold | at 0.8 s, once; there is no repeat |
| ‹ + › | hold 2 s | enter or leave presenter mode, at 2 s while the hold goes on |
| ‹ + › | hold 10 s | forget the paired computers |
A click on › or ‹ counts at the release so that pressing both buttons together never sends a click of the one that went down first. A slide gesture is cancelled, and everything is ignored until every button is released, when:
The time of a hold starts when the new class started, not when the 25 ms debounce ended, so a press of 790 ms stays a click. The host tests cover every class boundary (105 and 106 mV, 255 and 256 mV, 447 and 448 mV, 810 and 811 mV), the tolerance case of 13.3.2, a click of 790 ms and a hold of 810 ms, the release of two buttons one after the other, and a start with a button held.
The light uses only deep sleep, as v0.5. In deep sleep the chip is off apart from a small wake-up circuit, and it starts again from the boot loader when it wakes. Before sleep the firmware turns GPIO3 back into a digital input, and a low level on it wakes the chip. Every press is a valid digital low (13.3.3), so any of the three buttons wakes the light, and USB does too. After a wake-up the light stays awake at least 1.5 s.
The state machine is in controller.rs. Entering presenter mode (‹ + › held 2 s, or the PRESENTER command) switches the main LED off, ends the app link and starts keyboard advertising. It is refused in lockout and below 3.20 V. While it is on:
00 00 <usage> 00 00 00 00 00. The usages are Page Down 0x4E, Page Up 0x4B, Right 0x4F, Left 0x50, F5 0x3E, Esc 0x29 and B 0x05.The presenter settings are the key set, the idle time and the swap flag (13.6.1). They are stored with SAVE, or before the next sleep.
The light has one Bluetooth connection at a time. The address comes from the chip’s factory MAC address (with the Bluetooth offset), and it never changes, so paired computers find the light again. The name is FlashKa-XXXX, where XXXX is the last two bytes of the address in hexadecimal, upper case. The Bluetooth controller starts the first time the firmware asks for advertising while no button gesture is in progress, because its start-up blocks the other tasks for a moment and would make the buttons deaf right after a wake-up.
| Flashlight mode (for the app) | Presenter mode (keyboard) | |
|---|---|---|
| Advertising data | 21 bytes: flags and the 128-bit UUID of the FlashKa Control service | 25 bytes: flags, appearance 0x03C1 (keyboard), HID service UUID 0x1812, local name |
| Scan response | local name | local name |
| Interval | 100 ms for 30 s, then 500 ms | 30 ms for 30 s, then 152.5 ms (the value of Apple’s accessory guideline QA1931) |
| When | only in windows: light on, 120 s after it goes off, 120 s after ● 3 clicks, while USB is connected, while the app is connected and 30 s after it, and with KEEPALIVE | whenever no computer is connected |
The keyboard advertising data, checked on the host:
02 01 06 03 19 c1 03 03 02 12 18 0d 09 "FlashKa-ABCD"
flags appearance HID UUID local name (13 bytes)
There is one GATT layout for both modes, so a paired computer never sees it change. It has 38 attributes. The handles are those that trouble-host 0.6 generates (dumped on the host from the same gatt.rs):
| Handles | Service or characteristic | Access |
|---|---|---|
| 1 to 5 | GAP: device name, appearance (0x03C1, keyboard, in both modes) | read |
| 6 | GATT service. There is no Service Changed characteristic | – |
| 7 to 10 | Battery Service: Battery Level (9) and its CCCD (10) | read, notify |
| 11 to 13 | Device Information: PnP ID (13) | read |
| 14 to 26 | HID service 0x1812: HID Information (16), Report Map (18), Control Point (20), Protocol Mode (22), Input Report (24) with its CCCD (25) and Report Reference (26) | see below |
| 27 to 38 | FlashKa Control: Status (29) and its CCCD (30), Command (32), Profile (34), Info (36), Presenter (38) | as in 13.6.1 |
A CCCD is the small switch with which a computer turns notifications on for one characteristic. The Report Map has 45 bytes: a boot keyboard with an 8-byte input report, no report ID and no LED output report. The PnP ID is 02 09 12 01 00 60 00: vendor ID source USB-IF, vendor 0x1209 (pid.codes), product 0x0001 and version 0.6.0. Product 0x0001 is a test number. Apply for a free number at pid.codes before the firmware is given to other people (13.9).
Pairing.
todo!("OOB not implemented")) and the chip would panic. hci.rs looks at every packet from the radio. In a Pairing Request with the OOB flag it clears the Secure Connections bit, so trouble-host refuses the request at once. The byte logic is host-tested. It has not been tried on air (13.9).Bonds. The light stores up to 4 bonds. A fifth pairing replaces the oldest one. Each bond has the computer’s address, its two keys and its CCCD table. They are written to flash after a pairing, after a CCCD change and after “forget”, and they are given back to trouble-host at boot, before advertising. A computer that reconnects uses its stored key and starts encryption without a new pairing. The firmware sees the encrypted link (it looks every 20 ms) and puts back the stored CCCD table, so key notifications work at once. “Forget” clears the table, the flash record and trouble-host’s own list, and drops a computer that is connected.
In flashlight mode. A paired computer may reconnect by itself, as keyboards do. The light does not count it as “app connected”, because that would keep the radio window open for ever. It drops the link after 10 s, unless the computer uses the FlashKa Control service (the app running on that computer). While such a computer holds the link, an app on another device cannot connect.
The radio uses the default transmit power of esp-radio (+9 dBm). The light does not send a connection-parameter request. It uses what the computer asks (13.8, item 32).
The ring is driven by three LEDC channels (the chip’s PWM units) at 9.77 kHz with 13 bits. The LEDs are common anode, so a duty of 100 % means dark. The three colours are scaled by RGB_FULL_R, RGB_FULL_G and RGB_FULL_B, which are 255, 70 and 110 out of 255. They are the v0.5 values for the 1 kΩ red resistor and are not tuned for the 330 Ω red of v0.6 (13.3.4, 13.9). GPIO21 is also the UART0 transmit pin, so the boot log makes the ring flicker red at start-up, as in v0.5.
The patterns are in 13.2.5. The rules for showing them:
The flash layout is the one of v0.5 (firmware/partitions.csv, the same as the default table of espflash):
| Offset | Size | Content |
|---|---|---|
| 0x0000 | 0x8000 | bootloader (ESP-IDF second stage, from espflash 4.6.0) |
| 0x8000 | 0x1000 | partition table with its MD5 |
| 0x9000 | 0x6000 | nvs: the settings log at 0x9000 to 0xAFFF, the log of paired computers at 0xB000 to 0xCFFF, the rest unused |
| 0xF000 | 0x1000 | phy_init (not used by esp-radio) |
| 0x10000 | 0x3F0000 | factory: the application (469,200 bytes, 11.36 % used) |
Both logs work like the v0.5 settings log. Each uses two 4 KiB sectors. A save is appended after the newest record. When a sector is full, the log continues in the other sector, which is erased first. The newest record with a valid CRC wins, so a power failure never loses the previous state. Nothing is written if nothing changed. A failed write is tried again every 2 s and given up after 3 failures, so that sleep and reboot still happen.
| Record | Size | Content |
|---|---|---|
| Settings | 64 bytes, 64 per sector | magic AUR5 and format 2 (a v0.5 record, format 1, still loads); profile, level, flags, sequence number; the Custom profile (20 bytes); the presenter settings at bytes 32 to 35; CRC-32 |
| Paired computers | 256 bytes, 16 per sector | magic FKB1; sequence number; 4 bonds of 59 bytes each (flags, address, IRK, LTK, pairing number, CCCD table of 4 entries); CRC-32 |
The settings are saved on SAVE, when the button switches the light off, when lockout or the battery-empty latch changes, before sleep and before the reboot of a factory reset.
| Item | Value |
|---|---|
| Merged image, written at 0x0 | firmware/dist/flashka-fw-v0.6.0-merged.bin, 534,736 bytes, SHA-256 e21058791bab990439835fbd977eecfed385af5837405e36ae923cb6083a8fdd |
| ELF | firmware/dist/flashka-fw-v0.6.0.elf, 932,620 bytes, SHA-256 3a5a231d1d5d6083137c401a841b166161516727d068e4604a10fe900d1cad4b |
| Application | 469,200 bytes, 11.36 % of the 4,128,768 bytes (0x3F0000) of its partition |
| Flash | 428,778 bytes of code and constants |
| RAM | 90,104 bytes static (code in IRAM 30,676; data 9,636; bss 49,792, which includes a 36 KiB heap), 230,400 bytes left for the stack, and a further 64 KiB heap in the RAM that the bootloader frees |
| Build | release build 2 min 01 s, 0 warnings |
| Host tests | 149 passed, 0 failed (93 from v0.5 and 56 new); clippy 0 warnings |
| Tools | the distribution’s rustc 1.91 with build-std (RUSTC_BOOTSTRAP=1), espflash 4.6.0 |
The image was checked with a script: bootloader magic 0xE9 at 0x0; partition table at 0x8000 with a valid MD5; nvs at 0x9000 (0x6000 long), phy_init, factory at 0x10000 (0x3F0000 long); application magic 0xE9; application descriptor 0xABCD5432 with version “0.6.0”, name “flashka-fw” and date 2026-10-17; the nvs region is 0xFF.
Reproducible. The image depends only on the sources and the compiler (rustc 1.91.1), not on the folder it is built in: a clean build in the repository and a clean build of a copy in another folder gave the same merged image and the same ELF, byte for byte. build.sh sets SOURCE_DATE_EPOCH=1792195200000000, which is 2026-10-17 00:00:00 UTC, so that the build date inside the image does not come from the clock; the bootloader crate reads the value in microseconds (13.8, item 33). build.sh also maps the machine-specific source paths that panic messages contain (--remap-path-prefix), and the two crates form one Cargo workspace, so cargo hashes their paths relative to firmware/. The CI job Firmware build rebuilds the image on GitHub and compares it with the published one.
Writing it. Hold any button while you plug in USB: the ROM of the chip starts download mode. Then use the web installer (13.6.4), or:
espflash write-bin 0x0 firmware/dist/flashka-fw-v0.6.0-merged.bin
espflash monitor # firmware log over USB-Serial-JTAG
The merged image covers the nvs region with 0xFF, so it erases the settings and the paired computers. Remove FlashKa on each computer and pair again. To keep both, write only the application:
espflash flash --partition-table firmware/partitions.csv firmware/dist/flashka-fw-v0.6.0.elf
Building with ESP_LOG=debug logs every change of ladder class with its reading in mV. Use it for the bring-up (13.9).
These points follow from the firmware report. Each has an item in 13.8 with the reason.
| What you may notice | Item |
|---|---|
| A very short press on › or ‹ that wakes the light from sleep switches it on, as ● would | 24 |
| › or ‹ held 10 s with the light off, or any button held 10 s in lockout, makes the chip sleep without that button as a wake source | 27 |
| ‹ + › held from flashlight mode enters presenter mode at 2 s. If you keep holding, it forgets the paired computers at 10 s and stays in presenter mode | 20 |
| › and ‹ held in flashlight mode step one level at 0.8 s. There is no repeat | 21 |
| In presenter mode ● 2 and 3 clicks send nothing, ● 4 clicks lock the light and leave presenter mode, and the commands ON, LEVEL and OUTPUT_MA are refused with 0x81 | 22 |
| Presenter mode is refused below 3.20 V and ends when the voltage stays below it for 2 s | 23 |
| A paired computer that connects while the light is a flashlight is dropped after 10 s | 25 |
| FACTORY_RESET keeps the paired computers. FORGET_HOSTS removes them | 30 |
| The presenter current is about 20 mA, because the light does not use light sleep | 18 |
These limits come from the Bluetooth host and the hardware:
Two web pages belong to the light. Each is one HTML file in app/ that works offline and runs entirely inside the browser: nothing is uploaded, there is no account, no CDN and no web font. A check of the control app found no external request.
| File | What it does | Works in |
|---|---|---|
index.html |
start page with two links | every browser |
flashka-app.html |
control app: switch the light, set levels and profiles, see the battery, set up presenter mode, all over Bluetooth. It has a demo mode that needs no light | Chrome or Edge on Android, Windows, macOS, Linux and ChromeOS; on iPhone and iPad the free Bluefy browser. Not Firefox or Safari, which have no Web Bluetooth |
flashka-flasher.html |
firmware installer through the USB-C port (Web Serial) | Chrome or Edge on a computer. Not on phones or tablets |
docs/web_apps.md describes both pages in full.
The contract between firmware and app is docs/ble_protocol.md. Version 2 is version 1 (v0.5) plus:
| Item | Version 2 |
|---|---|
| Status | 20 bytes (v1: 18). Byte 0 is 2. Byte 18 is the mode: 0 flashlight, 1 presenter mode waiting for the computer, 2 presenter mode with a computer connected and the link encrypted. Byte 19 is the number of paired computers, 0 to 4 |
| Presenter characteristic | a7d40006-3c5e-4b8a-9f61-2d0c8e5b7a13, read and write (with response), 4 bytes |
| New commands | 0x0A PRESENTER (one byte: 1 enter, 0 leave) and 0x0B FORGET_HOSTS (one byte: 0xA5) |
| SAVE (0x08) | also stores the presenter settings |
| Names | the service is called “FlashKa Control”. Every UUID of version 1 is unchanged |
The four bytes of the Presenter characteristic:
| Byte | Field | Range (default) |
|---|---|---|
| 0 | key set: 0 is Page Up and Page Down, 1 is Left and Right arrow | 0 to 1 (0) |
| 1 | idle time in minutes: presenter mode ends after this time without a button press | 5 to 120 (30) |
| 2 | flags: bit 0 swaps › and ‹ (clicks and holds) | (0) |
| 3 | reserved: write 0, reads 0 | – |
A write that is not 4 bytes long, or has a key set above 1, is refused with 0x81. The idle time is clamped to 5 to 120. Undefined flag bits and the reserved byte are ignored. Read the characteristic back to see what the light really uses. A write takes effect at once, and SAVE (or the next sleep) stores it.
PRESENTER 1 switches the main LED off, disconnects the app about 0.3 s after the answer to the write, and starts keyboard advertising. PRESENTER 0 leaves presenter mode. FORGET_HOSTS removes all paired computers and shows white × 3 on the ring. The errors are ATT application error codes:
| Code | Meaning |
|---|---|
| 0x80 | refused: the button has priority. A button action happened in the last 3 s (Status byte 14 shows the time left) |
| 0x81 | invalid length, opcode or value. Also ON, LEVEL and OUTPUT_MA while presenter mode is on |
| 0x82 | refused by protection: battery empty, too hot. Also PRESENTER 1 below 3.20 V |
| 0x83 | refused: lockout is on. Also PRESENTER 1 |
If more than one applies, the firmware answers 0x81 first, then 0x80, 0x83 and 0x82. Every button action starts the 3 s lock, and during it the writes OFF, ON, LEVEL, PROFILE, OUTPUT_MA and PRESENTER fail with 0x80. IDENTIFY, SAVE, KEEPALIVE, LOCKOUT, FORGET_HOSTS, FACTORY_RESET and writes to the Presenter characteristic are never blocked. The spec gave no error codes for the new commands; the codes above are the ones the firmware and the app agreed on (13.8, item 38).
The versions work together:
| Combination | Result |
|---|---|
| A version 1 app with a v0.6 light | works. It reads bytes 0 to 17 and sees the version byte 2 |
| The FlashKa app with a v0.5 light (Status version 1) | everything of v0.5. The Presenter card is hidden, the Presenter characteristic is never read, the footer says “protocol v1”. The device chooser also finds names that start with “Aurora” (13.8, item 36) |
| The FlashKa app with a v0.6 light that has no Presenter characteristic | the card is hidden |
| A Status version other than 1 or 2 | the app works as far as it can and shows “Version mismatch” |
The Presenter card is shown only for a light that reports protocol 2 and offers the Presenter characteristic (Figure 13.17). It lists what each button does, with the key names of the chosen key set. Its controls are:
A light in lockout refuses PRESENTER 1 with 0x83, and a light with an empty battery refuses it with 0x82. The app checks the lockout itself before it sends. Browsers rarely pass an ATT error code on as a number. The app reads the code from the error text, and otherwise it guesses the reason from the last Status packet and says “probably”.

Figure 13.17 — The Presenter card of the control app in demo mode, on a phone-sized screen (390 px wide): the key list, the Start button with the warning that the app disconnects, and the choice of keys. Below the visible part come the idle time, the swap option, Apply and Save, the number of paired computers, Forget and the pairing help.
“Try demo mode” on the start screen runs a flashlight written in JavaScript inside the page. It is not a picture of a screen. It takes the same commands (1 to 3 bytes), 20-byte profile writes and 4-byte presenter writes as a real light, answers with the same 20-byte Status packets (made by the encoder and decoder that the real Bluetooth link uses) and returns the same error codes. A “Demo tools” card does what a person does to a real light: press the light’s button (which starts the 3 s lock), lose the connection, plug in USB, a hot day, a battery slider and faster time. It can also play a v0.6 light or a v0.5 light with protocol 1, and it has a switch “a paired computer is in range” and a button “hold ‹ and › for 10 s (forget computers)”. While the demo light is a keyboard the app is disconnected, so the banner offers “a computer pairs with the light” and “hold ‹ and › for 2 s (leave presenter mode)”, and then Reconnect.
The demo light follows the firmware rules for presenter mode (13.8, item 39):
The model of battery, heat and charging is small and simplified. Its numbers are plausible, not measured.
The installer writes the firmware through the USB-C port with esptool-js 0.7.0 over Web Serial. The page, esptool-js, the helper program for the ESP32-C3 and the firmware image are all in one file of 936,041 bytes, so nothing is downloaded while the firmware is installed. The file now holds the real firmware 0.6.0: flashka-fw-v0.6.0-merged.bin, 534,736 bytes, SHA-256 e21058791bab990439835fbd977eecfed385af5837405e36ae923cb6083a8fdd. For this chapter the image was decoded from the page and compared with firmware/dist/flashka-fw-v0.6.0-merged.bin: the two are identical.
Every installation puts the settings back to the factory defaults and removes the paired computers, because the image covers the nvs region with 0xFF (13.5.7). The page says so next to its factory-reset option: the saved settings go back to the defaults and the paired computers are forgotten. Its help text for the download mode names the round metal button and says that either small button works too (13.8, item 41).

Figure 13.18 — The installer after Connect, with the real firmware inside the page: name, version, size and SHA-256 of what will be written, and the two checks. The window is 1280 px wide.
The table lists every check that was run on the v0.6 design, with its result. The last column says where the result comes from:
| Code | Source |
|---|---|
| PCB | production/DRC_report.txt and the checks of tools/kicad/build_all.py (check_pcb.py, verify_netlist.py) |
| HOU | docs/checks/housing_v06_build_checks.txt and the files next to it (tools/housing/flashka_housing.py) |
| FIT | docs/checks/fit_pcb_report.txt |
| FW | cargo test in firmware/flashka-core and firmware/build.sh |
| APP | npm run test:all in tools/web |
| new | run again or calculated for this chapter (a calculated value says so) |
Everything was checked in software. No v0.6 board exists yet (13.9).
| Area | Check | Result | Source |
|---|---|---|---|
| Board | Netlist (verify_netlist.py) |
44 named nets, as expected | PCB |
| Schematic label overlaps | 0 | PCB | |
Pad nets of the board equal flashka_parts.py (check_pcb.py) |
courtyards, top keep-out X 15.6 to 19.4 mm / Y 0 to 3.3 mm, bottom circle r 4.4 mm, trough, housing parts and USB escape: all OK | PCB | |
| KiCad DRC | 0 errors, 0 unconnected pads, 0 footprint errors; 65 silkscreen warnings (40 overlap, 10 to the board edge, 7 over copper, 8 text size; v0.5: 33). Counted again from the report | PCB, new | |
| Router checks | 0 incomplete nets, 0 ground pads off the plane, clearance audit 0 (at least 0.101 mm) | PCB | |
| BOM and CPL against the board | 34 BOM lines. 68 designators in both files, none twice, sides match, every line has an LCSC number. CPL: 38 top, 30 bottom. Counted again | PCB, new | |
Rebuild twice with --no-route |
identical: BOM, CPL, netlist CSV, flashka.net, DRC report, schematic, board, PNG files and placement file. Gerber and drill files differ only in KiCad’s date lines |
PCB | |
| Full rebuild from the placement file | same copper: stage 1 is byte-identical and the finish gives the same tracks and vias. About 35 min | PCB | |
| Ladder levels, worst case | smallest margins: › 22.7 mV, ‹ + › 25.4 mV, ‹ 115.7 mV. ● 96 mV and “none” at least 240 mV (calculated) | FW, new | |
| Pressed level against the digital low limit | worst case 0.617 V, limit VIL 0.825 V (calculated) | new | |
| Ring current per LED | red 3.9 mA, green 7.0 mA, blue 6.0 mA at the typical forward voltage (calculated) | new | |
| Housing | Valid solids | 11 of 11 | HOU |
| Smallest walls | counterbore ledge 0.2 mm (radial); front tap to the head rear face 0.875 mm; lid over the RF pocket 0.9 mm; floor between the main counterbore and the slide hole 0.975 mm; floor between the previous-button hole and the RF window 1.0 mm; tail front wall 1.0 mm; window bore to the control-side face 1.075 mm; laser bore to the laser-side face 1.1 mm; floor RF window to the side wall 1.5 mm | HOU | |
| Metal under a seam groove | at least 1.11 mm (limit 1.1 mm) | HOU | |
| Seams to openings and holes | at least 0.945 mm (limit 0.5 mm) | HOU | |
| Laser marks | to grooves at least 0.358 mm, to openings at least 0.525 mm, 0 violations on 4 faces. Rivets: lid 151, floor 148, −Y 117, +Y 124 | HOU | |
| Stand-in board against the housing: centred, at −Y, at +Y | 0 / 0 / 0 overlaps above 0.02 mm³ | HOU | |
| Part-to-part overlaps (9 parts in place, caps at rest) | 0 above 0.02 mm³ | HOU | |
| Insertion path of the stand-in board (tilt about the S2 knob, 9° to 0°) | 0 hits | HOU | |
| Cap stroke, 3 caps at 3 board positions | free play 0.30 mm; actuator reached within 0.35 mm; cap pressed to 0.55 mm; no other contact | HOU | |
| Cap tolerance stack | free play 0.30 mm nominal, worst case 0.00 to 0.60 mm, root-sum-square minimum 0.142 mm; no preload on a switch at rest | HOU | |
| LED to tube gap | LED3 0.15 mm, LED4 0.15 mm; tube top to board 1.0 mm; SW1 body to tube bore 0.97 mm | HOU | |
| Hold-down preload | 34.0 N (soft 11.9 N, hard 66.3 N); target at least 3 N: met | HOU | |
| Hold-down movement at 3 N | at most 0.017 mm nominal and at most 0.033 mm soft and hard: met. The board leaves the ledge at SW2 / SW1 / SW3 at 8 / 6 / 2 N (nominal) | HOU | |
| Hold-down movement at 10 N | nominal SW1 0.030 mm, SW2 0.051 mm, SW3 0.121 mm; soft 0.078 / 0.155 / 0.276 mm; hard 0.028 / 0.034 / 0.070 mm. Limit 0.05 mm: not met at SW3, marginal at SW2 (13.4.5, 13.8 item 8) | HOU | |
| Drawings: every number against the parameter table | 347 entries, 275 distinct, 0 mismatches | HOU | |
| Drawings: probes through the 3D solids | 30 probes, 0 mismatches | HOU | |
| Drawings: layout check | 8 cosmetic notes (for example a tag over a line) | HOU | |
Interface keys components and pcb_outline of housing_interface.json |
identical to the file at the start of the work | HOU | |
| Masses (calculated from the solids) | tray 22.68 g, lid 7.14 g, tube 0.20 g, RF window insert 0.21 g, spacer ring 0.10 g, antenna spacer 1.07 g, main cap 0.86 g, slide cap 0.32 g | HOU | |
| Size comparison | length 94.4 mm is 88 % of v0.4 and 1.56 times the A9 Pro; envelope 30.2 cm³ (0.84 times v0.4, 2.08 times the A9 Pro) | HOU | |
| Fit with the real board | Collisions of 0.01 mm³ or more, board centred and pushed 0.24 mm to either wall | 0 | FIT |
| Contact-level slivers | 2 of 0.0026 mm³ each, at X 10.15 mm, Y ∓8.48 mm, Z 9.60 mm (board against a wall) | FIT | |
| Insertion path, 12 steps from 9° to 0° | no hit above the 0.002 mm³ threshold. J1 touches the tray with 0.0019 / 0.0018 / 0.0016 / 0.0011 mm³ at 9.0 / 8.25 / 7.5 / 6.75°; S2 to tray at least 0.215 mm | FIT | |
| Smallest clearances | tray 0.000 mm (J1 at the −Y wall; 0.211 mm centred); lid 0.410 mm (R25); RF window insert 1.900 mm; antenna spacer 0.150 mm; tube 0.150 mm (LED3); caps 0.300 mm; laser 0.300 mm; cell 1 2.640 mm; cell 2 1.100 mm; LED board 3.600 mm | FIT | |
| Free travel of the board | ±Y 0.229 mm; +X 0.149 mm; −X 0.261 mm; +Z 0.393 mm (S2 knob against the tray) | FIT | |
| Firmware | Host tests (cargo test in flashka-core) |
149 passed, 0 failed (93 from v0.5, 56 new). Run again: 149 passed | FW, new |
Lints (cargo clippy --all-targets) |
0 warnings. Run again: 0 warnings | FW, new | |
| Release build | 0 warnings; 2 min 01 s | FW | |
| Merged image | 534,736 bytes; SHA-256 e2105879…3a8fdd; application 469,200 bytes (11.36 % of its partition). The SHA-256 of the file on disk was computed again: it matches |
FW, new | |
| ELF | 932,620 bytes; SHA-256 3a5a231d…0cad4b; matches the file on disk |
FW, new | |
| Memory | flash 428,778 bytes; static RAM 90,104 bytes; 230,400 bytes left for the stack | FW | |
| Image structure | bootloader magic 0xE9; partition table with a valid MD5; application descriptor 0xABCD5432 with “0.6.0” and “flashka-fw”, dated 2026-10-17; nvs region 0xFF |
FW | |
| Reproducibility | two builds from a clean target: image and ELF byte-identical | FW | |
| GATT layout | 38 attributes; handles 18, 24, 25 and 26 need encryption, 16, 20 and 22 are open | FW | |
| Advertising bytes | keyboard 25 bytes, app 21 bytes | FW | |
Ladder tolerance (ladder_tolerance) |
with 1 % resistors and ±10 mV every class stays inside its band | FW | |
| Web pages | Type check (tsc) |
clean | APP |
| Node unit tests | 143 of 143 pass, 0 skipped. Run again with the real image present. The report, written before the image existed, counted 142 passed and 1 skipped | APP, new | |
| Browser tests of the app | 67 passed, 0 failed | APP | |
| Browser tests of the installer | 29 passed, 0 failed | APP | |
| External requests in the app | none | APP | |
| Contrast | 4.5:1 or better on the Presenter card, the forget dialog, the banner and the pill, in light and dark | APP | |
| Layout | no horizontal scroll at 320, 360, 390 and 1280 px; touch targets at least 40 px | APP | |
| Firmware inside the installer page | 534,736 bytes, SHA-256 equal to firmware/dist. Decoded from the page and compared byte for byte |
new |
The screenshots in app/screenshots/ were written by a run of the browser tests on 2026-10-18, after the real image was embedded in the installer page.
The numbers below follow the order of this chapter: items 1 to 7 are electronics, 8 to 17 mechanics, 18 to 35 firmware and 36 to 41 the web pages. Each item says what the working specification asked for, what was built and why. Item 8 is the only place where a target of the specification is not met, and item 11 is the only place where a design rule is not followed. The other items follow from the tools, the parts or the Bluetooth host.
Electronics (13.3)
FlashKa:WirePad_SMD_2_P3.0mm_N, at X 15.55 mm, Y 6.35 mm, bottom side), so that the peg of S2 fits between the pads.Mechanics (13.4)
Aurora:LED_RGB_1010_NH-B1010RGBT is still inside the components key of cad/housing_interface.json. That key is an interface value that the PCB build reads, so it was not changed. (Two file errors found during the review were fixed before release: note 2 on the tube drawing now says that the lit ring is the end face of the tube, and the bottom marking DXF was rebuilt so that its chevrons point the right way; the cause was an argument bug in flashka_marking.py that wrote the rebuilt files into the wrong folder.)Firmware (13.5)
status_layout now expects 20 bytes and version 2. laser_never_from_ble now starts its loop over invalid opcodes at 0x0C, because 0x0A and 0x0B exist, and it checks both explicitly. The specification changes what these tests assert. No other v0.5 test was removed or weakened.AUR5, with format version 2, so that a v0.5 record still loads.build.sh sets SOURCE_DATE_EPOCH and maps the source paths. The image must be the same on every build, wherever it is built. The value is 1792195200000000, because esp-bootloader-esp-idf 0.5.0 reads it in microseconds; that is a quirk of the crate. The path remapping and the shared Cargo workspace keep the folder of the build out of the image.Web pages (13.6)
FlashKa and Aurora, so that a v0.5 light is still found. The specification names only FlashKa-XXXX.The design was checked in software only. The points below need a board, a radio or a person. The bring-up steps of the firmware are in docs/firmware.md, section 7.1: steps 13 to 23 are new, and steps 1 to 12 of v0.5 still apply.
| No. | Point | What to do and what to expect |
|---|---|---|
| Before ordering | ||
| 1 | JLCCNC and the stainless caps | Upload both cap STEP files to the instant quote and read the answer (13.4.4). If it refuses them, send the STEP files and the PDF drawings to another CNC shop, or use the resin caps of the print kit |
| 2 | Orientation of the new parts at JLCPCB | Check pin 1 and the orientation of SW1 to SW3, LED3 and LED4 in the 3D preview of the order. The CPL rotations are 180° minus the KiCad rotation on the bottom side, with no correction (DFM-7) |
| 3 | Chevrons in the marking file | Open cad/FlashKa_marking_bottom.png. It is drawn as seen from below with the head at the left. The chevron at › (X 16.4 mm) must point to the head, which is left in that drawing, and the one at ‹ (X 30.8 mm) to the tail, as in Figure 13.3. The released files are right; check again after any rebuild of the marking files (13.10), and copy the new DXF files to order_files/2_JLCCNC/ |
| 4 | Product ID | 0x1209:0x0001 is a test number. Apply for a free product ID at pid.codes before the firmware goes to other people |
| On the board | ||
| 5 | NH-B1010RGBT-HF pad map | The drawing of the datasheet could not be read. The text says pin 1 anode, pin 2 red, pin 3 green, pin 4 blue. Light each colour on its own and see that the ring shows the right colour. If not, the pad map has to change |
| 6 | Ladder levels | With USB power, measure BTN against GND with a multimeter: none 3.30 V, ● 0 V, › 0.300 V, ‹ 0.595 V, ‹ + › 0.212 V, each ±10 mV with 1 % parts. After boot the readings must not move by more than about 5 mV (the internal pull-up of GPIO9 is off) |
| 7 | ADC readings | Build with ESP_LOG=debug and press each button. The logged millivolts must agree with the multimeter within ±10 mV, and the classes must change at 106, 256, 448 and 810 mV |
| 8 | Wake from deep sleep | Wake the light with ●, ›, ‹ and ‹ + ›. Hold ‹ + › from sleep: presenter mode after 2 s. A short press on › or ‹ switches the light on (13.8, item 24) |
| 9 | Ring brightness and balance | Tune RGB_FULL_R, _G and _B for the 330 Ω red (3.9 mA, 13.3.4). Look at each colour first with a bare tube and then in the housing, through 8.2 mm of printed resin. The ring must be even and bright enough in daylight |
| 10 | Presenter current | Measure it with and without a connected computer (estimate: about 20 mA, about 30 to 40 h from 800 mAh). Then let the idle timer end presenter mode (5 min in the app) and measure the sleep current |
| In the housing | ||
| 11 | Fit | Tilt the board in about the S2 knob. The front corner of J1 touches the tray at the start of the path (13.4.8). Check the 0.15 mm gaps (antenna spacer, tube), the wire path of J2 at the −Y corner and the notches of the antenna spacer |
| 12 | No button pressed at rest | With the lid closed, no cap may press its switch. The free play of 0.30 mm shrinks by the 0.08 mm bow of the board (13.4.2). After blasting, every cap must fall through its bore by its own weight (13.4.4) |
| 13 | The light-pipe tube | The ledge is only 0.2 mm wide, so glue the tube in. Check the printed step at Z 2.4 mm (±0.10 mm), and that the lit ring is even (13.4.3) |
| 14 | Button feel and the hold-down | Judge the feel of all three buttons: the 0.30 mm free play must not feel loose, and the click must come at about 1.6 N. Press ‹ (SW3) hard. If it feels soft, fit a harder can pad or plan the third pad (13.8, item 8). Measure the hardness of the real pads: the moduli of the model are assumptions |
| 15 | Pocket test | Carry the light for a week, locked and unlocked, and write down every accidental switch-on (MEC-6, 13.8 item 11) |
| 16 | Laser | Measure the output of the finished laser with a power meter and read the label on a real part. The capitals are only 0.6 to 0.67 mm high (LAS-3) |
| 17 | Bluetooth range | Measure the range with a hand over the floor and with a hand over the lid. The floor window is plugged with resin, which does not shield the radio, but the effect is not known (13.4.6) |
| Radio and computers | ||
| 18 | Pairing | Pair with Windows 11, Windows 10 (1803 or newer), macOS, Android, iOS and Linux, and check the help text of the app on each. With a phone app such as nRF Connect, the keyboard must advertise (appearance 0x03C1, service 0x1812) before pairing, and reading the Report Map must give error 0x05 |
| 19 | Reconnect and bonds | Power-cycle the light, enter presenter mode: the computer must reconnect without pairing, and the keys must work at once. Pair 5 computers: the first must be replaced. Hold ‹ + › for 10 s: white × 3, and no computer reconnects |
| 20 | Keys and programs | In a text editor, each click must give exactly one key. › held for 3 s must give exactly one F5 and no repeated Page Down; ‹ one Esc; ● one B. Try PowerPoint, Keynote, Google Slides, LibreOffice Impress and a PDF viewer with both key sets. The firmware manual and the app recommend the arrow keys for Keynote and Google Slides: confirm it, and correct both if a program behaves differently |
| 21 | The guard against OOB pairing | Send a pairing request with the OOB flag from a test tool: it must be refused (13.5.4). It has not been tried on air |
| 22 | A paired computer in flashlight mode | With a paired computer near, the computer reconnects by itself and is dropped after 10 s. The app on that computer must keep its link |
| 23 | The app against a real light | Test Web Bluetooth against a v0.6 light, in particular what Chrome does when a bonded light stops offering the control service in presenter mode |
| 24 | The installer | Write the image to a real light, and enter the download mode with each of the three buttons. Make the help text say “any button” and mention the paired computers (13.8, item 41) |
| 25 | A new placement | If the placement of the board changes, the router may need other load orders (FLASHKA_LOAD_SEED) or may not finish at all |
Names. The product is FlashKa Keychain v0.6, written “FlashKa” in text. The v0.5 folder keeps its Aurora names, and so do the v0.5 documents: chapter 12, the v0.5 housing notes and the v0.5 hardware package are in ../flashka-keychain-v05/. Output files start with FlashKa_. The two main scripts are tools/housing/flashka_housing.py and tools/kicad/flashka_parts.py (in v0.5: aurora_housing.py and aurora_parts.py). The firmware crates are flashka-core and flashka-fw, the image is firmware/dist/flashka-fw-v0.6.0-merged.bin, and the Bluetooth name is FlashKa-XXXX.
Licences. Scripts: GPL-3.0-or-later. Hardware design files: CERN-OHL-W-2.0. Documents, this chapter included: CC-BY-SA-4.0. The FlashKa Rust firmware, the web app and the web tools: MIT OR Apache-2.0. The source files carry an SPDX line with their licence.
| Folder | Content |
|---|---|
kicad/FlashKa_main/, kicad/FlashKa.pretty/ |
the KiCad project (schematic and board) and the footprint library |
tools/kicad/ |
the generators and the router. flashka_parts.py is the one table of parts, values and connections |
production/ |
gerbers, BOM, CPL, DRC report, layer pictures |
order_files/ |
1_JLCPCB, 2_JLCCNC and 3_JLC3DP. The last two have a README.txt |
cad/ |
STEP and STL of every part, the drawings (PDF, DXF, PNG), the marking files, housing_interface.json, FlashKa_main_pcb.step and the placement files |
tools/housing/ |
flashka_housing.py (the parameter table P and the checks), flashka_marking.py, drawings.py, fit_pcb.py, assemble.py and the render scripts |
renders/, assets/v06/ |
pictures |
firmware/ |
flashka-core, flashka-fw, build.sh, partitions.csv, dist/ |
app/, tools/web/ |
the web pages with their screenshots, and the build script and tests |
tools/parts_list.py |
writes the cost workbook FlashKa_Parts_List_v0.6.xlsx (its notes are in docs/bom_notes.md) |
docs/ |
this chapter, the build guide build_guide_v0.6.md, firmware.md, ble_protocol.md, web_apps.md, housing_v06_notes.md, bom_notes.md and checks/ (the logs of the checks) |
Where the specification went. Some code comments cite the working specification of the design by section number (“spec 2.8”). That file is not part of the repository; this table says where each part is described now:
| Specification | Subject | In this chapter |
|---|---|---|
| 0 | naming, licences | 13.10 |
| 1 | coordinates, the interface between housing and board | 13.3.5, 13.4 |
| 2.1 to 2.3 | bottom face, light-pipe tube, caps | 13.4.2, 13.4.3, 13.4.4 |
| 2.4 | PCB hold-down | 13.4.5 |
| 2.5 | second RF window | 13.4.6 |
| 2.6 to 2.8 | parts removed, laser label, fighter-jet skin | 13.1, 13.4.7 |
| 2.9, 2.11 | print kit, outputs | 13.4.1, 13.4.9, 13.10 |
| 2.10 | checks of the housing script | 13.4.8, 13.7 |
| 3.1 to 3.4 | parts table, ladder, placement and checks, outputs | 13.3.1, 13.3.2, 13.3.5, 13.3.6, 13.10 |
| 4.1, 4.2 | buttons, flashlight mode | 13.2.2, 13.5.1, 13.5.2 |
| 4.3 | presenter mode | 13.2.3, 13.2.4, 13.5.3, 13.5.4 |
| 4.4 | ring colours | 13.2.5, 13.5.5 |
| 4.5 | protocol version 2 | 13.6.1 |
| 4.6 | build outputs | 13.5.7 |
| 5 | docs and costs | 13.10 |
| 7 | open points | 13.9 |
Section 6 of the specification (how the files were organised during the design work) has no counterpart.
Rebuild commands. From the project folder:
# Board: outputs from the routed board (about 3 min), or a full rebuild (about 40 min)
python3 tools/kicad/build_all.py --no-route
python3 tools/kicad/build_all.py
# Firmware: host tests, release build, merged image, sizes, SHA-256
cd firmware && ./build.sh
rm -rf target && ./build.sh # the published image, byte for byte, in any folder
cd flashka-core && cargo test && cargo clippy --all-targets
# Web pages
cd tools/web && npm install
npm run build:release # app/flashka-flasher.html with the real image
npm run test:all # type check, node tests, browser tests
# Cost workbook
python3 tools/parts_list.py
# This chapter as PDF (needs python-markdown and Playwright with Chromium)
python3 docs/build_pdf.py ch13
The housing is rebuilt from tools/housing/. The first command writes the solids and runs all checks (about 75 s). fit_pcb.py needs cad/FlashKa_main_pcb.step.
python3 flashka_housing.py ../../cad
python3 flashka_marking.py ../../cad ../../cad --tex-dir ../../cad
FK_CACHE=.fk_cache python3 drawings.py ../../cad ../../cad
xvfb-run -a python3 render.py ../../cad ../../renders
python3 render_marking.py ../../cad ../../renders
xvfb-run -a python3 size_comparison.py ../../cad ../../renders
python3 fit_pcb.py ../../cad/FlashKa_main_pcb.step ../../docs/checks
python3 assemble.py ../../cad ../../cad/FlashKa_main_pcb.step ../../cad
The logs of the checks are in docs/checks/.