Firmware v3.1.0 for a 5 V Arduino Nano / ATmega328P, a 16-LED WS2812B RGB ring, and one KY-040 push rotary encoder. Designed for PCB repair, microscope inspection, photos, and possible video.
Choose a lighting shape, rotate the selected LEDs around your specimen, and adjust brightness with one hand. The device works without a computer after flashing.
- Full, half, quarter, and opposed illumination.
- Fifty distinct brightness levels, with smaller changes near minimum.
- Press-and-turn rotates the selected lights, including inside the shape selector.
- Every selector exit keeps the chosen pattern: hold, click, or idle timeout.
- USB controls for RGB balance, background fill, optional brightness acceleration, and capture lock.
- Quiet startup with output off, no animation, and temporal dithering disabled.
- CRC-protected settings, automatic pre-flash backups, verified writes, and rollback tools.
Hardware · Encoder controls · Linux setup · USB commands · Recovery · Troubleshooting
| Part | Tested configuration |
|---|---|
| Controller | Classic 5 V, 16 MHz Nano clone, ATmega328P, CH340 USB serial |
| Light | 16 addressable RGB WS2812B LEDs, GRB order, 800 kHz data |
| Input | KY-040 mechanical quadrature encoder with push switch, 20 detents/revolution |
| Connection | USB data cable to a Linux machine; jumpers and common ground |
This firmware targets the classic Nano, not the Nano Every, ESP32, RP2040, or a 3.3 V Nano variant. Other rings/counts and pin assignments require source changes and hardware validation.
| Nano pin | Connect to | Meaning |
|---|---|---|
| D9 | Ring DIN | LED data input; do not use DOUT |
| D4 | Encoder A | Measured quadrature channel A |
| D2 | Encoder B | Measured quadrature channel B |
| D3 | Encoder SW | Push button; LOW when pressed |
| 5V | Ring +5V and encoder + / VCC | 5 V rail |
| GND | Ring GND and encoder GND / − | Shared ground |
These are logical wiring diagrams, not physical header layouts or photographs. Follow printed Nano pin names. Encoder header order and CLK/DT labeling vary; A/B above are the channels verified on this device. Seller wiring for this build was incorrect. For general board orientation, consult the official Arduino Nano pinout.
Disconnect USB before changing wiring. Connect the ring to the 5 V rail, never to a GPIO, 3V3, or VIN. The tested arrangement powers the Nano and ring over USB; don't raise the current cap without measuring the complete device and checking the cable/source. Avoid connecting independent 5 V supplies together.
The firmware retains a 450 mA estimate-based FastLED limit. This is not measured whole-device USB current, and the Nano cannot sense actual light output, ring current, or temperature. Narrowing a sector does not automatically boost the active LEDs.
For new assemblies, Adafruit recommends a short data connection, a 300–500 Ω series data resistor near DIN, and appropriate supply decoupling; its guide describes a 500–1000 µF capacitor rated at least 6.3 V. These are general wiring recommendations, not a claim that those parts have been verified in the original assembly. NeoPixel best practices.
| Gesture | Normal operation | Inside the lighting selector |
|---|---|---|
| Turn | Adjust brightness | Choose Full → Half → Quarter → Opposed |
| Press and turn | Rotate selected lights | Rotate selected lights without changing shape |
| Short click | On/off | Keep selection and exit |
| Hold about 650 ms, then release | Enter selector while on | Keep selection and exit |
| No input for five seconds | No effect | Keep selection and exit |
Example: click on → hold/release → turn to Quarter → hold/release → press-and-turn to spin the four selected LEDs. Ordinary turning then adjusts brightness. No selector exit restores the old full-ring pattern.
- Holds act on release. A turn while pressed consumes the gesture, so releasing does not click or enter/exit a menu.
- One detent rotates one LED position, 22.5°. The direction is the sector's first LED, not its center. Physical clockwise and LED 0 depend on the ring and mounting.
- In Full mode, rotation updates stored direction without changing the all-on pattern.
- While off, turning prepares brightness and press-and-turn prepares direction without lighting up. Hold does not open the selector while off.
- Every reset starts off, including opening the USB serial port. Click to resume.
- Settings save after five seconds of inactivity. Saving pauses in the selector and its delay restarts after leaving.
- Capture lock ignores physical edits but permits click for blackout/restoration. A stationary two-second hold unlocks it.
Run these steps on the Linux machine physically connected to the Nano. WSL can build and manage Git, but a Nano attached to a separate machine must be accessed there through SSH. USB attachment to WSL is a separate setup step.
On Debian/Ubuntu, install Git, Python 3 with venv support, and g++ using your package manager, then:
git clone https://github.com/pgedeon/microscope-ring-light.git
cd microscope-ring-light
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtInstall Arduino CLI and place arduino-cli on PATH, then install the pinned board core and library:
arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.8
arduino-cli lib install FastLED@3.10.8
python tools/project.py buildbuild first runs host tests under AddressSanitizer/UndefinedBehaviorSanitizer, compiles for arduino:avr:nano, and checks memory budgets. It does not access the serial port. The 3.1.0 firmware uses 14,622 bytes flash and 1,000 bytes static SRAM on the tested toolchain. GitHub CI repeats the software checks without connecting hardware.
Tested tools: Arduino CLI 1.5.2-rc.1, Arduino AVR core 1.8.8, FastLED 3.10.8, Python 3 with pyserial 3.5, and g++. A CLI installed at ~/tools/bin/arduino-cli is also detected. Set ARDUINO_CLI to an absolute path for a custom installation and ARDUINO_DATA_DIR if your Arduino packages live outside ~/.arduino15.
ls -l /dev/serial/by-id/The original device uses /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0. Use your actual path. Ensure the current user can open it; on many Linux distributions that means membership in dialout and logging in again after the group is changed. Close IDE serial monitors and other programs using the port.
python tools/project.py --port /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 backupThis saves full flash, EEPROM, and a checksum manifest under backups/manual-<UTC timestamp>/. Remember that directory for rollback. The public repository contains source, not another owner's factory firmware, raw device backups, or compiled images.
python tools/project.py --port /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 flashThis builds/tests again, makes another full pre-write backup, writes the application, and verifies it in a fresh bootloader session. The tested loader uses STK500v1 at 57600 baud; application serial uses 115200. The writer checks the ATmega328P signature and protects the bootloader beginning at 0x7800. It does not burn fuses or replace the bootloader.
For this tested device, use the project uploader rather than Arduino IDE upload or arduino-cli upload. The historical investigation saw failures with other upload paths, but the old claim that AVRDUDE uses big-endian load addresses was incorrect: upstream AVRDUDE also sends the low address byte first. The original failure's cause is unresolved; this project retains its uploader because its backup, application/EEPROM restore, and readback have actually been verified.
After uploading, click the encoder to turn on. A dark ring at startup is expected.
Keep one connection open during a session. Opening this Nano's serial port resets it and turns output off; repeatedly reopening it is not continuous monitoring.
python tools/project.py --port /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 consoleCommands use 115200 baud, uppercase ASCII, and a newline terminator. Send one command at a time and wait for OK or ERR.
| Command | Purpose / bounds |
|---|---|
GET CAPABILITIES |
Firmware and protocol version, supported settings |
GET STATE |
Applied profile, output, selector, lock, dirty/save status |
SET LEVEL 20 |
Brightness index 1–50; not calibrated percent or lux |
SET GEOMETRY quarter |
full, half, quarter, opposed |
SET DIRECTION 4 |
First active LED, 0–15 |
SET FILL 25 |
Background drive relative to key sector, 0–255 |
SET RGB 255 240 220 |
Channel mix; each value 0–255 |
SET PROFILE 20 half 4 25 255 255 255 |
Atomic level, geometry, direction, fill, R, G, B |
OUTPUT ON / OUTPUT OFF |
Output independent of profile |
SET ACCEL 1 / SET ACCEL 0 |
Enable/disable faster brightening with quick turns |
CAPTURE LOCK / CAPTURE UNLOCK |
Freeze/unfreeze edits; output switching still works |
SAVE NOW |
Queue a persistent settings save |
WATCH ON / WATCH OFF |
Enable/disable rate-limited state events |
Invalid commands leave the profile unchanged. A valid setting command exits selection and updates the currently visible profile. SAVE NOW only queues work: poll until dirty=0 saving=0 save_fault=0 before disconnecting. Output on/off, selector, capture lock, and watch are volatile; brightness, geometry, direction, fill, RGB and acceleration are saved.
Two CRC16-protected settings records at EEPROM addresses 64 and 96 retain an older valid record during an interrupted replacement. The original firmware's schema-v3 settings at address 0 can be read for migration and are left intact. Invalid settings load a modest neutral-white profile and output off. Last-second unsaved edits cannot be guaranteed to survive a reset.
Git restores source; a device backup restores the board's flash and EEPROM. Source tags do not contain a reader's prior EEPROM or original factory image.
Restore a backup created by this project's tool:
python tools/project.py \
--port /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 \
--restore-from backups/manual-YOUR_TIMESTAMP \
rollbackReplace the directory with your real backup path. It must contain flash.bin, eeprom.bin, and manifest.json. Rollback makes a new backup first, validates checksums and the matching bootloader, writes only the application plus EEPROM, and verifies the result. The original device's accepted firmware/EEPROM restore was rehearsed successfully. Its private backup is retained by the owner, not redistributed here.
Public tags preserve source versions: accepted-v2 is the older decorative-light source; microscope-v3-first-build is 3.0.0; microscope-v3.1.0 is the current selector correction. Prefer a separate clone/worktree to inspect an old tag rather than discarding current work. Build the chosen source before uploading; v2 uses the historical sketch in legacy/, and its source alone does not reproduce saved EEPROM settings.
There are no animations or temporal dithering in the microscope build. Use a static profile and capture lock for longer exposures or video. The 800 kHz figure is the data rate, not the LEDs' internal PWM frequency. Firmware cannot change that internal PWM; test your actual camera for banding at the intended shutter speeds/frame rates.
RGB balance adjusts white appearance, not emitter spectra. No calibrated Kelvin, CRI, lux, fluorescence, transmitted illumination, or guaranteed darkfield is claimed. Two LED positions were previously suspected faulty and still need optical confirmation. Software acknowledgements cannot verify emitted light or detect a loose DIN jumper.
| Symptom | Check |
|---|---|
| Dark after reset/upload/USB connection | Expected: click on, check level/RGB, then verify 5V, GND, and DIN |
| Ring freezes but serial responds | Check the D9 → DIN connection; pixels can latch their last frame indefinitely |
| Turning seems to change shapes | You are in the selector; hold/release or click to exit, keeping the current shape |
| Press-turn changes nothing visible | Full mode lights every LED; select Half or Quarter first |
| Controls ignore edits | Check capture lock; unlock over USB or hold stationary for two seconds |
| Encoder direction differs on another module | Check the measured A/B channels and wiring rather than copying a seller diagram |
| Permission denied / port busy | Correct Linux user permissions; close other serial clients |
| No bootloader session | Check actual port, classic Nano/ATmega328P, USB data cable, reset, and 57600-baud loader |
| Color order or LED count wrong | This source fixes GRB and 16 LEDs; adapt and validate before using different hardware |
| Camera banding | Try suitable exposure/frame-rate settings; disabling temporal dithering does not remove LED PWM |
| Rollback backup missing | Public clones do not ship private dumps; use your own manifest-backed backup directory |
| Path | Contents |
|---|---|
firmware/microscope/ |
Current sketch, input/state logic, settings serialization, version |
tools/project.py |
Test/build/backup/flash/restore entry point |
tools/device.py |
Guarded STK500v1 reader/writer; HEX and raw BIN support |
tools/control.py |
Persistent serial console and live smoke check |
tests/ |
Host-side control and uploader/image tests |
docs/images/ |
Original SVG sources and README PNG diagrams |
tools/render_docs.py |
Regenerate the diagrams |
legacy/ |
Older accepted source and historical tools |
dev.ps1 |
Optional Windows → WSL → remote Linux workflow |
Run python tools/project.py test without hardware. python tools/project.py smoke requires this firmware and the connected Nano, resets it, and briefly enables modest static illumination; it ends off. Public clones skip two tests requiring private recovery dumps; core control, parsing, binary and HEX validation tests still run. Validation results distinguish software checks, live checks, and physical checks still pending.
To regenerate the PNG and SVG diagrams:
python -m pip install -r docs/requirements.txt
python tools/render_docs.py --pngThe diagrams use labeled logical signals, so they do not depend on a particular clone's connector orientation. The renderer creates SVG without extra dependencies; PNG output needs CairoSVG and its system Cairo library. Optional original-machine instructions are in docs/LOCAL-DEVELOPMENT.md. Planned features are in MICROSCOPE-LIGHT-PLAN.md.
The project's original firmware, tools, documentation, and diagrams are source-available under the PolyForm Noncommercial License 1.0.0. It permits use, modification, and sharing for permitted noncommercial purposes under its terms. This is a noncommercial source-available license, not GPL or an unrestricted open-source license.
Keep the license and required notice with redistributed copies. Third-party dependencies, archived vendor material, and generated firmware components retain their respective rights; see THIRD_PARTY.md. The full license terms govern permissions and exceptions.


