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 = 0x29
"""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."""

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)
}
"""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)."""

V2IP_AUDIO_DEFAULT_SAMPLE_RATE = 48000
V2IP_AUDIO_DEFAULT_CHANNELS    = 2
V2IP_AUDIO_MIN_CHANNELS        = 1
V2IP_AUDIO_MAX_CHANNELS        = 8

V2IP_SOURCE_RATE_MIN = 5
V2IP_SOURCE_RATE_MAX = 100
"""Valid encoder tx rate range (firmware V2IP_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 v2ip_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 (V2IP_SOURCE_RATE_MIN <= rate <= V2IP_SOURCE_RATE_MAX)

MXR_SCALING_FLAG_MODE_VALID    = (1 << 0)
MXR_SCALING_FLAG_OPTIONS_VALID = (1 << 1)
MXR_SCALING_FLAG_AUTO_SCALING  = (1 << 7)
"""mxr_scaling_config.flags. The mode/refresh pair and the options nibble are
separately valid: a sender carrying neither flag is offering no scaling at all."""

MXR_V2IP_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."""

V2IP_DSCP_MAX = 63
"""Highest DSCP value; the marking occupies the upper 6 bits of the IPv4 TOS byte."""

V2IP_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 v2ip_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_V2IP_DSCP_SET) == 0):
        return None
    return (raw & V2IP_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)
	V2IP_SOURCE        = (1 << 3)
	V2IP_SINK          = (1 << 4)
	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)
	BOOT_BIT           = (1 << 31)

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)
	V2IP_SOURCE_REMOTE = (1 << 13)
	V2IP_SINK_REMOTE   = (1 << 14)
	V2IP_SOURCE_LOCAL  = (1 << 15)
	V2IP_SINK_LOCAL    = (1 << 16)

	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.V2IP_SOURCE_REMOTE in self:
			rv += ", V2IP remote source"
		if BayFeaturesMask.V2IP_SINK_REMOTE in self:
			rv += ", V2IP remote sink"
		if BayFeaturesMask.V2IP_SOURCE_LOCAL in self:
			rv += ", V2IP local source"
		if BayFeaturesMask.V2IP_SINK_LOCAL in self:
			rv += ", V2IP 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)
	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)

	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 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)

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])

	@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_DOWN = 3 ACTION_VOLUME_UP = 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 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" return "Unknown"
[docs] @staticmethod def values() -> dict[int, str]: rv:dict[int, str] = {} for val in range(8): rv[val] = str(RCType(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 = 0 FPGA = 1 LINUX = 2 LOADING_OVERLAY = 3 # 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.FPGA.value): return "FPGA" if (self.value == FirmwareType.LINUX.value): return "Linux" if (self.value == FirmwareType.LOADING_OVERLAY.value): return "Loading Overlay" 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)