| layout | title |
|---|---|
default |
Battery Runtime Tests |
${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>
Runtime Comparison Under Different Conditions
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 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 | 51 Hrs | 21 Hrs | - | - | |||||||||
| 2000mAh Battery | 74 Hrs | 44 Hrs | 35 Hrs | - | - | ||||||||||
| 3000mAh Battery | 119 Hrs | 80 Hrs | 156 Hrs | 54 Hrs | 442 Hrs | 453 Hrs | - | - | |||||||
| 4000mAh Battery | 71 Hrs | - | - | ||||||||||||
| 5000mAh Battery | - | - |
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 |