fetch also the repeater status

This commit is contained in:
Nils
2025-09-12 21:22:12 +02:00
parent b041b76396
commit 2d67ac09a4
2 changed files with 111 additions and 38 deletions

View File

@@ -5,9 +5,9 @@ This script is a command bot that connects to a [MeshCore](https://github.com/me
> [!IMPORTANT]
> To prevent spam in public channels, this bot only responds in private channels!
The bot is also able to fetch and log telemetry sensor (currently only voltage) data from a repeater node.
The telemetry data is logged to a Comma-Separated Values (CSV) file.
The interval at which the telemetry data is fetched can be configured.
The bot is also able to fetch and log status (uptime, TX air time, last SNR, noise floor...) and telemetry sensor data (currently only voltage) from a repeater node.
The status and telemetry data is logged to a Comma-Separated Values (CSV) file.
The interval at which the status and telemetry data is fetched can be configured.
This bot is ideal for testing MeshCore setup with repeater and distance of communication.
| Client | Bot |
@@ -40,34 +40,55 @@ This bot is ideal for testing MeshCore setup with repeater and distance of commu
To run the bot use the following command:
```bash
node meshcore-bot.js --port [SERIAL_PORT] --repeater-public-key-prefix [REPEATER_PUBLIC_KEY_PREFIX] --repeater-password [REPEATER_PASSWORD] --telemetry-interval [TELEMETRY_INTERVAL_MINUTES] --csv [CSV_FILE]
node meshcore-bot.js --port [SERIAL_PORT] --repeater-public-key-prefix [REPEATER_PUBLIC_KEY_PREFIX] --repeater-password [REPEATER_PASSWORD] --repeater-interval [TELEMETRY_INTERVAL_MINUTES] --csv [CSV_FILE]
```
- `--port` or `-s` (optional): The serial port of the MeshCore device. Defaults to `/dev/cu.usbmodem1101`.
- `--repeater-public-key-prefix` or `-r` (optional): The public key prefix of a repeater node to fetch telemetry from. If provided, the telemetry feature is enabled.
- `--repeater-public-key-prefix` or `-r` (optional): The public key prefix of a repeater node to fetch status and telemetry from. If provided, this feature is enabled.
- `--repeater-password` or `-p` (optional): The password for the repeater. By default, this is an empty string.
- `--telemetry-interval` or `-t` (optional): The interval in minutes at which telemetry data is retrieved from the repeater. The default value is `15`.
- `--csv` or `-c` (optional): The CSV file in which the repeater's telemetry data is to be logged. If this file is specified, the telemetry data will be logged in this file.
- `--repeater-interval` or `-i` (optional): The interval in minutes at which status and telemetry data is retrieved from the repeater. The default value is `15`.
- `--csv` or `-c` (optional): The CSV file in which the repeater's status and telemetry data is to be logged. If this file is specified, the data will be logged in this file.
### Examples
**Basic:**
```bash
node meshcore-bot.js --port "/dev/ttyUSB0"
```
**With Repeater Telemetry:**
```bash
node meshcore-bot.js --port "/dev/ttyUSB0" --repeater-public-key-prefix "935c6b694200644710a374c250c76f7aed9ec2ff3e60261447d4eda7c246ce5d" --repeater-password "your-password" --telemetry-interval 5
```
This will connect to the device on `/dev/ttyUSB0` and fetch telemetry from the specified repeater every 5 minutes.
**With Repeater Status and Telemetry:**
**With Repeater Telemetry and CSV Logging:**
```bash
node meshcore-bot.js --port "/dev/ttyUSB0" --repeater-public-key-prefix "935c6b694200644710a374c250c76f7aed9ec2ff3e60261447d4eda7c246ce5d" --repeater-password "your-password" --telemetry-interval 5 --csv "telemetry.csv"
node meshcore-bot.js \
--port "/dev/ttyUSB0" \
--repeater-public-key-prefix "935c6b694200644710a374c250c76f7aed9ec2ff3e60261447d4eda7c246ce5d" \
--repeater-password "your-password" \
--repeater-interval 30
```
This will connect to the device on `/dev/ttyUSB0` and fetch telemetry from the specified repeater every 30 minutes.
**With Repeater and CSV Logging:**
```bash
node meshcore-bot.js \
--port "/dev/ttyUSB0" \
--repeater-public-key-prefix "935c6b694200644710a374c250c76f7aed9ec2ff3e60261447d4eda7c246ce5d" \
--repeater-password "your-password" \
--repeater-interval 30 \
--csv "telemetry.csv"
```
This will do the same as the previous example, but it will also log the telemetry data to `telemetry.csv`.
Example CSV:
```csv
timestamp,lpp_volts,batt_milli_volts,curr_tx_queue_len,noise_floor,last_rssi,n_packets_recv,n_packets_sent,total_air_time_secs,total_up_time_secs,n_sent_flood,n_sent_direct,n_recv_flood,n_recv_direct,err_events,last_snr,n_direct_dups,n_flood_dups
2025-09-12T19:06:07Z,3.97,3969,0,-111,-59,2029,1749,1399,700263,1672,77,1514,359,0,28,0,98
2025-09-12T19:08:32Z,3.96,3969,0,-110,-60,2033,1753,1401,700407,1676,77,1515,362,0,28,0,98
```
### Commands
- `.ping`: The bot will respond with "PONG! 🏓 (*hop count*)".

View File

@@ -5,6 +5,10 @@ import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import fs from 'fs';
function getTimestamp() {
return new Date().toISOString().slice(0, -5) + 'Z';
}
class LPPDecoder {
// Decode Cayenne Low Power Payload (LPP) for LoraWan
constructor() {
@@ -20,7 +24,7 @@ class LPPDecoder {
switch (type) {
// Source: https://discord.com/channels/1343693475589263471/1391673743453192242/1395240557176950876
case 0x74: { // static const LPP_VOLTAGE = 116;
const name = "voltage";
const name = "lpp_volts";
this.sensors.push({ channel, type, name, value: buffer.readInt16BE(i) / 100 });
i += 2; // 2 bytes 0.01V unsigned
break;
@@ -44,12 +48,12 @@ const argv = yargs(hideBin(process.argv))
.option('repeaterPublicKeyPrefix', {
alias: 'r',
type: 'string',
description: 'Public key prefix of the repeater to fetch telemetry from'
description: 'Public key of the repeater to fetch telemetry from'
})
.option('telemetryInterval', {
alias: 't',
.option('repeaterInterval', {
alias: 'i',
type: 'number',
description: 'Telemetry interval in minutes',
description: 'Repeater interval in minutes',
default: 15
})
.option('repeaterPassword', {
@@ -70,7 +74,7 @@ const argv = yargs(hideBin(process.argv))
const port = argv.port;
const repeaterPublicKeyPrefix = argv.repeaterPublicKeyPrefix;
const repeaterPassword = argv.repeaterPassword;
const telemetryIntervalMinutes = argv.telemetryInterval;
const telemetryIntervalMinutes = argv.repeaterInterval;
const telemetryIntervalMs = telemetryIntervalMinutes * 60 * 1000;
const csvFile = argv.csv;
@@ -180,12 +184,12 @@ connection.on(Constants.PushCodes.MsgWaiting, async () => {
});
async function onContactMessageReceived(message) {
console.log(`[${new Date().toISOString()}] Contact message`, message);
console.log(`[${getTimestamp()}] Contact message`, message);
}
async function onChannelMessageReceived(message) {
message.senderTimestampISO = (new Date(message.senderTimestamp * 1000)).toISOString();
console.log(`[${new Date().toISOString()}] Channel message`, message);
console.log(`[${getTimestamp()}] Channel message`, message);
// handle commands only in own channels, not in public channel with id 0
if(message.channelIdx > 0){
if(message.text.includes(".ping")){
@@ -200,36 +204,36 @@ async function onChannelMessageReceived(message) {
}
async function getRepeaterTelemetry(publicKeyPrefix, repeaterPassword) {
console.log("Fetching repeater telemetry...");
console.log("Fetching repeater status and telemetry...");
try {
const contact = await connection.findContactByPublicKeyPrefix(Buffer.from(publicKeyPrefix, "hex"));
if(!contact){
console.log("Repeater contact not found");
console.error("Repeater contact not found");
return;
}
// login to repeater and get repeater telemetry
// login to repeater and get repeater status telemetry
console.log("Logging in to repeater...");
await connection.login(contact.publicKey, repeaterPassword);
// get repeater status
console.log("Fetching status...");
const timestamp = getTimestamp(); // Store timestamp of first fetch for CSV
const status = await connection.getStatus(contact.publicKey);
console.log(`[${timestamp}] Repeater status`, status);
// get repeater telemetry
console.log("Fetching telemetry...");
const telemetry = await connection.getTelemetry(contact.publicKey);
//console.log("Repeater telemetry", telemetry);
console.log(`[${getTimestamp()}] Repeater telemetry`, telemetry);
let lpp_volts = 0.0;
if (telemetry.lppSensorData) {
try {
const lpp = new LPPDecoder();
const decoded = lpp.decode(telemetry.lppSensorData);
//console.log("Decoded repeater telemetry", decoded);
console.log(`[${getTimestamp()}] Decoded repeater telemetry`, decoded);
for (const sensor of decoded) {
if (sensor.name === "voltage") {
console.log(`Voltage: ${sensor.value} V`);
if (csvFile) {
const timestamp = new Date().toISOString();
const csvRow = `${timestamp},${sensor.value}\n`;
if (!fs.existsSync(csvFile)) {
fs.writeFileSync(csvFile, 'timestamp,voltage\n');
}
fs.appendFileSync(csvFile, csvRow);
}
if (sensor.name === "lpp_volts") {
lpp_volts = sensor.value;
console.log(`LPP Voltage: ${lpp_volts} V`);
}
}
} catch (e) {
@@ -237,8 +241,56 @@ async function getRepeaterTelemetry(publicKeyPrefix, repeaterPassword) {
}
}
if (csvFile) {
console.log("Write to CSV file...");
const header = [
'timestamp',
'lpp_volts',
'batt_milli_volts',
'curr_tx_queue_len',
'noise_floor',
'last_rssi',
'n_packets_recv',
'n_packets_sent',
'total_air_time_secs',
'total_up_time_secs',
'n_sent_flood',
'n_sent_direct',
'n_recv_flood',
'n_recv_direct',
'err_events',
'last_snr',
'n_direct_dups',
'n_flood_dups'
].join(',') + '\n';
const statusValues = [
timestamp,
lpp_volts,
status.batt_milli_volts,
status.curr_tx_queue_len,
status.noise_floor,
status.last_rssi,
status.n_packets_recv,
status.n_packets_sent,
status.total_air_time_secs,
status.total_up_time_secs,
status.n_sent_flood,
status.n_sent_direct,
status.n_recv_flood,
status.n_recv_direct,
status.err_events,
status.last_snr,
status.n_direct_dups,
status.n_flood_dups
].join(',') + '\n';
if (!fs.existsSync(csvFile)) {
fs.writeFileSync(csvFile, header);
}
fs.appendFileSync(csvFile, statusValues);
}
console.log("Done, waiting for the next interval.");
} catch(e) {
console.error("Error fetching repeater telemetry", e);
console.error("Error fetching repeater status or telemetry", e);
}
}