@nicolaudiegroup/nicolaudie-remote-protocol v1.2.0
Nicolaudie Remote Protocol
Node JS based implementation of the Nicolaudie Remote Protocol (NRP), allowing you to connect to various devices manufactured by Nicolaudie Group that have networking capabilities.
It uses @nicolaudiegroup/dasnet in the background to parse and serialize DASNet request/responses.
Documentation
For more information about the Nicolaudie Remote Protocol, DASNet, or to raise issues, please contact support@nicolaudiegroup.com
Installation
npm i @nicolaudiegroup/nicolaudie-remote-protocolBasic usage
NicolaudieRemoteProtocol handles the discovery and creation of device instances, which you can then use to communicate with compatible devices.
It provides multiple ways of finding/connecting to devices:
- Passive discovery: waits for a broadcast from devices - generally when they are powered on
- Active discovery: NRP broadcasts a message, to which devices respond
- Manual connection: you provide the necessary information to connect to the device
You can combine those various connection processes at different stages of your application lifecycle:
- Active discovery the first time the user launches the application, in order to suggest devices, with an option to manually add devices
- Active discovery with expected devices on subsequent launches of the application to verify that the previously added devices are still present on the network
- Passive discovery using events to continuously check if missing expected devices during the last active discovery re-appear on the network. Can also be used to suggest new devices on the network to the user.
Create a NRP instance
Only one NRP instance is needed, as the devices instances are pooled and automatically reused anyways.
import * as NRP from '@nicolaudiegroup/nicolaudie-remote-protocol'
const nrp = new NRP.NicolaudieRemoteProtocol()Passive discovery of devices on the network
Calling start will enable the passive discovery of devices present on the network, assuming the network is compatible with the following requirements:
- Allow UDP broadcast by device on port 2430
await nrp.start()You can also stop the passive discovery.
Active discovery of devices on the network
Calling discover will trigger an active discovery of devices present on the network, assuming the network is compatible with the following requirements:
- Allow UDP broadcast by NRP on port 2430
- Allow UDP broadcast or unicast by device to NRP on port 2430
const devices = await nrp.discover()The active discovery automatically enables the passive discovery without the need to call await nrp.start() separately.
Expect devices on the network
Calling expectDevice or expectDevices allows you to define which devices are expected to be found on the network when the active discovery process is running.
Later on, when calling discover with the findExpected option being true, it will resolve as soon as all expected devices are found. If any of the expected device cannot be found, the discovery process will timeout and throw an error. The returned devices map may contain devices that were discovered in the process but not expected.
This is also an opportunity to pass known device credentials.
nrp.expectDevice({
serial: 123456,
password: 'foo'
})
try {
const devices = await nrp.discover({ findExpected: true })
} catch (e) {
console.error(e)
}Additionally, expected devices can be removed from the list using:
nrp.clearExpectedDevices() // Do not expect any device anymore
nrp.removeExpectedDevice(123456) // Do not expect device with serial 123456 anymoreManually add a device
Devices can be added manually, instead of relying on the discovery.
This can also be used to update a device's information (eg. it's credentials) by providing the upsert parameter.
const upsert = true // Set to true to update the device if it already exists
const device = nrp.addDevice({
serial: 123456,
password: 'foo',
// Add any other relevant info: host, port, deviceId, name, user...
}, upsert)Since the discovery uses upsert, devices can be manually added first to define their known credentials/settings, and calling discover afterwards will locate them on the network (IP and port) and fill-in their metadata.
Retrieve a device
Devices associated with this NRP instance can be retrieved by their serial number. It throws an error if no device with a matching serial number is found.
try {
const device = nrp.getDevice(123456)
} catch (e) {
console.error(e)
}Additionally, the list of discovered/added devices is available as a map and as an array through getters.
NRP getters
const devices = nrp.devicesMap // Devices indexed by serial
const devices = nrp.devices // Array of devices
const expectedDevices = nrp.expectedDevices // Array of expected devices
const started = nrp.started // True when passive discovery is looking for devices on the networkEvents
The NRP instance emits a device event when a new device is added (either manually or through the discovery process), with the device instance as first argument.
nrp.on('device', (device) => {
// Do stuff with added/discovered device
})Stop the passive discovery
By default, it will also disconnect all devices on the instance.
await nrp.stop()If you wish to keep the devices connected, pass false as first argument, but remember to disconnect manually from each device later.
await nrp.stop(false) // Stop the NRP instance from listening to broadcasted messages without disconnecting devices
// Later...
// You can call it again with the `disconnectDevices` argument set to `true` (default) to disconnect from all devices
await nrp.stop(true)
// OR
await device.disconnect()Destroy the NRP instance
Use this to avoid memory leaks when you're done working with the NRP instance and it's devices.
This will also destroy instances of devices associated with this NRP instance, and remove all event listeners on the NRP instance.
await nrp.destroy()Basic device usage
Connect to a device
Connection to the device is done automatically when communicating with it for the first time, so no action is needed here.
However, manual connection to the device is still possible using the connect method, in order to verify the credentials are correct.
If no device credentials were provided by expecting, manually adding, or updating the device, they can be provided as argument to the connect method.
Invalid credentials or failure to connect to the device will make it throw an error.
await device.connect({ password: 'foo' })Load the show and zones/scenes status
This will load the list of zones and scenes, and retrieve their status. This is the recommended way of getting the show initially.
const show = await device.refreshStatus()A cached version of the show is also available through a getter, but it must be awaited as it may be loaded dynamically when called if the show was not loaded previously.
const show = await device.showThis getter should not be used before calling device.refreshStatus(), as it will only load the show but not it's status or the status of the scenes.
Update device instance
If the informations (password, IP address, etc.) in a device instance are not valid anymore, they can be updated.
It may be required to disconnect and reconnect to the device for some of the changes to be taken into account.
device.update({
password: 'bar',
keepAliveInterval: 2000,
})Device getters
const destroyed = device.destroyed // True when device instance was destroyed
const connected = device.connected // True when connection to the device is open
const authenticated = device.authenticated // True when authenticated to the device
const serial = device.serial // Serial number of the device
const name = device.name // Name of the device
const firmwareVersion = device.firmwareVersion // Firmware version of the device
const state = device.state // State of the device
const formFactor = device.formFactor // Form factor of the device
const deviceId = device.deviceId // Device ID of the device
const host = device.host // IP address of the device
const port = device.port // Port of the device
const user = device.user // Username of the device
const password = device.password // Password of the device
const supports = device.supports // List of supported device features
const show = await device.show // Show of the device, needs to be awaited
const info = device.info // Summary of the deviceDisconnect from a device
This will close the connection to the device.
await device.disconnect()Destroy a device instance
Destroying a device instance will disconnect it from the device, prevent the instance from being reused, remove all it's event listeners, and allow the device instance to be garbage collected when there are no more references to it in the application code.
await device.destroy()Basic show usage
Whenever using a show instance, please make sure the show.stale flag is false. If the show.stale flag is true, a new show instance needs to be retrieved by calling device.refreshStatus(). A stale event is also available on the show instance to detect whenever this flag becomes true.
let show
async function getShow() {
show = await device.refreshStatus()
show.once('stale', getShow)
}
await getShow()
// Do stuff with showGet a zone or scene
const zoneId = 2
const sceneId = 3
show.getZone(zoneId)
show.getScene(zoneId, sceneId)
show.getScene(sceneId)Set blackout status of the show
Passing true/false as argument enables/disables blackout
await show.setBlackout(true)Stop all scenes
Iterates over each currently active scene of the show to stop them.
await show.stop()Manipulate global modifiers
await show.setColor({ red, green, blue }) // Set global color modifier
await show.setDimmer(value) // Set global dimmer modifier
await show.setExtraColor(index, value) // Set global extra color modifier
await show.setHue(value) // Set global hue modifier
await show.setSaturation(value) // Set global saturation modifier
await show.setSpeed(value) // Set global speed modifier
await show.reset() // Reset modifiers globallyGetters
const zones = show.zones // Array of zones
const scenes = show.scenes // Array of all scenes of the show
const device = show.device // Device containing the show
const uuid = show.uuid // UUID of the show
const name = show.name // Name of the show
const timestamp = show.timestamp // Timestamp of the show
const version = show.version // Version of the show
const blackout = show.blackout // Blackout status of the show
const error = show.error // Error status of the show
const stale = show.stale // If true, the show needs to be read again from the device
const currentScenes = show.currentScenes // Active scene for each zoneBasic zone usage
Get a scene of the zone
const sceneId = 3
zone.getScene(sceneId)Start/pause/resume/stop a scene of the zone
Without arguments, it'll act on the currently active scene If there is no active scene in the zone, it'll fallback to the first scene of the zone
await zone.start()
await zone.pause()
await zone.resume()
await zone.stop()A sceneId can be provided explicitely
const sceneId = 3
await zone.start(sceneId)
await zone.pause(sceneId)
await zone.resume(sceneId)
await zone.stop(sceneId)The priority is available as second argument, and defaults to 100
const sceneId = 3
const priority = 101
await zone.start(sceneId, priority)
await zone.pause(sceneId, priority)
await zone.resume(sceneId, priority)
await zone.stop(sceneId, priority)Manipulate zone modifiers
await zone.setColor({ red, green, blue }) // Set zone color modifier
await zone.setDimmer(value) // Set zone dimmer modifier
await zone.setExtraColor(index, value) // Set zone extra color modifier
await zone.setHue(value) // Set zone hue modifier
await zone.setSaturation(value) // Set zone saturation modifier
await zone.setSpeed(value) // Set zone speed modifier
await zone.reset() // Reset modifiers in the zoneGetters
const scenes = zone.scenes // Array of scenes
const currentScene = zone.currentScene // Active scene
const show = zone.show // Show containing the zone
const device = zone.device // Device containing the show with the zone
const id = zone.id // ID of the zone
const name = zone.name // Name of the zoneBasic scene usage
Start/pause/resume/stop a scene of the zone
await scene.start()
await scene.pause()
await scene.resume()
await scene.stop()Manipulate scene modifiers
await scene.setColor({ red, green, blue }) // Set scene color modifier
await scene.setDimmer(value) // Set scene dimmer modifier
await scene.setExtraColor(index, value) // Set scene extra color modifier
await scene.setHue(value) // Set scene hue modifier
await scene.setSaturation(value) // Set scene saturation modifier
await scene.setSpeed(value) // Set scene speed modifier
await scene.reset() // Reset scene modifiersGetters
const show = scene.show // Show containing the scene
const device = scene.device // Device containing the show with the scene
const id = scene.id // ID of the scene
const name = scene.name // Name of the scene
const zoneId = scene.zoneId // ID of the zone of the scene
const zone = scene.zone // Zone of the scene
const state = scene.state // State of the scene
const dimmer = scene.dimmer // Dimmer modifier of the scene
const speed = scene.speed // Speed modifier of the scene
const red = scene.red // Red modifier of the scene
const green = scene.green // Green modifier of the scene
const blue = scene.blue // Blue modifier of the scene
const saturation = scene.saturation // Saturation modifier of the scene
const hue = scene.hue // Hue modifier of the scene
const extraColors = scene.extraColors // Extra colors modifier of the scene
const priority = scene.priority // Priority of the scene
const stopped = scene.stopped // True if the scene is stopped
const paused = scene.paused // True if the scene is paused
const running = scene.running // True if the scene is runningSummary of available events
| Emitter | Event | Internal | Debounced | Description | Arguments |
|---|---|---|---|---|---|
| NRP | device | A device was added | device | ||
| Device | showStatusUpdate | ✅ | The device received a show status response, provides raw ShowStatusResponse | response | |
| Device | zoneStatusUpdate | ✅ | The device received a zone status response, provides raw ZoneStatusResponse | response | |
| Device | sceneStatusUpdate | ✅ | The device received a scene status response, provides raw SceneStatusResponse | response | |
| Show | updated | ✅ | Some zones of the show were updated | updates | |
| Show | zoneUpdated | A zone of the show was updated | zone, updatedScenes | ||
| Show | currentScene | The currently active scene in a zone changed | newScene?, prevScene?, zone | ||
| Show | stale | A different show was found to be running on the device, and the new show (+ zones and scenes) needs to be loaded from the device via device.refreshStatus(). This show instance must not be used anymore. See basic show usage & show loading | none | ||
| Zone | updated | ✅ | Some scenes of the zone were updated | updatedScenes | |
| Zone | sceneUpdated | A scene of the zone was updated | scene, newValues, prevValues | ||
| Zone | currentScene | The currently active scene in the zone changed | newScene?, prevScene? | ||
| Scene | updated | ✅ | Some properties of the scene were updated | newValues, prevValues | |
| Scene | propertyUpdated | Some properties of the scene were updated | changed |
Example
Here's an example putting it all together:
import * as NRP from '@nicolaudiegroup/nicolaudie-remote-protocol'
// Create the NRP instance
const nrp = new NRP.NicolaudieRemoteProtocol()
const deviceA = {
serial: 123456,
password: 'foo',
}
const deviceB = {
serial: 789012,
password: 'bar',
}
// Step 1: Define expected devices
async function defineExpectedDevices() {
nrp.expectDevice(deviceA)
}
// Step 2: Trigger active discovery with expected devices
async function discoverDevices() {
console.log('Triggering active device discovery with expected devices...')
try {
const devices = await nrp.discover({ findExpected: true })
console.log(`Found ${devices.size} devices during active discovery.`)
} catch (error) {
console.error('Error during active discovery:', error)
}
}
// Step 3: Manually add device if necessary
async function addDeviceManually() {
// Here, we hope the device was discovered, and just upsert it's pasword
const device = nrp.addDevice(deviceB, true) // Add device with upsert option
console.log(`Manually added device: ${device.serial}`)
}
// Step 4: Retrieve and interact with the device
async function interactWithDevice() {
try {
const retrievedDevice = nrp.getDevice(deviceA.serial)
// Connect to the device (no need to provide credentials, as they were provided when expecting it)
await retrievedDevice.connect()
console.log(`Connected to device: ${retrievedDevice.name}`)
// Refresh device status
await retrievedDevice.refreshStatus()
console.log('Device status refreshed')
// Retrieve and start the scene
const show = await retrievedDevice.show
const scene = show.getScene(1, 3) // Assuming zone ID 1 exists and contains scene ID 3
if (!scene) throw new Error('Unable to find scene with ID 3 in zone with ID 1')
await scene.start() // Starting the scene
console.log(`Scene "${scene.name}" (${scene.zone?.name}) started`)
await NRP.Utils.timeout(1000) // Just wait 1sec
// Manipulate scene modifiers (example: setting color, dimmer)
await scene.setColor({
red: 0xFFFF,
green: 0,
blue: 0,
}) // Set the scene to red
console.log(`Scene "${scene.name}" (${scene.zone?.name}) color set to red`)
await NRP.Utils.timeout(1000) // Just wait 1sec
// Set additional scene modifiers (e.g., dimmer)
await scene.setDimmer(0x7FFF) // Set dimmer to 50%
console.log(`Scene "${scene.name}" (${scene.zone?.name}) dimmer set to 50%`)
await NRP.Utils.timeout(1000) // Just wait 1sec
// Reset scene modifiers
await scene.reset()
console.log(`Scene "${scene.name}" (${scene.zone?.name}) reset`)
await NRP.Utils.timeout(1000) // Just wait 1sec
await scene.stop()
console.log(`Scene "${scene.name}" (${scene.zone?.name}) stopped`)
} catch (e) {
console.error('Error interacting with device:', e)
}
}
// Step 5: Stop passive discovery
async function stopPassiveDiscovery() {
/*
We might also destroy the instance (and it's devices) if the application keeps running without NRP.
Here, since we just need the process to exit, stopping the instance (and it's devices) to empty
the event loop is enough.
*/
await nrp.stop()
console.log('Stopped passive device discovery and disconnected from devices')
}
async function main() {
// Step 1: Define expected devices
await defineExpectedDevices()
// Step 2: Trigger active discovery with expected devices
await discoverDevices()
// Step 3: Manually add device if necessary
await addDeviceManually()
// Step 4: Retrieve and interact with the device
await interactWithDevice()
// Step 5: Stop passive discovery
await stopPassiveDiscovery()
}
// Run the example
main().catch((e) => {
console.error('Error in the application:', e)
})Other
Enable debugging logs
NRP.debug.configure({ enabled: true })Enable only some logs by manipulating the contexts set
NRP.Debug.debug.contexts.clear()
NRP.Debug.debug.contexts.add(NRP.Debug.DebugContexts.Connection)
NRP.Debug.debug.contexts.add(NRP.Debug.DebugContexts.DeviceConnection)
NRP.Debug.debug.contexts.add(NRP.Debug.DebugContexts.RemoteConnection)
// ...Promise that resolves after the specified amount of time in milliseconds
await NRP.Utils.timeout(1000)
// If the second argument is true, it will reject instead
await NRP.Utils.timeout(1000, true)
// Optionally, the second argument can be a custom error when the timeout rejects
await NRP.Utils.timeout(1000, 'Too slow !')
// You can also control the promise directly if you want to resolve or reject earlier
const p = NRP.Utils.timeout(1000)
p.then(() => {
console.log('Resolved')
}).catch((e) => {
console.log('Rejected:', e)
})
if (Math.random() < 0.5) {
p.resolve()
} else {
p.reject('Too slow !')
}6 months ago
6 months ago
8 months ago
8 months ago
8 months ago
9 months ago
9 months ago
9 months ago