forked from iarv/meshcore-bot
270 lines
8.5 KiB
JavaScript
Executable File
270 lines
8.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { Constants, NodeJSSerialConnection } from "@liamcottle/meshcore.js";
|
|
import yargs from 'yargs';
|
|
import { hideBin } from 'yargs/helpers';
|
|
import fs from 'fs';
|
|
|
|
function getTimestamp() {
|
|
return new Date().toISOString().slice(0, -5) + 'Z';
|
|
}
|
|
|
|
const argv = yargs(hideBin(process.argv))
|
|
.option('port', {
|
|
alias: 's',
|
|
type: 'string',
|
|
description: 'Serial port to connect to',
|
|
default: '/dev/cu.usbmodem1101'
|
|
})
|
|
.option('repeaterPublicKeyPrefix', {
|
|
alias: 'r',
|
|
type: 'string',
|
|
description: 'Public key of the repeater to fetch status from'
|
|
})
|
|
.option('repeaterInterval', {
|
|
alias: 'i',
|
|
type: 'number',
|
|
description: 'Repeater interval in minutes',
|
|
default: 15
|
|
})
|
|
.option('repeaterPassword', {
|
|
alias: 'p',
|
|
type: 'string',
|
|
description: 'Repeater password',
|
|
default: ''
|
|
})
|
|
.option('csv', {
|
|
alias: 'c',
|
|
type: 'string',
|
|
description: 'CSV file to log status to'
|
|
})
|
|
.argv;
|
|
|
|
// get port from cli arguments
|
|
/*eslint no-undef: "off"*/
|
|
const port = argv.port;
|
|
const repeaterPublicKeyPrefix = argv.repeaterPublicKeyPrefix;
|
|
const repeaterPassword = argv.repeaterPassword;
|
|
const statusIntervalMinutes = argv.repeaterInterval;
|
|
const statusIntervalMs = statusIntervalMinutes * 60 * 1000;
|
|
const csvFile = argv.csv;
|
|
|
|
console.log(`Connecting to ${port}`);
|
|
if(repeaterPublicKeyPrefix){
|
|
console.log(`Repeater public key prefix: ${repeaterPublicKeyPrefix}`);
|
|
console.log(`Status interval: ${statusIntervalMinutes} minutes`);
|
|
if (csvFile) {
|
|
console.log(`Logging status to: ${csvFile}`);
|
|
}
|
|
}
|
|
|
|
// create connection
|
|
const connection = new NodeJSSerialConnection(port);
|
|
|
|
let reconnectInterval;
|
|
let statusInterval;
|
|
|
|
// wait until connected
|
|
connection.on("connected", async () => {
|
|
|
|
// we are now connected
|
|
console.log("Connected");
|
|
|
|
// query device info
|
|
try {
|
|
const device = await connection.deviceQuery();
|
|
console.log("Model:", device.manufacturerModel);
|
|
console.log("Firmware build date:", device.firmware_build_date);
|
|
} catch (e) {
|
|
console.error("Error getting device info", e);
|
|
}
|
|
|
|
// update clock on meshcore device
|
|
console.log("Sync Clock...");
|
|
try {
|
|
await connection.syncDeviceTime();
|
|
} catch (e) {
|
|
console.error("Error syncing device time", e);
|
|
}
|
|
|
|
// log contacts
|
|
console.log("Get Contacts...");
|
|
try {
|
|
const contacts = await connection.getContacts();
|
|
//console.log(`Contacts:`, contacts);
|
|
for(const contact of contacts) {
|
|
const typeNames = ["None", "Contact", "Repeater", "Room"];
|
|
const typeName = typeNames[contact.type] || "Unknown";
|
|
console.log(`${typeName}: ${contact.advName}; Public Key: ${Buffer.from(contact.publicKey).toString('hex')}`);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error retrieving contacts", e);
|
|
}
|
|
|
|
// log channels
|
|
console.log("Get Channels...");
|
|
try {
|
|
const channels = await connection.getChannels();
|
|
//console.log(`Channels:`, channels);
|
|
for(const channel of channels) {
|
|
if (channel.name) {
|
|
console.log(`${channel.channelIdx}: ${channel.name}`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Error retrieving channels", e);
|
|
}
|
|
|
|
// clear reconnect interval if it exists
|
|
if (reconnectInterval) {
|
|
clearInterval(reconnectInterval);
|
|
reconnectInterval = null;
|
|
}
|
|
|
|
if(repeaterPublicKeyPrefix){
|
|
// Start fetching interval
|
|
if (statusInterval) {
|
|
clearInterval(statusInterval);
|
|
}
|
|
statusInterval = setInterval(() => getRepeater(repeaterPublicKeyPrefix, repeaterPassword), statusIntervalMs);
|
|
getRepeater(repeaterPublicKeyPrefix, repeaterPassword); // Also fetch immediately on connect
|
|
}
|
|
});
|
|
|
|
// auto reconnect on disconnect
|
|
connection.on("disconnected", () => {
|
|
console.log("Disconnected, trying to reconnect...");
|
|
if (reconnectInterval) {
|
|
clearInterval(reconnectInterval);
|
|
}
|
|
reconnectInterval = setInterval(async () => {
|
|
await connection.connect();
|
|
}, 3000);
|
|
|
|
if (statusInterval) {
|
|
clearInterval(statusInterval);
|
|
statusInterval = null;
|
|
}
|
|
});
|
|
|
|
// listen for new messages
|
|
connection.on(Constants.PushCodes.MsgWaiting, async () => {
|
|
try {
|
|
const waitingMessages = await connection.getWaitingMessages();
|
|
for(const message of waitingMessages){
|
|
if(message.contactMessage){
|
|
await onContactMessageReceived(message.contactMessage);
|
|
} else if(message.channelMessage) {
|
|
await onChannelMessageReceived(message.channelMessage);
|
|
}
|
|
}
|
|
} catch(e) {
|
|
console.error("Message could not be retrieved", e);
|
|
}
|
|
});
|
|
|
|
async function onContactMessageReceived(message) {
|
|
console.log(`[${getTimestamp()}] Contact message`, message);
|
|
}
|
|
|
|
async function onChannelMessageReceived(message) {
|
|
message.senderTimestampISO = (new Date(message.senderTimestamp * 1000)).toISOString();
|
|
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")){
|
|
await connection.sendChannelTextMessage(message.channelIdx, "PONG! 🏓 (" + message.pathLen + ")");
|
|
return;
|
|
}
|
|
if(message.text.includes(".date")){
|
|
await connection.sendChannelTextMessage(message.channelIdx, (new Date()).toISOString());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// listen for adverts
|
|
connection.on(Constants.PushCodes.Advert, async (advert) => {
|
|
try {
|
|
console.log(`[${getTimestamp()}] Advert: ${Buffer.from(advert.publicKey).toString('hex')}`);
|
|
} catch(e) {
|
|
console.error("Advert could not be retrieved", e);
|
|
}
|
|
});
|
|
|
|
async function getRepeater(publicKeyPrefix, repeaterPassword) {
|
|
console.log("Fetching repeater status...");
|
|
try {
|
|
const contact = await connection.findContactByPublicKeyPrefix(Buffer.from(publicKeyPrefix, "hex"));
|
|
if(!contact){
|
|
console.error("Repeater contact not found");
|
|
return;
|
|
}
|
|
|
|
// login to repeater and get repeater status
|
|
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);
|
|
|
|
if (csvFile) {
|
|
console.log("Write to CSV file...");
|
|
const header = [
|
|
'timestamp',
|
|
'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,
|
|
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 status!", e);
|
|
}
|
|
}
|
|
|
|
// connect to meshcore device
|
|
try {
|
|
await connection.connect();
|
|
} catch (e) {
|
|
console.error("Failed to connect initially", e);
|
|
}
|