Self-made altitude sensor: ESP32-C3 SuperMini + BMP280 broadcasting pressure and temperature over standard BLE Environmental Sensing. Read into baldav the same way as the heart-rate strap (Kotlin BLE service -> file -> Rust -> JS), new device_type 'baro'. - BleHrService: 'baro' type subscribes to ESS 0x181A (pressure 0x2A6D uint32 0.1 Pa, temperature 0x2A6E int16 0.01 C), writes ble_baro_data.json + ble_baro_log.jsonl. Every baro branch is gated by deviceType=='baro', so HR/GPS paths are untouched. - ble.rs: BaroReading, ESS detection, ble_get_baro command (Android reads the file, desktop returns None). Registered in lib.rs. - frontend/js/baro.js: passive window.BARO - hypsometric pressure->altitude using the sensor temperature, anchored to GPS altitude at ride start. Nothing runs until BARO.start(), so with no barometer the app is unchanged. - tools/baro-sensor: firmware (PlatformIO main.cpp + Arduino sketch) that reads BMP280 at max oversampling and advertises the ESS service. Prep only: no hardware yet, verified by compile (build 0.1.538). Live wiring and ride-track integration pending the board.
116 lines
4.8 KiB
JavaScript
116 lines
4.8 KiB
JavaScript
// Barometric altitude from the DIY BLE sensor "BaroBalDav" (ESP32-C3 + BMP280).
|
|
// PASSIVE module: it only defines window.BARO. Nothing runs until BARO.start() is called, so with
|
|
// no barometer connected the app behaves exactly as before. Activation (start on a "baro" connect,
|
|
// plus the getGpsAlt hook) is wired in main.js only when a baro device is in use.
|
|
//
|
|
// How it works: the Kotlin BLE service writes the latest {pressure_pa, temp_c, ts} to a file; the
|
|
// Rust command `ble_get_baro` returns it. We turn pressure into altitude with the hypsometric
|
|
// formula using the sensor's own temperature, and anchor the absolute value to the GPS altitude at
|
|
// ride start. The sensor is excellent for relative change (~1 m); weather drift of the absolute
|
|
// baseline is handled by the GPS anchor (optionally re-anchored slowly).
|
|
let BARO = {};
|
|
(function () {
|
|
let R = 287.05; // specific gas constant for dry air, J/(kg*K)
|
|
let G = 9.80665; // standard gravity, m/s^2
|
|
|
|
let state = {
|
|
anchorPa: null, // reference pressure at the anchor point
|
|
anchorAltM: null, // absolute altitude (m) at the anchor point, from GPS
|
|
lastPa: null, // most recent pressure, Pa
|
|
lastTempC: null, // most recent chip temperature, C (for the hypsometric term)
|
|
lastTs: 0, // unix seconds of the last reading
|
|
lastAltM: null, // computed absolute altitude at the last reading
|
|
polling: false,
|
|
timer: null,
|
|
};
|
|
BARO.state = state;
|
|
|
|
// Height of a level at pressure `pa` above a level at pressure `refPa`, at air temperature tC.
|
|
// Hypsometric formula: dh = (R*T/g) * ln(refPa/pa). Measured temperature is used instead of a
|
|
// 15 C constant, so a hot or cold day does not bias the climb. Returns metres (positive = higher).
|
|
function deltaH(refPa, pa, tC) {
|
|
let T = (tC == null ? 15 : tC) + 273.15;
|
|
return (R * T / G) * Math.log(refPa / pa);
|
|
}
|
|
BARO.deltaH = deltaH;
|
|
|
|
// Pin the barometric altitude to a known absolute altitude (from GPS) at a given pressure.
|
|
// Call once at ride start; may be re-called later to slowly correct weather drift.
|
|
BARO.anchor = function (gpsAltM, pa) {
|
|
if (pa == null) pa = state.lastPa;
|
|
if (gpsAltM == null || pa == null) return false;
|
|
state.anchorAltM = gpsAltM;
|
|
state.anchorPa = pa;
|
|
state.lastAltM = BARO.altitude();
|
|
return true;
|
|
};
|
|
|
|
// Absolute altitude now = anchor altitude + barometric rise since the anchor. null until anchored.
|
|
BARO.altitude = function () {
|
|
if (state.anchorPa == null || state.lastPa == null) return null;
|
|
return state.anchorAltM + deltaH(state.anchorPa, state.lastPa, state.lastTempC);
|
|
};
|
|
|
|
// Fresh reading within 15 s?
|
|
BARO.isLive = function () {
|
|
return state.lastTs > 0 && (Date.now() / 1000 - state.lastTs) < 15;
|
|
};
|
|
|
|
// Override this to provide the current GPS altitude (m) for auto-anchoring; return null if no fix.
|
|
// Default: the altitude of the last recorded GPS point, if a recording is active.
|
|
BARO.getGpsAlt = function () {
|
|
try {
|
|
let pts = window.GPS && GPS.recording && GPS.recording.points;
|
|
if (pts && pts.length) {
|
|
let p = pts[pts.length - 1];
|
|
let a = p.altitude_srtm_m != null ? p.altitude_srtm_m : p.altitude;
|
|
return a != null ? a : null;
|
|
}
|
|
} catch (e) {}
|
|
return null;
|
|
};
|
|
|
|
async function tick() {
|
|
let r = null;
|
|
try { r = await window.__TAURI__.core.invoke("ble_get_baro"); } catch (e) {}
|
|
if (r && r.pressure_pa > 0) {
|
|
state.lastPa = r.pressure_pa;
|
|
if (r.temp_c != null) state.lastTempC = r.temp_c;
|
|
state.lastTs = r.timestamp || Math.floor(Date.now() / 1000);
|
|
// auto-anchor on the first good reading, once GPS has an altitude to anchor to
|
|
if (state.anchorPa == null) {
|
|
let g = null;
|
|
try { g = BARO.getGpsAlt(); } catch (e) {}
|
|
if (g != null) BARO.anchor(g, state.lastPa);
|
|
}
|
|
state.lastAltM = BARO.altitude();
|
|
}
|
|
}
|
|
BARO.tick = tick;
|
|
|
|
// Begin polling the sensor (~1 Hz). No-op if already running. Call this only when a baro device
|
|
// is connected, so the app does nothing extra when no barometer is in use.
|
|
BARO.start = function () {
|
|
if (state.polling) return;
|
|
state.polling = true;
|
|
let loop = async function () {
|
|
if (!state.polling) return;
|
|
await tick();
|
|
if (state.polling) state.timer = setTimeout(loop, 1000); // 1 s poll interval (a real delay)
|
|
};
|
|
loop();
|
|
};
|
|
|
|
BARO.stop = function () {
|
|
state.polling = false;
|
|
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
|
|
};
|
|
|
|
// Clear the anchor and readings (e.g. between rides) so the next ride re-anchors fresh.
|
|
BARO.reset = function () {
|
|
state.anchorPa = null; state.anchorAltM = null;
|
|
state.lastPa = null; state.lastTempC = null; state.lastTs = 0; state.lastAltM = null;
|
|
};
|
|
|
|
window.BARO = BARO;
|
|
})();
|