@linuxcnc-node/hal
Version:
Node.js bindings for LinuxCNC HAL library
365 lines (364 loc) • 12.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HalComponent = exports.DEFAULT_POLL_INTERVAL = void 0;
const events_1 = require("events");
const constants_1 = require("./constants");
const item_1 = require("./item");
/** Default polling interval in milliseconds for monitoring value changes */
exports.DEFAULT_POLL_INTERVAL = 10;
/**
* Represents a HAL component.
*
* This class provides functionality to create HAL components, pins, parameters,
* and interact with the HAL environment.
*
* @example
* ```typescript
* const comp = new HalComponent("my-component");
* const pin = comp.newPin("output", "float", "out");
* comp.ready();
* pin.setValue(123.45);
*
* // Listen for value changes
* pin.on('change', (newVal, oldVal) => {
* console.log(`Changed: ${oldVal} -> ${newVal}`);
* });
*
* // Listen for batch delta updates
* comp.on('delta', (delta) => {
* console.log(`${delta.changes.length} items changed at cursor ${delta.cursor}`);
* });
* ```
*/
class HalComponent extends events_1.EventEmitter {
/**
* Creates a new HAL component.
*
* @param name - The name of the component (e.g., "my-component").
* This will be registered with LinuxCNC HAL.
* @param prefix - Optional prefix for pins and parameters created by this component.
* If not provided, defaults to `name`.
*/
constructor(name, prefix) {
super();
this.pins = {};
this.params = {};
// Monitoring system
this.watchedItems = new Map();
this.monitoringTimer = null;
this.monitoringOptions = {
pollInterval: exports.DEFAULT_POLL_INTERVAL,
};
/** Monotonic cursor incremented on each batch update */
this.cursor = 0;
this.nativeInstance = new constants_1.halNative.HalComponent(name, prefix);
this.name = name;
this.prefix = prefix || name;
}
/**
* Checks if a HAL component with the given name exists in the system.
*
* @param name - The component name (e.g., "halui", "my-custom-comp").
* @returns `true` if the component exists, `false` otherwise.
*/
static exists(name) {
return constants_1.halNative.component_exists(name);
}
/**
* Checks if the HAL component with the given name has been marked as ready.
*
* @param name - The component name.
* @returns `true` if the component exists and is ready, `false` otherwise.
*/
static isReady(name) {
return constants_1.halNative.component_is_ready(name);
}
/**
* Gets the value of a pin or parameter by name.
*
* @param name - The nameSuffix of the pin or parameter.
* @returns The current value.
*/
getValue(name) {
return this.nativeInstance.getProperty(name);
}
/**
* Sets the value of a pin or parameter by name.
*
* @param name - The nameSuffix of the pin or parameter.
* @param value - The value to set.
* @returns The value that was set.
*/
setValue(name, value) {
if (typeof value !== "number" && typeof value !== "boolean") {
throw new TypeError("Value must be a number or boolean");
}
return this.nativeInstance.setProperty(name, value);
}
/**
* Creates a new HAL pin associated with this component.
*
* This method can only be called before `ready()` or after `unready()`.
*
* @param nameSuffix - The suffix for the pin name (e.g., "in1", "motor.0.pos").
* The full HAL name will be `this.prefix + "." + nameSuffix`.
* @param type - The data type of the pin (e.g., `"float"`, `"bit"`).
* See {@link HalType} for available types.
* @param direction - The direction of the pin (e.g., `"in"`, `"out"`, `"io"`).
* See {@link HalPinDir} for available directions.
* @returns A new `Pin` object instance.
* @throws Error if component is ready or if pin creation fails.
*/
newPin(nameSuffix, type, direction) {
const success = this.nativeInstance.newPin(nameSuffix, constants_1.HalTypeValue[type], constants_1.HalPinDirValue[direction]);
if (!success) {
console.error(`Failed to create pin '${nameSuffix}'`);
}
const pin = new item_1.Pin(this, nameSuffix, type, direction);
this.pins[nameSuffix] = pin;
this.setupItemListeners(pin, nameSuffix);
return pin;
}
/**
* Creates a new HAL parameter associated with this component.
*
* This method can only be called before `ready()` or after `unready()`.
*
* @param nameSuffix - The suffix for the parameter name.
* The full HAL name will be `this.prefix + "." + nameSuffix`.
* @param type - The data type of the parameter. See {@link HalType} for available types.
* @param direction - The writability of the parameter (`"ro"` for read-only,
* `"rw"` for read-write). See {@link HalParamDir}.
* @returns A new `Param` object instance.
* @throws Error if component is ready or if parameter creation fails.
*/
newParam(nameSuffix, type, direction) {
const success = this.nativeInstance.newParam(nameSuffix, constants_1.HalTypeValue[type], constants_1.HalParamDirValue[direction]);
if (!success) {
console.error(`Failed to create param '${nameSuffix}'`);
}
const param = new item_1.Param(this, nameSuffix, type, direction);
this.params[nameSuffix] = param;
this.setupItemListeners(param, nameSuffix);
return param;
}
/**
* Sets up auto-watch listeners for a pin or param.
* @private
*/
setupItemListeners(item, name) {
item.on("newListener", (event) => {
if (event === "change" && !this.watchedItems.has(name)) {
this.watchedItems.set(name, {
item,
lastValue: this.getValue(name),
});
this.ensureMonitoring();
}
});
item.on("removeListener", (event) => {
if (event === "change" && item.listenerCount("change") === 0) {
this.watchedItems.delete(name);
this.checkStopMonitoring();
}
});
}
/**
* Marks this component as ready and available to the HAL system.
*
* Once ready, pins can be linked, and parameters can be accessed by other
* HAL components or tools. Pins and parameters cannot be added after `ready()`
* is called, unless `unready()` is called first.
*/
ready() {
this.nativeInstance.ready();
}
/**
* Marks this component as not ready, allowing addition of more pins or parameters.
*
* `ready()` must be called again to make the component (and any new items)
* available to HAL.
*/
unready() {
this.nativeInstance.unready();
}
/**
* Retrieves a map of all `Pin` objects created for this component.
*
* @returns An object where keys are the `nameSuffix` of the pins and values
* are the corresponding `Pin` instances.
*/
getPins() {
return this.pins;
}
/**
* Retrieves a map of all `Param` objects created for this component.
*
* @returns An object where keys are the `nameSuffix` of the parameters and
* values are the corresponding `Param` instances.
*/
getParams() {
return this.params;
}
/**
* Gets a pin by name.
*
* @param name - The nameSuffix of the pin.
* @returns The Pin instance, or undefined if not found.
*/
getPin(name) {
return this.pins[name];
}
/**
* Gets a param by name.
*
* @param name - The nameSuffix of the param.
* @returns The Param instance, or undefined if not found.
*/
getParam(name) {
return this.params[name];
}
/**
* Gets the current cursor value for sync operations.
*
* The cursor is monotonically increasing and increments with each delta emit.
*
* @returns The current cursor value.
*/
getCursor() {
return this.cursor;
}
/**
* Gets a snapshot of all current item values.
*
* Useful for initial sync or resync after cursor mismatch.
*
* @returns Object with items map, current cursor, and timestamp.
*/
getSnapshot() {
const items = {};
for (const [name] of this.watchedItems.entries()) {
items[name] = this.getValue(name);
}
return {
items,
cursor: this.cursor,
timestamp: Date.now(),
};
}
/**
* Configures the monitoring system settings.
*
* The monitoring system will check for value changes at the specified interval.
*
* @param options - Configuration object with `pollInterval` property (in milliseconds).
* Default polling interval is 10ms.
*/
setMonitoringOptions(options) {
this.monitoringOptions = { ...this.monitoringOptions, ...options };
// Restart monitoring with new options if it's currently running
if (this.monitoringTimer && this.watchedItems.size > 0) {
this.stopMonitoring();
this.startMonitoring();
}
}
/**
* Retrieves the current monitoring configuration.
*
* @returns A copy of the current monitoring options.
*/
getMonitoringOptions() {
return { ...this.monitoringOptions };
}
/**
* Starts the monitoring timer if not already running.
* @private
*/
ensureMonitoring() {
if (!this.monitoringTimer && this.watchedItems.size > 0) {
this.startMonitoring();
}
}
/**
* Stops monitoring if no items are being watched.
* @private
*/
checkStopMonitoring() {
if (this.watchedItems.size === 0) {
this.stopMonitoring();
}
}
/**
* Starts the monitoring timer.
* @private
*/
startMonitoring() {
if (this.monitoringTimer) {
return;
}
this.monitoringTimer = setInterval(() => {
this.checkForChanges();
}, this.monitoringOptions.pollInterval || exports.DEFAULT_POLL_INTERVAL);
}
/**
* Stops the monitoring timer.
* @private
*/
stopMonitoring() {
if (this.monitoringTimer) {
clearInterval(this.monitoringTimer);
this.monitoringTimer = null;
}
}
/**
* Checks all watched items for value changes and emits events.
*
* Emits individual 'change' events on each HalItem, plus a batch 'delta'
* event on the component with all changes aggregated.
* @private
*/
checkForChanges() {
const changes = [];
for (const [name, watched] of this.watchedItems.entries()) {
try {
const currentValue = this.getValue(name);
if (currentValue !== watched.lastValue) {
const oldValue = watched.lastValue;
watched.lastValue = currentValue;
watched.item.emit("change", currentValue, oldValue);
changes.push({ name, value: currentValue });
}
}
catch (error) {
console.error(`Error checking value for ${name}:`, error);
}
}
// Emit batch delta if any changes occurred
if (changes.length > 0) {
this.cursor++;
const delta = {
changes,
cursor: this.cursor,
timestamp: Date.now(),
};
this.emit("delta", delta);
}
}
/**
* Cleans up the component, stops monitoring and removes all listeners.
*
* Should be called when the component is no longer needed to prevent memory leaks.
*/
dispose() {
this.stopMonitoring();
this.watchedItems.clear();
// Remove all listeners from pins and params
for (const pin of Object.values(this.pins)) {
pin.removeAllListeners();
}
for (const param of Object.values(this.params)) {
param.removeAllListeners();
}
}
}
exports.HalComponent = HalComponent;