Skip to content

Latest commit

 

History

History
1046 lines (941 loc) · 39.2 KB

File metadata and controls

1046 lines (941 loc) · 39.2 KB
layout title
default
Battery Runtime Tests
<title>Battery Runtime Tests</title> <style> .gps-toggle-cell{ text-align:center; padding:4px 2px; white-space:normal; vertical-align:middle; } .gps-toggle-label{ display:block; font-size:11px; font-weight:700; line-height:1.1; margin-bottom:4px; user-select:none; white-space:normal; } .gps-toggle-wrap{ display:inline-flex; flex-direction:column; align-items:center; gap:4px; } .gps-switch{ position:relative; display:inline-block; width:44px; height:24px; } .gps-switch input{ opacity:0; width:0; height:0; } .gps-slider{ position:absolute; inset:0; cursor:pointer; background:#c9c9c9; border-radius:999px; transition:0.2s; } .gps-slider:before{ content:""; position:absolute; width:18px; height:18px; left:3px; top:3px; background:#fff; border-radius:50%; transition:0.2s; box-shadow:0 2px 6px rgba(0,0,0,0.25); } .gps-switch input:checked + .gps-slider{ background:#5aa9ff; } .gps-switch input:checked + .gps-slider:before{ transform:translateX(20px); } </style> <script> function formatDaysHours(totalHours) { const h = Math.max(0, Math.floor(totalHours)); const days = Math.floor(h / 24); const hours = h % 24; if (days <= 0) return `${hours} hrs`; if (hours === 0) return (days === 1) ? `1 day` : `${days} days`; const dayText = (days === 1) ? `1 day` : `${days} days`; return `${dayText}
${hours} hrs`; } function convertHoursTextInCell(cell) { if (!cell) return; // Only touch plain text cells (skip cells that contain
, , etc) if (cell.children && cell.children.length > 0) return; const txt = (cell.textContent || "").trim(); // Match: "104 Hrs", "53 Hr", "110 hours", etc const m = txt.match(/^(\d+)\s*(h|hr|hrs|hour|hours)$/i); if (!m) return; const hours = parseInt(m[1], 10); if (Number.isNaN(hours)) return; cell.innerHTML = formatDaysHours(hours); } function updateStaticHourCells() { document.querySelectorAll("td, th").forEach(cell => { // Skip the dynamic progress cells (those become "Started ... ago") if (cell.id && cell.id.startsWith("progress")) return; // Do not touch your "Started ..." strings if any exist elsewhere if ((cell.textContent || "").includes("Started")) return; convertHoursTextInCell(cell); }); } function updateProgress() { const startTimes = [ //{ id: 'progress1', start: new Date('2026-02-07T12:57:00') }, // L1 Eink 3000mAh NO GPS // //{ id: 'progress2', start: new Date('2026-02-20T02:34:00') }, // Heltxt GPS ON // //{ id: 'progress3', start: new Date('2026-02-11T03:38:00') }, // T114 No GPS 3000mAh // //{ id: 'progress4', start: new Date('2026-02-11T16:50:00') }, // NRF-TXT GPS On // //{ id: 'progress5', start: new Date('2026-02-13T00:57:00') }, // Heltxt GPS OFF // //{ id: 'progress6', start: new Date('2025-01-10T13:40:00') }, // Wireless Paper // //{ id: 'progress7', start: new Date('2024-07-11T03:00:00') }, // //{ id: 'progress8', start: new Date('2024-07-09T22:25:00') }, // //{ id: 'progress9', start: new Date('2024-07-07T21:51:00') } // ]; const currentDate = new Date(); startTimes.forEach(item => { const el = document.getElementById(item.id); if (!el) return; const diffInHours = (currentDate - item.start) / (1000 * 60 * 60); el.innerHTML = `Started ${formatDaysHours(diffInHours)} ago`; }); } // Build the GPS toggle header row from the FIRST header row (data-gps-group + data-gps) function buildGpsToggleHeaderRows() { document.querySelectorAll("table").forEach(table => { const thead = table.querySelector("thead"); if (!thead) return; const rows = thead.querySelectorAll("tr"); if (!rows.length) return; const titleRow = rows[0]; const toggleRow = thead.querySelector("tr.gps-toggle-row"); if (!toggleRow) return; // Only replace rows we mark as placeholders if (!toggleRow.classList.contains("gps-toggle-row")) return; toggleRow.innerHTML = ""; // We need to emit EXACTLY the same number of visual columns as the title row // so everything after a GPS pair does not shift. const titleCells = Array.from(titleRow.children); for (let cellIndex = 0; cellIndex < titleCells.length; cellIndex++) { const titleCell = titleCells[cellIndex]; const span = titleCell.colSpan || 1; const group = titleCell.getAttribute("data-gps-group"); const mode = titleCell.getAttribute("data-gps"); // Normal header cell: create matching blank cells respecting colSpan if (!group || (mode !== "off" && mode !== "on")) { for (let i = 0; i < span; i++) toggleRow.appendChild(document.createElement("th")); continue; } // GPS pair handling: // - When we see the OFF header, we create TWO toggle cells (OFF + ON) // - We also consume the next title cell if it is the matching ON header if (mode === "off") { const offTh = document.createElement("th"); offTh.className = "gps-toggle-cell"; offTh.innerHTML = `
GPS OFF
`; const onTh = document.createElement("th"); onTh.className = "gps-toggle-cell"; onTh.innerHTML = `
GPS ON
`; const btGroup = titleCell.getAttribute("data-bt-group"); const btMode = titleCell.getAttribute("data-bt"); if (btGroup && (btMode === "off" || btMode === "on")) { offTh.setAttribute("data-bt-group", btGroup); offTh.setAttribute("data-bt", btMode); onTh.setAttribute("data-bt-group", btGroup); onTh.setAttribute("data-bt", btMode); } toggleRow.appendChild(offTh); toggleRow.appendChild(onTh); // If the very next header cell is the ON partner for the same group, skip it const next = titleCells[cellIndex + 1]; if (next) { const nextGroup = next.getAttribute("data-gps-group"); const nextMode = next.getAttribute("data-gps"); if (nextGroup === group && nextMode === "on") { cellIndex++; } } continue; } // If we ever hit a stray "on" without seeing "off" first, emit a blank toggleRow.appendChild(document.createElement("th")); } }); } // Build the BT toggle header row from the FIRST header row. // For non-BT GPS pairs, emit a single blank colspan=2 cell to avoid split placeholders. function buildBtToggleHeaderRows() { document.querySelectorAll("table").forEach(table => { const thead = table.querySelector("thead"); if (!thead) return; const titleRow = thead.querySelector("tr"); if (!titleRow) return; const btRow = thead.querySelector("tr.bt-toggle-row"); if (!btRow) return; btRow.innerHTML = ""; const titleCells = Array.from(titleRow.children); for (let cellIndex = 0; cellIndex < titleCells.length; cellIndex++) { const titleCell = titleCells[cellIndex]; const span = titleCell.colSpan || 1; const btGroup = titleCell.getAttribute("data-bt-group"); const btDefault = titleCell.getAttribute("data-bt-default") || "on"; const gpsGroup = titleCell.getAttribute("data-gps-group"); const gpsMode = titleCell.getAttribute("data-gps"); // BT-enabled pair: render OFF + ON control cells. if (btGroup && gpsMode === "off") { const offTh = document.createElement("th"); offTh.className = "gps-toggle-cell"; offTh.setAttribute("data-bt-group", btGroup); offTh.setAttribute("data-bt", "off"); offTh.innerHTML = `
BT OFF
`; const onTh = document.createElement("th"); onTh.className = "gps-toggle-cell"; onTh.setAttribute("data-bt-group", btGroup); onTh.setAttribute("data-bt", "on"); onTh.innerHTML = `
BT ON
`; btRow.appendChild(offTh); btRow.appendChild(onTh); const next = titleCells[cellIndex + 1]; if (next) { const nextBtGroup = next.getAttribute("data-bt-group"); const nextGpsMode = next.getAttribute("data-gps"); if (nextBtGroup === btGroup && nextGpsMode === "on") cellIndex++; } continue; } // Non-BT GPS pair: emit a single blank cell with colspan=2. if (!btGroup && gpsGroup && gpsMode === "off") { const next = titleCells[cellIndex + 1]; if (next) { const nextGpsGroup = next.getAttribute("data-gps-group"); const nextGpsMode = next.getAttribute("data-gps"); if (nextGpsGroup === gpsGroup && nextGpsMode === "on") { const blankPair = document.createElement("th"); blankPair.colSpan = 2; btRow.appendChild(blankPair); cellIndex++; continue; } } } const blank = document.createElement("th"); blank.colSpan = span; btRow.appendChild(blank); } }); } // Return the cell that occupies a given column index, respecting colSpan function getCellAtColumn(row, colIndex) { let colPos = 0; for (const cell of row.children) { const span = cell.colSpan || 1; if (colIndex >= colPos && colIndex < colPos + span) return cell; colPos += span; } return null; } // Find column indexes from the FIRST header row, respecting colSpans function findGroupColumnIndexes(table, group) { const firstHeaderRow = table.querySelector("thead tr"); if (!firstHeaderRow) return { offIndex: -1, onIndex: -1 }; let offIndex = -1; let onIndex = -1; let colPos = 0; Array.from(firstHeaderRow.children).forEach(cell => { const span = cell.colSpan || 1; const g = cell.getAttribute("data-gps-group"); const mode = cell.getAttribute("data-gps"); if (g === group && mode === "off") offIndex = colPos; if (g === group && mode === "on") onIndex = colPos; colPos += span; }); return { offIndex, onIndex }; } function initPerPairGpsToggles() { function collectRowPairs(table, offIndex, onIndex) { const rows = table.querySelectorAll("thead tr, tbody tr"); const pairs = []; rows.forEach(row => { // Keep BT control rows independent from GPS pair collapsing. if (row.classList && row.classList.contains("bt-toggle-row")) return; const offCell = getCellAtColumn(row, offIndex); const onCell = getCellAtColumn(row, onIndex); if (!offCell || !onCell || offCell === onCell) return; const inBody = !!row.closest("tbody"); const offWrap = !inBody ? offCell.querySelector(".gps-toggle-wrap") : null; const onWrap = !inBody ? onCell.querySelector(".gps-toggle-wrap") : null; const isToggleRow = !inBody && (offWrap || onWrap); // Cache body HTML once if (inBody) { if (offCell.dataset.gpsOffHtml === undefined) offCell.dataset.gpsOffHtml = offCell.innerHTML; if (onCell.dataset.gpsOnHtml === undefined) onCell.dataset.gpsOnHtml = onCell.innerHTML; } pairs.push({ offCell, onCell, inBody, isToggleRow, offWrap, onWrap }); }); return pairs; } function applyToPairs(pairs, showOn) { pairs.forEach(p => { const offCell = p.offCell; const onCell = p.onCell; // Always expand back first offCell.style.display = ""; onCell.style.display = ""; offCell.colSpan = 1; onCell.colSpan = 1; // Toggle row: put wrappers back where they belong before re-collapsing if (p.isToggleRow) { if (p.offWrap && !offCell.contains(p.offWrap)) offCell.appendChild(p.offWrap); if (p.onWrap && !onCell.contains(p.onWrap)) onCell.appendChild(p.onWrap); } // Body row: restore original HTML before re-collapsing if (p.inBody) { offCell.innerHTML = offCell.dataset.gpsOffHtml || ""; onCell.innerHTML = onCell.dataset.gpsOnHtml || ""; } const keepCell = showOn ? onCell : offCell; const hideCell = showOn ? offCell : onCell; // Collapse hideCell.style.display = "none"; keepCell.colSpan = 2; // Toggle row: move the correct wrapper into the visible cell if (p.isToggleRow) { const keepWrap = showOn ? p.onWrap : p.offWrap; if (keepWrap && !keepCell.contains(keepWrap)) keepCell.appendChild(keepWrap); keepCell.style.textAlign = "center"; } // Body row: swap the stored HTML into the visible cell if (p.inBody) { keepCell.innerHTML = showOn ? (onCell.dataset.gpsOnHtml || "") : (offCell.dataset.gpsOffHtml || ""); } }); } // Group inputs by data-gps-toggle so mirrors stay in sync const groups = new Map(); document.querySelectorAll("input[data-gps-toggle]").forEach(input => { const group = input.getAttribute("data-gps-toggle"); if (!groups.has(group)) groups.set(group, []); groups.get(group).push(input); }); groups.forEach((inputs, group) => { const tables = new Set(inputs.map(i => i.closest("table")).filter(Boolean)); tables.forEach(table => { const tableInputs = inputs.filter(i => i.closest("table") === table); if (!tableInputs.length) return; const { offIndex, onIndex } = findGroupColumnIndexes(table, group); if (offIndex === -1 || onIndex === -1) return; // Cache real cell pairs ONCE, before we collapse anything const pairs = collectRowPairs(table, offIndex, onIndex); function apply(checked) { // Sync all toggles in this table tableInputs.forEach(i => (i.checked = checked)); applyToPairs(pairs, !!checked); } // Default state apply(false); tableInputs.forEach(i => { i.addEventListener("change", () => apply(i.checked)); }); }); }); } function initPerPairBtToggles() { const groups = new Map(); document.querySelectorAll("input[data-bt-toggle]").forEach(input => { const group = input.getAttribute("data-bt-toggle"); if (!group) return; if (!groups.has(group)) groups.set(group, []); groups.get(group).push(input); }); groups.forEach((inputs, group) => { const tables = new Set(inputs.map(i => i.closest("table")).filter(Boolean)); tables.forEach(table => { const tableInputs = inputs.filter(i => i.closest("table") === table); if (!tableInputs.length) return; const btCells = Array.from(table.querySelectorAll(`[data-bt-group="${group}"][data-bt-on][data-bt-off]`)); if (!btCells.length) return; const btControlCells = Array.from( table.querySelectorAll(`.bt-toggle-row [data-bt-group="${group}"][data-bt]`) ); const syncGpsGroup = tableInputs[0].getAttribute("data-bt-sync-gps"); const syncGpsInputs = syncGpsGroup ? Array.from(table.querySelectorAll(`input[data-gps-toggle="${syncGpsGroup}"]`)) : []; function apply(showOn) { tableInputs.forEach(i => (i.checked = showOn)); btCells.forEach(cell => { const value = showOn ? (cell.getAttribute("data-bt-on") || "") : (cell.getAttribute("data-bt-off") || ""); cell.textContent = value; convertHoursTextInCell(cell); const gpsMode = cell.getAttribute("data-bt-gps"); if (gpsMode === "off") cell.dataset.gpsOffHtml = cell.innerHTML; if (gpsMode === "on") cell.dataset.gpsOnHtml = cell.innerHTML; }); if (btControlCells.length >= 2) { let offCell = null; let onCell = null; btControlCells.forEach(cell => { cell.style.display = ""; cell.colSpan = 1; const mode = cell.getAttribute("data-bt"); if (mode === "off") offCell = cell; if (mode === "on") onCell = cell; }); const keepCell = showOn ? onCell : offCell; const hideCell = showOn ? offCell : onCell; if (hideCell) hideCell.style.display = "none"; if (keepCell) keepCell.colSpan = 2; } // Re-apply current GPS state so visible cell updates after BT value swap. if (syncGpsInputs.length) syncGpsInputs[0].dispatchEvent(new Event("change")); } const defaultOn = tableInputs.some(i => i.getAttribute("data-bt-default") === "on"); apply(defaultOn); tableInputs.forEach(i => { i.addEventListener("change", () => apply(!!i.checked)); }); }); }); } window.addEventListener("load", () => { buildGpsToggleHeaderRows(); buildBtToggleHeaderRows(); updateProgress(); updateStaticHourCells(); // convert "104 Hrs" -> "4 days 8 hrs" initPerPairGpsToggles(); // cache AFTER conversion so toggles keep it initPerPairBtToggles(); }); setInterval(updateProgress, 3600000); </script>

Battery Runtime Tests

Runtime Comparison Under Different Conditions

Experiment #1 - Default Settings

Experiment conditions:
  • Firmware 2.7.19
  • Role: Client
  • Screen Timeout: 60 sec
  • Power Savemode: Disabled.
  • Frequency: 906
  • Mesh size: 20-30 active nodes
  • For GPS Only tests: Nodes were placed next to the window to allow continuous GPS satellite lock

Use case: Mobile Node/ Remote Node.

Battery Size Heltec ESP32 V2 Heltec ESP32 V3.1 Heltec ESP32 V3.2 Heltec Wireless Paper Wireless Stick Lite (V3) Heltec Wireless Tracker Heltec Vision Master E213 Heltec Vision Master E290 Heltec Meshnode T114 Heltec Meshnode T114 Lilygo T-Deck RAK19007 (RAK4631) RAK19003 (RAK4631) T1000E T1000E Thinknode M1 Thinknode M1 WIO Tracker L1 OLED WIO Tracker L1 OLED WIO Tracker L1 Eink WIO Tracker L1 Eink
700mAh Battery 64 Hrs 51 Hrs
1100mAh Battery 21 Hrs 10 Hrs 10 Hrs 9 Hrs 10 Hrs 9 Hrs 104 Hrs 62 Hrs 10 Hrs 154 Hrs 156 Hrs - -
1200mAh Battery - - - - - - - - - - - - - 62 Hrs 52 Hrs
2000mAh Battery 41 Hrs 21 Hrs 20 Hrs 20 Hrs 13 Hrs 19 Hrs 220 Hrs 119 Hrs 18 Hrs 307 Hrs - - 159 Hrs 132 Hrs
3000mAh Battery 60 Hrs 30 Hrs 30 Hrs 19 Hrs 320 Hrs 215 Hrs 26 Hrs 442 Hrs 453 Hrs - - 334 Hrs 183 Hrs
4000mAh Battery - -
5000mAh Battery - -

Experiment #2 - Best Power Saving Settings for Mobile Node/ Remote Node

Experiment conditions:
  • Firmware 2.3.17
  • Client Mode
  • Screen Timeout: 60 sec
  • Power Savemode Enabled.
    Details:
    • Note that RAK Battery Sizes cannot support this mode.
    • Power save mode is enabled to extend battery life, it does this by enabling Lite Sleep on ESP32 Battery Sizes when there's no traffic on the mesh.
    • The node will still retransmit any packets while on Lite Sleep and go back to sleep after.
    • The Node will wake from Lite Sleep when activity is detected on the mesh, when button is pressed or when sleep duration setting is reached.
    • During Lite sleep, the Bluetooth will go on Sleep Mode, making the node draw very low currents. But you will not be able to change settings with the app in this mode.
    • After the node is awake. It will automatically reconnect to the app and notify if any messages have been received. You can change settings when this happens.
  • Wait for Blutooth: 10 Sec
    Details:
    • The node will stay awake for this period of time if any packages are receiced to give the node time for the phone to reconnect.
  • Lite Sleep Duration: 1800 sec (30min)
    Details:
    • This setting tells the node how long to maintain Lite Sleep for, this way you can time when you can reconnect to remote nodes with the app should you need to change settings.
  • Frequency 906
  • Connected to Android phone via Bluetooth.

Use case: Mobile Node/ Remote Node.

Battery Size Heltec ESP32 V2 Heltec ESP32 V3.1 Heltec ESP32 V3.2 Heltec Wireless Paper Wireless Stick Lite (V3) Heltec Wireless Tracker Heltec Vision Master E213 Heltec Vision Master E290 Heltec Meshnode T114 Heltec Meshnode T114 Lilygo T-Deck RAK19007 (RAK4631) RAK19003 (RAK4631) T1000E T1000E
700mAh Battery 66 Hrs 53 Hr
1100mAh Battery 30 Hrs 19 Hrs
61 Hrs5 Nodes on Mesh - Firmware 2.5.17 - 1/10/25
51 Hrs 21 Hrs - -
2000mAh Battery 74 Hrs 44 Hrs
110 Hrs5 Nodes on Mesh - Firmware 2.5.17 - 1/12/25
35 Hrs - -
3000mAh Battery 119 Hrs 80 Hrs
156 Hrs4 Nodes on Mesh - Firmware 2.5.17 - 1/17/25
173 Hrs4 Nodes on Mesh - Firmware 2.5.17 - 1/17/25
156 Hrs 54 Hrs 442 Hrs 453 Hrs - -
4000mAh Battery 71 Hrs - -
5000mAh Battery - -

Experiment #3 - Best Power Saving Settings for Standalone Nodes

Experiment conditions:
  • Firmware 2.3.12
  • Client Mode
  • Screen Timeout: 60 sec
  • Power Savemode Enabled.
    Details:
    • Note that RAK devices cannot support this mode.
    • Power save mode is enabled to extend battery life, it does this by enabling Lite Sleep on ESP32 devices when there's no traffic on the mesh.
    • The node will still retransmit any packets while on Lite Sleep and go back to sleep after.
    • The Node will wake from Lite Sleep when activity is detected on the mesh, when button is pressed or when sleep duration setting is reached.
    • During Lite sleep, the Bluetooth will go on Sleep Mode, making the node draw very low currents. But you will not be able to change settings with the app in this mode.
    • After the node is awake. It will automatically reconnect to the app and notify if any messages have been received. You can change settings when this happens.
  • Lite Sleep Duration: 1800 sec (30min)
    Details:
    • This setting tells the node how long to maintain Lite Sleep for, this way you can time when you can reconnect to remote nodes with the app should you need to change settings.
  • Wait For Bluetooth: 10 sec
  • Frequency 906
  • Connected to Android phone via Bluetooth.
  • CardKB Attached (Tdeck Comes with its own keyboard)

Use case: Mobile Node/ Standalone

Results:
Battery Size Hel-txt Hel-txt Nrf-txt Nrf-txt Meshenger Meshenger Lilygo T-Deck
4000mAh Battery 126 Hrs 78 Hrs 276 Hrs 181 Hrs 166 Hrs 175 hrs 71 Hrs