Skip to content

Conversation

@DHaacke
Copy link

@DHaacke DHaacke commented Feb 3, 2026

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

flowchart LR
  A["Serial Port"] -->|UBX Messages| B["UBXReceiver Class"]
  B -->|Poll Commands| C["GNSS Device"]
  C -->|Responses| B
  B -->|UNIQID + MON-VER| D["ZTP Endpoint"]
  D -->|Chipcode| E["AssistNow Service"]
  E -->|MGA Data| B
  B -->|MGA Messages| C
  C -->|MGA-ACKs| B
  B -->|Cold Reset| C
  C -->|NAV-STATUS| B
  B -->|TTFF Result| F["Console Output"]
Loading

File Walkthrough

Relevant files
New feature
assist-now.js
Complete Node.js port of u-blox AssistNow with ZTP             

js/ublox/assist-now.js

  • New 566-line Node.js implementation replacing ublox.js with full
    AssistNow functionality
  • UBXReceiver class handles serial I/O, message validation, checksum
    verification, and ACK waiting
  • Implements ZTP authentication flow to obtain chipcode for AssistNow
    data download
  • Supports standalone CLI mode with argument parsing for port, baud
    rate, ZTP token, and orbit mode selection
  • Includes MGA message injection with retry logic and TTFF measurement
    after cold-reset
+566/-0 

…ty and (coming soon) Configurator functionality.
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

Branch Targeting Suggestion

You've targeted the master branch with this PR. Please consider if a version branch might be more appropriate:

  • maintenance-9.x - If your change is backward-compatible and won't create compatibility issues between INAV firmware and Configurator 9.x versions. This will allow your PR to be included in the next 9.x release.

  • maintenance-10.x - If your change introduces compatibility requirements between firmware and configurator that would break 9.x compatibility. This is for PRs which will be included in INAV 10.x

If master is the correct target for this change, no action is needed.


This is an automated suggestion to help route contributions to the appropriate branch.

@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

Copy link
Contributor

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
    });
  }
}

Comment on lines +542 to +555
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);
});
}
Copy link
Contributor

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]

Suggested change
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);
});
}

Comment on lines +509 to +513
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);
}
Copy link
Contributor

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]

Suggested change
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);
}

Comment on lines +515 to +520
// 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;
Copy link
Contributor

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]

Suggested change
// 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;

Comment on lines +276 to +287
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);
});
Copy link
Contributor

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]

Suggested change
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);
});

Comment on lines +476 to +480
} catch (err) {
console.error("Error:", err.message);
} finally {
gnss.close();
}
Copy link
Contributor

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]

Suggested change
} catch (err) {
console.error("Error:", err.message);
} finally {
gnss.close();
}
} catch (err) {
console.error("Error:", err);
throw err;
} finally {
gnss.close();
}

@DHaacke
Copy link
Author

DHaacke commented Feb 3, 2026

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

@DHaacke DHaacke closed this Feb 3, 2026
@DHaacke
Copy link
Author

DHaacke commented Feb 3, 2026

Inadvertently closed. Reopening....

@DHaacke DHaacke reopened this Feb 3, 2026
@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

Copy link
Contributor

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
}

Comment on lines +214 to +234
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);
});
}
Copy link
Contributor

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]

Suggested change
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);
});
}

Comment on lines +172 to +177
let start = this.buffer.indexOf(0xb5);
if (start === -1) {
this.buffer = Buffer.alloc(0);
return;
}
if (start > 0) this.buffer = this.buffer.subarray(start);
Copy link
Contributor

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]

Suggested change
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);

Comment on lines +220 to +233
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);
});
Copy link
Contributor

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]

Suggested change
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);
});

Comment on lines +509 to +513
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);
}
Copy link
Contributor

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]

Suggested change
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);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant