Skip to the content
PCBKitNew board
2 layers56 × 90 mmVertical sampler, the printed circuit board in 3D

Firmware, hardware and host software for a balloon-borne vertical air sampler for Ice Nucleating Particle (INP) collection.

Size
56 × 90 mm
Copper layers
2
Thickness
1.6 mm
Parts
22
Nets
46
Pads
120
Tracks, vias
154, 1
Pours
1
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
27 Apr 2026
Updated
9 Oct 2026

Overview

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.

README

README.md

Vertical Sampler

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.

System Overview

text
┌─────────────────────────┐        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       │
                                                        └─────────────────────────┘

Repository Structure

text
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.md

firmware/common/ is copied as flat modules to the root of CIRCUITPY. CircuitPython imports therefore remain simple:

python
import config
import lora
import pack

Payload Identities

UnitLoRa node addressEntry point
Ground station0x47firmware/ground_main.py
kenttarova0x71firmware/kenttarova_main.py
matorova0x93firmware/matorova_main.py

Addresses, GPIO assignments, calibration constants and safety limits are defined centrally in firmware/common/config.py.

Quick Start

1. Flash CircuitPython

bash
make download-circuitpython-image

Copy the resulting .uf2 to the Pico W while it is in BOOTSEL mode.

2. Install dependencies

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.

3. Deploy firmware

Set the CircuitPython mount point if needed:

bash
export CIRCUITPY_PATH=/media/$USER/CIRCUITPY

Deploy the required device image:

bash
make update-kenttarova
make update-matorova
make update-ground

Each target:

  1. Removes stale code.py.
  2. Copies all firmware/common/*.py modules to the Pico.
  3. Copies the selected entry point as main.py.

4. Control a payload

Run commands from the host computer:

bash
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 data

host/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.

Telemetry Format

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.

FieldTypeDescription
msg_typestrtelemetry, cmd_ack or cmd_err
payload_idstrkenttarova or matorova
rtc_timestrRTC timestamp in ISO 8601 format
gps_timeuint32/nullGPS UTC Unix epoch
gps_latitudefloat/nullDegrees
gps_longitudefloat/nullDegrees
gps_altitudefloat/nullMetres
rh_sensor_humidityfloatRelative humidity, %
rh_sensor_temperaturefloatTemperature, °C
pressure_sensor_pressurefloatPressure, mbar
pressure_sensor_temperaturefloatTemperature, °C
battery_voltagefloatCalibrated 6S battery voltage
cpu_temperaturefloatPico internal temperature, °C
flowfloatStandard L/min
rssiintLast received LoRa RSSI
pump_front_stateint0 or 1
pump_back_stateint0 or 1
valve_stateint0 or 1

RTC synchronization: on the first valid GPS fix, the payload sets the onboard RTC to GPS UTC. Subsequent rtc_time values remain valid even if the GPS temporarily loses its fix.

SD Logging

The SD-card handler writes:

FileContent
/sd/<payload_id>_log.txtHuman-readable events and diagnostics
/sd/<payload_id>_NNN.jsonlOne 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.

Safety Features

ConditionThresholdAction
Battery warning≤ 19.8 VWarning logged
Battery critical≤ 18.6 VPumps off, valve off, error logged
CPU temperature warning≥ 45 °CWarning logged
CPU temperature critical≥ 55 °CPumps off, valve off, error logged
Main-loop stall30 s watchdog timeoutPico reset

The watchdog is fed in the main loop, GPS wait loop, LoRa receive loop and failure-report loops.

PCB v1 Pin Map

FunctionPico GPIO
GPS UART TX/RXGP0 / GP1
I2C SDA/SCLGP2 / GP3
Pump frontGP6
ElectrovalveGP7
SD chip selectGP9
SPI SCK/MOSI/MISOGP10 / GP11 / GP12
LoRa chip selectGP16
OPC chip select (reserved)GP17
Pump backGP21
Battery monitor ADCGP27
Flowmeter ADCGP28

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.

Calibration

Flowmeter

The TSI 4121 flowmeter is configured as:

  • Signal range: 0–4 V corresponding to 0–20 Std L/min.
  • ADC divider: 10 kΩ series resistor and 32.6 kΩ to ground.
  • Divider ratio: (32.6 / (10.0 + 32.6)).
  • Zero offset: FLOW_OFFSET_LMIN in firmware/common/config.py.

Battery monitor

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.

Development Notes

  • 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.
  • The old breadboard firmware is preserved in the old_hardware branch.
  • See docs/TROUBLESHOOTING.md for deployment and communication diagnostics.

Parts

22
QtyValueFootprintReferences
20.1uCapacitor_SMD:C_1206_3216MetricC1, C2
24k7Resistor_SMD:R_1206_3216MetricR1, R2
110kResistor_SMD:R_1206_3216MetricR3
1220ufCapacitor_THT:CP_Radial_D6.3mm_P2.50mmC3
133kResistor_SMD:R_1206_3216MetricR4
1APMConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ6
1ElectrovalveConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ3
1FerriteBeadInductor_SMD:L_1206_3216MetricFB1
1FlowmeterConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ7
1GPSConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ4
1LoRaAdafuit_rfm9x:MODULE_3073U1
1MICRO SDAdafuit_rfm9x:MODULE_MICROSD_SPI_OR_SDIO_CARDU2
1OPCConnector_JST:JST_XH_B6B-XH-A_1x06_P2.50mm_VerticalJ8
1PowerConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ5
1Pressure I2CConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ9
1Pump BackConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ1
1Pump FrontConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ2
1RaspberryPi_PicoModule:RaspberryPi_Pico_Common_THTA1
1SW_PushButton_Switch_THT:SW_PUSH_6mmSW1
1Temp & RH I2CConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ10

Questions

How big is the Vertical sampler PCB?

It measures 56 × 90 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Vertical sampler use?

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.

Where can I download the Vertical sampler KiCad files?

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.

Can I use the Vertical sampler design in my own project?

Its repository states no licence, so its author keeps all rights: ask gfogwill before reusing the design.

Similar boards

  • 2 layers158 × 11 mmDaylight Matchstick, the printed circuit board in 3D

    Daylight Matchstick

    VoronDesign/Voron-Hardware

    Cool Hardware designed by the Voron Design group

    22 parts1.1kGPL-3.0
  • 2 layers57 × 65 mmRgb to hdmi, the printed circuit board in 3D

    Rgb to hdmi

    hoglet67/RGBtoHDMI

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

    22 parts1kGPL-3.0
  • 2 layers81 × 57.2 mmRP2040-Macropad, the printed circuit board in 3D

    RP2040-Macropad

    hackclub/OnBoard

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

    22 parts915
  • 2 layers54.6 × 70.9 mmBeagleLogic Cape, the printed circuit board in 3D

    BeagleLogic Cape

    abhishek-kakkar/BeagleLogic

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

    22 parts479GPL-3.0
  • 2 layers66.4 × 24.5 mmConnecting piece, the printed circuit board in 3D

    Connecting piece

    Chrismettal/flipper-zero-backpacks

    Backpack-style addon boards for the Flipper Zero!

    22 parts456
  • 2 layers51 × 70 mmDMG A04-01, the printed circuit board in 3D

    DMG A04-01

    HDR/NintendoPCBs

    Replacement pcb's for Nintendo games.

    22 parts321
  • 2 layers27.8 × 16 mmSpi 1v8 level shifter, the printed circuit board in 3D

    Spi 1v8 level shifter

    CRImier/MyKiCad

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

    22 parts253GPL-3.0