Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
build
.git
.github
16 changes: 16 additions & 0 deletions .github/workflows/docker-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Docker Linux test

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build and test in Docker
run: docker build -t nuclearnet.js-test .
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM node:20-bookworm

RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

RUN npm run build
CMD ["npm", "test"]
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

Node.js module for interacting with the [NUClear](https://github.com/Fastcode/NUClear) network.

## NUClearNet 2 (v2.0.0+)

Version 2 uses the redesigned **NUClearNet** library from [NUClear PR #190](https://github.com/Fastcode/NUClear/pull/190) (wire protocol **0x03**). It is **not** compatible with 1.x clients or NUClear builds that still use the old `NUClearNetwork` stack (protocol 0x02). Upgrade Node clients and NUClear robots together.

The vendored NUClear tree is updated via `git subtree` from the `houliston/nuclearnet-v2` branch (currently [NUClear@2441bdf](https://github.com/Fastcode/NUClear/commit/2441bdf7)).

Peer join events may arrive slightly later than in 1.x because connection requires both multicast announce and a unicast CONNECT handshake.

## Installation

The package contains a native module, so you'll need a working C++ compiler on your system to install and build it.
Expand Down Expand Up @@ -57,6 +65,25 @@ net.on('packet_type_a', function (packet) {
net.connect({ name: 'My Name' });
```

## Debugging

Logging is off by default. Enable tiered logs with `connect({ debug: ... })`, the constructor default, or the `NUCLEARNET_DEBUG` environment variable (`connect` wins when both are set).

| Level | JavaScript | Native (stderr) |
| ----- | ---------- | ----------------- |
| `info` | connect, join, leave, subscriptions | reset, shutdown, peer timeouts |
| `debug` | send, packets, listener subscribe/unsubscribe | handshake, announce/connect, send routing |
| `trace` | process wait scheduling | `process()` ticks, socket reads |

```js
const net = new NUClearNet({ debug: 'info' });
net.connect({ name: 'node-1', debug: 'debug' }); // overrides constructor for this session
```

```bash
NUCLEARNET_DEBUG=info node your-app.js
```

## API

See [`index.d.ts`](./index.d.ts) for types and API details.
22 changes: 16 additions & 6 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
'src/binding.cpp',
'src/NetworkBinding.cpp',
'src/NetworkListener.cpp',
'src/nuclear/src/extension/network/NUClearNetwork.cpp',
'src/nuclear/src/util/platform.cpp',
'src/nuclear/src/util/network/get_interfaces.cpp',
'src/nuclear/src/util/network/if_number_from_address.cpp',
'src/nuclear/src/nuclearnet/Discovery.cpp',
'src/nuclear/src/nuclearnet/Log.cpp',
'src/nuclear/src/nuclearnet/Fragmentation.cpp',
'src/nuclear/src/nuclearnet/NUClearNet.cpp',
'src/nuclear/src/nuclearnet/PacketDeduplicator.cpp',
'src/nuclear/src/nuclearnet/RTTEstimator.cpp',
'src/nuclear/src/nuclearnet/Reliability.cpp',
'src/nuclear/src/nuclearnet/Routing.cpp',
'src/nuclear/src/util/network/resolve.cpp',
'src/nuclear/src/util/platform.cpp',
'src/nuclear/src/util/serialise/xxhash.cpp'
],
'cflags': [],
'include_dirs': [
'<!@(node -p "require(\'node-addon-api\').include")',
'src/nuclear/src/include'
'src/nuclear/src'
],
"defines": [
# Restrict NAPI to v6 (to support Node v10)
Expand Down Expand Up @@ -82,7 +87,12 @@
],
[
'OS=="win"', {
'defines': [ '_HAS_EXCEPTIONS=1' ]
'defines': [ '_HAS_EXCEPTIONS=1' ],
'libraries': [
'ws2_32.lib',
'mswsock.lib',
'iphlpapi.lib'
]
}
]
]
Expand Down
14 changes: 12 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export interface NUClearNetOptions {

/** The MTU of the network. Used for splitting packets optimally. */
mtu?: number;

/**
* Enable debug logging. `true` is equivalent to `info`.
* Native logs go to stderr; JavaScript logs use `console.error` with a `[NUClearNet.js]` prefix.
* The `NUCLEARNET_DEBUG` environment variable applies when this option is omitted.
*/
debug?: boolean | 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
}

/**
Expand Down Expand Up @@ -131,8 +138,11 @@ export declare class NUClearNet {
/** Stores the `connect()` options. Is an empty object until `connect()` is called. */
options: Partial<NUClearNetOptions>;

/** Create a new NUClearNet instance. */
public constructor();
/**
* Create a new NUClearNet instance.
* @param options Optional default `debug` level (overridden by `connect({ debug })`).
*/
public constructor(options?: { debug?: NUClearNetOptions['debug'] });

/** Emitted when a peer joins or leaves the network. */
public on(event: 'nuclear_join' | 'nuclear_leave', callback: (peer: NUClearNetPeer) => void): this;
Expand Down
123 changes: 101 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@

const { NetworkBinding } = require('bindings')('nuclearnet');
const { EventEmitter } = require('events');
const { LEVELS, parseLogLevel, levelName, shouldLog } = require('./lib/log');

class NUClearNet extends EventEmitter {
constructor() {
/**
* @param {{ debug?: boolean | string }} [options]
*/
constructor(options = {}) {
super();

// Create a new network object
Expand All @@ -30,6 +34,7 @@ class NUClearNet extends EventEmitter {
this._active = false;
this._waiting = 0;
this._destroyed = false;
this._constructorDebug = options.debug;

// Stores the connect() options
this.options = {};
Expand All @@ -38,35 +43,25 @@ class NUClearNet extends EventEmitter {
this.on('newListener', (event) => {
this.assertNotDestroyed();

if (
event !== 'nuclear_join' &&
event !== 'nuclear_leave' &&
event !== 'nuclear_packet' &&
event !== 'newListener' &&
event !== 'removeListener' &&
event !== 'disconnect' &&
this.listenerCount(event) === 0
) {
if (this._isTypedPacketEvent(event) && this.listenerCount(event) === 0) {
const hash = this._net.hash(event);
this._callbackMap[hash] = event;
this._log(LEVELS.debug, 'subscribe listener', { type: event });
if (this._active) {
this._net.addSubscription(hash);
}
}
});

// We are no longer listening to this type
this.on('removeListener', (event) => {
// If we are no longer listening to this type
if (
event !== 'nuclear_join' &&
event !== 'nuclear_leave' &&
event !== 'nuclear_packet' &&
event !== 'newListener' &&
event !== 'removeListener' &&
event !== 'disconnect' &&
this.listenerCount(event) === 0
) {
// Get our hash and delete it
if (this._isTypedPacketEvent(event) && this.listenerCount(event) === 0) {
const hash = this._net.hash(event);
delete this._callbackMap[hash];
this._log(LEVELS.debug, 'unsubscribe listener', { type: event });
if (this._active) {
this._syncSubscriptions();
}
}
});

Expand All @@ -75,11 +70,52 @@ class NUClearNet extends EventEmitter {
this._net.onJoin(this._onJoin.bind(this));
this._net.onLeave(this._onLeave.bind(this));
this._net.onWait(this._onWait.bind(this));

this._applyLogLevel(parseLogLevel(this._constructorDebug, process.env.NUCLEARNET_DEBUG));
}

_applyLogLevel(level) {
this._logLevel = level;
this._net.setLogLevel(level);
}

/**
* @param {number} level
* @param {string} message
* @param {object} [fields]
*/
_log(level, message, fields) {
if (!shouldLog(this._logLevel, level)) {
return;
}
if (fields !== undefined) {
console.error('[NUClearNet.js]', levelName(level), message, fields);
} else {
console.error('[NUClearNet.js]', levelName(level), message);
}
}

/**
* @param {Buffer} hash
* @returns {string}
*/
_hashHex(hash) {
return '0x' + hash.toString('hex');
}

_onPacket(name, address, port, reliable, hash, payload) {
const eventName = this._callbackMap[hash];

this._log(LEVELS.debug, 'packet', {
peer: name,
address: address,
port: port,
type: eventName,
hash: this._hashHex(hash),
len: payload.length,
reliable: reliable,
});

// Construct our packet
const packet = {
peer: {
Expand All @@ -103,6 +139,7 @@ class NUClearNet extends EventEmitter {
}

_onJoin(name, address, port) {
this._log(LEVELS.info, 'join', { name: name, address: address, port: port });
this.emit('nuclear_join', {
name: name,
address: address,
Expand All @@ -111,6 +148,7 @@ class NUClearNet extends EventEmitter {
}

_onLeave(name, address, port) {
this._log(LEVELS.info, 'leave', { name: name, address: address, port: port });
this.emit('nuclear_leave', {
name: name,
address: address,
Expand All @@ -119,6 +157,7 @@ class NUClearNet extends EventEmitter {
}

_onWait(duration) {
this._log(LEVELS.trace, 'wait', { ms: duration });
++this._waiting;

setTimeout(() => {
Expand All @@ -128,11 +167,14 @@ class NUClearNet extends EventEmitter {
if (this._active) {
try {
this._net.process();
} catch {
} catch (err) {
// An error occurred during processing, disconnect.
// This needs to check again if this is still active, as multiple
// `_onWait` calls run concurrently, and only the first one to fail
// should disconnect.
this._log(LEVELS.error, 'process failed, disconnecting', {
error: err instanceof Error ? err.message : String(err),
});
if (this._active) {
this.disconnect();
}
Expand All @@ -147,6 +189,24 @@ class NUClearNet extends EventEmitter {
}, duration);
}

_isTypedPacketEvent(event) {
return (
event !== 'nuclear_join' &&
event !== 'nuclear_leave' &&
event !== 'nuclear_packet' &&
event !== 'newListener' &&
event !== 'removeListener' &&
event !== 'disconnect'
);
}

_syncSubscriptions() {
const eventNames = Object.values(this._callbackMap);
const hashes = eventNames.map((eventName) => this._net.hash(eventName));
this._log(LEVELS.info, 'sync subscriptions', { count: eventNames.length, types: eventNames });
this._net.setSubscriptions(hashes);
}

hash(data) {
this.assertNotDestroyed();
return this._net.hash(data);
Expand All @@ -158,12 +218,21 @@ class NUClearNet extends EventEmitter {
// Store the options
this.options = options;

const debugOption = options.debug !== undefined ? options.debug : this._constructorDebug;
this._applyLogLevel(parseLogLevel(debugOption, process.env.NUCLEARNET_DEBUG));

// Default some of the options
const name = options.name;
const address = options.address === undefined ? '239.226.152.162' : options.address;
const port = options.port === undefined ? 7447 : options.port;
const mtu = options.mtu === undefined ? 1500 : options.mtu;

this._log(LEVELS.info, 'connect', { name: name, address: address, port: port, mtu: mtu });

if (Object.keys(this._callbackMap).length > 0) {
this._syncSubscriptions();
}

// Connect to the network
this._net.reset(name, address, port, mtu);

Expand All @@ -177,6 +246,7 @@ class NUClearNet extends EventEmitter {
disconnect() {
this.assertNotDestroyed();

this._log(LEVELS.info, 'disconnect');
this._active = false;
this._net.shutdown();

Expand All @@ -189,6 +259,14 @@ class NUClearNet extends EventEmitter {
if (!this._active) {
throw new Error('The network is not currently connected');
} else {
const typeLabel =
typeof options.type === 'string' ? options.type : this._hashHex(options.type);
this._log(LEVELS.debug, 'send', {
type: typeLabel,
target: options.target,
reliable: options.reliable !== undefined ? options.reliable : false,
len: options.payload.length,
});
this._net.send(
options.type,
options.payload,
Expand All @@ -199,6 +277,7 @@ class NUClearNet extends EventEmitter {
}

destroy() {
this._log(LEVELS.info, 'destroy');
if (this._active) {
this.disconnect();
}
Expand Down
Loading
Loading