

MetSensorSuiteEmbeddedFirmware
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
12CC1101RGP
QFN-20-EP(4x4)-116dBm -40℃~+85℃ 1.8V~3.6V 12.3mA 12dBm 14.3mA 600Kbps ISM SPI QFN-20-EP(4x4) RF Transceiver ICs ROHSTexas InstrumentsRF Transceiver ICsNEO-M9N-00B
SMD-24P-167dBm -40℃~+85℃ 2.7V~3.6V 24s 2m 36mA@3.0V GPS、BDS、QZSS、Galileo、GLONASS RTC backup power UART、I2C、USB、SPI、JTAG SMD-24P GNSS Modules ROHSu-bloxGNSS ModulesADS1115IDGS
MSOP10Ultra-Small, Low-Power, 4 Channels 16-Bit Analog-to-Digital Converter with Internal ReferenceTexas InstrumentsUncategorisedBME280
LGA-8(2.5x2.5)-40℃~+85℃ 0%RH~100%RH 1.71V~3.6V 1.8uA 16bit I2C、SPI ±0.5℃ ±3%RH LGA-8(2.5x2.5) Temperature and Humidity Sensor ROHSBoschTemperature And Humidity SensorLM2596S-3.3
TO-263-5L-20℃~+70℃ 1 170kHz 2mA 3.3V 3A 4.5V~40V Buck Buck Built-in Fixed No TO-263-5L DC-DC Converters ROHSGRDC-DC ConvertersMS5611-01BA03
QFN-8(3x5)QFN-8(3x5) Pressure Sensors ROHSMEASPressure SensorsPPG102JA
插件-50℃~+500℃ ±0.15℃ 插件 Temperature Sensors ROHSLittelfuseTemperature SensorsB39921B3588U410
SMD3030-6P2.9dB 50Ω 915MHz SMD3030-6P SAW Filters ROHSQualcommSAW FiltersCC1190
UncategorisedTexas InstrumentsBMI-S-205-C
Cover for Frame of Standard Surface Mount Shield (Two-Piece), External Dimensions 25.86 x 38.56mmLairdUncategorisedRASPBERRY PI PICO 2
-- Raspberry Pi ROHSRaspberry PiRaspberry PiW3011
1206-40℃~+85℃ 1.1mm 1.4dBi、1.8dBi 1.5753GHz 1.6mm 2W 3.2mm 47.6MHz 50Ω 1206 Antennas ROHSPulse ElecAntennas
README
README.mdMet Pico Sensor Suite - Firmware Skeleton
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
| File | Status | Notes |
|---|---|---|
config.py | Real | All pins/addresses/constants in one place. Several TODOs remain. |
packet.py | Real | Fixed-width binary record format (PACKET_FORMAT = "<I15fBBBB", 68 bytes). |
bme280.py | Real | Full Bosch compensation formulas, forced-mode reads. I2C bus 1. |
rtd_ads1115.py | Real | Ratiometric RTD read + Callendar-Van Dusen (T >= 0 C only). I2C bus 1. |
ms5611.py | Real | PROM calibration + 1st/2nd-order temp/pressure compensation. I2C bus 2. |
bmi330.py | Unverified | Register map guessed from the related BMI323; may not function as intended - see caveat below. I2C bus 2. |
gps_neo_m9n.py | Real | NMEA GGA parsing: lat/lon/alt/num_sats/UTC time. |
sd_logger.py | Real | Mounts SD, appends packet.pack_record() output, periodic flush. |
imet_packet.py | Real | Builds the 38-byte iMet-1-RSB GPS+PTU-enhanced downlink frame + CRC-16. |
link_uart.py | Real | Frames and sends one 38-byte iMet frame per cycle to the RF Pico over UART. |
main.py | Real | Two 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 (seeimet_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 field | Source | Notes |
|---|---|---|
| GPS lat/lon/alt/num_sats/hour/min/sec | gps_* record fields | real NMEA GGA parsing; NAN/NO_DATA until a fix is acquired |
| P (pressure) | ms5611_pressure_hpa | TODO: confirm vs. BME280 as the primary pressure source |
| T (primary temp) | bme280_temp_c | |
| U (humidity) | bme280_humidity_pct | |
| Vbat | hardcoded NAN | TODO: no battery-voltage sensing exists yet (new ADC channel) |
| Tint (internal temp) | bme280_temp_c | TODO: currently duplicates T - confirm intent |
| Tpr (probe temp) | rtd_temp_c | |
| Tu (aux temp) | hardcoded NAN | TODO: 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.pyonto the board. Easiest path in Thonny/REPL:import mip; mip.install("sdcard"). This is the standard MicroPython SD-over-SPI block driver -sd_logger.pyimports it assdcard. - Skim
config.pyfor theTODOitems - 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.pypin numbers for both I2C buses, SPI (SD), GPS UART, the RF Pico link UART, andREGULATOR_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) andADS1115_GAIN(PGA setting), based on the RTD interface board. - Confirm
MS5611_I2C_ADDRandBMI330_I2C_ADDRonce those boards are wired. MS5611 and GPS now have real drivers, so a wrong address will surface as[WARN] ... init failedfrom_safe_initrather 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:
t= 12345ms T= 23.45C P=1006.32hPa RH= 45.2% RTD= 23.10Cgives 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.pynow sends a 38-byte iMet frame instead of the 65-bytepacket.pyrecord - 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.pymatches the field layout and CRC algorithm fromimet1rsb.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 - compileimet1rsb.c, feed it a known frame, and confirm itscrc16()agrees withimet_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 oflink_uart.pyso it can be matched. main.pycurrently 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| Qty | Value | Footprint | References |
|---|---|---|---|
| 7 | 100nF | Capacitor_SMD:C_0805_2012Metric | C41, C51, C91, C111, C141, C151, C181 |
| 6 | 4.7k | Resistor_SMD:R_0603_1608Metric | R07, R08, R12, R13, R15, R16 |
| 4 | 10K | Resistor_SMD:R_0603_1608Metric | R3, R04, R4, R5 |
| 3 | 0.1 uF | Capacitor_SMD:C_0805_2012Metric | C11, C12, C46 |
| 3 | 12pF | Capacitor_SMD:C_0603_1608Metric | C22, C52, C81 |
| 3 | 47pF | Capacitor_SMD:C_0603_1608Metric | C21, C26, C123 |
| 2 | 1.5pF | Capacitor_SMD:C_0805_2012Metric | C122, C131 |
| 2 | 10 uF | Capacitor_SMD:C_0603_1608Metric | C01, C02 |
| 2 | 10k | Resistor_SMD:R_0603_1608Metric | R11, R14 |
| 2 | 12nH | Inductor_SMD:L_0603_1608Metric | L121, L131 |
| 2 | 15pF | Capacitor_SMD:C_0603_1608Metric | C101, C132 |
| 2 | 18nH | Inductor_SMD:L_0603_1608Metric | L122, L132 |
| 2 | 1uF | Capacitor_SMD:C_0603_1608Metric | C1, C30 |
| 2 | 330 | Resistor_SMD:R_0603_1608Metric | R1, R2 |
| 2 | TestPoint | Connector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_Vertical | TP1, TP2 |
| 1 | 0.8pF | Capacitor_SMD:C_0805_2012Metric | C125 |
| 1 | 1.5nH | Inductor_SMD:L_1206_3216Metric | L130 |
| 1 | 1.9pF | Capacitor_SMD:C_0603_1608Metric | C121 |
| 1 | 10 | Resistor_SMD:R_0603_1608Metric | R161 |
| 1 | 100pF | Capacitor_SMD:C_0805_2012Metric | C124 |
| 1 | 1079 | footprints:1079_KEY | B41 |
| 1 | 10K OHM 0.1% 0.4W AXIAL | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R05 |
| 1 | 10nH | Inductor_SMD:L_0603_1608Metric | L25 |
| 1 | 10uF | Capacitor_SMD:C_0603_1608Metric | C2 |
| 1 | 11nH | Inductor_SMD:L_0603_1608Metric | L2 |
| 1 | 180 uF | Capacitor_SMD:CP_Elec_8x6.7 | C44 |
| 1 | 1N5820 | Diode_THT:D_A-405_P7.62mm_Horizontal | D41 |
| 1 | 1nF | Capacitor_SMD:C_0805_2012Metric | C27 |
| 1 | 2.9nH | Inductor_SMD:L_0402_1005Metric | L23 |
| 1 | 220pF | Capacitor_SMD:C_0603_1608Metric | C3 |
| 1 | 22nH | Inductor_SMD:L_0603_1608Metric | L21 |
| 1 | 26.000 MHz | Crystal:Crystal_SMD_2016-4Pin_2.0x1.6mm | X1 |
| 1 | 27pF | Capacitor_SMD:C_0805_2012Metric | C161 |
| 1 | 3.3k | Resistor_SMD:R_0603_1608Metric | R141 |
| 1 | 3.3pF | Capacitor_SMD:C_0805_2012Metric | C24 |
| 1 | 33 uH | Inductor_SMD:L_Bourns-SRN8040_8x8.15mm | L43 |
| 1 | 330/35 uF/V | Capacitor_SMD:CP_Elec_10x10 | C45 |
| 1 | 47 | Resistor_SMD:R_0603_1608Metric | R151 |
| 1 | 56K | Resistor_SMD:R_0603_1608Metric | R171 |
| 1 | 7.5nH | Inductor_SMD:L_0603_1608Metric | L22 |
| 1 | 7.5pF | Capacitor_SMD:C_1206_3216Metric | C25 |
| 1 | 9.1nH | Inductor_SMD:L_0402_1005Metric | L24 |
| 1 | ADS1115IDGS | Package_SO:TSSOP-10_3x3mm_P0.5mm | U4 |
| 1 | B39921B3588U410 | B39921B3588U410:B39162B3522U410_TDK-L | U3 |
| 1 | BME280 | Package_LGA:Bosch_LGA-8_2.5x2.5mm_P0.65mm_ClockwisePinNumbering | U5 |
| 1 | BMI-S-205-C | RF_Shielding:Laird_Technologies_BMI-S-203-F_26.21x26.21mm | P5 |
| 1 | Blue LED | LED_THT:LED_D5.0mm | D2 |
| 1 | CC1101 | Package_DFN_QFN:Texas_RGP0020H_VQFN-20-1EP_4x4mm_P0.5mm_EP2.4x2.4mm_ThermalVias | U1 |
| 1 | CC1190 | CC1190RGVT:RGV0016A | U2 |
| 1 | DNP | Capacitor_SMD:C_1206_3216Metric | C28 |
| 1 | FerriteBead | Inductor_SMD:L_1206_3216Metric | FB1 |
| 1 | LM2596S-3.3 | Package_TO_SOT_SMD:TO-263-5_TabPin3 | U8 |
| 1 | MS5611-01BA | Package_LGA:LGA-8_3x5mm_P1.25mm | U6 |
| 1 | Micro_SD_Card_Det1 | Connector_Card:microSD_HC_Molex_104031-0811 | J4 |
| 1 | NEO-M9N | RF_GPS:ublox_NEO | U7 |
| 1 | PPG102JA | footprints:PPG102JA | RT1 |
| 1 | RaspberryPi_Pico2 | Module:RaspberryPi_Pico_Common_SMD | A1 |
| 1 | Red LED | LED_THT:LED_D5.0mm | D1 |
| 1 | SMA | Connector_Coaxial:SMA_Molex_73251-1153_EdgeMount_Horizontal | J1 |
| 1 | SW_SPST | Button_Switch_THT:SW_DIP_SPSTx01_Slide_9.78x4.72mm_W7.62mm_P2.54mm | SW41 |
| 1 | Test Header | Connector_PinHeader_2.54mm:PinHeader_2x12_P2.54mm_Vertical | J2 |
| 1 | W3011 | footprints:XDCR_W3011 | AE1 |
Similar boards
- 2 layers40 × 30 mm

- 4 layers50 × 30 mm

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 mm

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 mm

Main rev b completed
iamdarshg/drone-arm
Reworked cheaper, maybe faster but less redundant drone controller.
165 parts2 - 2 layers41 × 59 mm

- 4 layers70 × 40 mm

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 mm

- 4 layers140 × 47 mm

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).