-
Notifications
You must be signed in to change notification settings - Fork 373
Replacing ublox.js with updated code in assist-now.js; #2551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…ty and (coming soon) Configurator functionality.
Branch Targeting SuggestionYou've targeted the
If This is an automated suggestion to help route contributions to the appropriate branch. |
PR Compliance Guide 🔍All compliance sections have been disabled in the configurations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High-level Suggestion
Replace the inefficient setInterval polling in the UBXReceiver class with a more robust, event-driven approach for handling asynchronous responses. This can be achieved using an event emitter or by resolving pending promises directly from the _onData method. [High-level, importance: 8]
Solution Walkthrough:
Before:
class UBXReceiver {
async sendAndWait(msg, prefix, timeoutMs) {
this.expectedPrefix = prefix;
this.messageQueue = [];
await this.send(msg);
return new Promise((resolve) => {
const interval = setInterval(() => {
if (this.messageQueue.length > 0) {
clearInterval(interval);
resolve(this.messageQueue.shift());
}
// ... timeout logic
}, 50);
});
}
}After:
class UBXReceiver {
constructor() {
// ...
this.pendingRequests = new Map(); // or a single pending promise
}
_onData(chunk) {
// ... parse message `msg`
// Find a matching pending request and resolve it
const resolver = this.findAndRemoveMatchingResolver(msg);
if (resolver) {
resolver(msg);
}
}
async sendAndWait(msg, prefix, timeoutMs) {
return new Promise((resolve, reject) => {
// Store resolver to be called from _onData
this.addPendingResolver(prefix, resolve);
this.send(msg);
// ... timeout logic that calls reject
});
}
}| if (import.meta.url === `file://${process.argv[1]}`) { | ||
| const args = parseArguments(); | ||
|
|
||
| console.log(`assistnow.js ${VERSION} – u-blox AssistNow using (ZTP)`); | ||
| console.log(`Port: ${args.port} @ ${args.baudRate} baud`); | ||
| console.log( | ||
| `Mode: ${args.useAssist ? (args.live ? "LIVE" : "PREDICTIVE") : "NO ASSIST"}`, | ||
| ); | ||
|
|
||
| assistNow(args).catch((err) => { | ||
| console.error("Fatal:", err); | ||
| process.exit(1); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Replace the brittle string comparison for entry point detection with a more robust method using path.resolve and url.fileURLToPath to handle cross-platform path differences. [general, importance: 6]
| if (import.meta.url === `file://${process.argv[1]}`) { | |
| const args = parseArguments(); | |
| console.log(`assistnow.js ${VERSION} – u-blox AssistNow using (ZTP)`); | |
| console.log(`Port: ${args.port} @ ${args.baudRate} baud`); | |
| console.log( | |
| `Mode: ${args.useAssist ? (args.live ? "LIVE" : "PREDICTIVE") : "NO ASSIST"}`, | |
| ); | |
| assistNow(args).catch((err) => { | |
| console.error("Fatal:", err); | |
| process.exit(1); | |
| }); | |
| } | |
| import { fileURLToPath } from 'node:url'; | |
| import { resolve } from 'node:path'; | |
| // ... (rest of the file) | |
| if (resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { | |
| const args = parseArguments(); | |
| console.log(`assistnow.js ${VERSION} – u-blox AssistNow using (ZTP)`); | |
| console.log(`Port: ${args.port} @ ${args.baudRate} baud`); | |
| console.log( | |
| `Mode: ${args.useAssist ? (args.live ? "LIVE" : "PREDICTIVE") : "NO ASSIST"}`, | |
| ); | |
| assistNow(args).catch((err) => { | |
| console.error("Fatal:", err); | |
| process.exit(1); | |
| }); | |
| } |
| if (!values.port || !values.ztpToken) { | ||
| console.error("Missing required arguments: -P <port> -z <ZTP token>"); | ||
| console.error("Run with --help for usage"); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Modify the argument validation to only require the ztpToken when AssistNow is active, allowing the script to run in --noAssist mode without a token. [possible issue, importance: 7]
| if (!values.port || !values.ztpToken) { | |
| console.error("Missing required arguments: -P <port> -z <ZTP token>"); | |
| console.error("Run with --help for usage"); | |
| process.exit(1); | |
| } | |
| if (!values.port || (!values.noAssist && !values.ztpToken)) { | |
| console.error( | |
| "Missing required arguments: -P <port>" + | |
| (!values.noAssist ? " -z <ZTP token>" : "") | |
| ); | |
| console.error("Run with --help for usage"); | |
| process.exit(1); | |
| } |
| // Mutually exclusive assist mode | ||
| let useAssist = true; | ||
| if (values.noAssist) useAssist = false; | ||
| else if (values.predictive) | ||
| useAssist = true; // default anyway | ||
| else if (values.live) useAssist = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Add a check to ensure that the mutually exclusive --predictive and --live arguments are not used at the same time, exiting with an error if they are. [general, importance: 7]
| // Mutually exclusive assist mode | |
| let useAssist = true; | |
| if (values.noAssist) useAssist = false; | |
| else if (values.predictive) | |
| useAssist = true; // default anyway | |
| else if (values.live) useAssist = true; | |
| if (values.predictive && values.live) { | |
| console.error("Options --predictive and --live are mutually exclusive"); | |
| process.exit(1); | |
| } | |
| // Mutually exclusive assist mode | |
| let useAssist = true; | |
| if (values.noAssist) useAssist = false; | |
| else if (values.predictive) useAssist = true; | |
| else if (values.live) useAssist = true; |
| const ack = await new Promise((r) => { | ||
| const iv = setInterval(() => { | ||
| if (this.messageQueue.length > 0) { | ||
| clearInterval(iv); | ||
| r(this.messageQueue.shift()); | ||
| } | ||
| }, 50); | ||
| setTimeout(() => { | ||
| clearInterval(iv); | ||
| r(null); | ||
| }, SERIAL_TIMEOUT_MS); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Add a done flag and keep the timeout handle so you can clearTimeout() on success, ensuring the promise resolves once and timers are always cleaned up. [Learned best practice, importance: 6]
| const ack = await new Promise((r) => { | |
| const iv = setInterval(() => { | |
| if (this.messageQueue.length > 0) { | |
| clearInterval(iv); | |
| r(this.messageQueue.shift()); | |
| } | |
| }, 50); | |
| setTimeout(() => { | |
| clearInterval(iv); | |
| r(null); | |
| }, SERIAL_TIMEOUT_MS); | |
| }); | |
| const ack = await new Promise((resolve) => { | |
| let done = false; | |
| const finalize = (value) => { | |
| if (done) return; | |
| done = true; | |
| clearInterval(iv); | |
| clearTimeout(to); | |
| resolve(value); | |
| }; | |
| const iv = setInterval(() => { | |
| if (this.messageQueue.length > 0) { | |
| finalize(this.messageQueue.shift()); | |
| } | |
| }, 50); | |
| const to = setTimeout(() => { | |
| finalize(null); | |
| }, SERIAL_TIMEOUT_MS); | |
| }); |
| } catch (err) { | ||
| console.error("Error:", err.message); | ||
| } finally { | ||
| gnss.close(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Re-throw (or return a rejected promise) after logging so consumers importing this module can detect and handle failures instead of receiving a “successful” resolution. [Learned best practice, importance: 5]
| } catch (err) { | |
| console.error("Error:", err.message); | |
| } finally { | |
| gnss.close(); | |
| } | |
| } catch (err) { | |
| console.error("Error:", err); | |
| throw err; | |
| } finally { | |
| gnss.close(); | |
| } |
|
Quick note: The older ublox.js code is still there and active. I have only added the new file assist-now.js. The new file can run standalone in Node (see instructions at top of code). I wanted to get the code pushed before I start the arduous task of integrating it with the Configurator UI which I plan on starting on in the coming days. Thanks, and holler if you have questions or concerns. -Doug |
|
Inadvertently closed. Reopening.... |
PR Compliance Guide 🔍All compliance sections have been disabled in the configurations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High-level Suggestion
Refactor assist-now.js to use the application's existing serial connection management instead of creating its own. This avoids hardware access conflicts and allows for proper integration into the Configurator. [High-level, importance: 9]
Solution Walkthrough:
Before:
// assist-now.js
import { SerialPort } from "serialport";
class UBXReceiver {
constructor(portPath, baudRate) {
// Creates a new, independent serial port connection.
this.port = new SerialPort({ path: portPath, baudRate, autoOpen: true });
this.port.on("data", (chunk) => this._onData(chunk));
// ...
}
// ...
}
async function runAssistNow(args) {
// Instantiates the receiver with a port path string.
const gnss = new UBXReceiver(args.port, args.baudRate);
// ... uses gnss to communicate
}After:
// assist-now.js
class UBXReceiver {
constructor(connection) { // Expects an existing connection object
// Reuses the application's main serial connection.
this.port = connection;
this.port.on("data", (chunk) => this._onData(chunk));
// ...
}
// ...
}
async function runAssistNow(args, connection) { // Pass connection from the main app
// Instantiates the receiver with the existing connection.
const gnss = new UBXReceiver(connection);
// ... uses gnss to communicate
}| async sendAndWait(msg, prefix, timeoutMs = SERIAL_TIMEOUT_MS) { | ||
| this.expectedPrefix = prefix; | ||
| this.messageQueue = []; // clear old messages | ||
|
|
||
| await this.send(msg); | ||
|
|
||
| return new Promise((resolve) => { | ||
| const start = Date.now(); | ||
| const interval = setInterval(() => { | ||
| if (this.messageQueue.length > 0) { | ||
| clearInterval(interval); | ||
| resolve(this.messageQueue.shift()); | ||
| } | ||
| if (Date.now() - start > timeoutMs) { | ||
| clearInterval(interval); | ||
| console.warn("Timeout waiting for response"); | ||
| resolve(null); | ||
| } | ||
| }, 50); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: To prevent a race condition in sendAndWait, replace the shared message queue with a temporary event listener for each request to ensure concurrent calls do not interfere with each other. [possible issue, importance: 9]
| async sendAndWait(msg, prefix, timeoutMs = SERIAL_TIMEOUT_MS) { | |
| this.expectedPrefix = prefix; | |
| this.messageQueue = []; // clear old messages | |
| await this.send(msg); | |
| return new Promise((resolve) => { | |
| const start = Date.now(); | |
| const interval = setInterval(() => { | |
| if (this.messageQueue.length > 0) { | |
| clearInterval(interval); | |
| resolve(this.messageQueue.shift()); | |
| } | |
| if (Date.now() - start > timeoutMs) { | |
| clearInterval(interval); | |
| console.warn("Timeout waiting for response"); | |
| resolve(null); | |
| } | |
| }, 50); | |
| }); | |
| } | |
| async sendAndWait(msg, prefix, timeoutMs = SERIAL_TIMEOUT_MS) { | |
| await this.send(msg); | |
| return new Promise((resolve, reject) => { | |
| const onMessage = (response) => { | |
| if (response.subarray(0, prefix.length).equals(prefix)) { | |
| cleanup(); | |
| resolve(response); | |
| } | |
| }; | |
| const onTimeout = () => { | |
| cleanup(); | |
| console.warn("Timeout waiting for response"); | |
| resolve(null); | |
| }; | |
| const cleanup = () => { | |
| this.port.removeListener("ubx-message", onMessage); | |
| clearTimeout(timeoutId); | |
| }; | |
| const timeoutId = setTimeout(onTimeout, timeoutMs); | |
| this.port.on("ubx-message", onMessage); | |
| }); | |
| } |
| let start = this.buffer.indexOf(0xb5); | ||
| if (start === -1) { | ||
| this.buffer = Buffer.alloc(0); | ||
| return; | ||
| } | ||
| if (start > 0) this.buffer = this.buffer.subarray(start); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Improve message synchronization by searching for the full two-byte UBX preamble (0xb5, 0x62) instead of just the first byte (0xb5) to prevent false positives. [general, importance: 6]
| let start = this.buffer.indexOf(0xb5); | |
| if (start === -1) { | |
| this.buffer = Buffer.alloc(0); | |
| return; | |
| } | |
| if (start > 0) this.buffer = this.buffer.subarray(start); | |
| const preamble = Buffer.from([0xb5, 0x62]); | |
| let start = this.buffer.indexOf(preamble); | |
| if (start === -1) { | |
| this.buffer = Buffer.alloc(0); | |
| return; | |
| } | |
| if (start > 0) this.buffer = this.buffer.subarray(start); |
| return new Promise((resolve) => { | ||
| const start = Date.now(); | ||
| const interval = setInterval(() => { | ||
| if (this.messageQueue.length > 0) { | ||
| clearInterval(interval); | ||
| resolve(this.messageQueue.shift()); | ||
| } | ||
| if (Date.now() - start > timeoutMs) { | ||
| clearInterval(interval); | ||
| console.warn("Timeout waiting for response"); | ||
| resolve(null); | ||
| } | ||
| }, 50); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Modify the promise in sendAndWait to reject with an error on timeout instead of resolving with null, enabling standard error handling for callers. [possible issue, importance: 7]
| return new Promise((resolve) => { | |
| const start = Date.now(); | |
| const interval = setInterval(() => { | |
| if (this.messageQueue.length > 0) { | |
| clearInterval(interval); | |
| resolve(this.messageQueue.shift()); | |
| } | |
| if (Date.now() - start > timeoutMs) { | |
| clearInterval(interval); | |
| console.warn("Timeout waiting for response"); | |
| resolve(null); | |
| } | |
| }, 50); | |
| }); | |
| return new Promise((resolve, reject) => { | |
| const start = Date.now(); | |
| const interval = setInterval(() => { | |
| if (this.messageQueue.length > 0) { | |
| clearInterval(interval); | |
| resolve(this.messageQueue.shift()); | |
| } | |
| if (Date.now() - start > timeoutMs) { | |
| clearInterval(interval); | |
| reject(new Error("Timeout waiting for response")); | |
| } | |
| }, 50); | |
| }); |
| if (!values.port || !values.ztpToken) { | ||
| console.error("Missing required arguments: -P <port> -z <ZTP token>"); | ||
| console.error("Run with --help for usage"); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Only require -z/ztpToken when AssistNow is enabled; allow running with -n/--noAssist without a token. [Learned best practice, importance: 6]
| if (!values.port || !values.ztpToken) { | |
| console.error("Missing required arguments: -P <port> -z <ZTP token>"); | |
| console.error("Run with --help for usage"); | |
| process.exit(1); | |
| } | |
| if (!values.port) { | |
| console.error("Missing required arguments: -P <port>"); | |
| console.error("Run with --help for usage"); | |
| process.exit(1); | |
| } | |
| if (!values.noAssist && !values.ztpToken) { | |
| console.error("Missing required arguments: -z <ZTP token> (unless using -n/--noAssist)"); | |
| console.error("Run with --help for usage"); | |
| process.exit(1); | |
| } |
User description
Replacing ublox.js with updated code in assist-now.js; Provides standalone functionality and (coming soon) Configurator functionality.
PR Type
Enhancement, New Feature
Description
Port of u-blox AssistNow Python example to Node.js with standalone and Configurator support
Implements Zero Touch Provisioning (ZTP) for secure orbit data download and injection
Provides UBX message handling with serial communication and ACK/NAK validation
Supports both predictive and live orbit modes with GNSS receiver cold-reset and TTFF measurement
Diagram Walkthrough
File Walkthrough
assist-now.js
Complete Node.js port of u-blox AssistNow with ZTPjs/ublox/assist-now.js
AssistNow functionality
verification, and ACK waiting
data download
rate, ZTP token, and orbit mode selection
after cold-reset