diff --git a/README.md b/README.md index 0d0c7d7..6ab5da5 100644 --- a/README.md +++ b/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*)". diff --git a/meshcore-bot.js b/meshcore-bot.js index ec98795..a312056 100755 --- a/meshcore-bot.js +++ b/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); } }