rclnodejs 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +25 -2
  2. package/binding.gyp +2 -0
  3. package/index.js +21 -4
  4. package/lib/action/client.js +1 -1
  5. package/lib/action/graph.js +1 -1
  6. package/lib/action/server.js +1 -1
  7. package/lib/action/server_goal_handle.js +1 -1
  8. package/lib/client.js +1 -1
  9. package/lib/clock.js +1 -1
  10. package/lib/context.js +1 -1
  11. package/lib/duration.js +1 -1
  12. package/lib/event_handler.js +1 -1
  13. package/lib/guard_condition.js +1 -1
  14. package/lib/lifecycle.js +1 -1
  15. package/lib/lifecycle_publisher.js +1 -1
  16. package/lib/logging.js +1 -1
  17. package/lib/message_serialization.js +171 -0
  18. package/lib/native_loader.js +173 -0
  19. package/lib/node.js +16 -1
  20. package/lib/parameter.js +4 -10
  21. package/lib/publisher.js +1 -1
  22. package/lib/serialization.js +1 -1
  23. package/lib/service.js +1 -1
  24. package/lib/subscription.js +17 -2
  25. package/lib/time.js +1 -1
  26. package/lib/time_source.js +1 -1
  27. package/lib/timer.js +1 -1
  28. package/lib/type_description_service.js +1 -1
  29. package/lib/utils.js +324 -0
  30. package/lib/validator.js +1 -1
  31. package/package.json +14 -18
  32. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  33. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  34. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  35. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  36. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  37. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  38. package/rosidl_convertor/idl_convertor.js +3 -2
  39. package/rosidl_gen/deallocator.js +1 -1
  40. package/rosidl_gen/generate_worker.js +1 -1
  41. package/rosidl_gen/generator.json +1 -1
  42. package/rosidl_gen/idl_generator.js +11 -24
  43. package/rosidl_gen/index.js +1 -1
  44. package/rosidl_gen/primitive_types.js +2 -2
  45. package/rosidl_gen/templates/action-template.js +68 -0
  46. package/rosidl_gen/templates/message-template.js +1113 -0
  47. package/rosidl_gen/templates/service-event-template.js +31 -0
  48. package/rosidl_gen/templates/service-template.js +44 -0
  49. package/rosidl_parser/rosidl_parser.js +2 -2
  50. package/scripts/install.js +113 -0
  51. package/scripts/tag_prebuilds.js +70 -0
  52. package/src/addon.cpp +3 -0
  53. package/third_party/ref-napi/index.js +15 -0
  54. package/third_party/ref-napi/lib/ref.js +1696 -0
  55. package/third_party/ref-napi/src/ref_napi_bindings.cpp +736 -0
  56. package/third_party/ref-napi/src/ref_napi_bindings.h +26 -0
  57. package/types/index.d.ts +17 -0
  58. package/types/node.d.ts +16 -1
  59. package/rosidl_gen/templates/CMakeLists.dot +0 -40
  60. package/rosidl_gen/templates/action.dot +0 -50
  61. package/rosidl_gen/templates/message.dot +0 -851
  62. package/rosidl_gen/templates/package.dot +0 -16
  63. package/rosidl_gen/templates/service.dot +0 -26
  64. package/rosidl_gen/templates/service_event.dot +0 -10
package/README.md CHANGED
@@ -31,6 +31,26 @@ npm i rclnodejs
31
31
 
32
32
  - **Note:** to install rclnodejs from GitHub: add `"rclnodejs":"RobotWebTools/rclnodejs#<branch>"` to your `package.json` dependency section.
33
33
 
34
+ ### Prebuilt Binaries
35
+
36
+ rclnodejs ships with prebuilt native binaries for common Linux configurations since `v1.5.2`, eliminating the need for compilation during installation. This significantly speeds up installation and reduces dependencies.
37
+
38
+ **Supported Platforms:**
39
+
40
+ - **Ubuntu 22.04 (Jammy)** - ROS 2 Humble
41
+ - **Ubuntu 24.04 (Noble)** - ROS 2 Jazzy, Kilted
42
+ - **Architectures:** x64, arm64
43
+ - **Node.js:** >= 16.20.2 (N-API compatible)
44
+
45
+ **Force Building from Source:**
46
+
47
+ If you need to build from source even when a prebuilt binary is available, set the environment variable:
48
+
49
+ ```bash
50
+ export RCLNODEJS_FORCE_BUILD=1
51
+ npm install rclnodejs
52
+ ```
53
+
34
54
  ## Documentation
35
55
 
36
56
  API [documentation](https://robotwebtools.github.io/rclnodejs/docs/index.html) is available online.
@@ -73,9 +93,12 @@ See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/t
73
93
 
74
94
  Create rich, interactive desktop applications using Electron and web technologies like Three.js. Build 3D visualizations, monitoring dashboards, and control interfaces that run on Windows, macOS, and Linux.
75
95
 
76
- Try the `electron_demo/turtle_tf2` demo for real-time coordinate frame visualization with dynamic transforms and keyboard-controlled turtle movement. More examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
96
+ | Demo | Description | Screenshot |
97
+ | :-----------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------: |
98
+ | **🐢 [turtle_tf2](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo/turtle_tf2)** | Real-time coordinate frame visualization with turtle control. Features TF2 transforms, keyboard control, and dynamic frame updates. | ![turtle_tf2](https://github.com/RobotWebTools/rclnodejs/blob/develop/electron_demo/turtle_tf2/turtle-tf2-demo.png?raw=true) |
99
+ | **🦾 [manipulator](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo/manipulator)** | Interactive two-joint robotic arm simulation. Features 3D joint visualization, manual/automatic control, and visual movement markers. | ![manipulator](https://github.com/RobotWebTools/rclnodejs/blob/develop/electron_demo/manipulator/manipulator-demo.png?raw=true) |
77
100
 
78
- ![demo screenshot](https://github.com/RobotWebTools/rclnodejs/blob/develop/electron_demo/turtle_tf2/turtle-tf2-demo.gif?raw=true)
101
+ Explore more examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
79
102
 
80
103
  ## License
81
104
 
package/binding.gyp CHANGED
@@ -42,9 +42,11 @@
42
42
  './src/rcl_timer_bindings.cpp',
43
43
  './src/rcl_utilities.cpp',
44
44
  './src/shadow_node.cpp',
45
+ './third_party/ref-napi/src/ref_napi_bindings.cpp',
45
46
  ],
46
47
  'include_dirs': [
47
48
  '.',
49
+ './third_party/ref-napi/src',
48
50
  '<(ros_include_root)',
49
51
  "<!@(node -p \"require('node-addon-api').include\")",
50
52
  ],
package/index.js CHANGED
@@ -18,7 +18,7 @@ const DistroUtils = require('./lib/distro.js');
18
18
  const RMWUtils = require('./lib/rmw.js');
19
19
  const { Clock, ROSClock } = require('./lib/clock.js');
20
20
  const ClockType = require('./lib/clock_type.js');
21
- const compareVersions = require('compare-versions');
21
+ const { compareVersions } = require('./lib/utils.js');
22
22
  const Context = require('./lib/context.js');
23
23
  const debug = require('debug')('rclnodejs');
24
24
  const Duration = require('./lib/duration.js');
@@ -37,7 +37,7 @@ const {
37
37
  } = require('./lib/parameter.js');
38
38
  const path = require('path');
39
39
  const QoS = require('./lib/qos.js');
40
- const rclnodejs = require('bindings')('rclnodejs');
40
+ const rclnodejs = require('./lib/native_loader.js');
41
41
  const tsdGenerator = require('./rostsd_gen/index.js');
42
42
  const validator = require('./lib/validator.js');
43
43
  const Time = require('./lib/time.js');
@@ -47,6 +47,7 @@ const ActionUuid = require('./lib/action/uuid.js');
47
47
  const ClientGoalHandle = require('./lib/action/client_goal_handle.js');
48
48
  const { CancelResponse, GoalResponse } = require('./lib/action/response.js');
49
49
  const ServerGoalHandle = require('./lib/action/server_goal_handle.js');
50
+ const { toJSONSafe, toJSONString } = require('./lib/message_serialization.js');
50
51
  const {
51
52
  getActionClientNamesAndTypesByNode,
52
53
  getActionServerNamesAndTypesByNode,
@@ -364,8 +365,7 @@ let rcl = {
364
365
 
365
366
  const version = await getCurrentGeneratorVersion();
366
367
  const forced =
367
- version === null ||
368
- compareVersions.compare(version, generator.version(), '<');
368
+ version === null || compareVersions(version, generator.version(), '<');
369
369
  if (forced) {
370
370
  debug(
371
371
  'The generator will begin to create JavaScript code from ROS IDL files...'
@@ -539,6 +539,23 @@ let rcl = {
539
539
  * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
540
540
  */
541
541
  ros2Launch: ros2Launch,
542
+
543
+ /**
544
+ * Convert a message object to be JSON-safe by converting TypedArrays to regular arrays
545
+ * and handling BigInt, Infinity, NaN, etc. for JSON serialization.
546
+ * @param {*} obj - The message object to convert
547
+ * @returns {*} A JSON-safe version of the object
548
+ */
549
+ toJSONSafe: toJSONSafe,
550
+
551
+ /**
552
+ * Convert a message object to a JSON string with proper handling of TypedArrays,
553
+ * BigInt, and other non-JSON-serializable values.
554
+ * @param {*} obj - The message object to convert
555
+ * @param {number} [space] - Space parameter for JSON.stringify formatting
556
+ * @returns {string} The JSON string representation
557
+ */
558
+ toJSONString: toJSONString,
542
559
  };
543
560
 
544
561
  const _sigHandler = () => {
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('../native_loader.js');
18
18
  const ActionInterfaces = require('./interfaces.js');
19
19
  const ActionUuid = require('./uuid.js');
20
20
  const ClientGoalHandle = require('./client_goal_handle.js');
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('../native_loader.js');
18
18
 
19
19
  /**
20
20
  * Get a list of action names and types for action clients associated with a node.
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('../native_loader.js');
18
18
  const ActionInterfaces = require('./interfaces.js');
19
19
  const ActionUuid = require('./uuid.js');
20
20
  const DistroUtils = require('../distro.js');
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('../native_loader.js');
18
18
  const ActionInterfaces = require('./interfaces.js');
19
19
  const Deferred = require('./deferred.js');
20
20
  const { GoalEvent } = require('./response.js');
package/lib/client.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
  const DistroUtils = require('./distro.js');
19
19
  const Entity = require('./entity.js');
20
20
  const debug = require('debug')('rclnodejs:client');
package/lib/clock.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
  const Time = require('./time.js');
19
19
  const ClockType = require('./clock_type.js');
20
20
 
package/lib/context.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
 
19
19
  let defaultContext = null;
20
20
 
package/lib/duration.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
  const S_TO_NS = 10n ** 9n;
19
19
 
20
20
  /**
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
  const DistroUtils = require('./distro.js');
19
19
  const Entity = require('./entity.js');
20
20
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  'use strict';
14
14
 
15
- const rclnodejs = require('bindings')('rclnodejs');
15
+ const rclnodejs = require('./native_loader.js');
16
16
  const Entity = require('./entity.js');
17
17
  const Context = require('./context.js');
18
18
 
package/lib/lifecycle.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
  const LifecyclePublisher = require('./lifecycle_publisher.js');
19
19
  const loader = require('./interface_loader.js');
20
20
  const Context = require('./context.js');
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
  const Logging = require('./logging.js');
19
19
  const Publisher = require('./publisher.js');
20
20
 
package/lib/logging.js CHANGED
@@ -15,7 +15,7 @@
15
15
  'use strict';
16
16
 
17
17
  const path = require('path');
18
- const rclnodejs = require('bindings')('rclnodejs');
18
+ const rclnodejs = require('./native_loader.js');
19
19
 
20
20
  /**
21
21
  * Enum for LoggingSeverity
@@ -0,0 +1,171 @@
1
+ // Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict';
16
+
17
+ /**
18
+ * Check if a value is a TypedArray
19
+ * @param {*} value - The value to check
20
+ * @returns {boolean} True if the value is a TypedArray
21
+ */
22
+ function isTypedArray(value) {
23
+ return ArrayBuffer.isView(value) && !(value instanceof DataView);
24
+ }
25
+
26
+ /**
27
+ * Check if a value needs JSON conversion (BigInt, functions, etc.)
28
+ * @param {*} value - The value to check
29
+ * @returns {boolean} True if the value needs special JSON handling
30
+ */
31
+ function needsJSONConversion(value) {
32
+ return (
33
+ typeof value === 'bigint' ||
34
+ typeof value === 'function' ||
35
+ typeof value === 'undefined' ||
36
+ value === Infinity ||
37
+ value === -Infinity ||
38
+ (typeof value === 'number' && isNaN(value))
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Convert a message to plain arrays (TypedArray -> regular Array)
44
+ * @param {*} obj - The object to convert
45
+ * @returns {*} The converted object with plain arrays
46
+ */
47
+ function toPlainArrays(obj) {
48
+ if (obj === null || obj === undefined) {
49
+ return obj;
50
+ }
51
+
52
+ if (isTypedArray(obj)) {
53
+ return Array.from(obj);
54
+ }
55
+
56
+ if (Array.isArray(obj)) {
57
+ return obj.map((item) => toPlainArrays(item));
58
+ }
59
+
60
+ if (typeof obj === 'object' && obj !== null) {
61
+ const result = {};
62
+ for (const key in obj) {
63
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
64
+ result[key] = toPlainArrays(obj[key]);
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+
70
+ return obj;
71
+ }
72
+
73
+ /**
74
+ * Convert a message to be fully JSON-safe
75
+ * @param {*} obj - The object to convert
76
+ * @returns {*} The JSON-safe converted object
77
+ */
78
+ function toJSONSafe(obj) {
79
+ if (obj === null || obj === undefined) {
80
+ return obj;
81
+ }
82
+
83
+ if (isTypedArray(obj)) {
84
+ return Array.from(obj).map((item) => toJSONSafe(item));
85
+ }
86
+
87
+ if (needsJSONConversion(obj)) {
88
+ if (typeof obj === 'bigint') {
89
+ // Convert BigInt to string with 'n' suffix to indicate it was a BigInt
90
+ return obj.toString() + 'n';
91
+ }
92
+ if (obj === Infinity) return 'Infinity';
93
+ if (obj === -Infinity) return '-Infinity';
94
+ if (typeof obj === 'number' && isNaN(obj)) return 'NaN';
95
+ if (typeof obj === 'undefined') return null;
96
+ if (typeof obj === 'function') return '[Function]';
97
+ }
98
+
99
+ if (Array.isArray(obj)) {
100
+ return obj.map((item) => toJSONSafe(item));
101
+ }
102
+
103
+ if (typeof obj === 'object' && obj !== null) {
104
+ const result = {};
105
+ for (const key in obj) {
106
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
107
+ result[key] = toJSONSafe(obj[key]);
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+
113
+ return obj;
114
+ }
115
+
116
+ /**
117
+ * Convert a message to a JSON string
118
+ * @param {*} obj - The object to convert
119
+ * @param {number} [space] - Space parameter for JSON.stringify formatting
120
+ * @returns {string} The JSON string representation
121
+ */
122
+ function toJSONString(obj, space) {
123
+ const jsonSafeObj = toJSONSafe(obj);
124
+ return JSON.stringify(jsonSafeObj, null, space);
125
+ }
126
+
127
+ /**
128
+ * Apply serialization mode conversion to a message object
129
+ * @param {*} message - The message object to convert
130
+ * @param {string} serializationMode - The serialization mode ('default', 'plain', 'json')
131
+ * @returns {*} The converted message
132
+ */
133
+ function applySerializationMode(message, serializationMode) {
134
+ switch (serializationMode) {
135
+ case 'default':
136
+ // No conversion needed - use native rclnodejs behavior
137
+ return message;
138
+
139
+ case 'plain':
140
+ // Convert TypedArrays to regular arrays
141
+ return toPlainArrays(message);
142
+
143
+ case 'json':
144
+ // Convert to fully JSON-safe format
145
+ return toJSONSafe(message);
146
+
147
+ default:
148
+ throw new TypeError(
149
+ `Invalid serializationMode: ${serializationMode}. Valid modes are: 'default', 'plain', 'json'`
150
+ );
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Validate serialization mode
156
+ * @param {string} mode - The serialization mode to validate
157
+ * @returns {boolean} True if valid
158
+ */
159
+ function isValidSerializationMode(mode) {
160
+ return ['default', 'plain', 'json'].includes(mode);
161
+ }
162
+
163
+ module.exports = {
164
+ isTypedArray,
165
+ needsJSONConversion,
166
+ toPlainArrays,
167
+ toJSONSafe,
168
+ toJSONString,
169
+ applySerializationMode,
170
+ isValidSerializationMode,
171
+ };
@@ -0,0 +1,173 @@
1
+ // Copyright (c) 2025, The Robot Web Tools Contributors
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { execSync } = require('child_process');
20
+ const bindings = require('bindings');
21
+ const debug = require('debug')('rclnodejs');
22
+ const { detectUbuntuCodename } = require('./utils');
23
+
24
+ let nativeModule = null;
25
+
26
+ // Simplified loader: only use prebuilt binaries with exact Ubuntu/ROS2/arch match
27
+ // Note: Prebuilt binaries are only supported on Linux (Ubuntu) platform
28
+ function customFallbackLoader() {
29
+ // Prebuilt binaries are only for Linux platform
30
+ if (process.platform !== 'linux') {
31
+ debug('Prebuilt binaries are only supported on Linux platform');
32
+ return null;
33
+ }
34
+
35
+ const rosDistro = process.env.ROS_DISTRO;
36
+ const arch = process.arch;
37
+ const ubuntuCodename = detectUbuntuCodename();
38
+
39
+ // Require all three components for exact match
40
+ if (!rosDistro || !ubuntuCodename || !arch) {
41
+ debug(
42
+ `Missing environment info - ROS: ${rosDistro}, Ubuntu: ${ubuntuCodename}, Arch: ${arch}`
43
+ );
44
+ return null;
45
+ }
46
+
47
+ const prebuildDir = path.join(
48
+ __dirname,
49
+ '..',
50
+ 'prebuilds',
51
+ `${process.platform}-${arch}`
52
+ );
53
+
54
+ if (!fs.existsSync(prebuildDir)) {
55
+ debug('No prebuilds directory found');
56
+ return null;
57
+ }
58
+
59
+ try {
60
+ // Look for exact match binary: {ros_distro}-{ubuntu_codename}-{arch}-rclnodejs.node
61
+ const exactMatchFilename = `${rosDistro}-${ubuntuCodename}-${arch}-rclnodejs.node`;
62
+ const exactMatchPath = path.join(prebuildDir, exactMatchFilename);
63
+
64
+ if (fs.existsSync(exactMatchPath)) {
65
+ debug(`Found exact match binary: ${exactMatchFilename}`);
66
+ return require(exactMatchPath);
67
+ }
68
+
69
+ debug(`No exact match found for: ${exactMatchFilename}`);
70
+ return null;
71
+ } catch (e) {
72
+ debug('Error in simplified prebuilt loader:', e.message);
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ // Simplified prebuilt binary loader: exact match or build from source
79
+ function loadNativeAddon() {
80
+ if (nativeModule) {
81
+ return nativeModule;
82
+ }
83
+
84
+ // Environment variable to force building from source
85
+ if (process.env.RCLNODEJS_FORCE_BUILD === '1') {
86
+ debug('Forcing build from source (RCLNODEJS_FORCE_BUILD=1)');
87
+
88
+ // Trigger actual compilation
89
+ try {
90
+ debug('Running forced node-gyp rebuild...');
91
+ execSync('npm run rebuild', {
92
+ stdio: 'inherit',
93
+ cwd: path.join(__dirname, '..'),
94
+ timeout: 300000, // 5 minute timeout
95
+ });
96
+
97
+ // Load the newly built binary
98
+ nativeModule = bindings('rclnodejs');
99
+ debug('Successfully force compiled and loaded from source');
100
+ return nativeModule;
101
+ } catch (compileError) {
102
+ debug('Forced compilation failed:', compileError.message);
103
+ throw new Error(
104
+ `Failed to force build rclnodejs from source: ${compileError.message}`
105
+ );
106
+ }
107
+ }
108
+
109
+ const rosDistro = process.env.ROS_DISTRO;
110
+ const ubuntuCodename = detectUbuntuCodename();
111
+
112
+ debug(
113
+ `Platform: ${process.platform}, Arch: ${process.arch}, Ubuntu: ${ubuntuCodename || 'unknown'}, ROS: ${rosDistro || 'unknown'}`
114
+ );
115
+
116
+ // Prebuilt binaries are only supported on Linux (Ubuntu)
117
+ if (process.platform === 'linux') {
118
+ // Try exact match prebuilt binary first
119
+ try {
120
+ const prebuiltModule = customFallbackLoader();
121
+ if (prebuiltModule) {
122
+ nativeModule = prebuiltModule;
123
+ return nativeModule;
124
+ }
125
+ } catch (e) {
126
+ debug('Exact match prebuilt loading failed:', e.message);
127
+ }
128
+
129
+ debug(
130
+ 'No exact match prebuilt binary found, falling back to build from source'
131
+ );
132
+ } else {
133
+ debug(
134
+ `Platform ${process.platform} does not support prebuilt binaries, will try existing build or compile from source`
135
+ );
136
+ }
137
+
138
+ try {
139
+ // Try to find existing built binary first (works on all platforms)
140
+ // The 'bindings' module will search standard locations like:
141
+ // - build/Release/rclnodejs.node
142
+ // - build/Debug/rclnodejs.node
143
+ // - compiled/{node_version}/{platform}/{arch}/rclnodejs.node
144
+ // etc.
145
+ nativeModule = bindings('rclnodejs');
146
+ debug('Found and loaded existing built binary');
147
+ return nativeModule;
148
+ } catch {
149
+ debug('No existing built binary found, triggering compilation...');
150
+
151
+ // Trigger actual compilation
152
+ try {
153
+ debug('Running node-gyp rebuild...');
154
+ execSync('npm run rebuild', {
155
+ stdio: 'inherit',
156
+ cwd: path.join(__dirname, '..'),
157
+ timeout: 300000, // 5 minute timeout
158
+ });
159
+
160
+ // Try to load the newly built binary
161
+ nativeModule = bindings('rclnodejs');
162
+ debug('Successfully compiled and loaded from source');
163
+ return nativeModule;
164
+ } catch (compileError) {
165
+ debug('Compilation failed:', compileError.message);
166
+ throw new Error(
167
+ `Failed to build rclnodejs from source: ${compileError.message}`
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ module.exports = loadNativeAddon();
package/lib/node.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const rclnodejs = require('bindings')('rclnodejs');
17
+ const rclnodejs = require('./native_loader.js');
18
18
 
19
19
  const ActionInterfaces = require('./action/interfaces.js');
20
20
  const Client = require('./client.js');
@@ -31,6 +31,7 @@ const {
31
31
  Parameter,
32
32
  ParameterDescriptor,
33
33
  } = require('./parameter.js');
34
+ const { isValidSerializationMode } = require('./message_serialization.js');
34
35
  const ParameterService = require('./parameter_service.js');
35
36
  const Publisher = require('./publisher.js');
36
37
  const QoS = require('./qos.js');
@@ -532,6 +533,14 @@ class Node extends rclnodejs.ShadowNode {
532
533
  options = Object.assign(options, { isRaw: false });
533
534
  }
534
535
 
536
+ if (options.serializationMode === undefined) {
537
+ options = Object.assign(options, { serializationMode: 'default' });
538
+ } else if (!isValidSerializationMode(options.serializationMode)) {
539
+ throw new TypeError(
540
+ `Invalid serializationMode: ${options.serializationMode}. Valid modes are: 'default', 'plain', 'json'`
541
+ );
542
+ }
543
+
535
544
  return options;
536
545
  }
537
546
 
@@ -666,6 +675,10 @@ class Node extends rclnodejs.ShadowNode {
666
675
  * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
667
676
  * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
668
677
  * @param {boolean} options.isRaw - The topic is serialized when true, default: false.
678
+ * @param {string} [options.serializationMode='default'] - Controls message serialization format:
679
+ * 'default': Use native rclnodejs behavior (respects enableTypedArray setting),
680
+ * 'plain': Convert TypedArrays to regular arrays,
681
+ * 'json': Fully JSON-safe (handles TypedArrays, BigInt, etc.).
669
682
  * @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
670
683
  * Confirm that your RMW supports content-filtered topics before use.
671
684
  * @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
@@ -1920,6 +1933,7 @@ class Node extends rclnodejs.ShadowNode {
1920
1933
  * isRaw: false,
1921
1934
  * qos: QoS.profileDefault,
1922
1935
  * contentFilter: undefined,
1936
+ * serializationMode: 'default',
1923
1937
  * }
1924
1938
  */
1925
1939
  Node.getDefaultOptions = function () {
@@ -1928,6 +1942,7 @@ Node.getDefaultOptions = function () {
1928
1942
  isRaw: false,
1929
1943
  qos: QoS.profileDefault,
1930
1944
  contentFilter: undefined,
1945
+ serializationMode: 'default',
1931
1946
  };
1932
1947
  };
1933
1948