OneIP

Streaming endpoints, stream sources and statistics.

Releases up to 5.11 named this API after the firmware’s internal names: V2IP for OneIP and FPGA for the video processor, as in device.v2ip_settings or V2IPFpgaFeature. Those spellings still work, with a DeprecationWarning, and will be removed in a future major release; see the API reference.

Pulse-Eight OneIP Devices

Pulse-Eight OneIP HDMI-over-IP devices expose additional streaming properties:

# stream source addresses
if device.is_oneip and device.oneip_sources:
    for source in device.oneip_sources:
        print(f"Video: {source.video.ip}:{source.video.port}")
        print(f"Audio: {source.audio.ip}:{source.audio.port}")

# stream details (encoder/decoder config)
# A configuration frame names its subject in the payload, so a controller's
# write for a transceiver reaches the transceiver's record - the controller's
# own stays empty, which is what the device always meant.
if device.oneip_details:
    details = device.oneip_details
    print(f"Video: {details.video}")
    print(f"TX rate: {details.tx_rate}")   # None when the sender offered no rate

    # per-stream DSCP marking (None from peers that predate it)
    if details.dscp and details.dscp.complete:
        print(f"DSCP: {details.dscp}")     # video / audio / anc, 0..63

# streaming statistics
# The subscription lapses 60s after the request and there is no free-running
# mode, so call this again inside the minute to keep the 1Hz reports coming.
await device.read_stats(enable=True)   # start collecting
# ... later ...
stats = device.oneip_stats

# what the sink's decoder recovered from the codestream it is being given.
# Three answers, and they mean different things:
if stats.decoder is None:
    pass                                  # sender predates MatrixOS 10.12.46
elif stats.decoder.reading is None:
    pass                                  # decoder has never answered
else:
    reading = stats.decoder.reading
    # Geometry is what says a picture was recovered. `format` never does: with
    # no stream it reads RGB, which is indistinguishable from a real RGB source.
    if reading.recovered:
        print(f"{reading.width}x{reading.height} {reading.format}")
    # `reason` for display, `causes` for logic. Every true cause sets its bit
    # in flags; which one keeps `reason` is a fixed priority in the video
    # processor, not the numbering. TX_BRIDGE_UNLOCKED ranks below every
    # input-side cause, so a repeating pipeline restart shows as bit 9 in
    # `causes` while `reason` names the input cause instead.
    print(f"{reading.reason}")            # None for a cause this build cannot name
    print(f"{reading.causes}")            # raw reason values, lowest first

Trusting a decoder reading after a switch

The values are read off the video processor every 2s and reported every 1s, latched between reads, so roughly every other report repeats a reading already seen — a frame arriving says nothing about freshness. reading.updates counts readings actually stored, so it stays still while a processor is stalled rather than implying a refresh. It is monotonic, never reset, and wraps at 65535 (~36h).

After changing what a sink is pointed at, wait for updates to advance by two before trusting the geometry. It ticks when the reply lands, not when the query is sent, so a single tick can carry an answer read fractionally before the switch.

before = device.oneip_stats.decoder.reading.updates
await sink.switch_source(...)
# ... wait until reading.updates - before >= 2 (mod 65536) ...

Colour depth is deliberately absent: the video processor answers depth from a driver constant rather than from the codestream, so it was withheld rather than shipped as a constant that looks like a measurement. Assert depth at the encoder’s input bay instead.

What this block cannot tell you

Whether a sink is off on purpose. A sink someone deliberately disabled reports NO_PACKETS, indefinitely — the same cause as a sink that should be receiving and is not. There is no enablement field here, so read that from MXR_OP_V2IP_DEVICE_CFG or the device’s HTTP status. IDLE sounds like the answer and is not: it is effectively unreachable in shipping firmware.

Whether a single sample is representative. reason during a teardown is a sequence, not a state — a measured one passed through SWITCH_PENDING before settling on NO_PACKETS six seconds later. Use updates to tell a fresh reading from a repeat, and note it cannot see a TX_BRIDGE_UNLOCKED flag carried forward across a format change: a value held over is a stored reading like any other.

Sink output scaling

A sink scales for two independent reasons: automatic scaling, and a configured output mode. Each is written behind its own validity bit, so moving one leaves the other where it is.

# what the sink reports, read only from a device whose hello carries
# CONFIG_INITIALISED - firmware without it builds the block over uninitialised
# stack, where the valid bit itself is noise. No released build announces it.
scaling = device.oneip_details.scaling
print(scaling.auto_scaling)        # None when the sender did not say
print(scaling.configured_mode)     # (signal type, refresh), None when it has none

# a mode is given as a depth and a colour space, never as a packed signal-type
# word read back off the wire: a sink with no mode reports the word carrying the
# index for "no depth", which a receiver decodes to zero and drops in silence.
mode = mx_remote.OneIPOutputMode(svd=16, depth=12,
                                colour=mx_remote.VideoColourSpace.YUV422, refresh=60)

await device.set_oneip_auto_scaling(False)   # turn it off before setting a mode
await device.set_oneip_output_mode(mode)
await device.set_oneip_auto_scaling(True)    # back on, if that is what you want

await device.clear_oneip_output_mode()       # the only way to say "no mode"

Set the mode with automatic scaling off. A sink scaling automatically refuses a mode whose format the attached display does not list, and refuses it in silence. Setting the mode first and turning automatic scaling back on afterwards is the order that survives, because the mode is checked while automatic scaling is still off.

These are refused for a unit below protocol 0x2F: released firmware drops a scaling write from management.

Nothing acknowledges any of these. set_oneip_output_mode returns False for a mode no sink would take, but a True only says the frame went out: the sink weighs the format against the display’s EDID and against what its own output stage can produce, and refuses silently either way. Read oneip_details.scaling back on the device’s next report to learn what it did.

A scaling change makes the device rebuild and rebroadcast its sink block, so expect device.oneip_sink to read empty for a moment afterwards - see what an empty sink block means.

What an empty sink block means

device.oneip_sink addresses that read as unset mean “no route, or the sink could not work one out”, never “definitely not subscribed”. This is the one part of a device configuration with no validity marker of its own, so a sink with nothing to say sends zeros and the library stores them. A sender leaves it empty whenever its own stream configuration does not resolve, which covers more than having no route: a selected source whose record has not arrived yet, the state after a restart at either end, missing audio bay configuration, or a stream failing its validity check.

Expect that reading rather than guarding against it. Any scaling change rebuilds and rebroadcasts the block, so an empty reading turns up most often during exactly the no-signal troubleshooting that prompted the change. A device’s periodic report puts a real route back within a minute of it having one, so wait one out rather than treat the first empty reading as an answer.

The library caches an empty reading rather than dropping it. A sink that has genuinely lost its route sends the same zeros, and so does every report after it, so refusing them would hold a route that nothing later could clear.

Only the device’s own report sets the block. A controller writing another device’s configuration sends it zeroed, and the library ignores that copy.

Device settings

A OneIP device reports its settings in its configuration: decoder auto-disable, HDMI off without signal, IR modulation, the status and network LEDs, quiet fan, CEC combo keys, and the infrared profiles. Each sits behind its own bit, so a device reports only the settings it has, and a write changes one without restating the rest.

from mx_remote import OneIPDeviceSetting

settings = device.oneip_settings             # None until the device has reported
print(settings.get(OneIPDeviceSetting.FAN_QUIET))   # None: the device lacks it
print(settings.ir_profile, settings.ir_profile_sink)

await device.set_oneip_setting(OneIPDeviceSetting.STATUS_LED, False)
await device.set_oneip_ir_profile(2)
await device.set_oneip_sink_ir_profile(mx_remote.ONEIP_IR_PROFILE_NOT_SET)  # follow the global port

A write returns False, without sending anything, for whatever the device would ignore in silence: a setting it has not reported, a profile out of range, or a device that has reported no settings at all. So a True is never a change that does not happen, though nothing acknowledges it either: the device answers by reporting its settings, and until then oneip_settings reads back what was written.

A write from another controller is cached only as far as the device takes it. The list of stored infrared profiles and whether the device’s clock is set are the device’s own, and are never taken from a write.

Power save

A device powers down by itself after a number of idle minutes, and during a daily window per weekday, kept in the device’s own time zone. Times are minutes after midnight, Monday first; a window that ends before it starts runs past midnight, and one that ends where it starts means none that day.

from mx_remote import OneIPPowerSaveSchedule

print(settings.auto_power_save)            # idle minutes, 0 for never
print(settings.power_save_schedule)        # e.g. "mon 22:00-07:00, ..."
print(settings.get(OneIPDeviceSetting.CLOCK_SET))

await device.set_oneip_auto_power_save(30)
await device.set_oneip_power_save_schedule(OneIPPowerSaveSchedule(
    start=(22 * 60,) * 5 + (0, 0), end=(7 * 60,) * 5 + (0, 0)))  # weeknights

A schedule time that is not a time of day is refused before it is sent. A write goes out at 176 bytes: a receiver with the schedule ignores the settings of a shorter frame, and one whose settings end at the idle minutes takes it all the same.

Power save is switched on or off. Switched on, a device powers down by itself to its power save level: reduced, low or off, each powering more down than the one before. The device picks the level itself, the one that saves the most while its input and display can still wake it, and reports it. Its unit status says which levels it supports and what wakes it at each.

from mx_remote import OneIPPowerSaveLevel, UnitPowerSave

print(settings.get(OneIPDeviceSetting.POWER_SAVE_DISABLED))         # False: switched on
print(settings.power_save_level)                                    # the level it picked
print(device.unit_status.power_save_level(OneIPPowerSaveLevel.LOW))  # e.g. SUPPORTED|INPUT_WAKE

await device.set_oneip_power_save(True)
await mx.set_all_oneip_power_save(False)   # every OneIP device of the mesh

Switched off, OneIPDeviceSetting.POWER_SAVE_DISABLED is on: the device does not power down by itself, and a power save request still powers it down. Switching on is written as a mesh controller writes it, with that switch clear and the power save level at 0, so a device whose firmware still takes the level as its setting returns to its own default.

set_oneip_power_save_level() writes a level, which switches power save on. A current device ignores the level; one whose firmware predates picking it takes it. A written level is never cached, from this client or another controller: power_save_level is what the device reports. A level write without the switch only clears POWER_SAVE_DISABLED on a device that reports it. A level this library does not name is refused before it is sent. Firmware without the switch never reports it, and switching is refused there.

OneIPDeviceSetting.CLOUD is a switch for whether a device connects to the cloud service. A device that does not report it connects regardless, and the switch is refused there.

PTP

A device’s PTP mode, domain and grandmaster priority1 are settings like the others, each behind its own bit.

from mx_remote import OneIPPtpMode

print(settings.ptp_mode, settings.ptp_domain, settings.ptp_priority1)

await device.set_oneip_ptp_mode(OneIPPtpMode.AUTO)   # follow, or lead when no one does
await device.set_oneip_ptp_domain(0)                 # up to ONEIP_PTP_DOMAIN_MAX
await device.set_oneip_ptp_priority1(128)            # lower wins

A mode this library does not name and a domain above ONEIP_PTP_DOMAIN_MAX are refused before they are sent, and not cached from another controller’s write.

How the device paces its PTP messages is five settings more, whose values travel in a block of their own: the announce, sync and delay request intervals as base-2 logarithms of seconds, the announce timeout and the messages’ DSCP.

from mx_remote import OneIPDeviceSetting, OneIPPtpMessages

print(settings.ptp_messages)   # None until the device reports one of them

await device.set_oneip_ptp_messages(
    OneIPDeviceSetting.PTP_SYNC | OneIPDeviceSetting.PTP_DSCP,
    OneIPPtpMessages(log_sync_interval=-3, dscp=46))   # 125 ms, EF

The SAP expiry in minutes and announcement interval in seconds travel in the same block, behind SAP_EXPIRE and SAP_INTERVAL, and are set the same way; SAP_LISTEN and SAP_ANNOUNCE are switches like the other on/off settings. A device with an encoder reports its streams’ TTL there too, behind STREAM_TTL; a TTL of 0 is refused, as a device changes nothing for one. A device with an HDMI output reports the angle its output is turned by, clockwise, behind SINK_ROTATION, with the step its video processor turns by. It takes 0 or a multiple of that step up to ONEIP_SINK_ROTATION_MAX, so any other angle is refused before it is sent, and the step is never written. set_all_oneip_device_settings can only check the range; each device judges the step.

m = settings.ptp_messages
print(m.sink_rotation, m.sink_rotation_step)   # e.g. 0 and 90
await device.set_oneip_ptp_messages(OneIPDeviceSetting.SINK_ROTATION,
                                    OneIPPtpMessages(sink_rotation=180))

Only the values behind the bits named are sent. A DSCP above ONEIP_DSCP_MAX is refused, and not cached from another controller’s write. A write carrying one of these runs on to 224 bytes, or 232 with the TTL or the rotation; every other write keeps the 176 every device knows.

What the device’s clock is actually doing is its PTP state, which only the device reports about itself and nothing writes:

ptp = device.oneip_ptp        # None: no report yet, no PTP, or in power save
if ptp is not None:
    print(ptp.mode, ptp.domain, ptp.lock, ptp.port_state)   # port_state per IEEE 1588
    print(ptp.followed)       # locked to a master since PTP started
    print(ptp.grandmaster)    # its clock identity, None while there is none

Stereo downmix

A device whose output can mix 5.1 and 7.1 PCM down to stereo reports how, once its video processor has answered for it: a mode, a level and the percent the LFE channel is mixed in at, which a controller writes, and the state and channel count, which only the device reports. A device that reports none has no downmix to set, so a write to one is refused, as is a mode, level or LFE percent out of range. The write sends the mode, level and LFE together; the state and channels are never sent.

from mx_remote import OneIPDownmix
from mx_remote.proto.Constants import OneIPDownmixMode, OneIPDownmixLevel

downmix = device.oneip_downmix    # None: no report yet, or no downmix
if downmix is not None:
    print(downmix.mode, downmix.level, downmix.lfe)   # LFE up to ONEIP_DOWNMIX_LFE_MAX
    print(downmix.state, downmix.channels)            # the device's own
    await device.set_oneip_downmix(OneIPDownmix(mode=OneIPDownmixMode.AUTO,
                                                level=OneIPDownmixLevel.NORMALISED, lfe=50))

The downmix ends the settings block, behind a second valid word, so the write runs on to 248 bytes and carries no other setting.

VLAN

A device that announces DeviceFeature.VLAN tags its uplink by a VLAN configuration: its own VLAN id, one per external port (the SFP port, the UTP port with PoE, then the UTP port), and the port pinned as the uplink. Ids run 0 to ONEIP_VLAN_ID_MAX, 0 meaning untagged; ports are numbered from 1 on the wire, 0 meaning detect the uplink.

from mx_remote import OneIPVlan

vlan = device.oneip_vlan          # None until the device reports one
print(vlan.device, vlan.port, vlan.pinned_uplink_port, vlan.active_uplink_port)
print(vlan.is_pending, vlan.revert_s)

await device.set_oneip_vlan(OneIPVlan(device=10, port=(0, 20, 0), uplink=2))

Only the device knows what it runs, so oneip_vlan is read only from the device describing itself, and a write is not cached. The device applies a change at once and reverts it after revert_s seconds unless the mesh controller, hearing it report the change as pending, confirms it.

Only the ids, the uplink and the trunk bit are written. Refused before sending: a device without the VLAN feature or that has not reported its configuration, an id above ONEIP_VLAN_ID_MAX, an uplink that names no port, and an SFP uplink on a device that reports no SFP port. The write goes out as a settings write that carries no setting plus the block, so a receiver that predates the block sees a frame it already understood.

Test pattern, tone and lip-sync

A sink whose video processor reports OneIPVideoProcessorFeature.SINK_TEST_PATTERN draws a test card on its output: a pattern, a test tone, and a lip-sync flash that marks a frame and beeps a set number of sample periods after it.

from mx_remote import OneIPTestPattern, OneIPTestSync, OneIPTestTone, OneIPToneMode

await sink.request_oneip_testcard()                        # the sink reports straight back
await sink.set_oneip_test_pattern(OneIPTestPattern.FLAT, 0x3050A0)
await sink.set_oneip_test_tone(OneIPTestTone(mode=OneIPToneMode.CONTINUOUS,
                                           freq=1000, level=-20, channels=2, rate=48000))
await sink.set_oneip_test_sync(OneIPTestSync(period=60, lead=0, offset=0, beep_ms=40))
await sink.set_oneip_test_pattern(OneIPTestPattern.OFF)

print(sink.oneip_testcard)   # None until the sink has reported

A pattern runs until it is turned off, and holds the output on while it does. The sink answers every request and change with its test card, on the group, so every client records it against the sink; a write is not cached. A sink with the feature but without the module that draws the test card does not answer.

Each value is checked against the ranges the sink accepts, since it drops what falls outside them in silence: see OneIPTestTone.is_valid() and OneIPTestSync.is_valid(). A tone turned off goes out whatever else it holds, as the sink stops it regardless. The frame needs protocol 0x2B, so a sink announcing less is refused as well.

Session descriptions

An output plays an AES67 stream from a session description it is handed, and reports what it plays when that changes and at least once a minute.

await sink.play_oneip_sdp(sdp_text)     # sent in 1 KB pages, played once the last is in
await sink.set_oneip_sdp_screen(1)      # what it shows while it plays audio alone
await sink.request_oneip_sink_sdp()     # report now
await sink.stop_oneip_sdp()

print(sink.oneip_sink_sdp)   # None until the output has reported

An output takes these only from its mesh controller or a management application, which this client announces itself as, and only from protocol 0x2E. A description is 1 to ONEIP_SDP_MAX bytes: an output drops a longer one after its last page, so one is refused before it is sent. Nothing acknowledges the pages; oneip_sink_sdp reads what the output reports next.

Overlay sync

A management unit can have the other units copy its overlay captions, logo choice and place, backgrounds and logo images. Watching one between units:

print(controller.osd_sync)         # the last sync it sent
print(unit.osd_sync_report)        # taking, done, failed or deferred, and why

This client can send one too, which every unit then takes:

from mx_remote import OsdCaption, OsdSyncContent, OsdSyncEntry, OsdSyncImage
from mx_remote.proto.Constants import OsdCaptionField, OsdSyncWhat

seq = await mx.send_osd_sync(OsdSyncContent(
    what=OsdSyncWhat.CAPTIONS,
    entries=(OsdSyncEntry(value=OsdCaption(text='Lobby'), set=OsdCaptionField.TEXT),),
    images=None))                  # or four OsdSyncImage, served by this client
await mx.resend_osd_sync(unit)     # one unit again
await mx.clear_osd_sync()          # stop answering for it

Each unit asks for the settings it lacks and takes only those, so this client holds the content and answers until it is replaced or cleared, announcing itself meanwhile as the mesh’s overlay sync manager: a unit waking from power save then takes this content rather than the controller’s. Logo images are served over HTTP from this client, on logo_port or a free port. Content a unit would refuse is refused before anything is sent; see send_osd_sync.

Overlay messages

A unit shows a message on its output, over the picture, until it restarts. The text is markup of at most OSD_MESSAGE_TEXT_MAX bytes with no control character, shown with a logo: 0 the built-in one, -1 none, 1 to 5 a slot. Its priority places it against the unit’s own messages, such as Locate Device.

from mx_remote.proto.Constants import OsdMessagePriority

await sink.show_osd_message('Back at <b>3</b>', logo=-1, priority=OsdMessagePriority.TOP)
await sink.request_osd_message()   # the unit answers with what it shows
await sink.clear_osd_message()

print(sink.osd_message)   # None until the unit has answered

A unit takes a message only from management, which this client announces itself as, and answers every write and request with the message it then has; on_osd_message_changed reports a change. The answer carries OsdMessageFlag.REFUSED when the unit did not take the write, as when its video processor has no message layer. A text, logo or priority a unit would refuse is refused before it is sent, as is a device that is not a OneIP output.

Stream encryption

A source unit from protocol 0x30 encrypts its video, audio and ancillary streams, and reports what a receiver needs to decrypt them whenever that changes and at least once a minute. The key itself never travels: every unit of a mesh holds the same one, and this library holds none.

await source.request_oneip_privacy()    # report now
await mx.request_all_oneip_privacy()    # every source unit of the mesh

print(source.oneip_privacy)   # None until the unit has reported

A request to a unit that is not a source, or that is below protocol 0x30, is refused before it is sent.

Every device at once

One broadcast changes settings on every OneIP device of the mesh. Each device applies the settings it has and ignores the rest, and none below protocol 0x2A applies any.

from mx_remote import OneIPDeviceSettings

await mx.set_all_oneip_device_settings(OneIPDeviceSettings(
    valid=OneIPDeviceSetting.STATUS_LED | OneIPDeviceSetting.AUTO_POWER_SAVE,
    flags=OneIPDeviceSetting.STATUS_LED,      # the LED on
    auto_power_save=30))

Nothing is cached from it: each device that applies a change reports its settings, and its oneip_settings reads that. What every device would ignore is refused rather than sent - no setting at all, one only a device reports about itself, a profile, PTP mode or PTP domain out of range, or a schedule time that is not a time of day.

Unit status and commands

A unit on protocol 0x2C reports its health and IP configuration with its periodic broadcast, and within a few seconds of a change to its flags, status or addresses.

from mx_remote import UnitFlag

status = device.unit_status     # None until the unit has reported
print(status.uptime, status.boot_count, status.reboot_count)
print(status.network)           # address, netmask, gateway, two DNS servers
print(status.fan_rpm, status.temperatures, status.status_message)
print(UnitFlag.DHCP in status.flags, status.network_pending)

A unit takes a command only from its mesh controller or a management application, which this client announces itself as:

from mx_remote import UnitNetConfig

await device.clear_unit_status()
await device.upgrade_unit_video_processor(force=False)
await device.set_unit_network(UnitNetConfig(
    ip='10.1.2.3', netmask='255.255.255.0', gateway='10.1.2.254', dns1='10.1.2.254'))
await device.confirm_unit_network()    # keep it, once the unit reports it
await device.set_unit_dhcp()

A changed IP configuration applies at once and reverts unless the mesh controller confirms it in time; status.network_pending says whether it still waits. Each command returns False without sending for a device that is not a OneIP unit, one below 0x2C, or an address and netmask a unit cannot hold. Nothing acknowledges a command: unit_status reads what the unit reports next.

Mesh and firmware

# mesh operations
await device.mesh_promote()   # promote to mesh master
await device.mesh_remove()    # remove from mesh
await source.auto_assign_oneip_source_addresses()  # drop addresses set by hand

# firmware versions
if device.oneip_firmware_versions:
    for fw_type, fw in device.oneip_firmware_versions.items():
        print(f"{fw_type}: {fw.version}")

A OneIP source acts on auto_assign_oneip_source_addresses() only from management, which this client announces itself as, and nothing acknowledges it: the source’s next configuration report carries the addresses it ends up with, in oneip_details. A device that is not a source, or announces less than protocol 0x2B, would ignore the operation, so it is refused before anything is sent.

Time zone and time

The mesh controller announces its time zone and clock with every periodic broadcast, and each device keeps its clock and power save windows by them. Both are recorded against the device that announced them.

print(controller.time_zone)   # TimeZone(zone='Europe/Amsterdam', rule='CET-1CEST,...')
print(controller.clock)       # its clock as of now, None until it has announced one

await mx.set_mesh_time_zone('Europe/Amsterdam', 'CET-1CEST,M3.5.0,M10.5.0/3')
await mx.clear_mesh_time_zone() # no time zone: the devices keep UTC
await mx.set_mesh_time()      # now; or pass a datetime

A device takes either only from the controller or a management application, which is what this client announces itself as; the controller takes them too and announces them from then on. A device keeps its own clock where it is within 2s of the time sent. A name or rule that is empty, holds a NUL or is too long for its field, and a time before 1970 or past 2106, are refused. A controller without a time zone announces an empty one.


Documentation index | Project README