Skip to the content
4 layers220 × 111.8 mmMetSensorSuiteEmbeddedFirmware, the printed circuit board in 3D

This is the source code that runs on the Sensor Suite pico which handles reading from sensors, calculating needed products and building data packets

Size
220 × 111.8 mm
Copper layers
4
Thickness
1.6 mm
Parts
91
Nets
91
Pads
439
Tracks, vias
383, 30
Pours
2
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
14 Jun 2026
Updated
15 Sep 2026

Overview

MetSensorSuiteEmbeddedFirmware is an open-source printed circuit board by Lowcost-Radiosonde-Research-Project, from the Lowcost-Radiosonde-Research-Project/MetSensorSuiteEmbeddedFirmware repository on GitHub, published under the BSD-3-Clause licence. It is a 4-layer board of 220 × 111.8 mm with 91 parts on 91 nets, routed with 383 tracks and 30 vias.

It is built around the CC1101RGP, NEO-M9N-00B and ADS1115IDGS, with the BME280, LM2596S-3.3 and MS5611-01BA03 among its other parts. Its parts include 36 capacitors, 19 resistors, 12 inductors, 8 ICs and 4 connectors. It belongs with the RP2040 and RP2350 boards of this library.

Main components

12

README

README.md

Met Pico Sensor Suite - Firmware Skeleton

Raspberry Pi License Firmware KiCad Status

Status as of latest revision. Every module now has a real implementation - BMI330's carries a significant caveat (see below and its module docstring) since its register map is unverified.

File layout

FileStatusNotes
config.pyRealAll pins/addresses/constants in one place. Several TODOs remain.
packet.pyRealFixed-width binary record format (PACKET_FORMAT = "<I15fBBBB", 68 bytes).
bme280.pyRealFull Bosch compensation formulas, forced-mode reads. I2C bus 1.
rtd_ads1115.pyRealRatiometric RTD read + Callendar-Van Dusen (T >= 0 C only). I2C bus 1.
ms5611.pyRealPROM calibration + 1st/2nd-order temp/pressure compensation. I2C bus 2.
bmi330.pyUnverifiedRegister map guessed from the related BMI323; may not function as intended - see caveat below. I2C bus 2.
gps_neo_m9n.pyRealNMEA GGA parsing: lat/lon/alt/num_sats/UTC time.
sd_logger.pyRealMounts SD, appends packet.pack_record() output, periodic flush.
imet_packet.pyRealBuilds the 38-byte iMet-1-RSB GPS+PTU-enhanced downlink frame + CRC-16.
link_uart.pyRealFrames and sends one 38-byte iMet frame per cycle to the RF Pico over UART.
main.pyRealTwo I2C buses, SD log + iMet frame to RF Pico link, per-sensor fault isolation.

Every sensor module exposes init_<name>(i2c, addr) returning an object with a read() method, and main.py wraps each init in try/except (OSError, ValueError) and each read in try/except OSError (the only error read() can raise) so one bad/missing sensor doesn't take down the loop or the log - it just logs NAN for that field and a [WARN] line to the console.

Bus layout

  • I2C bus 1 (I2C1*): ADS1115 (RTD) + BME280
  • I2C bus 2 (I2C2*): MS5611 + BMI330
  • UART (GPS_UART_*): NEO-M9N GPS
  • UART (LINK_UART_*): outbound link to the RF Pico, one framed 38-byte iMet-1-RSB frame per cycle (see imet_packet.py / link_uart.py)
  • SPI (SD_*): SD card

All pin numbers above are placeholders pending the Tuesday wiring session - see the TODOs in config.py.

BMI330 caveat - documentation lacking, driver may not work as intended

bmi330.py is written against the BMI323's register map (a related Bosch part) because verified BMI330-specific register documentation wasn't available when this was written. The register addresses, ACC_CONF/GYR_CONF bitfield values, and full-scale range assumptions are all unverified guesses. The driver may run without raising any error and still return meaningless numbers.

On Tuesday, init_bmi330() will print whatever it reads from the assumed CHIP_ID register (0x00) as a diagnostic - note that value down. If accel/gyro readings look like noise, sit constant, or are far outside physical range (accelerometer magnitude should be close to 1g at rest), the register map guess is wrong and bmi330.py needs revisiting against real BMI330 documentation (Bosch's BMI3 Sensor API repo is the best starting point once it covers BMI330). This isn't blocking for Wednesday - BMI330 isn't part of that test.

Field naming convention

packet.FIELD_NAMES prefixes every field with the sensor that owns it (bme280_*, ms5611_*, rtd_*, bmi330_*, gps_*). BMI330 fields are in milli-g / milli-deg/s (_mg / _mdps) rather than g / deg-per-s

  • worth keeping in mind when the real BMI330 driver is written, since the conversion from raw counts will need to land in those units.

The GPS record has gps_num_sats plus gps_hour/gps_minute/ gps_second (UTC time of fix), but no fix-type field - gps.read() returns a 7-tuple (lat, lon, alt, num_sats, hour, minute, second). Position/altitude are NAN and the satellite/time fields are NO_DATA_U8 until a checksum-valid GGA sentence with a non-zero fix quality has been parsed. If you want fix-quality (e.g. for SondeHub-style reporting) added too, that's a small change to packet.py (one more B field) and gps_neo_m9n.py's _parse_gga().

iMet-1-RSB downlink frame

Each cycle, after the SD record is written, main.py builds a separate 38-byte frame via imet_packet.pack_frame() - a GPS Data Packet (PKT_ID=0x02, 18 bytes) immediately followed by a PTU (enhanced) Data Packet (PKT_ID=0x04, 20 bytes), each with its own CRC-16, matching InterMet's "Appendix A - Binary Radiosonde Packet Definition" as decoded by imet1rsb. This frame - not the packet.py record - is what gets sent over link_uart to the RF Pico for AFSK modulation; packet.py stays SD-only.

Current sensor -> iMet field mapping (set in main.py, easy to change - it's just which record[...] value is passed to which imet_packet keyword argument):

iMet fieldSourceNotes
GPS lat/lon/alt/num_sats/hour/min/secgps_* record fieldsreal NMEA GGA parsing; NAN/NO_DATA until a fix is acquired
P (pressure)ms5611_pressure_hpaTODO: confirm vs. BME280 as the primary pressure source
T (primary temp)bme280_temp_c
U (humidity)bme280_humidity_pct
Vbathardcoded NANTODO: no battery-voltage sensing exists yet (new ADC channel)
Tint (internal temp)bme280_temp_cTODO: currently duplicates T - confirm intent
Tpr (probe temp)rtd_temp_c
Tu (aux temp)hardcoded NANTODO: no mapping defined - InterMet's intended use of this field is unclear

All of the NAN/sentinel placeholders above pack safely (verified in imet_packet.py's _scaled_*/_encode_altitude helpers - they map NAN to the field's "no data" value rather than raising), so the frame is well-formed even with everything still stubbed.

Before Tuesday (no hardware needed)

  • Get a copy of sdcard.py onto the board. Easiest path in Thonny/REPL: import mip; mip.install("sdcard"). This is the standard MicroPython SD-over-SPI block driver - sd_logger.py imports it as sdcard.
  • Skim config.py for the TODO items - nothing blocks you from reading the code now, but these are the values you'll set Tuesday.

Tuesday, once hardware is in hand

  • Update config.py pin numbers for both I2C buses, SPI (SD), GPS UART, the RF Pico link UART, and REGULATOR_EN_PIN (LM2596 3.3V rail enable) to match actual wiring.
  • Confirm RTD_MUX / REF_MUX (which ADS1115 differential pair is the RTD vs. the reference resistor) and ADS1115_GAIN (PGA setting), based on the RTD interface board.
  • Confirm MS5611_I2C_ADDR and BMI330_I2C_ADDR once those boards are wired. MS5611 and GPS now have real drivers, so a wrong address will surface as [WARN] ... init failed from _safe_init rather than silently logging NaN.
  • For BMI330: note the [INFO] BMI330 CHIP_ID register read as 0x.. line at startup and sanity-check accel/gyro readings against the caveat above - this driver's register map is a guess.
  • Run main.py. With only BME280 + RTD wired, expect [WARN] lines at startup for MS5611/BMI330/GPS init or SD mount (if not present) - that's expected, the loop should keep running and print live T/P/RH/RTD values once per second.

Wednesday (storm chase)

The console line each cycle:

text
t=   12345ms  T= 23.45C  P=1006.32hPa  RH= 45.2%  RTD= 23.10C

gives a live sanity check that BME280 and RTD agree roughly with each other and with ambient conditions. The SD card (if mounted) accumulates PACKET_SIZE-byte (68-byte) binary records in /sd/metlog.bin - decode with packet.unpack_record() in a small post-processing script, reading PACKET_SIZE bytes at a time.

Known gaps / next steps

  • RTD conversion only handles T >= 0 C (Callendar-Van Dusen quadratic inversion). The sub-zero cubic-term inversion needs to be added before the actual flight.
  • BMI330 is unverified - see the dedicated caveat section above and bmi330.py's module docstring before trusting any IMU data.
  • link_uart.py now sends a 38-byte iMet frame instead of the 65-byte packet.py record - see "iMet-1-RSB downlink frame" above for the sensor mapping and its open TODOs (Vbat sensing, GPS UTC time, P/Tint/Tu assignment).
  • imet_packet.py matches the field layout and CRC algorithm from imet1rsb.c's source, but hasn't been cross-checked byte-for-byte against a compiled run of that decoder yet. Worth doing once there's a spare moment - compile imet1rsb.c, feed it a known frame, and confirm its crc16() agrees with imet_packet.crc16().
  • The RF Pico's receive-side parser for link_uart's frame format (sync bytes + length + checksum) doesn't exist yet - that's firmware for the other board, out of scope here, but the frame format is documented at the top of link_uart.py so it can be matched.
  • main.py currently runs everything on a single core via a simple fixed-interval loop. If SD writes, link UART sends, or sensor reads start causing timing jitter once everything is real, revisit splitting acquisition and logging across the two cores with _thread.

Parts

91
QtyValueFootprintReferences
7100nFCapacitor_SMD:C_0805_2012MetricC41, C51, C91, C111, C141, C151, C181
64.7kResistor_SMD:R_0603_1608MetricR07, R08, R12, R13, R15, R16
410KResistor_SMD:R_0603_1608MetricR3, R04, R4, R5
30.1 uFCapacitor_SMD:C_0805_2012MetricC11, C12, C46
312pFCapacitor_SMD:C_0603_1608MetricC22, C52, C81
347pFCapacitor_SMD:C_0603_1608MetricC21, C26, C123
21.5pFCapacitor_SMD:C_0805_2012MetricC122, C131
210 uFCapacitor_SMD:C_0603_1608MetricC01, C02
210kResistor_SMD:R_0603_1608MetricR11, R14
212nHInductor_SMD:L_0603_1608MetricL121, L131
215pFCapacitor_SMD:C_0603_1608MetricC101, C132
218nHInductor_SMD:L_0603_1608MetricL122, L132
21uFCapacitor_SMD:C_0603_1608MetricC1, C30
2330Resistor_SMD:R_0603_1608MetricR1, R2
2TestPointConnector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_VerticalTP1, TP2
10.8pFCapacitor_SMD:C_0805_2012MetricC125
11.5nHInductor_SMD:L_1206_3216MetricL130
11.9pFCapacitor_SMD:C_0603_1608MetricC121
110Resistor_SMD:R_0603_1608MetricR161
1100pFCapacitor_SMD:C_0805_2012MetricC124
11079footprints:1079_KEYB41
110K OHM 0.1% 0.4W AXIALResistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_HorizontalR05
110nHInductor_SMD:L_0603_1608MetricL25
110uFCapacitor_SMD:C_0603_1608MetricC2
111nHInductor_SMD:L_0603_1608MetricL2
1180 uFCapacitor_SMD:CP_Elec_8x6.7C44
11N5820Diode_THT:D_A-405_P7.62mm_HorizontalD41
11nFCapacitor_SMD:C_0805_2012MetricC27
12.9nHInductor_SMD:L_0402_1005MetricL23
1220pFCapacitor_SMD:C_0603_1608MetricC3
122nHInductor_SMD:L_0603_1608MetricL21
126.000 MHzCrystal:Crystal_SMD_2016-4Pin_2.0x1.6mmX1
127pFCapacitor_SMD:C_0805_2012MetricC161
13.3kResistor_SMD:R_0603_1608MetricR141
13.3pFCapacitor_SMD:C_0805_2012MetricC24
133 uHInductor_SMD:L_Bourns-SRN8040_8x8.15mmL43
1330/35 uF/VCapacitor_SMD:CP_Elec_10x10C45
147Resistor_SMD:R_0603_1608MetricR151
156KResistor_SMD:R_0603_1608MetricR171
17.5nHInductor_SMD:L_0603_1608MetricL22
17.5pFCapacitor_SMD:C_1206_3216MetricC25
19.1nHInductor_SMD:L_0402_1005MetricL24
1ADS1115IDGSPackage_SO:TSSOP-10_3x3mm_P0.5mmU4
1B39921B3588U410B39921B3588U410:B39162B3522U410_TDK-LU3
1BME280Package_LGA:Bosch_LGA-8_2.5x2.5mm_P0.65mm_ClockwisePinNumberingU5
1BMI-S-205-CRF_Shielding:Laird_Technologies_BMI-S-203-F_26.21x26.21mmP5
1Blue LEDLED_THT:LED_D5.0mmD2
1CC1101Package_DFN_QFN:Texas_RGP0020H_VQFN-20-1EP_4x4mm_P0.5mm_EP2.4x2.4mm_ThermalViasU1
1CC1190CC1190RGVT:RGV0016AU2
1DNPCapacitor_SMD:C_1206_3216MetricC28
1FerriteBeadInductor_SMD:L_1206_3216MetricFB1
1LM2596S-3.3Package_TO_SOT_SMD:TO-263-5_TabPin3U8
1MS5611-01BAPackage_LGA:LGA-8_3x5mm_P1.25mmU6
1Micro_SD_Card_Det1Connector_Card:microSD_HC_Molex_104031-0811J4
1NEO-M9NRF_GPS:ublox_NEOU7
1PPG102JAfootprints:PPG102JART1
1RaspberryPi_Pico2Module:RaspberryPi_Pico_Common_SMDA1
1Red LEDLED_THT:LED_D5.0mmD1
1SMAConnector_Coaxial:SMA_Molex_73251-1153_EdgeMount_HorizontalJ1
1SW_SPSTButton_Switch_THT:SW_DIP_SPSTx01_Slide_9.78x4.72mm_W7.62mm_P2.54mmSW41
1Test HeaderConnector_PinHeader_2.54mm:PinHeader_2x12_P2.54mm_VerticalJ2
1W3011footprints:XDCR_W3011AE1

Similar boards

  • 2 layers40 × 30 mmEstacionTerrena, the printed circuit board in 3D

    EstacionTerrena

    dragondgold/CanSat

    Plataforma educativa basada en un satélite CanSat

    65 parts1GPL-3.0
  • 4 layers50 × 30 mmCC1101, the printed circuit board in 3D

    CC1101

    sneakylizard123-4/CC1101_Module

    A simple breakout-style radio module for long-range sub-GHz link without designing an RF front end from scratch.

    47 partsCERN-OHL-P-2.0
  • 4 layers25.4 × 25.4 mmZeo1.2 devboard, the printed circuit board in 3D

    Zeo1.2 devboard

    zeodotdev/zeo-boards

    ESP32-S3 development board with onboard BME280 (temperature, humidity, pressure) and LSM6DS3 (6-axis IMU) sensors. This repo contains the designs, ESP-IDF firmware, and a Python plotter for visualization.

    92 parts3
  • 4 layers140 × 100 mmMain rev b completed, the printed circuit board in 3D

    Main rev b completed

    iamdarshg/drone-arm

    Reworked cheaper, maybe faster but less redundant drone controller.

    165 parts2
  • 2 layers41 × 59 mmV0.0-CutieCat, the printed circuit board in 3D

    V0.0-CutieCat

    DevKitty-io/CutieCat

    The Cat-Shaped Hacking & Development Platform

    54 parts9MIT
  • 4 layers70 × 40 mmFlight comp srad-2026, the printed circuit board in 3D

    Flight comp srad-2026

    vito-pp/FlightComputer-SRAD-2026

    HW and SW documentation of a Student Researched And Developed (SRAD) flight computer in charge of telemetry for ITBA's rocketry team 2026

    86 parts3
  • 4 layers33.1 × 64.7 mmNrg3, the printed circuit board in 3D

    Nrg3

    panStamp/kicad

    Kicad projects

    135 parts15GPL-2.0
  • 4 layers140 × 47 mmTurpial, the printed circuit board in 3D

    Turpial

    btcven/turpial-hardware

    The hardware of the Turpial device.

    197 parts3

Questions

How big is the MetSensorSuiteEmbeddedFirmware PCB?

It measures 220 × 111.8 mm and has 4 copper layers, 1.6 mm thick.

What parts does the MetSensorSuiteEmbeddedFirmware use?

91 parts, built around the CC1101RGP, NEO-M9N-00B, ADS1115IDGS and BME280. 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 MetSensorSuiteEmbeddedFirmware KiCad files?

From the Lowcost-Radiosonde-Research-Project/MetSensorSuiteEmbeddedFirmware repository on GitHub, at the commit this page was made from: the board is hardware/hardware.kicad_pcb and its schematic hardware/I2C0.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the MetSensorSuiteEmbeddedFirmware design in my own project?

Its repository publishes it under the BSD-3-Clause licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).