mirror of
https://github.com/Cyclenerd/meshcore-bot.git
synced 2026-03-28 17:42:46 +01:00
fetch also the repeater status
This commit is contained in:
49
README.md
49
README.md
@@ -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*)".
|
||||
|
||||
100
meshcore-bot.js
100
meshcore-bot.js
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user