Summary
Connection.onConnected() awaits deviceQuery() before emitting connected, and deviceQuery() has no timeout. If the device accepts writes but never replies, the promise never settles, connected is never emitted, and no error is raised. The consumer is left with no event of any kind and waits indefinitely.
async onConnected() {
// tell device what protocol version we support
try {
await this.deviceQuery(Constants.SupportedCompanionProtocolVersion);
} catch(e) {
// ignore
}
// tell clients we are connected
this.emit("connected");
}
The try/catch handles a rejection, but nothing causes a rejection when the device is simply silent.
How a device ends up silent
This is easy to reach in practice. The companion firmware builds are mutually exclusive: Heltec_v3_companion_radio_ble does not compile in ENABLE_USB_INTERFACE, so a node flashed with the Bluetooth build enumerates a perfectly healthy serial port and then never answers a single frame. Selecting it in a Web Serial picker gives a port that opens successfully and goes nowhere.
I spent a while assuming my own code was at fault before realising the firmware variant was the cause, precisely because there was no error to go on.
A desynced frame parser produces the same symptom (#39).
Reproduction
Save as repro_devicequery.mjs in the root of a meshcore.js checkout and run node repro_devicequery.mjs:
import SerialConnection from "./src/connection/serial_connection.js";
// a device that accepts writes and never answers
class SilentDevice extends SerialConnection {
async write() {}
}
const conn = new SilentDevice();
let connectedFired = false;
let errorFired = false;
conn.on("connected", () => connectedFired = true);
conn.on("error", () => errorFired = true);
conn.onConnected();
await new Promise((r) => setTimeout(r, 5000));
console.log(`"connected" emitted: ${connectedFired}`);
console.log(`"error" emitted: ${errorFired}`);
Output
"connected" emitted: false
"error" emitted: false
It stays that way indefinitely.
Inconsistency with getSelfInfo()
getSelfInfo() already accepts an optional timeoutMillis:
getSelfInfo(timeoutMillis = null) {
...
if(timeoutMillis != null){
setTimeout(reject, timeoutMillis);
}
deviceQuery() takes no equivalent, and it is the call that gates the connected event, so a consumer cannot opt into a timeout even if it wants one. The same applies to getChannels(), which loops getChannel() until one rejects — against a silent device the first call never settles and the loop never terminates.
Suggested fix
Either of these would resolve it:
- Give
deviceQuery() an optional timeoutMillis like getSelfInfo() has, and pass a default from onConnected().
- Or emit
connected regardless of the handshake outcome, and let consumers detect an unresponsive device through their own calls, which they can already bound with getSelfInfo(timeoutMillis).
The first seems closer to the existing intent, since onConnected() already treats the handshake as best effort by swallowing rejections.
As a consumer I worked around this with an external watchdog that disconnects and reports an error if the device has not identified itself within 15 seconds, but the library emitting something would be better, since right now there is no signal to react to at all.
Environment
@liamcottle/meshcore.js 1.15.0, same code on master
- Web Serial in Chrome, Windows 11
- Heltec V3 flashed with
Heltec_v3_companion_radio_ble, connected over USB
Summary
Connection.onConnected()awaitsdeviceQuery()before emittingconnected, anddeviceQuery()has no timeout. If the device accepts writes but never replies, the promise never settles,connectedis never emitted, and no error is raised. The consumer is left with no event of any kind and waits indefinitely.The
try/catchhandles a rejection, but nothing causes a rejection when the device is simply silent.How a device ends up silent
This is easy to reach in practice. The companion firmware builds are mutually exclusive:
Heltec_v3_companion_radio_bledoes not compile inENABLE_USB_INTERFACE, so a node flashed with the Bluetooth build enumerates a perfectly healthy serial port and then never answers a single frame. Selecting it in a Web Serial picker gives a port that opens successfully and goes nowhere.I spent a while assuming my own code was at fault before realising the firmware variant was the cause, precisely because there was no error to go on.
A desynced frame parser produces the same symptom (#39).
Reproduction
Save as
repro_devicequery.mjsin the root of ameshcore.jscheckout and runnode repro_devicequery.mjs:Output
It stays that way indefinitely.
Inconsistency with getSelfInfo()
getSelfInfo()already accepts an optionaltimeoutMillis:deviceQuery()takes no equivalent, and it is the call that gates theconnectedevent, so a consumer cannot opt into a timeout even if it wants one. The same applies togetChannels(), which loopsgetChannel()until one rejects — against a silent device the first call never settles and the loop never terminates.Suggested fix
Either of these would resolve it:
deviceQuery()an optionaltimeoutMillislikegetSelfInfo()has, and pass a default fromonConnected().connectedregardless of the handshake outcome, and let consumers detect an unresponsive device through their own calls, which they can already bound withgetSelfInfo(timeoutMillis).The first seems closer to the existing intent, since
onConnected()already treats the handshake as best effort by swallowing rejections.As a consumer I worked around this with an external watchdog that disconnects and reports an error if the device has not identified itself within 15 seconds, but the library emitting something would be better, since right now there is no signal to react to at all.
Environment
@liamcottle/meshcore.js1.15.0, same code onmasterHeltec_v3_companion_radio_ble, connected over USB