Move meshtastic mqtt project to meshcore usb project
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user