Skip to content
Snowbird Power Systems

Reference build

Solar LoRa node

A Meshtastic-class LoRa node on a Snowbird Feather 1S with a 2000 mAh LiFePO4 cell and a 6 V panel, outdoors through a Kelowna winter.

An open weatherproof enclosure on a wooden post in a snowy alpine meadow, a Snowbird Feather 1S and a pouch cell mounted inside it, wired to a small solar panel angled toward the morning sun.

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.