Move meshtastic mqtt project to meshcore usb project

This commit is contained in:
zyphlar
2026-04-15 13:45:38 -07:00
parent 150c61fe65
commit 427fbc1b4e
28 changed files with 1550 additions and 4011 deletions
+305
View File
@@ -0,0 +1,305 @@
import { NodeJSSerialConnection, Constants } from '@liamcottle/meshcore.js';
import config from '../config/config.js';
import logger from '../utils/logger.js';
import { contactQueries, positionQueries, messageQueries } from '../database/queries.js';
class MeshCoreSerialClient {
constructor() {
this.connection = null;
this.connected = false;
this.selfInfo = null;
this.reconnectTimer = null;
this.reconnectDelay = 5000;
}
async connect() {
const { port } = config.serial;
logger.info(`Connecting to MeshCore device on ${port}`);
try {
this.connection = new NodeJSSerialConnection(port);
this._patchFrameHandler();
this._bindEvents();
await this.connection.connect();
} catch (error) {
logger.error(`Failed to open serial port ${port}: ${error.message}`);
this._scheduleReconnect();
}
}
// Intercept unknown push codes before they hit the library's console.log fallback.
_patchFrameHandler() {
const orig = this.connection.onFrameReceived.bind(this.connection);
this.connection.onFrameReceived = (frame) => {
const buf = Buffer.isBuffer(frame) ? frame : Buffer.from(frame);
const code = buf[0];
if (code === 0x8E) {
// "Contact heard" push (not in library's PushCodes yet):
// [0x8E][snr_int8][rssi_int8][7 bytes route/reserved][32 bytes full pubkey]
if (buf.length >= 42) {
const snr = (buf.readInt8(1) / 4).toFixed(1);
const rssi = buf.readInt8(2);
const pubkeyHex = buf.slice(10, 42).toString('hex');
logger.debug(`Contact heard: ${pubkeyHex.substring(0, 12)}… SNR=${snr}dB RSSI=${rssi}dBm`);
try { contactQueries.updateLastHeard.run(pubkeyHex); } catch {}
}
return;
}
if (code === 0x90) {
// Single-byte keepalive/notification — no payload, no action needed.
return;
}
orig(frame);
};
}
_bindEvents() {
// "connected" fires after deviceQuery succeeds (no data passed)
this.connection.on('connected', async () => {
this.connected = true;
this.reconnectDelay = 5000;
logger.info('Serial connected, requesting self info...');
try {
this.selfInfo = await this.connection.getSelfInfo();
logger.info(`Device: ${this.selfInfo.name || '(unnamed)'}`);
} catch (err) {
logger.warn('Failed to get self info:', err.message);
}
try {
await this.connection.syncDeviceTime();
logger.info('Device clock synced');
} catch (err) {
logger.warn('Failed to sync device clock:', err.message);
}
await this._syncContacts();
await this._drainMessages();
});
this.connection.on('disconnected', () => {
this.connected = false;
logger.warn('Disconnected from MeshCore device');
this._scheduleReconnect();
});
// New message queued on device
this.connection.on(Constants.PushCodes.MsgWaiting, async () => {
await this._drainMessages();
});
// New contact discovered (manual-add mode)
this.connection.on(Constants.PushCodes.NewAdvert, (contact) => {
this._upsertContact(contact);
});
// Contact re-advertised (auto-add mode)
this.connection.on(Constants.PushCodes.Advert, (data) => {
// Only pubkey is given; just update last_heard
if (data.publicKey) {
const hex = this._toHex(data.publicKey);
if (hex) {
try { contactQueries.updateLastHeard.run(hex); } catch {}
}
}
});
// Delivery confirmed
this.connection.on(Constants.PushCodes.SendConfirmed, ({ ackCode, roundTrip }) => {
if (ackCode != null) {
try {
messageQueries.markDelivered.run(String(ackCode));
logger.info(`Message delivered, ackCode=${ackCode}, rtt=${roundTrip}ms`);
} catch (err) {
logger.error('Error marking delivered:', err);
}
}
});
}
async _syncContacts() {
try {
const contacts = await this.connection.getContacts();
logger.info(`Synced ${contacts.length} contacts from device`);
for (const c of contacts) {
this._upsertContact(c);
}
} catch (err) {
logger.error('Failed to sync contacts:', err.message);
}
}
async _drainMessages() {
try {
const messages = await this.connection.getWaitingMessages();
if (messages.length > 0) logger.info(`Drained ${messages.length} queued messages`);
for (const m of messages) {
if (m.contactMessage) this._handleDirectMessage(m.contactMessage);
if (m.channelMessage) this._handleChannelMessage(m.channelMessage);
}
} catch (err) {
logger.debug(`Message drain ended: ${err.message}`);
}
}
_upsertContact(contact) {
try {
const pubkeyHex = this._toHex(contact.publicKey);
if (!pubkeyHex) return;
const lat = (contact.advLat != null && contact.advLat !== 0) ? contact.advLat / 1e6 : null;
const lon = (contact.advLon != null && contact.advLon !== 0) ? contact.advLon / 1e6 : null;
contactQueries.upsert.run(
pubkeyHex,
contact.advName || null,
contact.type ?? 0,
contact.flags ?? 0,
contact.outPathLen ?? 255,
contact.lastAdvert ?? null
);
if (lat != null && lon != null) {
positionQueries.insert.run(pubkeyHex, lat, lon, null);
}
} catch (err) {
logger.error('Error upserting contact:', err);
}
}
_handleDirectMessage(msg) {
try {
const fromHex = this._toHex(msg.pubKeyPrefix);
const text = msg.text || '';
logger.info(`Direct msg from ${fromHex}: ${text.substring(0, 80)}`);
if (fromHex) {
try { contactQueries.updateLastHeard.run(fromHex); } catch {}
}
messageQueries.insert.run(
fromHex || 'unknown',
null, // to_pubkey (unknown for received messages)
null, // channel_idx
0, // msg_type: direct
text,
null, // snr (not parsed by this library version)
msg.pathLen ?? null,
null, // ack_hash
msg.senderTimestamp ?? null
);
} catch (err) {
logger.error('Error handling direct message:', err);
}
}
_handleChannelMessage(msg) {
try {
const channelIdx = msg.channelIdx ?? 0;
const text = msg.text || '';
logger.info(`Channel ${channelIdx} msg: ${text.substring(0, 80)}`);
messageQueries.insert.run(
`chan:${channelIdx}`,
null,
channelIdx,
1, // msg_type: channel
text,
null, // snr
msg.pathLen ?? null,
null,
msg.senderTimestamp ?? null
);
} catch (err) {
logger.error('Error handling channel message:', err);
}
}
_toHex(buf) {
if (!buf) return null;
if (typeof buf === 'string') return buf;
if (buf instanceof Uint8Array || Buffer.isBuffer(buf)) return Buffer.from(buf).toString('hex');
return null;
}
async sendDirectMessage(pubkeyHex, text) {
if (!this.connected) throw new Error('Not connected to device');
// sendTextMessage expects full 32-byte key but only uses first 6 bytes internally
const keyBuf = Buffer.from(pubkeyHex, 'hex');
const result = await this.connection.sendTextMessage(keyBuf, text);
return result; // { result, expectedAckCrc, estTimeout }
}
async sendChannelMessage(channelIdx, text) {
if (!this.connected) throw new Error('Not connected to device');
await this.connection.sendChannelTextMessage(channelIdx, text);
return null;
}
async getChannels() {
if (!this.connected) throw new Error('Not connected to device');
return await this.connection.getChannels();
}
async listPorts() {
try {
const { SerialPort } = await import('serialport');
const ports = await SerialPort.list();
return ports.map(p => ({ path: p.path, manufacturer: p.manufacturer || null }));
} catch {
return [];
}
}
isConnected() {
return this.connected;
}
getSelfInfo() {
if (!this.selfInfo) return null;
return {
name: this.selfInfo.name || null,
publicKey: this.selfInfo.publicKey ? Buffer.from(this.selfInfo.publicKey).toString('hex') : null,
type: this.selfInfo.type ?? null,
txPower: this.selfInfo.txPower ?? null,
maxTxPower: this.selfInfo.maxTxPower ?? null,
advLat: this.selfInfo.advLat ?? null,
advLon: this.selfInfo.advLon ?? null,
radioFreq: this.selfInfo.radioFreq ?? null,
radioBw: this.selfInfo.radioBw ?? null,
radioSf: this.selfInfo.radioSf ?? null,
radioCr: this.selfInfo.radioCr ?? null,
};
}
disconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.connection) {
try { this.connection.close(); } catch {}
this.connection = null;
}
this.connected = false;
logger.info('Serial client disconnected');
}
_scheduleReconnect() {
if (this.reconnectTimer) return;
logger.info(`Reconnecting in ${this.reconnectDelay / 1000}s...`);
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = null;
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 60000);
await this.connect();
}, this.reconnectDelay);
}
}
export default new MeshCoreSerialClient();