docs: publish channels: v2.8.2-open.6
No files matched your search
@@ -0,0 +1,156 @@
|
||||
\| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres |---
|
||||
title: Telemetry & Sensors
|
||||
parent: User Guide
|
||||
nav_order: 9
|
||||
last_updated: 2026-09-18
|
||||
description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides.
|
||||
aliases:
|
||||
|
||||
- sensors
|
||||
- environment
|
||||
- weather
|
||||
- power-metrics
|
||||
|
||||
---
|
||||
|
||||
# Telemetry & Sensors
|
||||
|
||||
Meshtastic nodes can collect and share sensor data across the mesh network. Telemetry allows nodes equipped with sensors to broadcast environmental, power, and device health information, visible on the node detail screen and logged over time.
|
||||
|
||||
## Device Telemetry
|
||||
|
||||
All Meshtastic nodes report basic device telemetry:
|
||||
|
||||
| Metric | الوصف | Typical Range |
|
||||
| ------------- | ----------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Battery Level | Charge percentage | 0–100% |
|
||||
| شدة التيار | Battery voltage | 3.0–4.2V (LiPo) |
|
||||
| ChUtil | % of local airtime in use | 0–100% |
|
||||
| AirUtil | % of the last hour this node spent transmitting | 0–100% |
|
||||
| Uptime | Seconds since last boot | Varies |
|
||||
|
||||
## Environment Sensors
|
||||
|
||||
Supported environmental sensors:
|
||||
|
||||
### Temperature & Humidity
|
||||
|
||||
| Sensor | الحرارة | الرطوبة | Pressure | Notes |
|
||||
| ------- | ------- | ------- | -------- | ----------------------- |
|
||||
| BME280 | ✓ | ✓ | ✓ | Recommended all-in-one |
|
||||
| BME680 | ✓ | ✓ | ✓ | Adds gas resistance/IAQ |
|
||||
| SHT31 | ✓ | ✓ | — | High accuracy |
|
||||
| MCP9808 | ✓ | — | — | Precision temperature |
|
||||
| LPS22 | — | — | ✓ | Pressure only |
|
||||
|
||||
### Air Quality
|
||||
|
||||
| Sensor | Metric | Notes |
|
||||
| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| BME680 | Gas Resistance / IAQ | Volatile organic compounds |
|
||||
| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) |
|
||||
| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts |
|
||||
| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry |
|
||||
|
||||
### Soil
|
||||
|
||||
| Metric | Unit | Notes |
|
||||
| ---------- | ------- | ----------------------------------------------- |
|
||||
| Soil Temp | °C / °F | Reported alongside soil moisture by soil probes |
|
||||
| Soil Moist | % | Volumetric water content |
|
||||
|
||||
Both appear as info cards on the node detail screen, next to the other environment readings.
|
||||
|
||||
Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet.
|
||||
|
||||
### Light & UV
|
||||
|
||||
| Sensor | Metric |
|
||||
| -------- | -------------------------------------- |
|
||||
| OPT3001 | Ambient light (lux) |
|
||||
| VEML7700 | Ambient light (lux) |
|
||||
| LTR390 | UV index |
|
||||
|
||||
### Weather and Other Readings
|
||||
|
||||
| Metric | Unit | Where it appears |
|
||||
| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card |
|
||||
| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted |
|
||||
| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted |
|
||||
| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector |
|
||||
| Radiation | µR/h | Card and chart |
|
||||
| Weight | kg or lb | Card only — load cells, such as a beehive scale |
|
||||
| المسافة | mm or in | Card only — water level, from a distance sensor |
|
||||
| Dew point | °C or °F | Card only — computed from temperature and humidity |
|
||||
| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes |
|
||||
| ADC voltage | V | Card and chart, up to eight raw analog channels |
|
||||
|
||||
## Power Metrics
|
||||
|
||||
Nodes with INA-series power sensors can report:
|
||||
|
||||
| Metric | الوصف |
|
||||
| ---------- | ------------------------------- |
|
||||
| شدة التيار | Per-channel voltage reading |
|
||||
| الحالي | Per-channel current draw, in mA |
|
||||
|
||||
The node detail screen shows read-only cards for channels 1 to 3. Use the chart button on the **Power Metrics** row to open the chart screen, which lists a chip for every channel that reported data — up to eight — and charts the one you select. Rename a channel there, in the label field under the chips, to something like Solar or Battery. There is no separate wattage reading; the app charts voltage and current, and does not compute power from them.
|
||||
|
||||
Useful for monitoring solar charging or battery health on remote nodes.
|
||||
|
||||
## Configuring Telemetry
|
||||
|
||||
1. Navigate to **Settings → Module configuration → Telemetry**.
|
||||
2. Each metric group has its own enable toggle and its own interval:
|
||||
|
||||
- **Device Metrics** — battery, voltage, uptime, ChUtil, and AirUtil. Its enable toggle, **Send Device Telemetry**, appears only on firmware 2.7.12 and later; on older firmware you can change the interval but not turn the group off
|
||||
- **Environment Metrics** — temperature, humidity, pressure and the other sensor readings
|
||||
- **Air Quality Metrics** — particulate and CO₂ readings
|
||||
- **Power Metrics** — the per-channel voltage and current readings
|
||||
|
||||
Environment and Power each have an extra toggle to show their readings on the radio's own
|
||||
screen, and Environment has one more to show its temperatures there in Fahrenheit.
|
||||
|
||||
### Choosing an Interval
|
||||
|
||||
These are nominal values, not hard schedules. On a congested mesh the firmware automatically
|
||||
backs off to longer intervals based on how many nodes are online, so you do not need to
|
||||
hand-tune them for mesh size. Lengthen them deliberately only to save battery.
|
||||
|
||||
## Air Quality Metrics
|
||||
|
||||
Nodes with particulate matter or CO₂ sensors report air quality data:
|
||||
|
||||
| Metric | Unit | الوصف |
|
||||
| --------------------- | ----- | ---------------------------- |
|
||||
| PM1.0 | µg/m³ | Ultrafine particulate matter |
|
||||
| PM2.5 | µg/m³ | Fine particulate matter |
|
||||
| PM10 | µg/m³ | Coarse particulate matter |
|
||||
| CO₂ | ppm | Carbon dioxide concentration |
|
||||
|
||||
CO₂ sensors such as the SCD4x also report their own temperature and humidity, which appear alongside the readings above. From PM2.5 history the app additionally derives an **EPA NowCast AQI** value.
|
||||
|
||||
The CO₂ reading is color-coded by severity (Good → Stuffy → Poor → Unsafe → Evacuate). See [Node Metrics — Air Quality](node-metrics#air-quality-metrics) for the exact ppm bands, colors, and AQI detail.
|
||||
|
||||
Air quality data can be viewed as info cards on the node detail screen, charted over time, and exported to CSV.
|
||||
|
||||
## Viewing Telemetry
|
||||
|
||||
1. Navigate to **Nodes** and select a node.
|
||||
2. The **Telemetry** section lists a row for every metric type — Device, Environment, Air Quality, Power, and the rest — whether or not this node has reported it. A row fills in with readings, and grows a chart button, once that node has actually sent that kind of telemetry. An empty row means nothing has arrived yet, not that the sensor is missing.
|
||||
3. Use the chart button on a row to open that metric's history, where you can pick a time frame and export the readings as CSV.
|
||||
|
||||

|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No environment data showing?** The remote node needs a physical sensor connected (e.g., BME280 on I2C). Device telemetry (battery, uptime) is always available, but environment metrics require hardware.
|
||||
- **Stale readings?** Check the reporting interval — very long intervals (7200s+) mean data updates infrequently. Also verify the remote node is still online.
|
||||
- **Sensor conflict on I2C bus?** Some sensors share I2C addresses. If you have multiple sensors on the same bus, check for address collisions in the radio's serial debug output.
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Node Metrics](node-metrics) — view telemetry data on the node detail screen
|
||||
- [Settings — Modules & Admin](settings-module-admin) — telemetry module configuration
|
||||
- [Units & Locale](units-and-locale) — temperature and pressure display units
|
||||
@@ -0,0 +1,3 @@
|
||||
.site-nav ul li a {
|
||||
background-image: linear-gradient(-90deg, #e0e1eb 0%, rgba(224, 225, 235, 0.8) 80%, rgba(224, 225, 235, 0) 100%);
|
||||
}
|
||||
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,584 @@
|
||||
(function (jtd, undefined) {
|
||||
|
||||
// Event handling
|
||||
|
||||
jtd.addEvent = function(el, type, handler) {
|
||||
if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
|
||||
}
|
||||
jtd.removeEvent = function(el, type, handler) {
|
||||
if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler);
|
||||
}
|
||||
jtd.onReady = function(ready) {
|
||||
// in case the document is already rendered
|
||||
if (document.readyState!='loading') ready();
|
||||
// modern browsers
|
||||
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', ready);
|
||||
// IE <= 8
|
||||
else document.attachEvent('onreadystatechange', function(){
|
||||
if (document.readyState=='complete') ready();
|
||||
});
|
||||
}
|
||||
|
||||
// Show/hide mobile menu
|
||||
|
||||
function initNav() {
|
||||
jtd.addEvent(document, 'click', function(e){
|
||||
var target = e.target;
|
||||
while (target && !(target.classList && target.classList.contains('nav-list-expander'))) {
|
||||
target = target.parentNode;
|
||||
}
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
target.ariaExpanded = target.parentNode.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
|
||||
const siteNav = document.getElementById('site-nav');
|
||||
const mainHeader = document.getElementById('main-header');
|
||||
const menuButton = document.getElementById('menu-button');
|
||||
|
||||
disableHeadStyleSheets();
|
||||
|
||||
jtd.addEvent(menuButton, 'click', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
if (menuButton.classList.toggle('nav-open')) {
|
||||
siteNav.classList.add('nav-open');
|
||||
mainHeader.classList.add('nav-open');
|
||||
menuButton.ariaExpanded = true;
|
||||
} else {
|
||||
siteNav.classList.remove('nav-open');
|
||||
mainHeader.classList.remove('nav-open');
|
||||
menuButton.ariaExpanded = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The <head> element is assumed to include the following stylesheets:
|
||||
// - a <link> to /assets/css/just-the-docs-head-nav.css,
|
||||
// with id 'jtd-head-nav-stylesheet'
|
||||
// - a <style> containing the result of _includes/css/activation.scss.liquid.
|
||||
// To avoid relying on the order of stylesheets (which can change with HTML
|
||||
// compression, user-added JavaScript, and other side effects), stylesheets
|
||||
// are only interacted with via ID
|
||||
|
||||
function disableHeadStyleSheets() {
|
||||
const headNav = document.getElementById('jtd-head-nav-stylesheet');
|
||||
if (headNav) {
|
||||
headNav.disabled = true;
|
||||
}
|
||||
|
||||
const activation = document.getElementById('jtd-nav-activation');
|
||||
if (activation) {
|
||||
activation.disabled = true;
|
||||
}
|
||||
}
|
||||
// Site search
|
||||
|
||||
function initSearch() {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('GET', '/Meshtastic-Android/v2.8.2-open.6/assets/js/search-data.json', true);
|
||||
|
||||
request.onload = function(){
|
||||
if (request.status >= 200 && request.status < 400) {
|
||||
var docs = JSON.parse(request.responseText);
|
||||
|
||||
lunr.tokenizer.separator = /[\s\-/]+/
|
||||
|
||||
var index = lunr(function(){
|
||||
this.ref('id');
|
||||
this.field('title', { boost: 200 });
|
||||
this.field('content', { boost: 2 });
|
||||
this.field('relUrl');
|
||||
this.metadataWhitelist = ['position']
|
||||
|
||||
for (var i in docs) {
|
||||
|
||||
this.add({
|
||||
id: i,
|
||||
title: docs[i].title,
|
||||
content: docs[i].content,
|
||||
relUrl: docs[i].relUrl
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
searchLoaded(index, docs);
|
||||
} else {
|
||||
console.log('Error loading ajax request. Request status:' + request.status);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function(){
|
||||
console.log('There was a connection error');
|
||||
};
|
||||
|
||||
request.send();
|
||||
}
|
||||
|
||||
function searchLoaded(index, docs) {
|
||||
var index = index;
|
||||
var docs = docs;
|
||||
var searchInput = document.getElementById('search-input');
|
||||
var searchResults = document.getElementById('search-results');
|
||||
var mainHeader = document.getElementById('main-header');
|
||||
var currentInput;
|
||||
var currentSearchIndex = 0;
|
||||
|
||||
function showSearch() {
|
||||
document.documentElement.classList.add('search-active');
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
document.documentElement.classList.remove('search-active');
|
||||
}
|
||||
|
||||
function update() {
|
||||
currentSearchIndex++;
|
||||
|
||||
var input = searchInput.value;
|
||||
if (input === '') {
|
||||
hideSearch();
|
||||
} else {
|
||||
showSearch();
|
||||
// scroll search input into view, workaround for iOS Safari
|
||||
window.scroll(0, -1);
|
||||
setTimeout(function(){ window.scroll(0, 0); }, 0);
|
||||
}
|
||||
if (input === currentInput) {
|
||||
return;
|
||||
}
|
||||
currentInput = input;
|
||||
searchResults.innerHTML = '';
|
||||
if (input === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
var results = index.query(function (query) {
|
||||
var tokens = lunr.tokenizer(input)
|
||||
query.term(tokens, {
|
||||
boost: 10
|
||||
});
|
||||
query.term(tokens, {
|
||||
wildcard: lunr.Query.wildcard.TRAILING
|
||||
});
|
||||
});
|
||||
|
||||
if ((results.length == 0) && (input.length > 2)) {
|
||||
var tokens = lunr.tokenizer(input).filter(function(token, i) {
|
||||
return token.str.length < 20;
|
||||
})
|
||||
if (tokens.length > 0) {
|
||||
results = index.query(function (query) {
|
||||
query.term(tokens, {
|
||||
editDistance: Math.round(Math.sqrt(input.length / 2 - 1))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length == 0) {
|
||||
var noResultsDiv = document.createElement('div');
|
||||
noResultsDiv.classList.add('search-no-result');
|
||||
noResultsDiv.innerText = 'No results found';
|
||||
searchResults.appendChild(noResultsDiv);
|
||||
|
||||
} else {
|
||||
var resultsList = document.createElement('ul');
|
||||
resultsList.classList.add('search-results-list');
|
||||
searchResults.appendChild(resultsList);
|
||||
|
||||
addResults(resultsList, results, 0, 10, 100, currentSearchIndex);
|
||||
}
|
||||
|
||||
function addResults(resultsList, results, start, batchSize, batchMillis, searchIndex) {
|
||||
if (searchIndex != currentSearchIndex) {
|
||||
return;
|
||||
}
|
||||
for (var i = start; i < (start + batchSize); i++) {
|
||||
if (i == results.length) {
|
||||
return;
|
||||
}
|
||||
addResult(resultsList, results[i]);
|
||||
}
|
||||
setTimeout(function() {
|
||||
addResults(resultsList, results, start + batchSize, batchSize, batchMillis, searchIndex);
|
||||
}, batchMillis);
|
||||
}
|
||||
|
||||
function addResult(resultsList, result) {
|
||||
var doc = docs[result.ref];
|
||||
|
||||
var resultsListItem = document.createElement('li');
|
||||
resultsListItem.classList.add('search-results-list-item');
|
||||
resultsList.appendChild(resultsListItem);
|
||||
|
||||
var resultLink = document.createElement('a');
|
||||
resultLink.classList.add('search-result');
|
||||
resultLink.setAttribute('href', doc.url);
|
||||
resultsListItem.appendChild(resultLink);
|
||||
|
||||
var resultTitle = document.createElement('div');
|
||||
resultTitle.classList.add('search-result-title');
|
||||
resultLink.appendChild(resultTitle);
|
||||
|
||||
// note: the SVG svg-doc is only loaded as a Jekyll include if site.search_enabled is true; see _includes/icons/icons.html
|
||||
var resultDoc = document.createElement('div');
|
||||
resultDoc.classList.add('search-result-doc');
|
||||
resultDoc.innerHTML = '<svg viewBox="0 0 24 24" class="search-result-icon" aria-hidden="true"><use xlink:href="#svg-doc"></use></svg>';
|
||||
resultTitle.appendChild(resultDoc);
|
||||
|
||||
var resultDocTitle = document.createElement('div');
|
||||
resultDocTitle.classList.add('search-result-doc-title');
|
||||
resultDocTitle.innerHTML = doc.doc;
|
||||
resultDoc.appendChild(resultDocTitle);
|
||||
var resultDocOrSection = resultDocTitle;
|
||||
|
||||
if (doc.doc != doc.title) {
|
||||
resultDoc.classList.add('search-result-doc-parent');
|
||||
var resultSection = document.createElement('div');
|
||||
resultSection.classList.add('search-result-section');
|
||||
resultSection.innerHTML = doc.title;
|
||||
resultTitle.appendChild(resultSection);
|
||||
resultDocOrSection = resultSection;
|
||||
}
|
||||
|
||||
var metadata = result.matchData.metadata;
|
||||
var titlePositions = [];
|
||||
var contentPositions = [];
|
||||
for (var j in metadata) {
|
||||
var meta = metadata[j];
|
||||
if (meta.title) {
|
||||
var positions = meta.title.position;
|
||||
for (var k in positions) {
|
||||
titlePositions.push(positions[k]);
|
||||
}
|
||||
}
|
||||
if (meta.content) {
|
||||
var positions = meta.content.position;
|
||||
for (var k in positions) {
|
||||
var position = positions[k];
|
||||
var previewStart = position[0];
|
||||
var previewEnd = position[0] + position[1];
|
||||
var ellipsesBefore = true;
|
||||
var ellipsesAfter = true;
|
||||
for (var k = 0; k < 5; k++) {
|
||||
var nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
|
||||
var nextDot = doc.content.lastIndexOf('. ', previewStart - 2);
|
||||
if ((nextDot >= 0) && (nextDot > nextSpace)) {
|
||||
previewStart = nextDot + 1;
|
||||
ellipsesBefore = false;
|
||||
break;
|
||||
}
|
||||
if (nextSpace < 0) {
|
||||
previewStart = 0;
|
||||
ellipsesBefore = false;
|
||||
break;
|
||||
}
|
||||
previewStart = nextSpace + 1;
|
||||
}
|
||||
for (var k = 0; k < 10; k++) {
|
||||
var nextSpace = doc.content.indexOf(' ', previewEnd + 1);
|
||||
var nextDot = doc.content.indexOf('. ', previewEnd + 1);
|
||||
if ((nextDot >= 0) && (nextDot < nextSpace)) {
|
||||
previewEnd = nextDot;
|
||||
ellipsesAfter = false;
|
||||
break;
|
||||
}
|
||||
if (nextSpace < 0) {
|
||||
previewEnd = doc.content.length;
|
||||
ellipsesAfter = false;
|
||||
break;
|
||||
}
|
||||
previewEnd = nextSpace;
|
||||
}
|
||||
contentPositions.push({
|
||||
highlight: position,
|
||||
previewStart: previewStart, previewEnd: previewEnd,
|
||||
ellipsesBefore: ellipsesBefore, ellipsesAfter: ellipsesAfter
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (titlePositions.length > 0) {
|
||||
titlePositions.sort(function(p1, p2){ return p1[0] - p2[0] });
|
||||
resultDocOrSection.innerHTML = '';
|
||||
addHighlightedText(resultDocOrSection, doc.title, 0, doc.title.length, titlePositions);
|
||||
}
|
||||
|
||||
if (contentPositions.length > 0) {
|
||||
contentPositions.sort(function(p1, p2){ return p1.highlight[0] - p2.highlight[0] });
|
||||
var contentPosition = contentPositions[0];
|
||||
var previewPosition = {
|
||||
highlight: [contentPosition.highlight],
|
||||
previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd,
|
||||
ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter
|
||||
};
|
||||
var previewPositions = [previewPosition];
|
||||
for (var j = 1; j < contentPositions.length; j++) {
|
||||
contentPosition = contentPositions[j];
|
||||
if (previewPosition.previewEnd < contentPosition.previewStart) {
|
||||
previewPosition = {
|
||||
highlight: [contentPosition.highlight],
|
||||
previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd,
|
||||
ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter
|
||||
}
|
||||
previewPositions.push(previewPosition);
|
||||
} else {
|
||||
previewPosition.highlight.push(contentPosition.highlight);
|
||||
previewPosition.previewEnd = contentPosition.previewEnd;
|
||||
previewPosition.ellipsesAfter = contentPosition.ellipsesAfter;
|
||||
}
|
||||
}
|
||||
|
||||
var resultPreviews = document.createElement('div');
|
||||
resultPreviews.classList.add('search-result-previews');
|
||||
resultLink.appendChild(resultPreviews);
|
||||
|
||||
var content = doc.content;
|
||||
for (var j = 0; j < Math.min(previewPositions.length, 3); j++) {
|
||||
var position = previewPositions[j];
|
||||
|
||||
var resultPreview = document.createElement('div');
|
||||
resultPreview.classList.add('search-result-preview');
|
||||
resultPreviews.appendChild(resultPreview);
|
||||
|
||||
if (position.ellipsesBefore) {
|
||||
resultPreview.appendChild(document.createTextNode('... '));
|
||||
}
|
||||
addHighlightedText(resultPreview, content, position.previewStart, position.previewEnd, position.highlight);
|
||||
if (position.ellipsesAfter) {
|
||||
resultPreview.appendChild(document.createTextNode(' ...'));
|
||||
}
|
||||
}
|
||||
}
|
||||
var resultRelUrl = document.createElement('span');
|
||||
resultRelUrl.classList.add('search-result-rel-url');
|
||||
resultRelUrl.innerText = doc.relUrl;
|
||||
resultTitle.appendChild(resultRelUrl);
|
||||
}
|
||||
|
||||
function addHighlightedText(parent, text, start, end, positions) {
|
||||
var index = start;
|
||||
for (var i in positions) {
|
||||
var position = positions[i];
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = text.substring(index, position[0]);
|
||||
parent.appendChild(span);
|
||||
index = position[0] + position[1];
|
||||
var highlight = document.createElement('span');
|
||||
highlight.classList.add('search-result-highlight');
|
||||
highlight.innerHTML = text.substring(position[0], index);
|
||||
parent.appendChild(highlight);
|
||||
}
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = text.substring(index, end);
|
||||
parent.appendChild(span);
|
||||
}
|
||||
}
|
||||
|
||||
jtd.addEvent(searchInput, 'focus', function(){
|
||||
setTimeout(update, 0);
|
||||
});
|
||||
|
||||
jtd.addEvent(searchInput, 'keyup', function(e){
|
||||
switch (e.keyCode) {
|
||||
case 27: // When esc key is pressed, hide the results and clear the field
|
||||
searchInput.value = '';
|
||||
break;
|
||||
case 38: // arrow up
|
||||
case 40: // arrow down
|
||||
case 13: // enter
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
jtd.addEvent(searchInput, 'keydown', function(e){
|
||||
switch (e.keyCode) {
|
||||
case 38: // arrow up
|
||||
e.preventDefault();
|
||||
var active = document.querySelector('.search-result.active');
|
||||
if (active) {
|
||||
active.classList.remove('active');
|
||||
if (active.parentElement.previousSibling) {
|
||||
var previous = active.parentElement.previousSibling.querySelector('.search-result');
|
||||
previous.classList.add('active');
|
||||
}
|
||||
}
|
||||
return;
|
||||
case 40: // arrow down
|
||||
e.preventDefault();
|
||||
var active = document.querySelector('.search-result.active');
|
||||
if (active) {
|
||||
if (active.parentElement.nextSibling) {
|
||||
var next = active.parentElement.nextSibling.querySelector('.search-result');
|
||||
active.classList.remove('active');
|
||||
next.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
var next = document.querySelector('.search-result');
|
||||
if (next) {
|
||||
next.classList.add('active');
|
||||
}
|
||||
}
|
||||
return;
|
||||
case 13: // enter
|
||||
e.preventDefault();
|
||||
var active = document.querySelector('.search-result.active');
|
||||
if (active) {
|
||||
active.click();
|
||||
} else {
|
||||
var first = document.querySelector('.search-result');
|
||||
if (first) {
|
||||
first.click();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
jtd.addEvent(document, 'click', function(e){
|
||||
if (e.target != searchInput) {
|
||||
hideSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Switch theme
|
||||
|
||||
jtd.getTheme = function() {
|
||||
var cssFileHref = document.querySelector('[rel="stylesheet"]').getAttribute('href');
|
||||
return cssFileHref.substring(cssFileHref.lastIndexOf('-') + 1, cssFileHref.length - 4);
|
||||
}
|
||||
|
||||
jtd.setTheme = function(theme) {
|
||||
var cssFile = document.querySelector('[rel="stylesheet"]');
|
||||
cssFile.setAttribute('href', '/Meshtastic-Android/v2.8.2-open.6/assets/css/just-the-docs-' + theme + '.css');
|
||||
}
|
||||
|
||||
// Note: pathname can have a trailing slash on a local jekyll server
|
||||
// and not have the slash on GitHub Pages
|
||||
|
||||
function navLink() {
|
||||
var pathname = document.location.pathname;
|
||||
|
||||
var navLink = document.getElementById('site-nav').querySelector('a[href="' + pathname + '"]');
|
||||
if (navLink) {
|
||||
return navLink;
|
||||
}
|
||||
|
||||
// The `permalink` setting may produce navigation links whose `href` ends with `/` or `.html`.
|
||||
// To find these links when `/` is omitted from or added to pathname, or `.html` is omitted:
|
||||
|
||||
if (pathname.endsWith('/') && pathname != '/') {
|
||||
pathname = pathname.slice(0, -1);
|
||||
}
|
||||
|
||||
if (pathname != '/') {
|
||||
navLink = document.getElementById('site-nav').querySelector('a[href="' + pathname + '"], a[href="' + pathname + '/"], a[href="' + pathname + '.html"]');
|
||||
if (navLink) {
|
||||
return navLink;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // avoids `undefined`
|
||||
}
|
||||
|
||||
// Scroll site-nav to ensure the link to the current page is visible
|
||||
|
||||
function scrollNav() {
|
||||
const targetLink = navLink();
|
||||
if (targetLink) {
|
||||
targetLink.scrollIntoView({ block: "center" });
|
||||
targetLink.removeAttribute('href');
|
||||
}
|
||||
}
|
||||
|
||||
// Find the nav-list-link that refers to the current page
|
||||
// then make it and all enclosing nav-list-item elements active.
|
||||
|
||||
function activateNav() {
|
||||
var target = navLink();
|
||||
if (target) {
|
||||
target.classList.toggle('active', true);
|
||||
}
|
||||
while (target) {
|
||||
while (target && !(target.classList && target.classList.contains('nav-list-item'))) {
|
||||
target = target.parentNode;
|
||||
}
|
||||
if (target) {
|
||||
target.classList.toggle('active', true);
|
||||
target = target.parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Document ready
|
||||
|
||||
jtd.onReady(function(){
|
||||
if (document.getElementById('site-nav')) {
|
||||
initNav();
|
||||
activateNav();
|
||||
scrollNav();
|
||||
}
|
||||
initSearch();
|
||||
});
|
||||
|
||||
// Accessibility: set tabindex=0 on each code highlight block, so screenreaders
|
||||
// can focus over (particularly important if there's horizontal scroll)
|
||||
// see: https://dequeuniversity.com/rules/axe/4.9/scrollable-region-focusable?application=axeAPI
|
||||
|
||||
jtd.onReady(() => {
|
||||
document
|
||||
.querySelectorAll("div.highlight")
|
||||
.forEach(codeBlock => codeBlock.setAttribute("tabindex", "0"));
|
||||
});
|
||||
|
||||
// Copy button on code
|
||||
|
||||
jtd.onReady(function(){
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
console.log('Window does not have a secure context, therefore code clipboard copy functionality will not be available. For more details see https://web.dev/async-clipboard/#security-and-permissions');
|
||||
return;
|
||||
}
|
||||
|
||||
var codeBlocks = document.querySelectorAll('div.highlighter-rouge, div.listingblock > div.content, figure.highlight');
|
||||
|
||||
// note: the SVG svg-copied and svg-copy is only loaded as a Jekyll include if site.enable_copy_code_button is true; see _includes/icons/icons.html
|
||||
var svgCopied = '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copied"></use></svg>';
|
||||
var svgCopy = '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copy"></use></svg>';
|
||||
|
||||
codeBlocks.forEach(codeBlock => {
|
||||
var copyButton = document.createElement('button');
|
||||
var timeout = null;
|
||||
copyButton.type = 'button';
|
||||
copyButton.ariaLabel = 'Copy code to clipboard';
|
||||
copyButton.innerHTML = svgCopy;
|
||||
codeBlock.append(copyButton);
|
||||
|
||||
copyButton.addEventListener('click', function () {
|
||||
if(timeout === null) {
|
||||
var code = (codeBlock.querySelector('pre:not(.lineno, .highlight)') || codeBlock.querySelector('code')).innerText;
|
||||
window.navigator.clipboard.writeText(code);
|
||||
|
||||
copyButton.innerHTML = svgCopied;
|
||||
|
||||
var timeoutSetting = 4000;
|
||||
|
||||
timeout = setTimeout(function () {
|
||||
copyButton.innerHTML = svgCopy;
|
||||
timeout = null;
|
||||
}, timeoutSetting);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(window.jtd = window.jtd || {});
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Screenshots
|
||||
|
||||
This directory is the **single source of truth** for screenshot assets referenced by the
|
||||
documentation pages. It is consumed by both:
|
||||
|
||||
- the **Jekyll docs site** (markdown references `../../assets/screenshots/{name}.png`), and
|
||||
- the **in-app docs browser** — `:feature:docs:syncDocsToComposeResources` bundles this
|
||||
directory into compose resources at `files/docs/assets/screenshots/`.
|
||||
|
||||
`DocImageWiringTest` (in `:feature:docs`) fails the build if a doc page references an image
|
||||
that is not present here.
|
||||
|
||||
## Two source modules
|
||||
|
||||
Doc screenshots come from Compose Preview Screenshot Testing references in **two** modules:
|
||||
|
||||
- **`:screenshot-tests`** — visual-regression gate (CI runs `:screenshot-tests:validateDebugScreenshotTest`).
|
||||
Holds atomic, dual-purpose components (signal/battery/hops info, list items, preference widgets,
|
||||
alerts) that are both regression-checked **and** used as doc images. Don't reframe these for docs.
|
||||
- **`:docs-screenshots`** — generate-only, **not** gated in CI. Holds doc-framed compositions whose
|
||||
framing is tuned for the docs site (e.g. the firmware status crops, the connections BLE-scan /
|
||||
empty-state crops). Reframing one here never churns the regression gate.
|
||||
|
||||
`copyDocsScreenshots` (in `:screenshot-tests`) aggregates the reference images from **both** modules.
|
||||
|
||||
## Updating Screenshots
|
||||
|
||||
After changing a UI component, regenerate references for whichever module owns the wrapper, then copy:
|
||||
|
||||
```bash
|
||||
./gradlew :screenshot-tests:updateDebugScreenshotTest # regression references
|
||||
./gradlew :docs-screenshots:updateDebugScreenshotTest # doc-framed composition references
|
||||
./gradlew :screenshot-tests:copyDocsScreenshots # refresh this directory from both
|
||||
```
|
||||
|
||||
`copyDocsScreenshots` copies **only** the light-mode reference images that have a semantic
|
||||
alias in `screenshot-tests/docs-screenshot-aliases.properties`, renaming them on the way.
|
||||
Commit the refreshed PNGs together with the reference-image changes.
|
||||
|
||||
## Adding a Screenshot for a New Doc Page
|
||||
|
||||
1. Add (or reuse) a `Preview*`/`*Preview` composable with representative mock data in the
|
||||
feature module. Add a `Screenshot*` wrapper: in **`:docs-screenshots`** if it's a doc-framed
|
||||
composition (full screen / doc-specific crop), or in **`:screenshot-tests`** if it's an atomic
|
||||
component you also want regression-gated. If the component renders timestamps, give it a
|
||||
`timeTextOverride`-style parameter so renders stay deterministic across machines.
|
||||
2. Make sure the test class is covered by a pattern in `screenshot-tests/docs-screenshots-manifest.txt`
|
||||
(the patterns are `**/{Class}Kt/...`, so they match in either module).
|
||||
3. Map the semantic name in `screenshot-tests/docs-screenshot-aliases.properties`:
|
||||
`{page-id}_{description}.png=Screenshot{Name}_Light_{hash}_0.png`
|
||||
4. Run the relevant update task(s) + `copyDocsScreenshots`, then reference the image from the doc page.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```
|
||||
{page-id}_{description}.png
|
||||
```
|
||||
|
||||
Examples: `onboarding_welcome.png`, `connections_bluetooth_scan.png`, `discovery_preset_result.png`.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- PNG format, light-mode only (dark variants live in the reference directory)
|
||||
- Name screenshots to match the docs page they appear in
|
||||
- Keep filenames lowercase with underscores
|
||||
- Prefer CST-generated screenshots — they render real app composables, so they cannot drift from
|
||||
reality. Avoid hand-pasted captures: a stray screenshot from another app slipped in this way
|
||||
before (the old `connections_wifi_*.png` were from an unrelated WiFi-provisioning app, not
|
||||
Meshtastic). If a manual capture is unavoidable, it must be a genuine Meshtastic-Android screen.
|
||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 92 KiB |