1.2.0 • Published 6 months ago

@nicolaudiegroup/nicolaudie-remote-protocol v1.2.0

Weekly downloads
-
License
AGPL-3.0-or-later
Repository
gitlab
Last release
6 months ago

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

SDK references

For more information about the Nicolaudie Remote Protocol, DASNet, or to raise issues, please contact support@nicolaudiegroup.com

Installation

npm i @nicolaudiegroup/nicolaudie-remote-protocol

Basic 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:

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 anymore

Manually 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 network

Events

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.show

This 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 device

Disconnect 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 show

Get 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 globally

Getters

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 zone

Basic 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 zone

Getters

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 zone

Basic 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 modifiers

Getters

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 running

Summary of available events

EmitterEventInternalDebouncedDescriptionArguments
NRPdeviceA device was addeddevice
DeviceshowStatusUpdateThe device received a show status response, provides raw ShowStatusResponseresponse
DevicezoneStatusUpdateThe device received a zone status response, provides raw ZoneStatusResponseresponse
DevicesceneStatusUpdateThe device received a scene status response, provides raw SceneStatusResponseresponse
ShowupdatedSome zones of the show were updatedupdates
ShowzoneUpdatedA zone of the show was updatedzone, updatedScenes
ShowcurrentSceneThe currently active scene in a zone changednewScene?, prevScene?, zone
ShowstaleA 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 loadingnone
ZoneupdatedSome scenes of the zone were updatedupdatedScenes
ZonesceneUpdatedA scene of the zone was updatedscene, newValues, prevValues
ZonecurrentSceneThe currently active scene in the zone changednewScene?, prevScene?
SceneupdatedSome properties of the scene were updatednewValues, prevValues
ScenepropertyUpdatedSome properties of the scene were updatedchanged

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 !')
}