Source code for mx_remote.proto.Constants

######################################################
##            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 OsdCaptionFormat(IntEnum): '''How a caption's text is read.''' PLAIN = 0 MARKDOWN = 1 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 OneIPSdpMediaClock(IntEnum): '''Where a described stream's media clock comes from.''' NONE = 0 DIRECT = 1 '''Direct, at an offset from the reference clock.''' SENDER = 2 '''The sender's.'''
[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())