UNPKG

@linuxcnc-node/hal

Version:

Node.js bindings for LinuxCNC HAL library

365 lines (364 loc) 12.5 kB
"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;