Skip to content

Repository files navigation

MX Remote - Rust Client for Pulse-Eight MatrixOS devices

crates.io docs.rs CI

A client for Pulse-Eight AV distribution hardware: video and audio matrices, HDMI-over-IP encoders and decoders, multiviewers and 8-zone amplifiers, all driven over the local network. It covers discovery, video and audio routing, volume, remote-control key passthrough, HDMI-over-IP streaming and multiviewer control.

If you want to drive Pulse-Eight neo, OneIP or ProAmp8 hardware from your own software or from a home automation system, this is the library for it.

Documentation: docs.rs/mx-remote, the API reference generated from the source, with the C ABI at docs.rs/mx-remote-ffi.

cargo add mx-remote

Both crates are on crates.io, so there is nothing here to clone or vendor: mx-remote for Rust, and mx-remote-ffi for the C ABI, which carries mx_remote.h and mx_remote.hpp in the package.

What is MX Remote?

MX Remote is the protocol these devices use to discover and control one another over UDP, by multicast or by broadcast. They all run the same MatrixOS firmware, which speaks it natively. This library is a client implementation of that protocol, written to open the devices up to third-party software.

Devices announce themselves and their bays, report signal, audio, streaming and power state as it changes, and accept routing and configuration commands. The library discovers them, keeps a snapshot of what they have reported, and sends those commands.

Supported devices

  • neo: HDBaseT video and audio matrices. The neo:4, neo:8 and neo:X, and the splitters.
  • OneIP: HDMI-over-IP units. Transmitter (TX), Receiver (RX), Transceiver (TZ) and Multiviewer.
  • ProAmp8: an 8-zone audio amplifier with Dolby decoding.

Three ways to use it

All three sit on the same core:

Consumer What you get
Rust the mx-remote crate
C libmx_remote_ffi.a and include/mx_remote.h
C++ the same archive, plus include/mx_remote.hpp

Other languages

Two other clients speak the same protocol, each in its own repository:

The three are independent implementations rather than bindings over a shared core.

Rust

use std::sync::{Arc, OnceLock};

use mx_remote::{Config, DeviceUid, EventHandler, Remote};

static CLIENT: OnceLock<Arc<Remote>> = OnceLock::new();

struct Printer;

impl EventHandler for Printer {
    fn on_device_update(&self, device: DeviceUid) {
        let Some(info) = CLIENT.get().and_then(|c| c.device(device)) else {
            return;
        };
        println!("{device} {} {}", info.model, info.name);
    }
}

let remote = Arc::new(Remote::new(Config::default(), Arc::new(Printer))?);
let _ = CLIENT.set(Arc::clone(&remote));
remote.start()?;

A handler is handed to the client that will call it, so it cannot hold one at the time it is built; the example fills the client in before starting, which is before anything can call back.

Events say what moved, and the snapshot beside them says what it moved to. Handler methods run on the receive thread, so they should return quickly.

cargo run --example discover is the same program, complete.

C

cargo build -p mx-remote-ffi --release
cc -Iinclude prog.c target/release/libmx_remote_ffi.a -lpthread -ldl -lm

The archive has no runtime to initialise and takes no signal handlers from the host process. What it needs from the platform is the toolchain's to decide, and

cargo rustc -p mx-remote-ffi --release --crate-type staticlib -- \
    --print native-static-libs

prints the current list for the target being built. A link that fails for a missing symbol is asking for one of those.

On Windows with MSVC, build the archive against the same C runtime the program linking it uses, or the two disagree over the runtime at link time. A program on the static CRT (/MT) wants

set RUSTFLAGS=-Ctarget-feature=+crt-static
cargo build -p mx-remote-ffi --release

and links mx_remote_ffi.lib alongside the libraries that same command names. CI builds and prints both, so what it reports is what the release archive needs.

#include <mx_remote.h>

/* The client cannot be its own userdata: it does not exist when the table is
 * handed over, so it reaches the callbacks through a struct that does. */
struct app { mxr_remote_t *remote; };

static void on_device_update(void *userdata, mxr_uid_t device) {
    mxr_device_info_t info;
    if (mxr_device(((struct app *)userdata)->remote, device, &info) == MXR_OK)
        printf("%s %s\n", info.model, info.name);
}

int main(void) {
    struct app app = {0};
    mxr_callbacks_t cb = {0};
    cb.on_device_update = on_device_update;

    /* Zeroing a config asks for every default; NULL does the same. */
    app.remote = mxr_remote_new(NULL, &cb, &app);
    mxr_remote_start(app.remote);
    /* ... */
    mxr_remote_free(app.remote);
    return 0;
}

Three conventions run through the header, and knowing them is most of knowing the API:

  • A device is addressed by value. There is no handle for a device or a bay: a device is an mxr_uid_t, a bay is an mxr_bay_uid_t, and state is read by passing one in and having a caller-owned struct filled out.
  • Every call returns, whatever happens. A panic becomes MXR_ERR_PANIC; nothing unwinds across the boundary.
  • A borrowed pointer lives for one call. Strings and arrays handed to a callback point into memory the library owns and reuses.

Every failure code is negative and MXR_OK is zero, so if (rc < 0) is a complete test.

include/mx_remote.h is generated from the Rust source by scripts/gen-header.sh and checked in; CI fails if regenerating it produces a diff. That diff only proves the header is what cbindgen produces, and cbindgen expands no macros, so an entry point that comes out of one is in the archive and absent from the header. scripts/check-abi.sh catches that by reading the archive's exports directly.

C++

include/mx_remote.hpp is a header-only layer over the same archive: a move-only mxr::Remote that closes and joins in its destructor, mxr::Uid and mxr::BayUid value types, and an mxr::Handler base class with a virtual method per event.

#include <mx_remote.hpp>

class Printer : public mxr::Handler {
public:
    mxr::Remote *remote = nullptr;

    void on_device_update(mxr::Uid device) override {
        mxr_device_info_t info;
        if (mxr_device(remote->get(), device, &info) == MXR_OK)
            std::cout << info.model << ' ' << info.name << '\n';
    }
};

Printer printer;                          // declared first, destroyed last
mxr::Remote remote = mxr::Remote::open(nullptr, &printer);
printer.remote = &remote;
remote.start();

Declare the handler before the client: the client's destructor joins the threads that call into the handler, so the handler has to be destroyed second.

examples/c and examples/cpp hold the complete versions of both, built by make -C examples.

Requirements

  • Rust 1.79 or newer, edition 2021. CI builds on that version and on stable.
  • Linux, macOS or Windows. Selecting an interface that has no address of its own, such as a tagged VLAN, is Linux-only.
  • For the C and C++ headers: any C99 and C++11 compiler.

Layout

mx-remote/          the core crate: wire format, runtime, control surface
mx-remote-ffi/      the C ABI over it, and the only unsafe in the workspace
include/            the generated C header and the hand-written C++ one
examples/           the C and C++ examples; the Rust one is a cargo example
scripts/            gen-header.sh and check-abi.sh, which the build below runs

Building

cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings

./scripts/gen-header.sh && git diff --exit-code include/mx_remote.h
cargo build -p mx-remote-ffi && make -C examples
./scripts/check-abi.sh

Licence

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Rust client library for Pulse-Eight MatrixOS devices over UDP multicast/broadcast, with a C ABI

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages