Siddhant Kumar
Project 048 · Environment

High-Altitude Telemetry Node.

A weather station engineered to survive a glacier or a mountain ridge — extreme cold, wind, ice and total isolation — and still report reliably for a full season.

Advanced 16–24 hours 34 min read LoRaWeatherRemote
Jump to source Bill of materials
High-Altitude Telemetry Node — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Advanced
Build time
16–24 hours
Indicative cost
₹18,000 – ₹35,000 (site-dependent)
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Environment
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A weather station engineered to survive a glacier or a mountain ridge — extreme cold, wind, ice and total isolation — and still report reliably for a full season.

The places we most need mountain weather data are the places hardest to put an instrument: a glacier surface, a high ridge, an avalanche start zone, a remote pass. These sites drive water supply, avalanche risk and climate research, yet they sit far above any power line or cell tower, in temperatures that flatten ordinary batteries, under winds that tear away anything not built to hold, and under snow and rime ice that bury or freeze the very sensors you came to read. A weather station that works fine in a backyard will be dead within a week up there. This project is about the engineering that lets a node survive — because in high-altitude telemetry, ruggedness and power are the hard problems, and the sensing is almost the easy part.

The node measures the mountain essentials — temperature, humidity and barometric pressure, wind speed and direction, and snow depth (an ultrasonic sensor looking down at the snow surface, which is what actually matters for hydrology and avalanche work) — but every one of those measurements is shaped by the environment. The temperature sensor must be shielded and, ideally, aspirated so sun on the housing does not fake a warm reading; the anemometer must shed rime ice or its bearings freeze; the snow sensor's ultrasonic pulse must be temperature-corrected because the speed of sound changes sharply across the huge temperature range of a mountain day. The design treats each sensor's failure mode in the cold as a first-class problem.

The two dominant engineering constraints are power and communication. Lithium batteries lose capacity in the cold and — critically — must not be charged below freezing, so the power system has to manage temperature, not just voltage, and the node must sip energy through long, dark, storm-bound periods. And with no cellular coverage, the node reports over long-range LoRa to a valley gateway or, where even that is impossible, over a satellite link (e.g. Iridium) that costs real power and money per message, forcing a discipline of infrequent, compact, prioritised reporting. Everything is logged locally so a week-long storm that severs the link loses nothing. The result is a station that does the unglamorous thing brilliantly: it stays alive and keeps reporting from a place that is actively trying to kill it, turning a blank spot on the weather map into a season of data.

A photovoltaic solar panel in sunlight
Solar power on a cold-managed battery keeps the station alive through dark, storm-bound alpine winters. Photograph sourced from Wikimedia Commons — Solar panel.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Measures temperature, humidity, pressure, wind and snow depth in extreme conditions
  • Shields/aspirates the temperature sensor and temperature-corrects the snow reading
  • Manages battery temperature — never charging lithium below freezing
  • Sips power through long, dark, storm-bound periods on solar + battery
  • Reports over long-range LoRa, or satellite where there is no other link
  • Logs locally so a multi-day link outage loses no data
  • Prioritises and compacts messages when every satellite byte costs power and money

Real-World Applications

SettingHow it is used
Glaciology and climate researchSurface energy-balance and mass-balance data from glaciers and ice fields where no permanent station exists.
Avalanche forecastingWind, temperature and snow-depth data from start zones and ridges that feed avalanche risk assessment.
Mountain hydrology / water supplySnowpack and weather data that drive melt and runoff forecasts for downstream water and hydropower.
Remote alpine infrastructureWeather awareness for high passes, huts, telescopes and communication sites in the mountains.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Cold-survival power design (temperature-aware charging, deep sleep)
  • Rime-ice-tolerant, wind-resistant sensor mounting
  • Aspirated/shielded temperature and temperature-corrected snow depth
  • LoRa or satellite backhaul for total isolation
  • Local logging for multi-day outages
  • Prioritised, compact reporting to conserve energy and airtime
  • Season-long unattended operation in a hostile environment

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelAdvanced
Estimated completion time16–24 hours
Indicative build cost₹18,000 – ₹35,000 (site-dependent)
Primary disciplineEnvironment
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Cold-weather battery and solar management (temperature-aware charging)
  • Ruggedising and mounting sensors against wind and rime ice
  • Aspirated temperature measurement and temperature-corrected ranging
  • Low-power design and prioritised, compact telemetry
  • LoRa and satellite (e.g. Iridium) backhaul with local logging

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
BME280 pressure/humidity/temperature sensor
Self-heating skews temperature by ~1 °C — read in forced mode, not continuous.
300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz1₹420
JSN-SR04T waterproof ultrasonic sensor
The 25 cm blind zone matters — mount it above the maximum expected water level.
25–450 cm, ±1 cm, IP67 sealed transducer, 45° beam1₹450
DS18B20 waterproof temperature probe
Dozens can share one GPIO — you address them by ROM code.
−55 to +125 °C, ±0.5 °C from −10 to +85 °C, 9–12-bit resolution, unique 64-bit ROM ID1₹160
SX1278 LoRa 433 MHz module (Ra-02)
Never power the radio without an antenna — the PA will destroy itself.
−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF121₹480
20 W 12 V polycrystalline solar panel
Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame1₹1,200
CN3791 MPPT solar charge controller
Set the MPPT point to ~80 % of panel Voc for polycrystalline modules.
4.5–28 V in, MPPT set by resistor divider, 2 A charge to a 1S Li-ion pack1₹320
18650 Li-ion cell 3400 mAh + holder
Never charge below 0 °C; always use a protected cell or a BMS.
3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge1₹450
Anemometer + wind vane (rugged)
Sonic anemometers avoid frozen bearings but cost more
Ice-shedding cup/sonic anemometer and vane rated for alpine wind1₹3,500
Cold-capable battery + heater/insulation
The single hardest sub-system; get it right
LiFePO4 or cold-rated pack, insulated box, charge-gate/heater below 0 °C1₹2,500
Satellite modem (optional)
Only where LoRa cannot reach; airtime is metered
Iridium SBD modem for sites with no LoRa path to a gateway1₹12,000
Aspirated radiation shieldSolar radiation shield with a low-power aspiration fan for true air temperature1₹1,500
Guyed mast + rugged enclosureWind-rated mast, guy wires, sealed UV/cold enclosure, rime-shedding surfaces1₹3,000

Estimated total: ₹26,430, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
BME280 pressure/humidity/temperature sensor300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz1.7–3.6 V (module has 3.3 V LDO)I²C (0x76/0x77) or SPIDatasheet
JSN-SR04T waterproof ultrasonic sensor25–450 cm, ±1 cm, IP67 sealed transducer, 45° beam5 VTrigger/Echo or UARTDatasheet
DS18B20 waterproof temperature probe−55 to +125 °C, ±0.5 °C from −10 to +85 °C, 9–12-bit resolution, unique 64-bit ROM ID3.0–5.5 V1-Wire (multi-drop)Datasheet
SX1278 LoRa 433 MHz module (Ra-02)−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF123.3 VSPIDatasheet
20 W 12 V polycrystalline solar panelVmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame12 V nominalMC4 / screw terminalsDatasheet
CN3791 MPPT solar charge controller4.5–28 V in, MPPT set by resistor divider, 2 A charge to a 1S Li-ion pack4.5–28 VSolder padsDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
BME280 pressure/humidity/temperature sensor1.7–3.6 V (module has 3.3 V LDO)0.4Self-heating skews temperature by ~1 °C — read in forced mode, not continuous.
JSN-SR04T waterproof ultrasonic sensor5 V30The 25 cm blind zone matters — mount it above the maximum expected water level.
DS18B20 waterproof temperature probe3.0–5.5 V1.5Dozens can share one GPIO — you address them by ROM code.
SX1278 LoRa 433 MHz module (Ra-02)3.3 V120Never power the radio without an antenna — the PA will destroy itself.
20 W 12 V polycrystalline solar panel12 V nominal1140Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
CN3791 MPPT solar charge controller4.5–28 V2000Set the MPPT point to ~80 % of panel Voc for polycrystalline modules.

Summed typical draw is 3451.9 mA. With a 1.5× design margin the supply should deliver at least 5200 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
Adafruit BME280 2.2.xCompensation maths for the Bosch pressure/humidity/temperature sensor.Library Manager → "Adafruit BME280 Library"
OneWire + DallasTemperature 2.3.x / 3.9.xBus enumeration and conversion commands for DS18B20 probes.Library Manager → "DallasTemperature" (pulls OneWire)
Adafruit Unified Sensor 1.1.xCommon sensor event abstraction; a dependency of most Adafruit drivers.Library Manager → "Adafruit Unified Sensor"
LoRa (sandeepmistry) 0.8.0SX127x radio configuration, packet TX/RX and callbacks.Library Manager → "LoRa" by Sandeep Mistry
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

High-Altitude Telemetry Node — system block diagramFunctional block diagram of the High-Altitude Telemetry Node system. Measure (hardened)T/RH/pressureaspirated BME280Windice-sheddingSnow depthtemp-correctedBattery tempcharge gateSurvive + decideESP32sip power, logPower mgmtcold chargingBackhaulLoRavalley gatewaySatelliteif no LoRaUsersForecast/researchweather + snowrightrightnone
High-Altitude Telemetry Node — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

High-Altitude Telemetry Node — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBBME280GPIO 21/22Air temp/RH/pressure(I²C, shielded)Snow depth (ultrasonic)GPIO 26/25Distance to snowsurfaceAnemometerGPIO 27 / 16-17Wind speed (+direction)DS18B20 (battery)GPIO 4Battery temperature(charge gate)LoRa / Sat modemGPIO 18/19/23 / 16-17BackhaulAspiration fanGPIO 13Low-power fan fortemp shieldCharge enableGPIO 12Gate charging onbattery tempMPPT + panelBattery busSolar charging
High-Altitude Telemetry Node — wiring schematic
PeripheralPeripheral pinController pinSignal
BME280SDA/SCLGPIO 21/22Air temp/RH/pressure (I²C, shielded)
Snow depth (ultrasonic)TRIG/ECHOGPIO 26/25Distance to snow surface
AnemometerPULSE/UARTGPIO 27 / 16-17Wind speed (+ direction)
DS18B20 (battery)DQGPIO 4Battery temperature (charge gate)
LoRa / Sat modemSPI / UARTGPIO 18/19/23 / 16-17Backhaul
Aspiration fanINGPIO 13Low-power fan for temp shield
Charge enableENGPIO 12Gate charging on battery temp
MPPT + panelOUTBattery busSolar charging

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • Sense battery temperature (DS18B20 on the pack) and gate charging via the charge-enable line — never let the MPPT charge the lithium pack below 0 °C.
  • Mount the BME280 inside an aspirated radiation shield; run the low-power fan only when needed so its draw does not dominate the energy budget.
  • Temperature-correct the snow-depth ultrasonic reading using air temperature — the speed of sound varies enough across a mountain day to matter to centimetres.
  • Choose an ice-shedding or sonic anemometer and mount all moving parts to shed rime; frozen bearings are the classic alpine failure.
  • Guy the mast for peak gusts and seal the enclosure against spindrift; route the satellite/LoRa antenna clear of the mast and ice build-up.
An ESP32 development board with the ESP-WROOM-32 module and USB connector
ESP32 module sipping power: gating cold charging, sampling hardened sensors, logging and reporting frugally. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

High-Altitude Telemetry Node — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · BME280 pressure/humidity/temperaturesensor · JSN-SR04T waterproof ultrasonic sensor · DS18B20 waterprooftemperature probeDriver layerwifi · bme · onewire · unifiedApplication logicsampling loop · filtering · thresholds · state machineTransport layerLoRa (long SF) or satellite → gateway → weather system · TLS · retry andbackoffPresentation layerdashboard · mobile notifications · historical charts
High-Altitude Telemetry Node — architecture stack

Working Principle

At altitude the sensing is standard meteorology; the difficulty is that the environment corrupts or destroys naive measurements, so each sensor needs a survival strategy. Air temperature must come from an aspirated, shielded sensor: mountain sun is intense and an unshielded probe heats its own body several degrees above the true air temperature, and even a passive shield can read warm in still, sunny conditions, which is why a small fan drawing air past the sensor gives the honest reading energy budget permitting. Snow depth — often the whole point of the station — is measured by timing an ultrasonic echo off the snow surface, but the speed of sound falls with temperature, so the same echo time means a different distance at −25 °C than at +5 °C; without temperature correction the snow record drifts by centimetres over a day, which is the difference between useful and useless for hydrology. Wind is measured by an anemometer whose enemy is rime ice that locks the bearings; ice-shedding designs or sonic anemometers (no moving parts) are chosen precisely for that.

The power problem is the one that most often kills alpine stations, and it has a cruel twist: lithium-ion batteries not only lose capacity in the cold but must not be charged below 0 °C, because charging a frozen cell plates lithium metal and permanently damages (and can be a safety hazard) it. So the power system cannot just track voltage — it must sense the battery's temperature and gate charging, refusing solar charge when the pack is below freezing (or warming the pack, or using a cold-charge-tolerant chemistry). Combined with short winter days, frequent storm-obscured sun, and deep cold sapping capacity, this forces a design that sleeps almost all the time, wakes briefly to sample and log, and treats every milliwatt-hour as scarce. The station's longevity is decided here, not in the sensors.

Communication is the second dominant constraint and shapes the whole reporting philosophy. In a valley-visible site, long-range LoRa reaches a gateway cheaply, and the node can report fairly often. Where terrain blocks any LoRa path — deep in a range, on the far side of a ridge — the only option is a satellite link such as Iridium Short-Burst Data, and satellite airtime costs meaningful power and money per message and per byte. That inverts the usual telemetry mindset: instead of streaming, the node hoards data locally and transmits infrequently, compactly, and by priority — sending a hazardous change (a rapid pressure drop, a wind spike, a snow-loading event) promptly while batching routine observations into rare, dense packets. Local logging underpins all of it: a storm that severs the link for a week must not create a hole in the record, so the node always writes first and transmits when it can.

The unifying principle is that a high-altitude node is judged almost entirely on uptime in adversity. A backyard station is judged on accuracy; an alpine station is judged on whether it is still reporting after the first blizzard, the first −30 °C night, the first week without sun, the first rime event. So the engineering effort goes where the failures are: temperature-managed power, ice-and-wind-hardened mechanics, honest shielded sensing, and a frugal, resilient reporting discipline. Get those right and the ordinary weather sensors inside will deliver a season of data from a place that has never had any — which is exactly why these stations are worth the trouble.

The maths behind it

Temperature-corrected snow depth

plainTemperature-corrected snow depth
Ultrasonic distance to the snow surface:

  c(T) = 331.3 + 0.606·T_air   (m/s)
  distance = c(T) · t_echo / 2
  snow_depth = sensor_height − distance

Across a mountain day T_air can swing 30 °C+, changing c by
~5% — several cm of apparent depth if uncorrected.

Cold-charge gate

plainCold-charge gate
Protect the lithium pack:

  allow_charge = (T_batt > T_CHG_MIN)    (T_CHG_MIN ~ 0–5 °C)

Below the threshold, inhibit the charger (or warm the pack).
Discharge is usually permitted colder than charge, but
capacity falls — budget for reduced usable Ah in the cold.

Energy budget in the dark

plainEnergy budget in the dark
Survive the worst dark/storm run of D days:

  usable_Wh ≥ D · daily_consumption
  daily_consumption = wake_energy·N + comms_energy·M + sleep·24h

Satellite comms dominate M-term energy → keep M small.
Size battery for cold-derated capacity AND the longest
expected sunless period, not the average.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

High-Altitude Telemetry Node — firmware flowchartControl flow through the main program loop. Wake on scheduleRead weather + snow + batterytempBattery above 0 °C?Allow chargingInhibit chargingAllow chargingInhibit chargingLog locallyReporting slot and powerOK?Send compact prioritised packetDefer, keep loggingSend compact prioritisedpacketDefer, keep loggingDeep sleep
High-Altitude Telemetry Node — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. Build the power system first

    Assemble a cold-capable battery in an insulated box with a temperature sensor on the pack, charged through an MPPT controller whose charge path is gated by a charge-enable line the ESP32 controls.

    Verify that below 0 °C the charger is inhibited, and size the pack for cold-derated capacity across your longest expected sunless period.

  2. Mount and harden the sensors

    Fit the temperature/humidity sensor in an aspirated radiation shield; mount an ice-shedding or sonic anemometer and vane; aim the snow-depth ultrasonic sensor straight down at the snow from a fixed, known height.

    Guy the mast for peak gusts and ensure surfaces shed rime; seal the enclosure against spindrift.

  3. Set up backhaul and logging

    Fit LoRa for valley-visible sites or a satellite modem where terrain blocks it; mount the antenna clear of the mast and ice. Confirm local logging works before relying on any link.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

  1. Gate charging on battery temperature

    Read the battery temperature every cycle and enable charging only above the safe threshold, inhibiting it (or warming the pack) when the battery is too cold.

    cppcold-power.ino
    #define T_CHG_MIN 2.0f      // never charge lithium below this (deg C)
    #define PIN_CHG_EN 12
    
    // Called each wake before allowing any solar charge current.
    void manageCharging(float battTempC) {
      bool allow = battTempC > T_CHG_MIN;
      digitalWrite(PIN_CHG_EN, allow ? HIGH : LOW);   // gate the MPPT charge path
      // Optionally: if cold but sun is available and a heater exists,
      // warm the pack toward T_CHG_MIN before enabling charge.
    }
    bool allow = battTempC > T_CHG_MINCharging is permitted only when the battery is above the safe threshold, protecting the cell from the permanent damage of cold-charging.
    digitalWrite(PIN_CHG_EN, allow ? HIGH : LOW)The ESP32 physically gates the charge path, so temperature — not just voltage — governs whether solar energy reaches the battery.
    // warm the pack toward T_CHG_MINWhere a small heater exists and sun is available, the pack can be warmed into the safe range so charging can resume, trading a little energy for battery health.
  2. Sample, correct and log

    Read weather and snow depth, temperature-correct the snow reading, run the aspiration fan only as needed, and write every observation to local storage before considering transmission.

  3. Report by priority within the energy/airtime budget

    Send routine observations in infrequent, compact batches; promote a hazardous change (rapid pressure drop, wind spike, snow-loading) to an immediate compact message — but only if the power budget allows.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cpphigh-altitude-node.ino
/* ═══════════════════════════════════════════════════════════════
   High-Altitude Telemetry Node — ESP32, hardened weather, LoRa/sat

   Survives extreme cold, wind and isolation: temperature-gated
   charging, aspirated/corrected sensing, deep-sleep power sipping,
   local logging, and infrequent prioritised backhaul over LoRa or
   satellite.
   ══════════════════════════════════════════════════════════════════ */

#include <Wire.h>
#include <Adafruit_BME280.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <LoRa.h>
#include <SPI.h>
#include <Preferences.h>
#include <math.h>

#define PIN_TRIG   26
#define PIN_ECHO   25
#define PIN_WIND   27
#define OW_BATT     4
#define PIN_CHG_EN 12
#define PIN_FAN    13
#define LORA_CS     5
#define LORA_RST   14
#define LORA_DIO0   2

#define SENSOR_HEIGHT_CM 300.0f   // snow sensor height above ground
#define T_CHG_MIN         2.0f
#define WIND_K            2.4f     // km/h per Hz — calibrate
#define ROUTINE_EVERY     6        // send routine batch every 6 wakes (e.g. hourly*6)

Adafruit_BME280 bme;
OneWire ow(OW_BATT); DallasTemperature battT(&ow);
Preferences prefs;

RTC_DATA_ATTR uint16_t wakeCount = 0;
RTC_DATA_ATTR float prevPressure = NAN;
volatile uint32_t windPulses = 0;
void IRAM_ATTR windISR(){ windPulses++; }

float snowDepthCm(float tAir) {
  float c = (331.3f + 0.606f * tAir) / 10000.0f;    // cm/us
  digitalWrite(PIN_TRIG,LOW); delayMicroseconds(2);
  digitalWrite(PIN_TRIG,HIGH); delayMicroseconds(10); digitalWrite(PIN_TRIG,LOW);
  long us = pulseIn(PIN_ECHO,HIGH,40000);
  if(!us) return NAN;
  float dist = us * c / 2.0f;
  return SENSOR_HEIGHT_CM - dist;
}

float windKmh() {
  windPulses = 0;
  attachInterrupt(PIN_WIND, windISR, FALLING);
  delay(3000);                                        // 3 s count window
  detachInterrupt(PIN_WIND);
  return (windPulses / 3.0f) * WIND_K;
}

void logLocal(float t,float rh,float p,float wind,float snow) { /* append */ }

void sendPacket(bool hazard, float t,float rh,float p,float wind,float snow) {
  SPI.begin();
  LoRa.setPins(LORA_CS, LORA_RST, LORA_DIO0);
  LoRa.begin(433E6); LoRa.setSpreadingFactor(11);     // long range
  LoRa.beginPacket();
  LoRa.printf("{\"n\":48,\"t\":%.1f,\"rh\":%.0f,\"p\":%.0f,"
              "\"wind\":%.1f,\"snow\":%.0f,\"haz\":%d}",
              t, rh, p, wind, snow, hazard?1:0);
  LoRa.endPacket();
  LoRa.sleep();
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_TRIG,OUTPUT); pinMode(PIN_ECHO,INPUT);
  pinMode(PIN_WIND,INPUT_PULLUP);
  pinMode(PIN_CHG_EN,OUTPUT); pinMode(PIN_FAN,OUTPUT);
  Wire.begin(21,22); bme.begin(0x76);
  battT.begin();
  wakeCount++;

  // ── manage cold charging FIRST ──
  battT.requestTemperatures();
  float tBatt = battT.getTempCByIndex(0);
  digitalWrite(PIN_CHG_EN, tBatt > T_CHG_MIN ? HIGH : LOW);

  // ── aspirate then read air temperature honestly ──
  digitalWrite(PIN_FAN, HIGH); delay(20000);          // fan 20 s (budget)
  float tAir = bme.readTemperature();
  float rh   = bme.readHumidity();
  float p    = bme.readPressure()/100.0f;             // hPa
  digitalWrite(PIN_FAN, LOW);

  float snow = snowDepthCm(tAir);
  float wind = windKmh();

  logLocal(tAir, rh, p, wind, snow);                  // always log first

  // ── hazard detection promotes an immediate message ──
  float dP = isnan(prevPressure)? 0 : p - prevPressure;
  prevPressure = p;
  bool hazard = (dP < -3.0f) || (wind > 80.0f);       // fast drop or gale

  bool routineSlot = (wakeCount % ROUTINE_EVERY) == 0;
  if (hazard || routineSlot)
    sendPacket(hazard, tAir, rh, p, wind, snow);      // else just log + sleep

  // ── deep sleep to sip power ──
  esp_sleep_enable_timer_wakeup(600ULL * 1000000ULL); // 10 min base
  esp_deep_sleep_start();
}

void loop() {}   // deep sleep restarts setup()
digitalWrite(PIN_CHG_EN, tBatt > T_CHG_MINThe very first action each wake is to gate charging on battery temperature, so a frozen pack is never charged even for an instant.
digitalWrite(PIN_FAN, HIGH); delay(20000)The aspiration fan runs briefly before the temperature read so sun on the shield does not fake a warm reading — a deliberate, budgeted energy spend for an honest number.
float snow = snowDepthCm(tAir)Snow depth is measured with the speed of sound corrected for the current air temperature, keeping the record accurate across the huge daily temperature swing.
bool hazard = (dP < -3.0f) || (wind > 80.0f)A rapid pressure drop or a gale promotes an immediate report even outside the routine slot — hazardous change is worth the airtime; calm weather waits.
esp_sleep_enable_timer_wakeup(600ULLThe node deep-sleeps between short wakes, sipping power so it can survive long dark, cold, storm-bound stretches on a cold-derated battery.

Configuration & Calibration

Configuration steps

  • Set the snow-sensor height, wind constant, and the routine reporting cadence (balance freshness against energy/airtime).
  • Set T_CHG_MIN and, if used, heater behaviour for your battery chemistry.
  • Choose LoRa (long spreading factor) or satellite backhaul and the hazard-promotion thresholds.
  • Size the battery and panel for cold-derated capacity across your longest expected sunless period.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Snow depth

    Verify the temperature-corrected distance against a physical measurement at a couple of temperatures; confirm the sensor height and correction.

  2. Temperature shield

    Compare the aspirated reading against a reference in strong sun and calm air; if it reads warm without the fan, the aspiration is doing its job.

  3. Power in the cold

    Test the charge-gate at sub-zero temperatures and measure real capacity cold, so the energy budget reflects reality, not datasheet room-temperature figures.

Network Architecture & Connectivity

High-Altitude Telemetry Node — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsAlpine nodeESP32 hardenedOther nodesnetworkLoRa SF11 / satValley gatewayor Iridium groundMQTT/SBDWeather/research storearchive + alertsForecast/researchweather + snowPhonehazard alerts
High-Altitude Telemetry Node — network topology

Communication protocol

Routine observations batch into infrequent compact packets; hazardous changes promote to immediate messages. Over satellite, every byte costs power and money, so payloads are minimal and prioritised, and local logging backs the whole record.

Topic / endpointDirectionPayload
alt/node/48/obsnode → gatewaycompact weather + snow observation
alt/node/48/hazardnode → gatewaypromoted rapid-change event
alt/node/48/healthnode → gatewaybattery temp/charge, RSSI, backlog size

Message contract between the device and the broker.

Cloud platform configuration

A store archives the season's data for forecasting and research, raises hazard alerts, and tracks each node's power and link health so a struggling station is noticed before it goes silent.

Dashboard setup

Weather and snow-depth trends per station, hazard markers, and a power/link-health panel (battery temperature, charge state, backlog).

Mobile app integration

Hazard alerts (rapid pressure drop, gale, snow-loading) and a warning if a node's battery or link health is failing.

Security considerations

  • Sign observations so research/forecast data cannot be spoofed.
  • Keep local logging authoritative so a lost or metered link never loses the record.
  • Alert on node silence or falling battery health so a rescue/service visit can be planned before total failure.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Chill the battery below 0 °CCharging is inhibited; discharge still works
Put the temperature sensor in strong sunAspirated reading stays near true air temp; unaspirated reads warm
Vary air temperature and range to a targetCorrected snow depth stays accurate across temperatures
Simulate a rapid pressure drop / galeNode promotes an immediate hazard message
Sever the link for a simulated multi-day periodLocal log continues; backlog forwards on reconnect
Run a long low-sun cold cycleNode survives on budget; power sub-system holds up

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

The dashboard shows the station's weather (temperature, humidity, pressure, wind) and snow depth over time, flags hazard-promoted messages, and shows battery temperature/charge state and link health.

jsonalt-packet.json
{
  "n": 48,
  "t": -18.4,
  "rh": 72,
  "p": 631,
  "wind": 46.2,
  "snow": 184,
  "haz": 0
}

A compact routine observation at −18 °C, low mountain pressure (631 hPa reflects the altitude), moderate wind and 184 cm of snow — the kind of data these sites have never before provided, delivered on a tight energy and airtime budget.

A LoRa radio transceiver module
A LoRa (or satellite) link carries compact, prioritised weather and snow data out of total isolation. Photograph sourced from Wikimedia Commons — LoRa module.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Station dies in the first cold spell

Likely cause. Battery cold-charged/damaged or undersized for cold

Fix. Verify the charge-gate; use cold-capable chemistry/insulation/heater; size for cold-derated capacity and long dark runs

Temperature reads too warm on sunny days

Likely cause. Shield not aspirated / poor radiation shield

Fix. Add aspiration; improve the radiation shield; run the fan before reading

Snow depth drifts with the day

Likely cause. No temperature correction of the ultrasonic reading

Fix. Apply the speed-of-sound correction with air temperature

Anemometer stops in cold

Likely cause. Rime ice freezing the bearings

Fix. Use an ice-shedding or sonic anemometer; mount to shed rime

Data gaps after storms

Likely cause. Link severed without local logging

Fix. Ensure local logging + backlog forwarding; log before transmitting

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

Performance Optimisation

  • Deep-sleep almost all the time; the aspiration fan and any comms are the main awake-energy costs, so budget them explicitly.
  • Minimise satellite messages — batch routine data, promote only genuine hazards.
  • Size everything for cold-derated capacity and the longest sunless period, not average conditions.
  • Log locally and forward backlogs rather than blocking on a metered or intermittent link.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
  • Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.

Safety Precautions

  • Never charge lithium below freezing — protect the pack and avoid the safety hazard of cold-charging.
  • Install and service in the mountains only with proper alpine safety, avalanche awareness and never alone.
  • Guy masts and secure enclosures for peak wind and ice loads so the station cannot become a hazard.
  • Treat the station as an input to expert forecasting (e.g. avalanche), not an authority in itself.
  • Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
  • Never power an RF module without its antenna fitted — the reflected power destroys the output stage. Check your local licence-free band and duty-cycle limits before transmitting.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Service before and after the season: inspect for ice/wind damage, reseal enclosures, check guy tension.
  • Re-verify the charge-gate and battery health each season; cold ages packs.
  • Clear rime from sensors and antenna; confirm the snow sensor's line of sight and height.
  • Check local logging and backlog forwarding, and clean the solar panel of snow/rime.
  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
  • Wash the panel every few weeks in dusty conditions; a visible dust film costs 15–25 % of the harvest.
  • Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add incoming/outgoing radiation and surface-temperature sensors for full energy-balance research.
  • Add a small pack heater with a smart budget to enable cold-day charging.
  • Combine LoRa and satellite adaptively — LoRa when a gateway is reachable, satellite as fallback.
  • On-device detection of snow-loading/avalanche-relevant events for smarter hazard promotion.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add over-the-air firmware updates so you never have to physically reach a deployed node again.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

What is the hardest part of a high-altitude station?

Power and survival, not sensing. Keeping a lithium battery healthy (never charging it below freezing), sipping energy through dark storms, and mounting sensors to survive wind and rime are what decide whether the station lives.

Why can't you charge the battery when it's cold?

Charging lithium-ion below 0 °C plates lithium metal, permanently damaging the cell and risking safety. So the node senses battery temperature and gates charging, or warms the pack first.

Why does snow depth need temperature correction?

It is measured by an ultrasonic echo, and the speed of sound changes with temperature. Across a mountain day's huge temperature swing, an uncorrected reading drifts by centimetres — enough to spoil hydrology data.

How does it report with no cell coverage?

Long-range LoRa to a valley gateway where terrain allows, or a satellite modem (e.g. Iridium) where it does not. Satellite airtime is costly, so reporting is infrequent, compact and prioritised, with everything logged locally.

What happens during a week-long storm?

It keeps sampling and logging locally on its energy budget, promotes any hazard it detects if power allows, and forwards the backlog once the link and sun return — so the storm leaves no gap.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. Automatic weather stations in mountains — overviewReference
  2. Lithium-ion charging at low temperatureReference
  3. Ultrasonic snow-depth sensing and temperature correctionUSGS
  4. Iridium Short-Burst Data (SBD) telemetryReference
  5. Radiation shields and aspirated temperature measurementReference