npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

node-red-contrib-merging

v0.3.0

Published

Node-RED nodes to monitor and control Merging Technologies RAVENNA devices (Hapi / Horus / Anubis). Thin wrapper over the merging-ravenna library. Unofficial; not affiliated with Merging Technologies.

Readme

node-red-contrib-merging

Node-RED nodes to monitor and control Merging Technologies RAVENNA devices (Hapi, Horus, Anubis) over their web-UI control protocol — CometD/Bayeux over WebSocket. A thin wrapper over the merging-ravenna library.

Unofficial. Not affiliated with or endorsed by Merging Technologies. The protocol is undocumented and may change between firmware versions. Always keep a way to restore your settings.

With these nodes you can:

  • read any parameter the device exposes — gain, mute, trims, roll-off filter, output reference level — and react when it changes (knob, web UI, front panel, or your own automation all echo back);
  • set those parameters from a flow (a Stream Deck, a schedule, MQTT, …);
  • read and write the device's system domain — sample rate, frame size, clock source, PTP, plus health telemetry (temperature, uptime, PTP lock);
  • inspect device state on demand — modeled values, or the raw tree (RTP sessions, network, NMOS, …);
  • know when the device goes online/offline.

Contents


Install

From your Node-RED user directory (usually ~/.node-red):

npm install node-red-contrib-merging

Then restart Node-RED. A Merging category appears in the palette with five nodes.

Node version note. Node-RED's palette installer runs with --engine-strict. If you're on a very new Node (e.g. 24) you may hit EBADENGINE from an unrelated transitive dependency. Pinning Node-RED to Node 20/22 is the clean fix; see Troubleshooting.


Quick start

  1. Drag in a merging-state node and double-click it.
  2. Next to Device, click the pencil and add a merging-config: enter your device's IP (e.g. 192.168.0.150) and deploy.
  3. Power on the device. The state node's status dot goes green (online), and on connect it emits the current value of everything in scope.
  4. Turn a knob on the unit (or change something in its web UI) — the state node emits a message describing the change.

To set something, add a merging-set node, pick a Module and Parameter, and feed its input the value as msg.payload.


Core concepts

How the device talks

Everything runs over one WebSocket session per device (CometD/Bayeux multiplexes several logical channels onto it). The state you care about lives in one big JSON tree — the same object the unit's web UI returns from System → Get Device Status. The device delivers it two ways:

  • Whole tree — the entire state in one object, { path: "$", value: {…} }. Sent on request: on connect, and whenever you ask (a refresh).
  • Narrow deltas — when one thing changes, just the changed slice is pushed (e.g. { path: "…id==60…outs", value: { attenuation: -300 } }). Pushed on any change — a knob, the web UI, the front panel, another client, or your own set echoing back.

The library turns those into what these nodes emit:

  • audio changes arrive as narrow deltas → streamed per-key as they happen;
  • the catalog (the audio parameter list) and the system snapshot (clock/PTP/health) are rebuilt from the whole tree → read on connect / refresh.

That asymmetry is why the two domains behave differently below: audio controls stream (there's a delta for every change), but system values only ever appear in a whole-tree snapshot — so you poll a refresh to read them. (A stable value like sample_rate never "streams", because nothing emits a delta for it.)

The device (config) node

merging-config holds one shared, self-healing WebSocket session to a device. Every set/state/device node references it, so they share a single connection. It reconnects automatically (exponential backoff) if the device drops, and a data-liveness watchdog tears down and re-establishes a wedged-but-open socket — so after a power-cycle, state is always re-read from the device rather than assumed.

You usually have one config node per physical device.

Modules and parameters

A RAVENNA device is organised into modules — the cards and functional blocks it contains. Each module has a numeric id and a name. On a Hapi MkII, for example:

| id | name | what it is | |-----|-------------|------------------------------------| | 60 | D/A 1 | the analog output card (8 channels)| | 30 | Headphone | the headphone output (2 channels) | | 0 | ZMAN | clock/network + device health | | 2 | Sync | PTP / clock source | | 100 | SPDIF | S/PDIF I/O |

Within a module, each controllable setting is a parameter with a stable key: attenuation (output gain), mute, channel_trim (per-channel), roll_off_filter, out_max_level, and so on.

The catalog

The catalog is the list of controllable parameters that the library discovered live from your specific device by reading its own capability metadata. It's not a fixed list baked into this package — it reflects whatever cards and channels your unit actually has, at its current firmware.

Each catalog entry describes one parameter:

{
  moduleId: 60, moduleName: "D/A 1",
  key: "attenuation",
  value: -20,            // current value, in display units (dB here)
  raw: -200,             // raw device value (tenths of a dB)
  unit: "dB",            // dB | bool | enum | int
  min: -120, max: 0, step: 0.5,   // advertised range, where applicable
  enum: null,            // { label: intValue } for enum params
  channelIndex: undefined,        // set for per-channel params
  settable: true,
  confidence: "confirmed"
}

The editor dropdowns (Module → Parameter → Channel) are populated from the catalog, and you can fetch it into a flow with msg.query = "catalog" on a merging-state node. Audio parameter keys repeat across modules/channels, so the catalog is an array.

The System pseudo-module

Beyond per-module audio controls, the device has a system domain: sample rate, frame size, clock source, PTP settings, and read-only health telemetry (CPU temperature, uptime, PTP lock status, grandmaster ID). These don't belong to one audio card, so this package surfaces them as an always-present System entry in the Module dropdown of both the set and state nodes.

System parameters are grouped (Clock / Sync / PTP / Device / Health / Advanced) and keyed by a unique name — sample_rate, frame_size, sync_source, ptp_domain, temperature, uptime, … Because their keys are unique, the system snapshot is an object keyed by name (see queries).

⚠️ Writing system parameters re-clocks the device. Changing sample rate, frame size, or clock source interrupts audio. Do it with audio stopped. Read-only telemetry (temperature, PTP lock, uptime) can never be set. Some settables carry a dependency note (e.g. PTP priorities apply only in manual-grandmaster mode).

Topics and messages

These nodes don't talk to MQTT themselves — each emits a plain Node-RED message on its output, which you wire wherever you like (mqtt out, a function, a dashboard…).

Topics are neutral identifiers with no project-specific prefix:

<moduleId>/<key>              e.g.  60/attenuation
<moduleId>/<key>/<channel>    e.g.  60/channel_trim/2     (per-channel params)
system/<key>                  e.g.  system/sample_rate    (system params)

Add your own prefix downstream (a change node, or the topic on your mqtt out).

Units and enums

  • dB (attenuation, channel_trim): you work in dB; the library converts to the device's internal tenths and clamps to the advertised range.
  • bool (mute, many system flags): true/false.
  • enum (roll_off_filter, out_max_level, sync_source, sample_rate, …): send either the label string or the integer — e.g. setting out_max_level accepts "+24 dBu" or 1; sample_rate accepts "48 kHz" or 48000. Label matching is case-insensitive, and the editor lists the allowed labels for the selected parameter.
  • int / string / celsius / percent / seconds / datetime: system telemetry units.

The nodes

merging-config (Device)

The connection. Fields: Host (IP or hostname) and an optional CometD path (default /cometd/handshake). One per device. Not placed in a flow — it's the shared config the other nodes reference.

merging-set

Sets a parameter. Two modes:

By parameter (default) — choose from cascading dropdowns populated live from your device:

  • Module — an audio module or the System pseudo-module.
  • Parameter — the keys available on that module (system params are grouped by section).
  • Channel — appears only for per-channel params like channel_trim, as a 1-based dropdown bounded to the module's channel count.

The value to set comes from msg.payload (or msg.payload.value if you send an object). For enum parameters the editor shows the accepted labels.

An incoming message can override the target, so one node can drive anything dynamically:

msg = { moduleId: 60, param: "attenuation", payload: -18 };          // audio
msg = { moduleId: 60, param: "channel_trim", channelIndex: 2, payload: -3 };
msg = { moduleId: "system", param: "sample_rate", payload: "48 kHz" }; // system

Raw mode — a different operation: publishes msg.path + msg.value straight to the device (no unit conversion, no clamping), for parameters not yet modelled.

merging-set does not emit a confirmation. Wire a merging-state node to observe the device's echo (the change comes back through it like any other).

merging-state

The read side. It emits messages two ways:

On change (streaming). Whenever any in-scope parameter changes — from any source: a physical knob, the web UI, the front panel, or your own merging-set — it emits a message (<moduleId>/<key> or system/<key>). This is edge-driven; nothing is emitted while values sit still.

On connect (snapshot). With the On connect option (default on), when the device connects — or when you deploy the flow while it's already connected — the node emits the current value of everything in its scope as a one-time snapshot. Each snapshot message carries retained: true but is otherwise identical to a live change, so downstream logic treats them uniformly (this is the MQTT "retained message" pattern). Turn it off to emit on change only.

Scope. Filter by Module (a specific audio module, the System pseudo-module, or all) and Parameter (a specific key or all). System telemetry also flows under "all modules".

This is the reactive read node. Its only input action is msg.query = "refresh" (below). For ad-hoc snapshots of state — the whole tree, a subtree, or the system/catalog as one object — use the merging-inspect node instead.

Live telemetry (temperature, uptime, PTP lock/jitter)

As noted under How the device talks, system values only appear in a whole-tree snapshot, and the streaming output only fires on a change. So to read one live — a stable value like sample_rate, or read-only telemetry like temperature/uptimepoll a refresh on an interval:

inject  (Repeat: interval, e.g. every 5 s;  msg.query = "refresh")
   │
   ▼
merging-state  (Module: System;  Parameter: sample_rate / temperature / uptime — or "all")
   │
   ▼
your dashboard / chart / mqtt out

Each refresh re-reads the device and emits the current value(s) of whatever the node is scoped to — changed or not — as system/<key> messages marked retained: true. One polling inject refreshes the whole device, so other state nodes get fresh readings too. (For a one-shot whole-snapshot object, use merging-inspect with Fresh on.)

merging-inspect

The imperative read node — for looking at device state on demand. Send it any message and it replies once with a snapshot (pull, not stream). Return picks the shape:

  • Raw subtree (by path) — a slice of the device's native state tree at Path (a dotted/$ path: $ for the whole tree, or e.g. network.PTP.Status, identity, _modules). This is the only way to reach parts the model doesn't cover — RTP sessions/SDPs, network, NMOS, _connections, factory info. msg.topic = the path, msg.payload = the raw value.

  • System (modeled object) — the system snapshot keyed by name (so msg.payload.sample_rate.value), because system keys are unique:

    { sample_rate: { value: "44.1 kHz", raw: 44100, unit: "Hz", group: "Clock", settable: true, readonly: false, note: null },
      ptp_lock_status: { value: 3, raw: 3, unit: "int", group: "PTP", settable: false, readonly: true, note: null }, … }
  • Catalog (modeled list) — the discovered audio parameters as an array (units, ranges, enum labels, settable flags) — for enumerating what the device offers.

Fresh re-reads the device first (one round-trip) so the reply is current; otherwise it returns the last-known state instantly. An incoming message can override msg.mode (subtree/system/catalog), msg.path, and msg.fresh, so one node serves different reads dynamically.

state vs inspect. merging-state is reactive — it pushes you scoped changes as they happen (and refresh re-emits its scope). merging-inspect is imperative — you ask, it returns one snapshot. Use state to track/echo live values; use inspect to grab a chunk of state when you need it.

merging-device

Emits online / offline as a message once per reachability transition (edge-triggered, de-duplicated), with retain: true and a reason. Topic is configurable (default device). Wire it to MQTT for a "device present?" light, or to trigger re-sync logic. It also re-emits the current state once on deploy so a fresh consumer learns the truth.

The set and state nodes already show online/offline as their status dot in the editor; merging-device is only needed when you want that state as a message.


Message reference

merging-state — on change / on connect:

audio:   topic = "<moduleId>/<key>"  or  "<moduleId>/<key>/<channel>"
         payload = value             (dB / bool / int)
         + moduleId, key, raw, unit  (+ channelIndex for per-channel)
         + retained: true            (on-connect snapshot only)

system:  topic = "system/<key>"
         payload = value             (string label / number / bool)
         + system: true, key, raw, unit, group, readonly, settable
         + retained: true            (on-connect snapshot only)

merging-set — input:

payload  = the value (or { value })           required
moduleId = audio module id, or "system"        optional override
param    = parameter key                       optional override
channelIndex = 0-based channel                 optional override (per-channel params)
# Raw mode: path + value sent verbatim

merging-inspect — one reply per input:

input:  msg.mode  = "subtree" | "system" | "catalog"   (optional override of the node)
        msg.path  = dotted/$ path                        (subtree mode)
        msg.fresh = true|false                           (re-read device first?)
reply:  subtree → topic = <path> (or "tree"),  payload = raw subtree
        system  → topic = "system",            payload = object keyed by name
        catalog → topic = "catalog",           payload = array of entries

merging-device — output:

topic   = "device" (configurable)
payload = "online" | "offline"
retain  = true
reason  = string | null

Examples

Mirror device volume to MQTT, and set it from MQTT

  • mqtt in (your control topic) → merging-set (Module D/A 1, Parameter attenuation) — sets output gain in dB from msg.payload.
  • merging-state (Module D/A 1, Parameter attenuation) → change node (set msg.topic to your path) → mqtt out — publishes the device's echo so your UI and any DSP fader track the real value, no matter who changed it.

Dashboard of clock + health

Scope a state node to System (leave On connect on) for clock/PTP settings on connect and as they change. For read-only telemetry that drifts (temperature, uptime, PTP lock/jitter), drive it with an interval inject sending msg.query = "refresh" — see Live telemetry above.

Inspect device state on demand

  • inject (a button) → merging-inspect (Return: Raw subtree, Path: sessions, Fresh on) → debug — dump the live RTP sources/sinks and their SDPs (data the model doesn't expose). Swap Path for network.PTP.Status, identity, or $ for the whole tree.
  • For modeled state, set Return to System and read msg.payload.sample_rate.value (etc.) in a function node; or Catalog to enumerate every settable audio parameter and its range.

"Device present?" indicator

  • merging-device (topic device) → mqtt out → a Stream Deck button that lights when the unit is powered on.

Troubleshooting

  • Editor shows "Loading device parameters…". When you open a set/state node, it fetches the device's parameter list and only reveals the Module/Parameter fields once it has them — so you never pick from a stale or half-loaded list. If the device is unreachable, the set node falls back to a built-in parameter list (so you can still configure offline); the state node waits until it can reach the device. Either way, Refresh re-pulls. The result is memoized for the editor session, so reopening is instant; the hardware's module/parameter set only changes if it's physically re-carded.
  • EBADENGINE when installing via the palette (newer Node). Node-RED's installer uses --engine-strict, and a transitive dependency may not yet declare your Node version. Cleanest fix: run Node-RED on Node 20/22. Quick unblock: install the tarball from a shell without engine-strict (npm install <tarball>).
  • A system write didn't take. Some system params have conditions — e.g. PTP priorities only apply when ptp_manual_master is true (the editor notes this).
  • Audio glitched when I changed sample rate / frame size / clock source. Expected — those re-clock the device. Change them with audio stopped.
  • I set a dB value and it didn't go as low/high as I asked. Values are clamped to the device's advertised range for that parameter.

Relationship to merging-ravenna

This package is a thin Node-RED layer over the merging-ravenna library, which does the real work: the resilient session, parameter discovery (the catalog), unit conversion, the system-domain read/write model, and device-fingerprinting tools. If you want the same control from plain Node.js (no Node-RED), use the library directly.

The package keywords include hapi, horus, and anubis so it stays discoverable by device name, even though it's family-generic.

License

MIT