What it does
A mesh relay and weather node on a fence post. It wakes every thirty seconds to service the LoRa radio, forwards packets for the mesh, reads a barometer once a minute, and sleeps the rest of the time. The host is a Feather with an SX1262 radio; the Snowbird Feather 1S sits under it on stacking headers and runs the whole thing from one LiFePO4 cell and a panel the size of a paperback.
The point of the build is the winter. The node has to survive December, when the panel sees two and a half sun-hours on a good day and the cell is below freezing most mornings. Snowbird's charger holds charging off while the cell is cold and runs the load straight from the panel, so a sunny morning at minus ten still keeps the radio up without ever pushing current into a frozen cell.
Parts
| Part | Role | Example |
|---|---|---|
| Snowbird Feather 1S | Charger, BMS, gauge, 3.3 V rail, Bluetooth telemetry | snowbird-feather-1s |
| LoRa Feather host | Mesh radio and application MCU | nRF52840 Feather with SX1262 |
| LiFePO4 cell, 2000 mAh | Energy store, 3-pin JST-PH with 10 k thermistor | 26650 pouch-replacement cell with NTC lead |
| Solar panel, 6 V 3 W | Input | Unframed 6 V panel, 165 × 135 mm, bare leads to 2-pin JST-PH |
| Barometer | Weather reading over Qwiic | BMP390 Qwiic breakout |
| Enclosure | Weather | IP67 polycarbonate box, cable glands for the panel lead |
| Stacking headers | Host sits above Snowbird | Feather stacking header set |
Wiring
| Snowbird pin | Device pin | Note |
|---|---|---|
| 3V | Host 3V | Hard-wired through the header. The host runs from Snowbird's 3.3 V rail |
| EN jumper (closed) | Host EN | Ships closed. Pulls the host's EN low so its own regulator stays off |
| SDA, SCL | Host SDA, SCL | Qwiic I2C, 3.3 V. Snowbird answers at 0x36 |
| INT (jumper 11, closed) | Host D11 | Closed for this build so a power-lost or temperature-hold event wakes the host |
| PG (jumper A2) | open | Not used; read over I2C instead |
| CHG_DONE (jumper A3) | open | Not used |
| EN5V (jumper A5) | open | The 5 V rail stays off; nothing here needs it |
| BAT, NTC, GND (JST-PH) | Cell +, thermistor, cell − | Cell thermistor is the one the charger gates on |
| Solar (JST-PH 2-pin) | Panel + and − | Polarity is marked on the silkscreen; the input is reverse-protected anyway |
| Qwiic 2 | BMP390 | Second Qwiic connector, 100 mm cable |
| Host BAT, USB | not connected | Never bridge these. The host's charger must not see the cell |
The chemistry jumper is set to LiFePO4 before the cell is plugged in. The firmware refuses a Li-ion profile on this board after that, which is the point.
Power budget
| Load | Duty | Average current | Daily energy |
|---|---|---|---|
| nRF52840 host, sleeping | 97 % | 18 µA | 0.0014 Wh |
| LoRa receive window | 2.5 %, every 30 s | 0.4 mA | 0.032 Wh |
| LoRa transmit, 22 dBm | 0.3 %, mesh relaying | 0.35 mA | 0.028 Wh |
| Barometer read | once a minute | 5 µA | 0.0004 Wh |
| Snowbird idle with BLE advertising at 2 s | 100 % | 42 µA | 0.0033 Wh |
| Total | 0.8 mA | 0.065 Wh |
At 0.065 Wh a day a 2000 mAh LiFePO4 cell (6.4 Wh) runs the node for about 98 days with no sun at all. The 3 W panel delivers around 7 Wh on a December day with 2.5 sun-hours, which is a hundred times the load, so the cell only has to bridge the mornings when it is too cold to charge. The library reports harvestedToday_mWh() so the margin is a number on the dashboard, not a guess.
The panel is a 6 V class on purpose. A 12 V-class panel's open-circuit voltage climbs about 0.3 % per degree of cooling and crosses Snowbird's 22 V input ceiling near minus ten, which is exactly the morning this node needs to work. A 6 V panel never gets near the ceiling at any temperature the site sees, and at 3 W it is already fifty times the load.
Sketch
#include <Wire.h>
#include <Snowbird.h>
#include <RadioLib.h>
#include <Adafruit_BMP3XX.h>
Snowbird pmic;
Adafruit_BMP3XX bmp;
SX1262 radio = new Module(8, 14, 12, 13); // NSS, DIO1, RESET, BUSY
volatile bool pmicEvent = false;
void onPmicInt() { pmicEvent = true; }
void setup() {
Wire.begin();
pmic.begin(Wire);
pmic.setChemistry(Chem::LiFePO4); // refused if the jumper disagrees
pmic.setCapacity_mAh(2000);
pmic.setChargeCurrent_mA(1000); // 0.5 C; clamped by the input anyway
pmic.set5V(false); // nothing here needs 5 V
pmic.setTempLimits(TempLimits::charge(0, 45).discharge(-20, 60));
pmic.onEvent(POWER_LOST | TEMP_HOLD | BMS_TRIP, [](uint32_t ev) {
pmicEvent = true;
});
pinMode(11, INPUT_PULLUP); // INT jumper closed on position 11
attachInterrupt(digitalPinToInterrupt(11), onPmicInt, FALLING);
bmp.begin_I2C();
radio.begin(906.875, 250.0, 11, 5, 0x2B, 22);
radio.startReceive();
}
void serviceRadio() {
if (radio.available()) {
uint8_t buf[256];
size_t len = radio.getPacketLength();
radio.readData(buf, len);
meshForward(buf, len); // relay for the mesh
}
}
void sendTelemetry() {
float pressure = bmp.readPressure() / 100.0f;
TelemetryPacket t;
t.pressure_hPa = pressure;
t.soc = pmic.soc();
t.voltage_mV = pmic.voltage_mV();
t.solar_mW = pmic.solarPower_mW();
t.harvested_mWh = pmic.harvestedToday_mWh();
t.cell_C = pmic.batteryTemp_C();
t.faults = pmic.faults();
radio.transmit((uint8_t*)&t, sizeof(t));
radio.startReceive();
}
void loop() {
static uint32_t lastTelemetry = 0;
serviceRadio();
if (millis() - lastTelemetry > 60000) {
sendTelemetry();
lastTelemetry = millis();
}
if (pmicEvent) {
pmicEvent = false;
uint32_t f = pmic.faults();
if (f & FAULT_BMS_TRIP) radio.sleep(); // let the cell recover before relaying
}
sleepFor(30000); // host sleep; Snowbird keeps the rail up
}
Field notes
The first version used a 12 V-class 10 W panel because it was on the shelf. The node went quiet on the first clear morning in November, and the event log showed the input over-voltage protection tripping at sunrise: the panel was at open circuit, cold, and over the ceiling. Swapping to a 6 V panel fixed it and gave up nothing; at this load the 3 W panel has more than a day's energy in the first hour of sun.
Watch the cell temperature, not the air. The box runs five to eight degrees warmer than outside once the sun is on it, and the charge window opens most days by ten in the morning even when the air stays below zero. batteryTemp_C() is the number to log. On the coldest week of the test, with daytime highs of minus fourteen, the cell never reached zero and the node ran the whole week on the cell, dropping from 91 % to 84 %.
The host's own charger is the thing that bites people. With EN pulled low and BAT and USB left unconnected, the host is just a load. Leave the EN jumper open by mistake and both regulators fight on the 3V rail; it works, badly, and the idle current triples.
Measured over 94 days outdoors: 0.74 mA average host current against the 0.8 mA budget, 97 % of the days ending with the cell above 80 %, and the gauge reading within 2 % of a coulomb counter on the bench afterward. The LiFePO4 curve is flat enough that a voltage-only gauge would have been useless here; Snowbird's coulomb counting is what makes the state of charge honest.
The BLE advertising was left on so a phone at the fence line can read state of charge without climbing the post. At 2 s it costs 14 µA over the non-advertising idle, which this budget does not notice.