######################################################
## MX Remote Python Interface ##
## ##
## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
######################################################
'''Protocol constants, feature flags, and enumeration types for the MX Remote binary protocol.'''
from enum import IntEnum, IntFlag
from typing import TypeVar
MXR_PROTOCOL_VERSION = 0x30
"""Highest mx_remote protocol version this library understands.
Mirrors the firmware's own protocol version. Bump
when adding RX support for an opcode revision; transmitters stamp the
per-opcode minimum from MXR_OPCODE_VERSIONS so older peers still parse the
frame.
This is also a receive ceiling: Remote.process_frame drops any frame stamped
above it, unparsed. So it has to reach the stamp of every layout revision
decoded below, or that revision goes silent on the mesh rather than noisy -
0x29 is what a MatrixOS 10.12.46 unit stamps on V2IP_STATS.
0x2A is also what MatrixOS reads a hello by: a device on 0x2A or later is given
60s to hello again rather than 15s, and a unit only slows its own hello to one
every 20-30s once every device it knows is on 0x2A.
0x2B is the row of V2IP_TESTCARD, so a sink's test card report arrives stamped
0x2B.
0x2E is the row of V2IP_SINK_SDP, so an output's session description report
arrives stamped 0x2E, 0x2F the row of V2IP_OSD_SYNC and 0x30 the row of
V2IP_PRIVACY."""
MXR_PROTOCOL_VERSION_SLOW_HELLO = 0x2A
"""First protocol version whose devices may hello only every 20-30s."""
MXR_OPCODE_VERSIONS: dict[int, int] = {
0x00: 0x01, # SYS_HELLO
0x01: 0x01, # SYS_DISCOVER
0x02: 0x01, # SYS_BAY_CONFIG
0x03: 0x01, # SYS_LINKS
0x04: 0x1B, # DEV_CONNECT
0x05: 0x01, # DEV_POWER_CHANGE
0x06: 0x01, # DEV_SIGNAL_OLD
0x07: 0x01, # DEV_EDID
0x08: 0x01, # MX_ROUTE
0x09: 0x01, # MX_SET_ROUTE
0x0A: 0x19, # RC_IR
0x0B: 0x01, # RC_KEY
0x0C: 0x0C, # RC_TX_KEY
0x0D: 0x01, # RC_ACTION
0x0E: 0x0C, # RC_TX_ACTION
0x0F: 0x01, # AUDIO_VOLUME_UP
0x10: 0x01, # AUDIO_VOLUME_DOWN
0x11: 0x01, # AUDIO_CLIP
0x12: 0x01, # AUDIO_VOLUME_MUTE
0x13: 0x01, # AUDIO_SET_ROUTE
0x14: 0x11, # AUDIO_SET_VOLUME
0x15: 0x01, # SYS_TEMPERATURE
0x16: 0x01, # PDU_STATE
0x17: 0x01, # CEC_RAW_RX
0x18: 0x01, # CEC_RAW_TX
0x19: 0x01, # CEC_SET_TYPE
0x1A: 0x01, # CEC_GET_TYPE
0x1B: 0x01, # CEC_SET_ADDRESS
0x1C: 0x01, # CEC_GET_ADDRESS
0x1D: 0x01, # CEC_GET_BUS_STATUS
0x1E: 0x01, # CEC_GET_DEVICE_STATUS
0x1F: 0x06, # V2IP_SOURCE_SWITCH
0x20: 0x06, # V2IP_LINK_REMOTE
0x21: 0x06, # V2IP_DETECT_BAYS
0x22: 0x06, # CHANGE_BAY_NAME
0x23: 0x07, # SYS_BAY_CONFIG_SECONDARY
0x24: 0x07, # V2IP_MANUAL_SOURCE_SWITCH
0x25: 0x00, # RESERVED_0 (retired opcode; never reused)
0x26: 0x09, # SYS_BAY_V2IP_SOURCES
0x27: 0x06, # BAY_HIDE
0x28: 0x01, # SYS_REBOOT
0x29: 0x22, # NET_LINK_STATUS
0x2A: 0x06, # FIRMWARE_VERSION
0x2B: 0x01, # SYS_MONITORING_PULSE
0x2C: 0x06, # V2IP_UPGRADE_FPGA
0x2D: 0x02, # VIDEO_CLOCK_RATE_OLD
0x2E: 0x06, # V2IP_BLIST_REGISTER
0x2F: 0x06, # V2IP_BLIST_UNREGISTER
0x30: 0x06, # TOPOLOGY
0x31: 0x06, # BAY_SIGNAL_STATUS
0x32: 0x06, # BAY_MIRROR_STATUS
0x33: 0x00, # RESERVED_1 (retired opcode; never reused)
0x34: 0x08, # BAY_EDID_PROFILE
0x35: 0x0A, # SETUP_STATUS
0x36: 0x0B, # SET_MASTER
0x37: 0x0C, # SET_INSTALLER
0x38: 0x0E, # BAY_FILTER_STATUS
0x39: 0x0F, # BAY_STATUS
0x3A: 0x0F, # SYS_FACTORY_RESET
0x3B: 0x1D, # MESH_OPERATION
0x3C: 0x11, # V2IP_DEVICE_CFG
0x3D: 0x1C, # AMP_ZONE_SETTINGS
0x3E: 0x1C, # AMP_DOLBY_STATE
0x3F: 0x13, # V2IP_STATS
0x40: 0x14, # V2IP_TILING
0x41: 0x15, # V2IP_POWER_SAVE
0x42: 0x16, # V2IP_MULTIVIEWER
0x43: 0x1A, # V2IP_AUDIO
0x44: 0x1C, # V2IP_BAY_MAPPINGS
0x45: 0x1D, # RC_SETTINGS
0x46: 0x1E, # SYSSTATUS
0x47: 0x1F, # DEBUG
0x48: 0x23, # RC_IR_TX
0x49: 0x28, # V2IP_VIDEOWALL (owned by the v2ipwall module; no MatrixOS handler)
0x4A: 0x2A, # SYS_PING
0x4B: 0x2A, # TIME_ZONE
0x4C: 0x2A, # V2IP_SETTINGS_ALL
0x4D: 0x2A, # TIME
0x4E: 0x2B, # V2IP_TESTCARD
0x4F: 0x2C, # SYS_UNIT_STATUS
0x50: 0x2C, # SYS_UNIT_COMMAND
0x51: 0x2D, # BAY_CONTENT
0x52: 0x2E, # V2IP_SINK_SDP
0x53: 0x2F, # V2IP_OSD_SYNC
0x54: 0x30, # V2IP_PRIVACY
}
"""Per-opcode minimum compatible protocol version, mirroring the firmware's
own opcode table. Transmitters stamp the
header.protocol with the value for the opcode being sent so that older
receivers correctly reject frames they cannot decode.
These are deliberately low. The receiver drops any frame whose per-opcode byte
exceeds its own MXR_PROTOCOL_VERSION, and a ProAmp8 caps at 0x22 - so an opcode
whose payload only ever grew trailing fields keeps its original version, and
new-format detection stays on payload length."""
MXR_CFG_SIGNAL_STATUS_LEN = 14
"""Width of the signal description in mxr_cfg_signal. The 2-byte signal type
follows it, so the union is 16 bytes and the description is not."""
MXR_DEVICE_NAME_LEN = 16
"""Width of the fixed-size name/serial fields on the wire.
The field carries no terminator of its own when the value fills it, so read
exactly this many bytes and only then cut at a NUL - scanning on runs into the
neighbouring struct member."""
MXR_FW_VERSION_LEN = 128
"""Width of the fixed-size firmware version name field (mxr_fw_version.name)."""
ONEIP_AUDIO_DEFAULT_SAMPLE_RATE = 48000
ONEIP_AUDIO_DEFAULT_CHANNELS = 2
ONEIP_AUDIO_MIN_CHANNELS = 1
ONEIP_AUDIO_MAX_CHANNELS = 8
ONEIP_SOURCE_RATE_MIN = 5
ONEIP_SOURCE_RATE_MAX = 100
"""Valid encoder tx rate range (firmware ONEIP_SOURCE_RATE_MIN/MAX, in units of
10Mb/s). A V2IP_DEVICE_CFG sender with no rate to offer deliberately puts a
value outside this range in ``tx_rate``; both the firmware's cache update and
its apply drop that as invalid, which is what keeps address-only and scaling
writes from resetting a peer's rate."""
def oneip_rate_valid(rate:int|None) -> bool:
'''True when a tx_rate carries an actual rate (firmware v2ip_source_rate_valid()).'''
return (rate is not None) and (ONEIP_SOURCE_RATE_MIN <= rate <= ONEIP_SOURCE_RATE_MAX)
MXR_SCALING_FLAG_MODE_VALID = (1 << 0)
MXR_SCALING_FLAG_OPTIONS_VALID = (1 << 1)
MXR_SCALING_FLAG_OPTIONS2_VALID = (1 << 4)
MXR_SCALING_FLAG_MATCH_SOURCE = (1 << 5)
MXR_SCALING_FLAG_SKIP_420 = (1 << 6)
MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
"""mxr_scaling_config.flags. The mode/refresh pair and each options group are
separately valid: a sender carrying none of the three markers is offering no
scaling at all.
Firmware that has the second group sets its marker on every configuration it
sends about itself, so that bit doubles as the report that the device has those
options at all. Bits 2 and 3 have no meaning."""
MXR_SCALING_OPTIONS2_SETTINGS = (MXR_SCALING_FLAG_MATCH_SOURCE | MXR_SCALING_FLAG_SKIP_420)
"""The settings behind MXR_SCALING_FLAG_OPTIONS2_VALID, which carries both."""
ONEIP_SCALING_REFRESH_MIN = 24
ONEIP_SCALING_REFRESH_MAX = 120
"""Refresh rate range a OneIP output stage accepts, in Hz. A receiver replaces
anything outside it with 50 rather than refusing the write, so a 0 here asks
for 50Hz instead of asking for nothing."""
MXR_ONEIP_DSCP_SET = 0x80
"""DSCP 0 (CS0) is a legal marking, so a zero byte cannot mean "absent": each
dscp byte in a V2IP_DEVICE_CFG options word carries this bit alongside its
0..63 value, and a sender that leaves the byte zero is read as carrying no
marking at all."""
ONEIP_DSCP_MAX = 63
"""Highest DSCP value; the marking occupies the upper 6 bits of the IPv4 TOS byte."""
ONEIP_DSCP_DEFAULT = 16
"""CS2, the marking the video processor applies at boot and the value firmware
falls back to when a peer sends no marking."""
def oneip_dscp_value(raw:int|None) -> int|None:
'''Decode one dscp byte from a V2IP_DEVICE_CFG options word, or None when unset.'''
if (raw is None) or ((raw & MXR_ONEIP_DSCP_SET) == 0):
return None
return (raw & ONEIP_DSCP_MAX)
_EnumT = TypeVar('_EnumT', bound=IntEnum)
def decode_enum(cls:type[_EnumT], value:int|None) -> _EnumT|None:
'''Convert a value off the wire into an enum member, tolerating unknowns.
Yields the enum's own UNKNOWN where one is defined, None otherwise. A driver
must not break because a firmware update taught a device a value this build
has not seen.
Never clamp instead. Zero is a meaningful member of most of these enums, so
folding an unknown value into it gives a confidently wrong answer rather than
an unknown one.
'''
if (value is None):
return getattr(cls, 'UNKNOWN', None)
try:
return cls(value)
except ValueError:
return getattr(cls, 'UNKNOWN', None)
class DeviceFeature(IntFlag):
'''Device feature flags reported in hello frames.'''
IR_RX = (1 << 0)
IR_TX = (1 << 1)
CEC = (1 << 2)
ONEIP_SOURCE = (1 << 3)
ONEIP_SINK = (1 << 4)
V2IP_SOURCE = (1 << 3)
'''Deprecated: use ONEIP_SOURCE.'''
V2IP_SINK = (1 << 4)
'''Deprecated: use ONEIP_SINK.'''
VIDEO_ROUTING = (1 << 5)
AUDIO_ROUTING = (1 << 6)
VOLUME_CONTROL = (1 << 7)
AUDIO_RETURN = (1 << 8)
REMOTE_CONTROL = (1 << 9)
SETUP_COMPLETED = (1 << 10)
MESH_MASTER = (1 << 11)
STATUS_NOTIFY = (1 << 12)
STATUS_WARNING = (1 << 13)
STATUS_ERROR = (1 << 14)
STATUS_REBOOTING = (1 << 15)
MESH_MEMBER = (1 << 16)
AUDIO_AMPLIFIER = (1 << 17)
BOOTING = (1 << 18)
MANAGER = (1 << 19)
STATUS_POWER_SAVE = (1 << 20)
MESH = (1 << 21)
MULTIVIEWER = (1 << 22)
STATUS_CRASHED = (1 << 23)
VIDEO_WALL = (1 << 24)
CONFIG_INITIALISED = (1 << 25)
STATUS_MCAST_FAULT = (1 << 26)
'''Receives one or more peers only by broadcast, because its multicast receive path is faulty.'''
POWER_SAVE = (1 << 27)
'''Powers its video processor down in power save. A device without it refuses to enter power save.'''
VLAN = (1 << 28)
'''Tags its uplink by the VLAN configuration it reports, and takes one written to it.'''
OSD_SYNC = (1 << 29)
'''Copies the overlay settings and logos of another unit when asked.'''
STATUS_UPGRADING = (1 << 30)
'''Is downloading or installing a firmware or video processor upgrade. STATUS_REBOOTING follows when it reboots to complete it.'''
BOOT_BIT = (1 << 31)
'''Flips on every reboot, so a peer that sees it change knows the device restarted.'''
class OneIPVideoProcessorFeature(IntFlag):
'''What a OneIP device's video processor supports, as the device reports it in
its configuration.
Read-only, and a device's own: it fills the field in only on the frame
describing itself, and leaves it zero on one it sends to configure another
device. There is no write path.
Bits are assigned by the video processor and only ever appended, so a bit
this library has no name for is a later capability rather than an error. A
device reports no features at all until its processor answers, and an older
processor answers with none of the optional commands, so an empty mask is
never a capability set.'''
SOURCE_DSCP = (1 << 0)
'''Applies a DSCP marking to the streams it sources.'''
SINK_AUDIO_FORMAT = (1 << 1)
'''Reports the audio format arriving at its sink.'''
SINK_TILING_WINDOW = (1 << 2)
'''Places a tiling window on its sink.'''
SINK_OVERLAY_STATE = (1 << 3)
'''Reports the state of its sink's overlay.'''
SINK_STATE = (1 << 4)
'''Reports its sink's state.'''
SINK_STREAM_INFO = (1 << 5)
'''Reports information about the stream its sink receives.'''
SINK_TEST_PATTERN = (1 << 8)
'''Draws a test pattern, tone and lip-sync flash on its sink's output.'''
[docs]
class OneIPDeviceSetting(IntFlag):
'''The device settings a OneIP configuration can carry, each on its own bit.
The same bits serve as the settings a frame carries and as the values of the
on/off ones among them. IR_PROFILE to POWER_SAVE_SCHEDULE and the PTP
settings carry a value elsewhere in the block and have no on/off value of
their own.
A device reports every setting it has, so one it never reports is one it
does not have.'''
SINK_CHECK_POWER = (1 << 0)
'''Disables the decoder while the display is off.'''
SINK_OFF_NO_SIGNAL = (1 << 1)
'''Disables the HDMI output while there is no signal.'''
IR_TX_MODULATED = (1 << 2)
'''Sends infrared modulated.'''
STATUS_LED = (1 << 3)
'''Lights the status LED.'''
NETWORK_LED = (1 << 4)
'''Lights the network port LEDs.'''
FAN_QUIET = (1 << 5)
'''Runs the fan in quiet mode.'''
CEC_COMBO_KEYS = (1 << 6)
'''Accepts CEC combo keys.'''
CEC_COMBO_INPUT = (1 << 7)
'''Accepts CEC combo keys for the device's own input.'''
IR_PROFILE = (1 << 8)
'''The infrared profile of the device's global infrared port.'''
IR_PROFILE_SINK = (1 << 9)
'''The infrared profile of the output's infrared port.'''
IR_PROFILES = (1 << 10)
'''The infrared profiles stored on the device. Reported by the device
itself and never written.'''
AUTO_POWER_SAVE = (1 << 11)
'''The minutes a device stays idle before it powers down by itself.'''
POWER_SAVE_SCHEDULE = (1 << 12)
'''The daily windows in which the device powers down.'''
CLOCK_SET = (1 << 13)
'''The device's clock has been set, on/off. Reported by the device itself
and never written.'''
PTP_MODE = (1 << 14)
'''The PTP mode the device runs.'''
PTP_DOMAIN = (1 << 15)
'''The PTP domain the device runs in.'''
PTP_PRIORITY1 = (1 << 16)
'''The priority1 the device announces as a PTP grandmaster.'''
AES67 = (1 << 17)
'''Sends the source's audio as an AES67 flow, where the source allows.'''
POWER_SAVE_LEVEL = (1 << 18)
'''The level the device powers down to in power save. A device picks it
itself: the level that saves the most while its input and display can
still wake it. Written without POWER_SAVE_DISABLED, it switches power save
on, and the level written is ignored, except by a device whose firmware
predates picking it, which takes the level as its setting.'''
PTP_ANNOUNCE = (1 << 19)
'''The interval between the device's PTP announce messages.'''
PTP_SYNC = (1 << 20)
'''The interval between the device's PTP sync messages.'''
PTP_DELAY_REQ = (1 << 21)
'''The interval between the device's PTP delay requests.'''
PTP_ANNOUNCE_TIMEOUT = (1 << 22)
'''The announce intervals before the device drops a silent PTP master.'''
PTP_DSCP = (1 << 23)
'''The DSCP of the device's PTP messages.'''
SAP_EXPIRE = (1 << 24)
'''The minutes after which the device forgets a SAP announcement it heard.'''
SAP_LISTEN = (1 << 25)
'''Listens for SAP announcements.'''
SAP_ANNOUNCE = (1 << 26)
'''Sends SAP announcements.'''
SAP_INTERVAL = (1 << 27)
'''The seconds between the device's SAP announcements.'''
POWER_SAVE_DISABLED = (1 << 28)
'''Never powers down by itself: automatic power save and the power save
schedule leave the device running, and switching this on wakes a device
either has powered down. A power save request still powers it down.
Writing POWER_SAVE_LEVEL switches it off.'''
STREAM_TTL = (1 << 29)
'''The TTL of the device's streams, video, audio and ancillary alike.
Reported only by a device with an encoder.'''
CLOUD = (1 << 30)
'''Connects to the cloud service. A device that does not report it
connects regardless.'''
SINK_ROTATION = (1 << 31)
'''The angle the device's HDMI output is turned by. Reported only by a
device with an HDMI output.'''
ONEIP_DEVICE_SETTING_SWITCHES = OneIPDeviceSetting(0xFF) | OneIPDeviceSetting.AES67 \
| OneIPDeviceSetting.SAP_LISTEN | OneIPDeviceSetting.SAP_ANNOUNCE | OneIPDeviceSetting.POWER_SAVE_DISABLED \
| OneIPDeviceSetting.CLOUD
"""The settings that are on or off, as opposed to carrying a value, and that
can be written."""
ONEIP_DEVICE_SETTINGS_REPORTED_ONLY = OneIPDeviceSetting.IR_PROFILES | OneIPDeviceSetting.CLOCK_SET
"""The settings only a device reports about itself, which no write carries."""
ONEIP_DEVICE_SETTINGS_PTP = OneIPDeviceSetting.PTP_MODE | OneIPDeviceSetting.PTP_DOMAIN \
| OneIPDeviceSetting.PTP_PRIORITY1
"""The PTP settings."""
ONEIP_DEVICE_SETTINGS_PTP_MESSAGES = OneIPDeviceSetting.PTP_ANNOUNCE | OneIPDeviceSetting.PTP_SYNC \
| OneIPDeviceSetting.PTP_DELAY_REQ | OneIPDeviceSetting.PTP_ANNOUNCE_TIMEOUT | OneIPDeviceSetting.PTP_DSCP
"""The PTP message settings."""
ONEIP_DEVICE_SETTINGS_BLOCK = ONEIP_DEVICE_SETTINGS_PTP_MESSAGES | OneIPDeviceSetting.SAP_EXPIRE \
| OneIPDeviceSetting.SAP_INTERVAL | OneIPDeviceSetting.STREAM_TTL | OneIPDeviceSetting.SINK_ROTATION
"""The settings whose values a frame carries in a block of their own behind the
rest, OneIPPtpMessages: the PTP message settings, the two SAP values, the
stream TTL and the output's rotation."""
ONEIP_STREAM_TTL_DEFAULT = 64
"""The TTL a OneIP device's streams carry until it is set."""
ONEIP_SINK_ROTATION_MAX = 359
"""The largest angle a OneIP device's HDMI output is turned by, in degrees."""
[docs]
class OneIPPtpMode(IntEnum):
'''How a OneIP device takes part in PTP.'''
OFF = 0
'''No PTP.'''
FOLLOWER = 1
'''Follows a grandmaster, and never becomes one.'''
AUTO = 2
'''Follows a grandmaster, or becomes one when there is none.'''
ONEIP_PTP_DOMAIN_MAX = 127
"""The highest PTP domain a OneIP device runs in."""
BAY_CONTENT_KEY_MAX = 31
"""The most bytes in a BayContentField key."""
BAY_CONTENT_VALUE_MAX = 255
"""The most bytes in a BayContentField value, UTF-8 encoded."""
[docs]
class OsdSyncWhat(IntFlag):
'''What an overlay sync copies.'''
CAPTIONS = (1 << 0)
LOGO = (1 << 1)
'''Which logo is shown, and where.'''
BACKGROUNDS = (1 << 2)
'''Background colours.'''
LOGO_SLOTS = (1 << 3)
'''The logo images in the four slots.'''
[docs]
class OsdMessageFlag(IntFlag):
'''What a unit says of the message on its output.'''
SET = (1 << 0)
'''A message is set. In a write: show it, rather than take it down.'''
SHOWN = (1 << 1)
'''The output shows it, rather than another message ahead of it.'''
SUPPORTED = (1 << 2)
'''The unit's video processor composes the message layer.'''
REFUSED = (1 << 3)
'''The unit did not take the write it answers, and reports the message it
kept.'''
[docs]
class OsdMessagePriority(IntEnum):
'''Where a message stands against the unit's own messages: its upgrade,
Locate Device and encrypted source messages.'''
LOW = 0
'''After them.'''
NORMAL = 1
'''Between them.'''
TOP = 2
'''Before them.'''
OSD_MESSAGE_TEXT_MAX = 255
"""The longest message text a unit takes, in bytes."""
[docs]
class OsdSyncState(IntEnum):
'''How far a unit has got with taking an overlay sync.'''
TAKING = 0
DONE = 1
'''Every setting written and committed.'''
FAILED = 2
'''Failed, for the reason the report gives.'''
DEFERRED = 3
'''Kept for later, as the unit's video processor is off in power save.'''
[docs]
class OsdSyncSize(IntEnum):
'''The output size an overlay setting applies at.'''
LARGE = 1
'''Large outputs, which every value but SMALL also means.'''
SMALL = 2
[docs]
class OsdCaptionAlign(IntEnum):
'''How a caption's lines are aligned.'''
LEFT = 0
CENTRE = 1
RIGHT = 2
DEFAULT = 0xFF
'''The layout's default.'''
[docs]
class OsdCaptionField(IntFlag):
'''The caption fields an overlay setting gives. A field left out keeps what
the unit has.'''
FONT = (1 << 0)
SIZE = (1 << 1)
X = (1 << 2)
Y = (1 << 3)
ALIGN = (1 << 4)
FORMAT = (1 << 5)
COLOUR = (1 << 6)
WRAP = (1 << 7)
TEXT = (1 << 8)
[docs]
class OsdLogoField(IntFlag):
'''The logo fields an overlay setting gives. A field left out keeps what the
unit has.'''
LOGO = (1 << 0)
X = (1 << 1)
Y = (1 << 2)
OSD_SYNC_SLOTS = 4
"""The logo slots an overlay sync can copy."""
OSD_SYNC_RECORDS_MAX = 128
"""The most settings one overlay sync carries."""
OSD_SYNC_FONT_MAX = 31
"""The longest caption font name, in bytes."""
OSD_SYNC_TEXT_MAX = 360
"""The longest caption text a unit shows, in bytes. It cuts a longer one short."""
OSD_SYNC_IMAGE_MAX = 512 * 1024
"""The largest logo image a unit takes, in bytes."""
OSD_SYNC_IMAGE_WIDTH_MAX = 1920
"""The widest logo image a unit takes, in pixels."""
OSD_SYNC_IMAGE_HEIGHT_MAX = 1080
"""The tallest logo image a unit takes, in pixels."""
OSD_SYNC_PERCENT_MAX = 100
"""The largest size a logo is shown at, in percent."""
ONEIP_SDP_MAX = 2048
"""The longest session description a OneIP output takes, in bytes."""
[docs]
class OneIPSdpFlag(IntFlag):
'''The flags of a OneIP output's session description report.'''
ACTIVE = (1 << 0)
'''The output plays a stream from a description.'''
AUDIO_ONLY = (1 << 1)
'''And nothing else, as no video is routed to it.'''
CLOCK = (1 << 2)
'''The clock mode and state hold the video processor's answer.'''
[docs]
class OneIPSdpEncoding(IntEnum):
'''The sample encoding of a stream a OneIP output plays from a description.'''
NONE = 0
L16 = 1
'''16-bit linear PCM.'''
L24 = 2
'''24-bit linear PCM.'''
AM824 = 3
[docs]
class OneIPSdpRefClock(IntEnum):
'''The reference clock a described stream names.'''
NONE = 0
PTP = 1
TRACEABLE = 2
LOCAL_MAC = 3
'''The sender's local MAC.'''
[docs]
class OneIPSdpClockMode(IntEnum):
'''What a OneIP output's video processor times a described stream by.'''
ARRIVAL = 0
PTP = 1
SENDER = 2
[docs]
class OneIPSdpClockState(IntEnum):
'''How far a OneIP output's video processor has got with a described
stream's timing.'''
IDLE = 0
SERVO = 1
'''Running its servo.'''
ALIGNING = 2
TIMED = 3
PARKED = 4
UNTIMED = 5
'''Playing untimed.'''
UNSUPPORTED = 6
'''The timing is not supported.'''
[docs]
class OneIPPowerSaveLevel(IntEnum):
'''How far a OneIP device powers down in power save, each level saving more
than the one before.'''
NONE = 0
'''No level.'''
REDUCED = 1
'''Part of the video processing powers down.'''
LOW = 2
'''All of the video processing powers down.'''
OFF = 3
'''Everything but the network powers down.'''
[docs]
class OneIPDownmixMode(IntEnum):
'''When a OneIP output mixes 5.1 and 7.1 PCM down to stereo.'''
OFF = 0
'''Never.'''
AUTO = 1
'''When the display's EDID lists two PCM channels or fewer.'''
ALWAYS = 2
'''Always.'''
[docs]
class OneIPDownmixLevel(IntEnum):
'''How loud a OneIP output's stereo downmix is.'''
NORMALISED = 0
'''Scaled so the mix never clips.'''
UNSCALED = 1
'''Unscaled, with dialogue at the level it has in stereo.'''
ONEIP_DOWNMIX_LFE_MAX = 100
"""The largest percent a OneIP output mixes its LFE channel in at."""
[docs]
class UnitPowerSave(IntFlag):
'''What a unit offers at one power save level.'''
SUPPORTED = (1 << 0)
'''The unit can power down to this level.'''
INPUT_WAKE = (1 << 1)
'''A signal on the HDMI input wakes the unit.'''
OUTPUT_WAKE = (1 << 2)
'''The display powering on wakes the unit, and the unit can power it on.'''
UNIT_POWER_SAVE_LEVELS = 4
"""The power save levels a unit reports what it offers at, from
OneIPPowerSaveLevel.REDUCED up."""
[docs]
class UnitFlag(IntFlag):
'''The flags of a unit's status report.'''
REBOOT_REQUIRED = (1 << 0)
'''A changed setting takes effect after a reboot.'''
VIDEO_PROCESSOR_UPGRADE = (1 << 1)
'''A newer video processor image is available.'''
DHCP = (1 << 2)
'''The IP configuration comes from DHCP.'''
NET_PENDING = (1 << 3)
'''The IP configuration was changed through the mesh, and is reverted
unless the mesh controller confirms it in time.'''
FAN_QUIET = (1 << 4)
'''The fan runs in quiet mode.'''
EXTERNAL_PSU = (1 << 5)
'''Powered by an external supply rather than PoE.'''
VIDEO_PROCESSOR_CONFIGURED = (1 << 6)
'''The video processor runs with its IP configured. Without it the encoder
and decoder health carry nothing.'''
ENCODER_ACTIVE = (1 << 7)
'''The encoder is enabled.'''
DECODER_ACTIVE = (1 << 8)
'''The decoder is enabled.'''
AUTO_DISABLE = (1 << 9)
'''The decoder disables itself without a display.'''
AUTO_DISABLE_ACTIVE = (1 << 10)
'''The decoder disabled itself without a display.'''
OFF_NO_SIGNAL = (1 << 11)
'''The HDMI output switches off without a signal.'''
OFF_NO_SIGNAL_ACTIVE = (1 << 12)
'''The HDMI output switched off without a signal.'''
POWER_CHECK = (1 << 13)
'''Display power is checked before the decoder is enabled.'''
BOOT_COUNT = (1 << 14)
'''The boot count is reported.'''
FAN_SPEED = (1 << 15)
'''The fan speed is reported.'''
[docs]
class UnitCommandKind(IntEnum):
'''What a unit command asks of its unit.'''
CLEAR_STATUS = 0
'''Clear the current system status.'''
VIDEO_PROCESSOR_UPGRADE = 1
'''Upgrade the video processor to the latest image.'''
NET_CONFIG = 2
'''Change the IP configuration.'''
NET_CONFIRM = 3
'''Keep a pending IP configuration.'''
[docs]
class UnitCommandFlag(IntFlag):
'''The flags of a unit command.'''
FORCE = (1 << 0)
'''With VIDEO_PROCESSOR_UPGRADE: flash the image even if it is running
already.'''
DHCP = (1 << 1)
'''With NET_CONFIG: use DHCP, ignoring the addresses.'''
UNIT_MAX_TEMPERATURES = 6
"""The most temperature sensors a unit reports."""
[docs]
class OneIPPtpFlag(IntFlag):
'''The flags of a OneIP device's PTP state.'''
VALID = (1 << 0)
'''The device runs PTP and the block holds its state. A block without it
holds nothing: the device has no PTP, or is in power save.'''
FOLLOWED = (1 << 1)
'''The device locked to a master since its PTP started, so its mesh holds
a time.'''
[docs]
class OneIPPtpLock(IntEnum):
'''How far a OneIP device's PTP clock has locked to its master.'''
UNLOCKED = 0
'''Not locked.'''
LOCKING = 1
'''Locking.'''
LOCKED = 2
'''Locked.'''
ONEIP_MINUTES_PER_DAY = 24 * 60
"""Minutes in a day: every time in a power save schedule is below this."""
ONEIP_IR_PROFILE_NOT_SET = -1
"""The infrared profile an output's port reports when it follows the device's
global one."""
ONEIP_IR_PROFILE_MAX = 18
"""One past the highest infrared profile: the default profile is 0, the Atlona
profile 1, and user-defined profiles 1 to 16 are 2 to 17."""
[docs]
class OneIPTestPattern(IntEnum):
'''The test pattern a OneIP sink draws on its output.'''
OFF = 0
'''No pattern.'''
BARS = 1
'''Colour bars.'''
FLAT = 2
'''One flat colour, the one the test card carries.'''
RAMP = 3
'''A ramp.'''
GRID = 4
'''A grid.'''
STRIP = 5
'''A strip.'''
CARD = 6
'''A test card.'''
[docs]
class OneIPToneMode(IntEnum):
'''The test tone a OneIP sink plays on its output.'''
OFF = 0
'''No tone.'''
CONTINUOUS = 1
'''A continuous tone.'''
IDENT = 2
'''A channel ident.'''
LINEUP = 3
'''A line-up tone, which needs two channels or more.'''
BEEP = 4
'''A beep on each lip-sync mark.'''
[docs]
class OneIPTestcardFlag(IntFlag):
'''What a OneIP sink reports about its test card.'''
SUPPORTED = (1 << 0)
'''The sink can draw the test card.'''
SHOWING = (1 << 1)
'''The output shows the pattern.'''
PLAYING = (1 << 2)
'''The tone plays on the output.'''
PATTERN_PENDING = (1 << 3)
'''A pattern change has yet to reach the video processor.'''
TONE_PENDING = (1 << 4)
'''A tone change has yet to reach the video processor.'''
SYNC_PENDING = (1 << 5)
'''A lip-sync change has yet to reach the video processor.'''
ONEIP_TONE_FREQ_MIN = 20
"""The lowest test tone frequency, in Hz."""
ONEIP_TONE_FREQ_MAX = 20000
"""The highest test tone frequency, in Hz."""
ONEIP_TONE_LEVEL_MIN = -60
"""The quietest test tone level, in dBFS; the loudest is 0."""
ONEIP_TONE_CHANNELS_MAX = 8
"""The most channels a test tone plays on."""
ONEIP_TONE_RATES = (44100, 48000, 96000)
"""The sample rates a test tone plays at, in Hz."""
ONEIP_SYNC_OFFSET_MAX = 0xFFFFFF
"""The largest lip-sync beep offset, in sample periods."""
ONEIP_SYNC_BEEP_MS_MIN = 1
"""The shortest lip-sync beep, in milliseconds."""
ONEIP_SYNC_BEEP_MS_MAX = 10000
"""The longest lip-sync beep, in milliseconds."""
[docs]
class OneIPVlanFlag(IntFlag):
'''The flags of a OneIP device's VLAN configuration.'''
VALID = (1 << 0)
'''The block carries a configuration. A block without it carries nothing,
whatever its other bytes hold.'''
TRUNK = (1 << 1)
'''Untagged frames arriving on the uplink are dropped instead of reaching the
device.'''
PENDING = (1 << 2)
'''Reported by the device: the configuration is applied, and reverted unless
the mesh controller confirms it. Never written.'''
CONFIRM = (1 << 3)
'''Sent by the mesh controller: a pending configuration it heard, which the
device then keeps.'''
HAS_SFP = (1 << 4)
'''Reported by the device: it has an SFP port. Never written.'''
ONEIP_VLAN_PORTS = 3
"""The external network ports a OneIPVlan covers, in the order of its port
field: the SFP port, the UTP port with PoE, then the UTP port."""
ONEIP_VLAN_PORT_SFP = 0
"""The index of the SFP port in OneIPVlan.port."""
ONEIP_VLAN_ID_MAX = 4094
"""The highest VLAN id: 0 means untagged, and 4095 is reserved by IEEE 802.1Q."""
BAY_FEATURE_DOLBY_IN_POS = 24
class BayFeaturesMask(IntFlag):
'''Bay feature flags reported in bay config frames.'''
HDMI_OUT = (1 << 0)
HDMI_IN = (1 << 1)
AUDIO_DIG_OUT = (1 << 2)
AUDIO_DIG_IN = (1 << 3)
AUDIO_ANA_OUT = (1 << 4)
AUDIO_ANA_IN = (1 << 5)
IR_IN = (1 << 6)
IR_OUT = (1 << 7)
AUDIO_AMP_OUT = (1 << 8)
RC_OUT = (1 << 9)
RC_IN = (1 << 10)
DOLBY = (1 << 11)
AUTO_OFF = (1 << 12)
ONEIP_SOURCE_REMOTE = (1 << 13)
ONEIP_SINK_REMOTE = (1 << 14)
ONEIP_SOURCE_LOCAL = (1 << 15)
ONEIP_SINK_LOCAL = (1 << 16)
V2IP_SOURCE_REMOTE = (1 << 13)
'''Deprecated: use ONEIP_SOURCE_REMOTE.'''
V2IP_SINK_REMOTE = (1 << 14)
'''Deprecated: use ONEIP_SINK_REMOTE.'''
V2IP_SOURCE_LOCAL = (1 << 15)
'''Deprecated: use ONEIP_SOURCE_LOCAL.'''
V2IP_SINK_LOCAL = (1 << 16)
'''Deprecated: use ONEIP_SINK_LOCAL.'''
def toJson(self) -> str:
return '{' + f'"features":"{int(self)}"' + '}'
def __str__(self) -> str:
rv = ""
if BayFeaturesMask.HDMI_IN in self:
rv += ", HDMI input"
if BayFeaturesMask.HDMI_OUT in self:
rv += ", HDMI output"
if BayFeaturesMask.AUDIO_DIG_OUT in self:
rv += ", digital audio output"
if BayFeaturesMask.AUDIO_DIG_IN in self:
rv += ", digital audio input"
if BayFeaturesMask.AUDIO_ANA_OUT in self:
rv += ", analog audio output"
if BayFeaturesMask.AUDIO_ANA_IN in self:
rv += ", analog audio input"
if BayFeaturesMask.AUDIO_AMP_OUT in self:
rv += ", amplifier audio output"
if BayFeaturesMask.DOLBY in self:
rv += ", Dolby"
if BayFeaturesMask.IR_IN in self:
rv += ", IR input"
if BayFeaturesMask.IR_OUT in self:
rv += ", IR output"
if BayFeaturesMask.RC_IN in self:
rv += ", remote control input"
if BayFeaturesMask.RC_OUT in self:
rv += ", remote control output"
if BayFeaturesMask.AUTO_OFF in self:
rv += ", auto standby"
if BayFeaturesMask.ONEIP_SOURCE_REMOTE in self:
rv += ", OneIP remote source"
if BayFeaturesMask.ONEIP_SINK_REMOTE in self:
rv += ", OneIP remote sink"
if BayFeaturesMask.ONEIP_SOURCE_LOCAL in self:
rv += ", OneIP local source"
if BayFeaturesMask.ONEIP_SINK_LOCAL in self:
rv += ", OneIP local sink"
if (len(rv) != 0):
return rv[2:]
return "none"
def __repr__(self) -> str:
return str(self)
class BayStatusMask(IntFlag):
'''Bay status flags.'''
FAULT = (1 << 0)
HIDDEN = (1 << 1)
POWERED = (1 << 2)
'''The amplifier zone the bay drives is powered on; only a ProAmp8v2 sets it.'''
SIGNAL_DETECTED = (1 << 3)
HPD_DETECTED = (1 << 4)
SIGNAL_SCRAMBLED = (1 << 5)
HDBT_CONNECTED = (1 << 6)
CEC_DETECTED = (1 << 7)
POWERED_ON = (1 << 8)
POWERED_OFF = (1 << 9)
AUDIO_ARC_HDMI = (1 << 10)
AUDIO_ARC_OPTICAL = (1 << 11)
AUDIO_ARC_ANALOG = (1 << 12)
OFFLINE = (1 << 13)
DECODER_DISABLED = (1 << 14)
ENCODER_DISABLED = (1 << 15)
CEC_DISABLED = (1 << 20)
ENCODER_ERROR = (1 << 21)
AUTO_NAME = (1 << 24)
'''The bay's name was generated - a default, or taken from CEC or the EDID -
rather than set by a user. A device that predates this bit never sets it,
so a clear bit does not prove a user set the name.'''
def __str__(self) -> str:
rv = ""
if BayStatusMask.FAULT in self:
rv += ", fault detected"
if BayStatusMask.HIDDEN in self:
rv += ", hidden"
if BayStatusMask.POWERED in self:
rv += ", power enabled"
if BayStatusMask.SIGNAL_DETECTED in self:
rv += ", signal detected"
if BayStatusMask.HPD_DETECTED in self:
rv += ", hpd detected"
if BayStatusMask.HDBT_CONNECTED in self:
rv += ", hdbt link"
if BayStatusMask.CEC_DETECTED in self:
rv += ", cec detected"
if BayStatusMask.POWERED_ON in self:
rv += ", remote powered on"
if BayStatusMask.POWERED_OFF in self:
rv += ", remote powered off"
if BayStatusMask.AUDIO_ARC_HDMI in self:
rv += ", hdmi arc"
if BayStatusMask.AUDIO_ARC_OPTICAL in self:
rv += ", optical arc"
if BayStatusMask.AUDIO_ARC_ANALOG in self:
rv += ", analog arc"
if BayStatusMask.OFFLINE in self:
rv += ", offline"
if BayStatusMask.DECODER_DISABLED in self:
rv += ", decoder disabled"
if BayStatusMask.ENCODER_DISABLED in self:
rv += ", encoder disabled"
if BayStatusMask.CEC_DISABLED in self:
rv += ", cec disabled"
if BayStatusMask.ENCODER_ERROR in self:
rv += ", encoder error"
if BayStatusMask.AUTO_NAME in self:
rv += ", generated name"
if len(rv) != 0:
return rv[2:]
return "none"
# Bay status bit-field accessors (not flag bits), mirroring the firmware's
# MXR_BAY_STATUS_RC_TYPE and MXR_BAY_STATUS_HDCP_STATUS.
BAY_STATUS_RC_TYPE_SHIFT = 16
BAY_STATUS_RC_TYPE_MASK = (0xF << BAY_STATUS_RC_TYPE_SHIFT)
BAY_STATUS_HDCP_STATUS_SHIFT = 22
BAY_STATUS_HDCP_STATUS_MASK = (0x3 << BAY_STATUS_HDCP_STATUS_SHIFT)
def bay_status_rc_type(status: int) -> int:
'''Return the RC type (4 bits at MXR_BAY_STATUS_RC_TYPE) from a bay-status word.'''
return (status & BAY_STATUS_RC_TYPE_MASK) >> BAY_STATUS_RC_TYPE_SHIFT
def bay_status_hdcp(status: int) -> int:
'''Return the HDCP status (2 bits at MXR_BAY_STATUS_HDCP_STATUS) from a bay-status word.'''
return (status & BAY_STATUS_HDCP_STATUS_MASK) >> BAY_STATUS_HDCP_STATUS_SHIFT
MXR_SIG_BPP_UNKNOWN = 0
MXR_SIG_BPP_UNSET = 5
_MXR_SIG_BPP_VALUES: dict[int, int] = {
1: 8,
2: 10,
3: 12,
4: 16,
}
def mxr_sig_bpp_get(bpp: int) -> int:
'''Map an mxr_signal_type bpp *index* to the bit depth it stands for.
The field is an index, not a value:
1=8, 2=10, 3=12, 4=16, 0=unknown and 5 the unset sentinel - reading it as a
bit depth is a common trap. Returns 0 for both unknown and unset.'''
return _MXR_SIG_BPP_VALUES.get(bpp, MXR_SIG_BPP_UNKNOWN)
_MXR_SIG_BPP_WRITABLE_INDICES: dict[int, int] = {
8: 1,
10: 2,
12: 3,
}
def mxr_sig_bpp_index(depth: int) -> int|None:
'''Map a bit depth to the mxr_signal_type bpp *index* that stands for it.
Only the three depths a OneIP output stage accepts are here. Index 4 names
16bpp, which mxr_sig_bpp_get() reads back from a device, but the output
stage refuses it - offering it as something to write would build a frame
that is decoded cleanly and then dropped in silence.
Returns None for a depth no index names.'''
return _MXR_SIG_BPP_WRITABLE_INDICES.get(depth)
class MxrSignalType:
'''The 2-byte mxr_signal_type carried in scaling configs and bay signal reports.
byte 0 is the CTA-861 svd (0 when the signal is not HDMI); byte 1 packs
color:4 in the low nibble, then non_int:1 and bpp:3 in the top bits.'''
def __init__(self, data: bytes) -> None:
if len(data) < 2:
raise ValueError(f"invalid mxr_signal_type size: {len(data)}")
self._svd = int(data[0])
self._flags = int(data[1])
@staticmethod
def from_parts(svd: int, colour: int, bpp_index: int) -> 'MxrSignalType':
'''Build the word a scaling write carries, from the fields it consumes.
non_int is left clear: the receiving struct carries the bit and the
apply path does not read it.
Building rather than editing is the point. A sink with no mode
configured reports the word with the unset bpp index in it, so a caller
that read that word back and filled in an svd would send an index no
depth uses, which the receiver decodes to zero and rejects without
answering.'''
flags = (colour & 0xF) | ((bpp_index & 0x7) << 5)
return MxrSignalType(bytes([svd & 0xFF, flags]))
@property
def byte_value(self) -> bytes:
'''The two bytes as they sit on the wire.'''
return bytes([self._svd, self._flags])
@property
def value(self) -> int:
'''Raw 16-bit value.'''
return (self._flags << 8) | self._svd
@property
def svd(self) -> int|None:
'''CTA-861 short video descriptor, or None when no format is set.
Gate on is_set, never on svd == 0: 0 is a real value meaning the signal
is not HDMI. An unset signal type zeroes the whole word and stores the
sentinel in bpp, so svd, colour and non_int beside it are meaningless.'''
return self._svd if self.is_set else None
@property
def color(self) -> int|None:
'''Colour space (see VideoColourSpace), or None when no format is set.'''
return (self._flags & 0xF) if self.is_set else None
@property
def non_int(self) -> bool|None:
'''Non-integer (1000/1001) frame rate, or None when no format is set.'''
return ((self._flags & (1 << 4)) != 0) if self.is_set else None
@property
def bpp_index(self) -> int:
'''Raw bpp index as carried on the wire.'''
return ((self._flags >> 5) & 0x7)
@property
def bpp(self) -> int|None:
'''Bit depth the bpp index stands for.
None when no format is set, 0 when a format is set but its depth is not
known. Those are different answers and collapsing them loses the only
distinction this field carries.'''
return mxr_sig_bpp_get(self.bpp_index) if self.is_set else None
@property
def is_set(self) -> bool:
'''False while the signal type still carries the unset sentinel.'''
return (self.bpp_index != MXR_SIG_BPP_UNSET)
def __str__(self) -> str:
if not self.is_set:
return "unset"
bpp = self.bpp
return f"svd {self.svd}, color {self.color}, {bpp}bpp" if (bpp != 0) else f"svd {self.svd}, color {self.color}"
def __repr__(self) -> str:
return str(self)
class VideoWallOperation(IntEnum):
'''What a V2IP_VIDEOWALL frame asks the addressed sink to do with its window.
Mirrors vw_mesh_op in the v2ipwall module, which owns this opcode.'''
PREVIEW = 0
'''Show the window without storing it, so it dies on reboot or revert.'''
STORE = 1
'''Show the window and keep it as this sink's window.'''
REVERT = 2
'''Drop the preview and go back to the stored window.
A revert carries no geometry: the sender zeroes the window and raster, and
the receiver ignores those bytes entirely.'''
def __str__(self) -> str:
if self.value == VideoWallOperation.PREVIEW.value:
return "preview"
if self.value == VideoWallOperation.STORE.value:
return "store"
if self.value == VideoWallOperation.REVERT.value:
return "revert"
return "unknown"
def __repr__(self) -> str:
return str(self)
# Geometry a sink will accept for a wall window. The video processor's vdma is
# built without dre, so the buffer start must be 256-bit aligned - a multiple of
# 64 pixels at 4 pixels per clock - and 64x64 is the scaler minimum. A unit
# reports its running bitstream's own values over HTTP (step_x, step_width,
# min_width, min_height); prefer those over these defaults where available.
VIDEO_WALL_POS_X_STEP = 64
VIDEO_WALL_WIDTH_STEP = 4
VIDEO_WALL_MIN_WIDTH = 64
VIDEO_WALL_MIN_HEIGHT = 64
def video_wall_geometry_valid(pos_x:int, width:int, height:int) -> bool:
'''True when a wall window satisfies the constraints a sink enforces.
A zero width or height is the wire spelling of "clear the wall, show the
full frame" and is always accepted.'''
if (width == 0) or (height == 0):
return True
return ((pos_x % VIDEO_WALL_POS_X_STEP) == 0) and ((width % VIDEO_WALL_WIDTH_STEP) == 0) and (width >= VIDEO_WALL_MIN_WIDTH) and (height >= VIDEO_WALL_MIN_HEIGHT)
class LinkFeature(IntFlag):
'''Virtual link feature flags.'''
NONE = 0
VIDEO_HDMI = (1 << 0)
AUDIO_OPTICAL = (1 << 1)
AUDIO_ANALOG = (1 << 2)
IR = (1 << 3)
RC = (1 << 4)
[docs]
class RCAction(IntEnum):
'''Remote control actions.'''
ACTION_POWER_TOGGLE = 0
ACTION_POWER_ON = 1
ACTION_POWER_OFF = 2
ACTION_VOLUME_UP = 3
ACTION_VOLUME_DOWN = 4
ACTION_VOLUME_MUTE = 5
[docs]
class RCKey(IntEnum):
'''Remote control key codes (CEC/IR).'''
KEY_0 = 0
KEY_1 = 1
KEY_2 = 2
KEY_3 = 3
KEY_4 = 4
KEY_5 = 5
KEY_6 = 6
KEY_7 = 7
KEY_8 = 8
KEY_9 = 9
KEY_SELECT = 10
KEY_BACK = 11
KEY_UP = 12
KEY_DOWN = 13
KEY_LEFT = 14
KEY_RIGHT = 15
KEY_MENU = 16
KEY_CONTENT_MENU = 17
KEY_CHANNEL_UP = 18
KEY_CHANNEL_DOWN = 19
KEY_PLAY = 20
KEY_PAUSE = 21
KEY_STOP = 22
KEY_RECORD = 23
KEY_FAST_FORWARD = 24
KEY_REWIND = 25
KEY_RED = 26
KEY_GREEN = 27
KEY_YELLOW = 28
KEY_BLUE = 29
KEY_HELP = 30
KEY_INFORMATION = 31
KEY_TEXT = 32
KEY_GUIDE = 33
KEY_VIDEO_ON_DEMAND = 34
KEY_PREVIOUS_CHANNEL = 80
KEY_3D_MODE = 81
KEY_SUBTITLE = 82
KEY_SOUND_SELECT = 83
KEY_INPUT_SELECT = 84
KEY_EJECT = 85
KEY_NEXT_CHAPTER = 86
KEY_PREVIOUS_CHAPTER = 87
KEY_CUSTOM_CEC = 1280
KEY_INTERACTIVE = 128
KEY_SEARCH = 129
KEY_SKY = 130
KEY_CUSTOM_SKY = 2048
[docs]
class RCType(IntEnum):
'''Remote control protocol type.'''
IR = 0
CEC = 1
SKY_UK = 2
TIVO = 3
KODI = 4
DISH = 5
DIRECTV = 6
MX_REMOTE = 7
INTERNAL = 8
'''Routed inside the device itself.'''
def __str__(self) -> str:
if self.value == RCType.IR.value:
return "IR"
if self.value == RCType.CEC.value:
return "CEC"
if self.value == RCType.SKY_UK.value:
return "Sky"
if self.value == RCType.TIVO.value:
return "TiVo"
if self.value == RCType.KODI.value:
return "Kodi"
if self.value == RCType.DISH.value:
return "Dish"
if self.value == RCType.DIRECTV.value:
return "DirecTV"
if self.value == RCType.MX_REMOTE.value:
return "MX-Remote"
if self.value == RCType.INTERNAL.value:
return "Internal"
return "Unknown"
[docs]
@staticmethod
def values() -> dict[int, str]:
rv:dict[int, str] = {}
for val in RCType:
rv[val.value] = str(val)
return rv
class RCStatus(IntEnum):
'''RC driver connection state on the source, forwarded in mxr_rc_config.
Mirrors mxr_rc_status_t. UNKNOWN is not a state: it means the field was not
populated, which is what an older source firmware sends. Values above the
last defined one are treated as unknown-future rather than clamped, so a
firmware update cannot break a driver over an enum it has not seen.'''
UNKNOWN = 0
NOT_CONFIGURED = 1
DETECTING = 2
CONNECTED = 3
def __str__(self) -> str:
if self.value == RCStatus.NOT_CONFIGURED.value:
return "not configured"
if self.value == RCStatus.DETECTING.value:
return "detecting"
if self.value == RCStatus.CONNECTED.value:
return "connected"
return "unknown"
def __repr__(self) -> str:
return str(self)
MXR_RC_STATUS_NAME_LEN = 15
'''Longest RC driver status string, excluding its terminator.
Unlike the MXR_DEVICE_NAME_LEN fields, the array is one byte longer than the
longest value it can hold, so a full-length name still carries its NUL.'''
class EdidProfile(IntEnum):
'''EDID profile presets for HDMI sources.'''
def __str__(self) -> str:
if self.value == EdidProfile.TEMPLATE_1080P_STEREO.value:
return '1080p stereo'
if self.value == EdidProfile.FIXED.value:
return 'fixed'
if self.value == EdidProfile.TEMPLATE_4K.value:
return '4K'
if self.value == EdidProfile.TEMPLATE_1080P_5_1.value:
return '1080p 5.1'
if self.value == EdidProfile.TEMPLATE_720P.value:
return '720p'
if self.value == EdidProfile.TEMPLATE_1080P_7_1.value:
return '1080p 7.1'
if self.value == EdidProfile.TEMPLATE_4K_HDR_STEREO.value:
return '4K HDR Stereo'
if self.value == EdidProfile.TEMPLATE_4K_HDR_7_1.value:
return '4K HDR 7.1'
if self.value == EdidProfile.TEMPLATE_4K_HDR_AVR_ONLY.value:
return '4K HDR AVR'
if self.value == EdidProfile.LOWEST_COMMON_DENOMINATOR.value:
return 'lowest common denominator'
if self.value == EdidProfile.LOWEST_COMMON_DENOMINATOR_ALL.value:
return 'lowest common denominator (all sinks)'
if self.value == EdidProfile.TEMPLATE_4K_HDR_ATMOS.value:
return '4K HDR Dolby Atmos'
if (self.value >= EdidProfile.SINK_1.value) and (self.value <= EdidProfile.SINK_32.value):
return f'copy from sink #{self.value - EdidProfile.SINK_1.value + 1}'
if self.value == EdidProfile.UNKNOWN.value:
# decode_enum lands here for any value this build has no member for,
# so it is a profile we cannot name, not one somebody configured
return 'unknown'
return f"custom #{self.value}"
TEMPLATE_1080P_STEREO = 0
FIXED = 1
TEMPLATE_4K = 2
TEMPLATE_1080P_5_1 = 3
TEMPLATE_720P = 4
TEMPLATE_1080P_7_1 = 5
TEMPLATE_4K_7_1 = 6
TEMPLATE_4K_HDR_STEREO = 7
TEMPLATE_4K_HDR_7_1 = 8
TEMPLATE_4K_HDR_AVR_ONLY = 9
LOWEST_COMMON_DENOMINATOR = 10
LOWEST_COMMON_DENOMINATOR_ALL = 11
TEMPLATE_4K_HDR_ATMOS = 12
SINK_1 = 101
SINK_2 = 102
SINK_3 = 103
SINK_4 = 104
SINK_5 = 105
SINK_6 = 106
SINK_7 = 107
SINK_8 = 108
SINK_9 = 109
SINK_10 = 110
SINK_11 = 111
SINK_12 = 112
SINK_13 = 113
SINK_14 = 114
SINK_15= 115
SINK_16 = 116
SINK_17 = 117
SINK_18 = 118
SINK_19 = 119
SINK_20 = 120
SINK_21 = 121
SINK_22 = 122
SINK_23 = 123
SINK_24 = 124
SINK_25 = 125
SINK_26 = 126
SINK_27 = 127
SINK_28 = 128
SINK_29 = 129
SINK_30 = 130
SINK_31 = 131
SINK_32 = 132
CUSTOM_0 = 500
CUSTOM_1 = 501
CUSTOM_2 = 502
CUSTOM_3 = 503
CUSTOM_4 = 504
CUSTOM_5 = 505
CUSTOM_6 = 506
CUSTOM_7 = 507
CUSTOM_8 = 508
CUSTOM_9 = 509
CUSTOM_10 = 510
UNKNOWN = 0xFFF
def values(nb_sinks:int|None=None) -> dict[int, str]: # type: ignore
rv:dict[int, str] = {}
for val in range(13):
rv[val] = str(EdidProfile(val))
if nb_sinks is None:
nb_sinks = 32
for val in range(101, 101 + nb_sinks):
rv[val] = str(EdidProfile(val))
return rv
class FirmwareType(IntEnum):
'''Firmware component type identifiers.'''
UNKNOWN = -1
'''A component this build has no name for. Never on the wire.'''
MCU = 0
'''The unit's own firmware.'''
VIDEO_PROCESSOR = 1
'''The video processor's image.'''
RTOS = 2
'''The video processor's RTOS image.'''
LOADER = 3
'''The video processor's loader image.'''
BOOTLOADER = 4
'''The unit's bootloader.'''
FPGA = 1
'''Deprecated: use VIDEO_PROCESSOR.'''
LINUX = 2
'''Deprecated: use RTOS.'''
LOADING_OVERLAY = 3
'''Deprecated: use LOADER.'''
# No clamping __init__ here: IntEnum rejects an unknown value before __init__
# would run, so a guard there never fires - it only looks like one. Decode
# wire values through decode_enum(), which yields UNKNOWN for anything this
# build does not recognise.
def __str__(self) -> str:
if (self.value == FirmwareType.MCU.value):
return "MCU"
if (self.value == FirmwareType.VIDEO_PROCESSOR.value):
return "Video Processor"
if (self.value == FirmwareType.RTOS.value):
return "RTOS"
if (self.value == FirmwareType.LOADER.value):
return "Loader"
if (self.value == FirmwareType.BOOTLOADER.value):
return "Bootloader"
return "Unknown"
def __repr__(self) -> str:
return str(self)
class UtpLinkSpeed(IntEnum):
''' UTP link speed '''
UNKNOWN = 0
''' unknown speed '''
L_10M = 1
''' 10Mbit/s '''
L_100M = 2
''' 100Mbit/s '''
L_200M = 3
''' 200Mbit/s '''
L_1G = 4
''' 1Gbit/s '''
def __str__(self) -> str:
if self.value == UtpLinkSpeed.L_10M.value:
return '10Mbit/s'
if self.value == UtpLinkSpeed.L_100M.value:
return '100Mbit/s'
if self.value == UtpLinkSpeed.L_200M.value:
return '200Mbit/s'
if self.value == UtpLinkSpeed.L_1G.value:
return '1Gbit/s'
return 'Unknown'
def __repr__(self) -> str:
return str(self)
# The V2IP spelling of a name here, from before the rename to OneIP.
from ..deprecated import module_getattr as _module_getattr
# Installed through globals() so the generated stubs carry no __getattr__, which
# would make a type checker read every unknown name as Any instead of reporting
# it; to a type checker the old names do not exist.
globals()['__getattr__'] = _module_getattr(__name__, globals())