
Daylight Matchstick
VoronDesign/Voron-Hardware
Cool Hardware designed by the Voron Design group

Firmware, hardware and host software for a balloon-borne vertical air sampler for Ice Nucleating Particle (INP) collection.
Vertical sampler is an open-source printed circuit board by gfogwill, from the gfogwill/vertical_sampler repository on GitHub. It is a 2-layer board of 56 × 90 mm with 22 parts on 46 nets, routed with 154 tracks and 1 via.
Its parts include 10 connectors, 4 resistors, 3 capacitors, 2 ICs and 1 switch. It belongs with the RP2040 boards of this library.
Firmware, hardware and host software for a balloon-borne vertical air sampler for Ice Nucleating Particle (INP) collection.
Payload and ground-station Raspberry Pi Pico W boards run CircuitPython and communicate over LoRa at 868 MHz. The system supports two payload identities: kenttarova and matorova.
┌─────────────────────────┐ LoRa 868 MHz ┌─────────────────────────┐
│ PAYLOAD │◄───────────────────────────►│ GROUND STATION │
│ Raspberry Pi Pico W │ │ Raspberry Pi Pico W │
│ │ │ │
│ - GPS (+ RTC sync) │ │ - Receives telemetry │
│ - SHT85 RH/temp sensor │ │ - Relays to PC via USB │
│ - LPS25H pressure │ │ - Forwards commands │
│ - Pumps + electrovalve │ └─────────────────────────┘
│ - SD logging │ │
│ - Battery + flow meter │ USB Serial
│ - Watchdog │ │
└─────────────────────────┘ ┌─────────────────────────┐
│ Host computer │
│ host/cli.py │
│ host/quickview.py │
└─────────────────────────┘vertical_sampler/
├── firmware/ # CircuitPython code deployed to Pico boards
│ ├── common/ # Copied flat to every CIRCUITPY drive
│ │ ├── config.py # PCB v1 GPIOs, calibration, limits, LoRa addresses
│ │ ├── payload.py # Active payload drivers and mission loop
│ │ ├── lora.py # RFM9x wrapper
│ │ ├── pressure_sensor.py # LPS25H driver
│ │ ├── sdcard.py # SD mount, JSONL data and log writes
│ │ ├── logging.py
│ │ ├── pack.py # Binary LoRa telemetry format
│ │ ├── led.py
│ │ ├── adafruit_gps.py # Vendored CircuitPython dependency
│ │ └── adafruit_rfm9x.py # Vendored CircuitPython dependency
│ ├── kenttarova_main.py # kenttarova payload entry point
│ ├── matorova_main.py # matorova payload entry point
│ └── ground_main.py # Ground-station entry point
├── host/ # Python 3 programs running on the control PC
│ ├── cli.py
│ └── quickview.py
├── pcb/ # KiCad PCB v1 project
├── docs/
│ └── TROUBLESHOOTING.md
├── Makefile
└── README.mdfirmware/common/ is copied as flat modules to the root of CIRCUITPY. CircuitPython imports therefore remain simple:
import config
import lora
import pack| Unit | LoRa node address | Entry point |
|---|---|---|
| Ground station | 0x47 | firmware/ground_main.py |
| kenttarova | 0x71 | firmware/kenttarova_main.py |
| matorova | 0x93 | firmware/matorova_main.py |
Addresses, GPIO assignments, calibration constants and safety limits are defined centrally in firmware/common/config.py.
make download-circuitpython-imageCopy the resulting .uf2 to the Pico W while it is in BOOTSEL mode.
The repository vendors adafruit_gps.py and adafruit_rfm9x.py. Install the remaining required CircuitPython libraries in the Pico lib/ directory, including the dependencies required by the RFM9x driver such as adafruit_bus_device.
Set the CircuitPython mount point if needed:
export CIRCUITPY_PATH=/media/$USER/CIRCUITPYDeploy the required device image:
make update-kenttarova
make update-matorova
make update-groundEach target:
code.py.firmware/common/*.py modules to the Pico.main.py.Run commands from the host computer:
python host/cli.py kenttarova data
python host/cli.py kenttarova pump front on
python host/cli.py kenttarova pump back off
python host/cli.py kenttarova valve on
python host/cli.py matorova datahost/quickview.py is available for local data inspection and visualization.
The host dashboards automatically use the latest sea-level pressure observation
from FMI station Kittilä Matorova (fmisid=101985) as QNH. The value is
refreshed every 10 minutes through the public
FMI WFS API. If FMI is unavailable, the last
fresh FMI value is retained and then the manual/default value of 1013.25 hPa
is used. Pass --qnh VALUE to select a different fallback, or
--no-auto-qnh to disable automatic updates. Logged monitor samples include
_qnh_hpa, _qnh_source and _qnh_observation_time so derived altitude
remains traceable.
Each payload sample is logged as JSONL when an SD card is available and sent over LoRa as a packed binary packet defined in firmware/common/pack.py.
| Field | Type | Description |
|---|---|---|
msg_type | str | telemetry, cmd_ack or cmd_err |
payload_id | str | kenttarova or matorova |
rtc_time | str | RTC timestamp in ISO 8601 format |
gps_time | uint32/null | GPS UTC Unix epoch |
gps_latitude | float/null | Degrees |
gps_longitude | float/null | Degrees |
gps_altitude | float/null | Metres |
rh_sensor_humidity | float | Relative humidity, % |
rh_sensor_temperature | float | Temperature, °C |
pressure_sensor_pressure | float | Pressure, mbar |
pressure_sensor_temperature | float | Temperature, °C |
battery_voltage | float | Calibrated 6S battery voltage |
cpu_temperature | float | Pico internal temperature, °C |
flow | float | Standard L/min |
rssi | int | Last received LoRa RSSI |
pump_front_state | int | 0 or 1 |
pump_back_state | int | 0 or 1 |
valve_state | int | 0 or 1 |
RTC synchronization: on the first valid GPS fix, the payload sets the onboard RTC to GPS UTC. Subsequent
rtc_timevalues remain valid even if the GPS temporarily loses its fix.
The SD-card handler writes:
| File | Content |
|---|---|
/sd/<payload_id>_log.txt | Human-readable events and diagnostics |
/sd/<payload_id>_NNN.jsonl | One JSON object per sample cycle |
If the SD card is unavailable or fails while operating, the payload continues running and transmitting telemetry over LoRa. Logging degrades to the serial console instead of stopping the mission.
| Condition | Threshold | Action |
|---|---|---|
| Battery warning | ≤ 19.8 V | Warning logged |
| Battery critical | ≤ 18.6 V | Pumps off, valve off, error logged |
| CPU temperature warning | ≥ 45 °C | Warning logged |
| CPU temperature critical | ≥ 55 °C | Pumps off, valve off, error logged |
| Main-loop stall | 30 s watchdog timeout | Pico reset |
The watchdog is fed in the main loop, GPS wait loop, LoRa receive loop and failure-report loops.
| Function | Pico GPIO |
|---|---|
| GPS UART TX/RX | GP0 / GP1 |
| I2C SDA/SCL | GP2 / GP3 |
| Pump front | GP6 |
| Electrovalve | GP7 |
| SD chip select | GP9 |
| SPI SCK/MOSI/MISO | GP10 / GP11 / GP12 |
| LoRa chip select | GP16 |
| OPC chip select (reserved) | GP17 |
| Pump back | GP21 |
| Battery monitor ADC | GP27 |
| Flowmeter ADC | GP28 |
The RFM9x reset line is not routed on PCB v1. config.LORA_RESET_DUMMY remains because the CircuitPython RFM9x driver requires a reset-pin argument.
The TSI 4121 flowmeter is configured as:
FLOW_OFFSET_LMIN in firmware/common/config.py.BATTERY_CAL_FACTOR in firmware/common/config.py converts the Pico ADC voltage to the measured 6S battery voltage. Recalibrate against a multimeter if the divider or analog front-end changes.
firmware/common/payload.py contains the active, hardware-validated payload drivers and mission loop.firmware/common/config.py is the single source of truth for PCB pins, calibration, operating limits and LoRa addresses.old_hardware branch.docs/TROUBLESHOOTING.md for deployment and communication diagnostics.| Qty | Value | Footprint | References |
|---|---|---|---|
| 2 | 0.1u | Capacitor_SMD:C_1206_3216Metric | C1, C2 |
| 2 | 4k7 | Resistor_SMD:R_1206_3216Metric | R1, R2 |
| 1 | 10k | Resistor_SMD:R_1206_3216Metric | R3 |
| 1 | 220uf | Capacitor_THT:CP_Radial_D6.3mm_P2.50mm | C3 |
| 1 | 33k | Resistor_SMD:R_1206_3216Metric | R4 |
| 1 | APM | Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical | J6 |
| 1 | Electrovalve | Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical | J3 |
| 1 | FerriteBead | Inductor_SMD:L_1206_3216Metric | FB1 |
| 1 | Flowmeter | Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical | J7 |
| 1 | GPS | Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical | J4 |
| 1 | LoRa | Adafuit_rfm9x:MODULE_3073 | U1 |
| 1 | MICRO SD | Adafuit_rfm9x:MODULE_MICROSD_SPI_OR_SDIO_CARD | U2 |
| 1 | OPC | Connector_JST:JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical | J8 |
| 1 | Power | Connector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical | J5 |
| 1 | Pressure I2C | Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical | J9 |
| 1 | Pump Back | Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical | J1 |
| 1 | Pump Front | Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical | J2 |
| 1 | RaspberryPi_Pico | Module:RaspberryPi_Pico_Common_THT | A1 |
| 1 | SW_Push | Button_Switch_THT:SW_PUSH_6mm | SW1 |
| 1 | Temp & RH I2C | Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical | J10 |
It measures 56 × 90 mm and has 2 copper layers, 1.6 mm thick.
22 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the gfogwill/vertical_sampler repository on GitHub, at the commit this page was made from: the board is pcb/pcb.kicad_pcb and its schematic pcb/pcb.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository states no licence, so its author keeps all rights: ask gfogwill before reusing the design.

VoronDesign/Voron-Hardware
Cool Hardware designed by the Voron Design group

hoglet67/RGBtoHDMI
Bare-metal Raspberry Pi project that provides pixel-perfect sampling of Retro Computer RGB/YUV video and conversion to HDMI

hackclub/OnBoard
💡 Join 1,000 teenagers and make your first PCB with a $100 grant!

abhishek-kakkar/BeagleLogic
A Software Suite that implements a logic analyzer with the PRU on the BeagleBone / BeagleBone Black.

Chrismettal/flipper-zero-backpacks
Backpack-style addon boards for the Flipper Zero!



CRImier/MyKiCad
My KiCad boards. Can't be bothered uploading individually anymore, so here you go.