mx-remote 4.7.8__tar.gz → 5.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mx_remote-4.7.8 → mx_remote-5.0.0}/CLAUDE.md +47 -4
- {mx_remote-4.7.8 → mx_remote-5.0.0}/PKG-INFO +60 -17
- {mx_remote-4.7.8 → mx_remote-5.0.0}/README.md +58 -15
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/Interface.py +242 -4
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/Interface.pyi +41 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/const.py +10 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/const.pyi +3 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/BayConfig.py +25 -5
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/BayConfig.pyi +3 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Constants.py +251 -10
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Constants.pyi +61 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Factory.py +30 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameAmpZoneSettings.py +52 -4
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +2 -0
- mx_remote-5.0.0/mx_remote/proto/FrameAudioClip.py +44 -0
- mx_remote-4.7.8/mx_remote/proto/FrameRCIr.pyi → mx_remote-5.0.0/mx_remote/proto/FrameAudioClip.pyi +3 -2
- mx_remote-5.0.0/mx_remote/proto/FrameAudioSetRoute.py +55 -0
- mx_remote-5.0.0/mx_remote/proto/FrameAudioSetRoute.pyi +14 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBase.py +31 -6
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBase.pyi +4 -2
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayConfig.py +17 -10
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayConfigSecondary.py +13 -10
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayStatus.py +3 -3
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayStatus.pyi +1 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameFirmwareVersion.py +13 -8
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameFirmwareVersion.pyi +2 -2
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameHeader.py +4 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameLinks.py +17 -13
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameMeshOperation.py +2 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameMeshOperation.pyi +1 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameNetworkStatus.py +5 -4
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameNetworkStatus.pyi +1 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameRCAction.py +2 -5
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameRCAction.pyi +1 -1
- mx_remote-5.0.0/mx_remote/proto/FrameRCIr.py +85 -0
- mx_remote-5.0.0/mx_remote/proto/FrameRCIr.pyi +23 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameRCKey.py +2 -4
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameRCKey.pyi +1 -1
- mx_remote-5.0.0/mx_remote/proto/FrameRCSettings.py +150 -0
- mx_remote-5.0.0/mx_remote/proto/FrameRCSettings.pyi +28 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameRoutingChange.py +5 -5
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetRoute.py +11 -8
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetupStatus.py +1 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSignalStatusNew.py +58 -8
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSignalStatusNew.pyi +8 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameTXRCAction.py +28 -10
- mx_remote-5.0.0/mx_remote/proto/FrameTXRCKey.py +71 -0
- mx_remote-5.0.0/mx_remote/proto/FrameTXRCKey.pyi +18 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameTxIR.py +15 -6
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPAudio.py +21 -4
- mx_remote-5.0.0/mx_remote/proto/FrameV2IPDetectBays.py +30 -0
- mx_remote-5.0.0/mx_remote/proto/FrameV2IPDetectBays.pyi +7 -0
- mx_remote-5.0.0/mx_remote/proto/FrameV2IPDeviceConfiguration.py +168 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +7 -2
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPMultiviewer.py +46 -15
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +6 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPSources.py +11 -1
- mx_remote-5.0.0/mx_remote/proto/FrameV2IPVideoWall.py +155 -0
- mx_remote-5.0.0/mx_remote/proto/FrameV2IPVideoWall.pyi +32 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/LinkConfig.py +2 -2
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/V2IPConfig.py +3 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/V2IPStats.py +43 -4
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/V2IPStats.pyi +4 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Bay.py +56 -22
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Bay.pyi +2 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/ConnectionAsync.py +42 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Device.py +57 -24
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Device.pyi +6 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Remote.py +13 -2
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Remote.pyi +1 -1
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/V2IP.py +2 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/pyproject.toml +13 -0
- mx_remote-5.0.0/tests/audio_ir.py +41 -0
- mx_remote-5.0.0/tests/capture.py +51 -0
- mx_remote-5.0.0/tests/e2e.py +108 -0
- mx_remote-5.0.0/tests/handlers.py +124 -0
- mx_remote-5.0.0/tests/merge.py +112 -0
- mx_remote-5.0.0/tests/newops.py +102 -0
- mx_remote-5.0.0/tests/offsets.py +59 -0
- mx_remote-5.0.0/tests/proto_stamp.py +60 -0
- mx_remote-5.0.0/tests/rcsettings.py +79 -0
- mx_remote-5.0.0/tests/run.py +57 -0
- mx_remote-5.0.0/tests/smoke.py +45 -0
- mx_remote-5.0.0/tests/sources.py +74 -0
- mx_remote-5.0.0/tests/stats.py +73 -0
- mx_remote-5.0.0/tests/structs.py +93 -0
- mx_remote-5.0.0/tests/txresult.py +105 -0
- mx_remote-5.0.0/tests/unknowns.py +58 -0
- mx_remote-4.7.8/.claude/agents/semble-search.md +0 -30
- mx_remote-4.7.8/.claude/settings.local.json +0 -9
- mx_remote-4.7.8/mx_remote/proto/FrameRCIr.py +0 -29
- mx_remote-4.7.8/mx_remote/proto/FrameRCSettings.py +0 -18
- mx_remote-4.7.8/mx_remote/proto/FrameRCSettings.pyi +0 -4
- mx_remote-4.7.8/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -108
- {mx_remote-4.7.8 → mx_remote-5.0.0}/.gitignore +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/LICENSE +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/hatch_build.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/Uid.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/__init__.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/__init__.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/main.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/main.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameHello.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FramePDUState.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FramePDUState.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPStreamDetails.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPStreamDetails.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/PDUState.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/PDUState.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Svd.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/Svd.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/py.typed +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/P8PDU.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/P8PDU.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-4.7.8 → mx_remote-5.0.0}/mx_remote/remote/V2IP.pyi +0 -0
|
@@ -21,10 +21,44 @@ mxr # console app: discover devices, log frames live
|
|
|
21
21
|
mxr -l <local_ip> # bind to a specific interface
|
|
22
22
|
mxr -b # broadcast mode instead of multicast
|
|
23
23
|
mxr -i <capture> [-f <ip>] # parse a MatrixOS/Wireshark capture file offline
|
|
24
|
+
python tests/run.py # run every protocol test
|
|
25
|
+
python tests/run.py e2e stats # run named suites only
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
There is
|
|
27
|
-
|
|
28
|
+
There is no lint config in this repo.
|
|
29
|
+
|
|
30
|
+
`tests/` holds standalone scripts with no test framework behind it — each builds
|
|
31
|
+
frames, feeds them through `Remote.process_frame`, and asserts what was decoded.
|
|
32
|
+
A suite prints what it decoded and ends with `ALL OK`, so it can be run on its
|
|
33
|
+
own as well as through `run.py`.
|
|
34
|
+
|
|
35
|
+
Two things they pin that code review does not catch:
|
|
36
|
+
|
|
37
|
+
- **Struct sizes, not just field offsets.** The V2IP stats blocks are 20 and 44
|
|
38
|
+
bytes only because the firmware's `ALIGN(8)` sits where GCC ignores it. Writing
|
|
39
|
+
that declaration the other way round shifts every block after the first, and a
|
|
40
|
+
wrong split still totals 128.
|
|
41
|
+
- **Field widths.** Padding in the fixtures is filled with recognisable bytes, so
|
|
42
|
+
a field read at the right offset with the wrong width returns a wrong value. A
|
|
43
|
+
zero-filled fixture cannot catch that: the widened read returns the same
|
|
44
|
+
answer.
|
|
45
|
+
- **Which handlers run at all.** `tests/handlers.py` reports how many frame
|
|
46
|
+
`process()` methods the other suites execute and fails if one loses its
|
|
47
|
+
coverage. A handler nothing calls is invisible to every other check here,
|
|
48
|
+
since they all measure tests that run.
|
|
49
|
+
|
|
50
|
+
`tests/capture.py` holds real frames off a live mesh, with the device uids
|
|
51
|
+
replaced. Its expected values come from firmware behaviour rather than from this
|
|
52
|
+
decoder, which is the property a synthetic fixture cannot have — assert against
|
|
53
|
+
something external, or a fixture only proves the decoder agrees with itself.
|
|
54
|
+
|
|
55
|
+
Validate wire changes against live hardware as well, by running `mxr` or
|
|
56
|
+
replaying a capture file with `mxr -i`.
|
|
57
|
+
|
|
58
|
+
A client's uid is persisted at `~/.mxr-uid`, one file per account, and
|
|
59
|
+
`process_frame` drops any frame carrying our own uid. Two clients run from one
|
|
60
|
+
account are a single identity to the mesh and each discards everything the other
|
|
61
|
+
sends, so a second one needs its own `uid_path`.
|
|
28
62
|
|
|
29
63
|
### Versioning
|
|
30
64
|
Version is single-sourced in `mx_remote/const.py` (`VERSION = '...'`); `pyproject.toml`
|
|
@@ -67,8 +101,17 @@ Three layers, decoupled by abstract base classes:
|
|
|
67
101
|
`remote_id == self.uid` (our own echoes) are skipped.
|
|
68
102
|
|
|
69
103
|
### Frame wire format
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
A 24-byte header, then the payload:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
0 [0x50, 0x38, protocol, 0x00] "P8" magic + protocol version
|
|
108
|
+
4 sender uid 16 bytes, the device that sent this frame
|
|
109
|
+
20 opcode u16 LE
|
|
110
|
+
22 payload length u16 LE
|
|
111
|
+
24 payload
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Built by `proto/Factory.py::create_mxr_frame`, decoded by `proto/FrameHeader.py`.
|
|
72
115
|
|
|
73
116
|
## Working on the protocol
|
|
74
117
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: mx_remote
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Python 3 library for interfacing with MX Remote compatible devices
|
|
5
5
|
Project-URL: Homepage, https://github.com/opdenkamp/mx-remote/
|
|
6
6
|
Project-URL: Documentation, https://github.com/opdenkamp/mx-remote/
|
|
@@ -30,14 +30,39 @@ Requires-Dist: aiohttp>=3.10.5
|
|
|
30
30
|
Requires-Dist: netifaces>=0.11.0
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
|
-
# MX Remote
|
|
33
|
+
# MX Remote — Pulse-Eight device interface
|
|
34
34
|
|
|
35
|
-
Python 3 library for
|
|
35
|
+
Python 3 library for discovering and controlling [Pulse-Eight](https://www.pulse-eight.com/)
|
|
36
|
+
AV distribution hardware over a local network: video/audio matrices, HDMI-over-IP
|
|
37
|
+
encoders/decoders, multiviewers, and audio amplifiers. Supports device discovery,
|
|
38
|
+
video/audio routing, volume control, remote-control key passthrough, HDMI-over-IP
|
|
39
|
+
streaming, multiviewer control, and more.
|
|
40
|
+
|
|
41
|
+
If you are looking to integrate Pulse-Eight **neo**, **OneIP**, or **ProAmp8** devices into
|
|
42
|
+
your own software or home-automation system, this is the library for it.
|
|
43
|
+
|
|
44
|
+
## What is MX Remote?
|
|
45
|
+
|
|
46
|
+
MX Remote is the network protocol these Pulse-Eight devices use to discover and control one
|
|
47
|
+
another over UDP (multicast or broadcast). All of them run the shared **MatrixOS** firmware,
|
|
48
|
+
which speaks this protocol natively. This library is a client implementation of that
|
|
49
|
+
protocol — its purpose is to expose the devices to third-party software.
|
|
50
|
+
|
|
51
|
+
## Supported devices
|
|
52
|
+
|
|
53
|
+
All devices below run MatrixOS and are controlled through the same protocol:
|
|
54
|
+
|
|
55
|
+
- **[Pulse-Eight neo](https://www.pulse-eight.com/)** — HDBaseT video/audio matrices
|
|
56
|
+
(neo:4, neo:8, neo:X, and splitters)
|
|
57
|
+
- **[Pulse-Eight OneIP](https://www.pulse-eight.com/p/248/oneip-tx)** — HDMI-over-IP
|
|
58
|
+
units: Transmitter (TX), Receiver (RX), Transceiver (TZ), and Multiviewer
|
|
59
|
+
- **[Pulse-Eight ProAmp8](https://www.pulse-eight.com/p/219/proamp-8)** — 8-zone audio amplifier with
|
|
60
|
+
Dolby support
|
|
36
61
|
|
|
37
62
|
## Requirements
|
|
38
63
|
|
|
39
64
|
- Python 3.11 or later
|
|
40
|
-
- Network access to
|
|
65
|
+
- Network access to one or more of the Pulse-Eight devices above (multicast or broadcast)
|
|
41
66
|
|
|
42
67
|
## Installation
|
|
43
68
|
|
|
@@ -91,11 +116,19 @@ mx = mx_remote.Remote(target_ip="10.8.8.255", port=8811)
|
|
|
91
116
|
|
|
92
117
|
# offline mode for processing capture files
|
|
93
118
|
mx = mx_remote.Remote(open_connection=False)
|
|
119
|
+
|
|
120
|
+
# separate identity, to run concurrent clients from one account
|
|
121
|
+
mx = mx_remote.Remote(uid_path="/var/lib/myapp/.mxr-uid")
|
|
94
122
|
```
|
|
95
123
|
|
|
124
|
+
The uid at `uid_path` (default `~/.mxr-uid`) is this client's identity on the mesh:
|
|
125
|
+
addressing is by uid in the payload, and a frame carrying our own uid is dropped as
|
|
126
|
+
an echo. Two clients sharing a uid are one peer to every device and each silently
|
|
127
|
+
discards everything the other sends, so give concurrent clients separate paths.
|
|
128
|
+
|
|
96
129
|
### Device
|
|
97
130
|
|
|
98
|
-
A `DeviceBase` represents a physical device on the network (matrix, OneIP unit, amplifier). Devices are automatically registered when they respond to discovery requests.
|
|
131
|
+
A `DeviceBase` represents a physical device on the network (a Pulse-Eight neo matrix, OneIP unit, or ProAmp8 amplifier). Devices are automatically registered when they respond to discovery requests.
|
|
99
132
|
|
|
100
133
|
```python
|
|
101
134
|
# look up a device by serial number or unique ID
|
|
@@ -111,17 +144,23 @@ device.version # firmware version
|
|
|
111
144
|
device.online # True if responding
|
|
112
145
|
device.status # DeviceStatus enum (ONLINE, OFFLINE, REBOOTING, BOOTING, INACTIVE)
|
|
113
146
|
device.features # DeviceFeatures bitmask
|
|
147
|
+
device.config_initialised # False if the device broadcasts config blocks built from
|
|
148
|
+
# uninitialised memory; scaling, bay 0 addresses and the
|
|
149
|
+
# rc target padding are unreliable from it
|
|
114
150
|
device.temperatures # dict of temperature sensor readings
|
|
115
151
|
|
|
116
152
|
# device type checks
|
|
117
|
-
device.is_v2ip # OneIP device
|
|
118
|
-
device.is_video_matrix # video matrix
|
|
153
|
+
device.is_v2ip # Pulse-Eight OneIP HDMI-over-IP device
|
|
154
|
+
device.is_video_matrix # neo video matrix
|
|
119
155
|
device.is_audio_matrix # audio-only matrix
|
|
120
|
-
device.is_amp # audio amplifier
|
|
156
|
+
device.is_amp # ProAmp8 audio amplifier
|
|
121
157
|
device.is_oneip_tx # OneIP transmitter
|
|
122
158
|
device.is_oneip_rx # OneIP receiver
|
|
123
159
|
device.is_oneip_tz # OneIP transceiver
|
|
124
160
|
device.is_oneip_multiviewer # OneIP multiviewer
|
|
161
|
+
device.supports_video_wall # sink can crop its source to a video wall window
|
|
162
|
+
# (advertised ~1s after boot by the v2ipwall module,
|
|
163
|
+
# and never withdrawn once advertised)
|
|
125
164
|
|
|
126
165
|
# iterate bays
|
|
127
166
|
for port, bay in device.bays.items():
|
|
@@ -336,24 +375,28 @@ log = await device.get_log()
|
|
|
336
375
|
result = await device.get_api("system/status")
|
|
337
376
|
```
|
|
338
377
|
|
|
339
|
-
## OneIP
|
|
378
|
+
## Pulse-Eight OneIP Devices
|
|
340
379
|
|
|
341
|
-
OneIP devices expose additional streaming properties:
|
|
380
|
+
[Pulse-Eight OneIP](https://www.pulse-eight.com/p/248/oneip-tx) HDMI-over-IP devices expose additional streaming properties:
|
|
342
381
|
|
|
343
382
|
```python
|
|
344
|
-
#
|
|
383
|
+
# stream source addresses
|
|
345
384
|
if device.is_v2ip and device.v2ip_sources:
|
|
346
385
|
for source in device.v2ip_sources:
|
|
347
386
|
print(f"Video: {source.video.ip}:{source.video.port}")
|
|
348
387
|
print(f"Audio: {source.audio.ip}:{source.audio.port}")
|
|
349
388
|
|
|
350
|
-
#
|
|
389
|
+
# stream details (encoder/decoder config)
|
|
351
390
|
if device.v2ip_details:
|
|
352
391
|
details = device.v2ip_details
|
|
353
392
|
print(f"Video: {details.video}")
|
|
354
|
-
print(f"TX rate: {details.tx_rate}")
|
|
393
|
+
print(f"TX rate: {details.tx_rate}") # None when the sender offered no rate
|
|
394
|
+
|
|
395
|
+
# per-stream DSCP marking (None from peers that predate it)
|
|
396
|
+
if details.dscp and details.dscp.complete:
|
|
397
|
+
print(f"DSCP: {details.dscp}") # video / audio / anc, 0..63
|
|
355
398
|
|
|
356
|
-
#
|
|
399
|
+
# streaming statistics
|
|
357
400
|
await device.read_stats(enable=True) # start collecting
|
|
358
401
|
# ... later ...
|
|
359
402
|
stats = device.v2ip_stats
|
|
@@ -368,9 +411,9 @@ if device.v2ip_firmware_versions:
|
|
|
368
411
|
print(f"{fw_type}: {fw.version}")
|
|
369
412
|
```
|
|
370
413
|
|
|
371
|
-
## OneIP Multiviewer
|
|
414
|
+
## Pulse-Eight OneIP Multiviewer
|
|
372
415
|
|
|
373
|
-
Control
|
|
416
|
+
Control [OneIP Multiviewer](https://www.pulse-eight.com/p/248/oneip-tx)-specific settings:
|
|
374
417
|
|
|
375
418
|
```python
|
|
376
419
|
from mx_remote import (
|
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
# MX Remote
|
|
1
|
+
# MX Remote — Pulse-Eight device interface
|
|
2
2
|
|
|
3
|
-
Python 3 library for
|
|
3
|
+
Python 3 library for discovering and controlling [Pulse-Eight](https://www.pulse-eight.com/)
|
|
4
|
+
AV distribution hardware over a local network: video/audio matrices, HDMI-over-IP
|
|
5
|
+
encoders/decoders, multiviewers, and audio amplifiers. Supports device discovery,
|
|
6
|
+
video/audio routing, volume control, remote-control key passthrough, HDMI-over-IP
|
|
7
|
+
streaming, multiviewer control, and more.
|
|
8
|
+
|
|
9
|
+
If you are looking to integrate Pulse-Eight **neo**, **OneIP**, or **ProAmp8** devices into
|
|
10
|
+
your own software or home-automation system, this is the library for it.
|
|
11
|
+
|
|
12
|
+
## What is MX Remote?
|
|
13
|
+
|
|
14
|
+
MX Remote is the network protocol these Pulse-Eight devices use to discover and control one
|
|
15
|
+
another over UDP (multicast or broadcast). All of them run the shared **MatrixOS** firmware,
|
|
16
|
+
which speaks this protocol natively. This library is a client implementation of that
|
|
17
|
+
protocol — its purpose is to expose the devices to third-party software.
|
|
18
|
+
|
|
19
|
+
## Supported devices
|
|
20
|
+
|
|
21
|
+
All devices below run MatrixOS and are controlled through the same protocol:
|
|
22
|
+
|
|
23
|
+
- **[Pulse-Eight neo](https://www.pulse-eight.com/)** — HDBaseT video/audio matrices
|
|
24
|
+
(neo:4, neo:8, neo:X, and splitters)
|
|
25
|
+
- **[Pulse-Eight OneIP](https://www.pulse-eight.com/p/248/oneip-tx)** — HDMI-over-IP
|
|
26
|
+
units: Transmitter (TX), Receiver (RX), Transceiver (TZ), and Multiviewer
|
|
27
|
+
- **[Pulse-Eight ProAmp8](https://www.pulse-eight.com/p/219/proamp-8)** — 8-zone audio amplifier with
|
|
28
|
+
Dolby support
|
|
4
29
|
|
|
5
30
|
## Requirements
|
|
6
31
|
|
|
7
32
|
- Python 3.11 or later
|
|
8
|
-
- Network access to
|
|
33
|
+
- Network access to one or more of the Pulse-Eight devices above (multicast or broadcast)
|
|
9
34
|
|
|
10
35
|
## Installation
|
|
11
36
|
|
|
@@ -59,11 +84,19 @@ mx = mx_remote.Remote(target_ip="10.8.8.255", port=8811)
|
|
|
59
84
|
|
|
60
85
|
# offline mode for processing capture files
|
|
61
86
|
mx = mx_remote.Remote(open_connection=False)
|
|
87
|
+
|
|
88
|
+
# separate identity, to run concurrent clients from one account
|
|
89
|
+
mx = mx_remote.Remote(uid_path="/var/lib/myapp/.mxr-uid")
|
|
62
90
|
```
|
|
63
91
|
|
|
92
|
+
The uid at `uid_path` (default `~/.mxr-uid`) is this client's identity on the mesh:
|
|
93
|
+
addressing is by uid in the payload, and a frame carrying our own uid is dropped as
|
|
94
|
+
an echo. Two clients sharing a uid are one peer to every device and each silently
|
|
95
|
+
discards everything the other sends, so give concurrent clients separate paths.
|
|
96
|
+
|
|
64
97
|
### Device
|
|
65
98
|
|
|
66
|
-
A `DeviceBase` represents a physical device on the network (matrix, OneIP unit, amplifier). Devices are automatically registered when they respond to discovery requests.
|
|
99
|
+
A `DeviceBase` represents a physical device on the network (a Pulse-Eight neo matrix, OneIP unit, or ProAmp8 amplifier). Devices are automatically registered when they respond to discovery requests.
|
|
67
100
|
|
|
68
101
|
```python
|
|
69
102
|
# look up a device by serial number or unique ID
|
|
@@ -79,17 +112,23 @@ device.version # firmware version
|
|
|
79
112
|
device.online # True if responding
|
|
80
113
|
device.status # DeviceStatus enum (ONLINE, OFFLINE, REBOOTING, BOOTING, INACTIVE)
|
|
81
114
|
device.features # DeviceFeatures bitmask
|
|
115
|
+
device.config_initialised # False if the device broadcasts config blocks built from
|
|
116
|
+
# uninitialised memory; scaling, bay 0 addresses and the
|
|
117
|
+
# rc target padding are unreliable from it
|
|
82
118
|
device.temperatures # dict of temperature sensor readings
|
|
83
119
|
|
|
84
120
|
# device type checks
|
|
85
|
-
device.is_v2ip # OneIP device
|
|
86
|
-
device.is_video_matrix # video matrix
|
|
121
|
+
device.is_v2ip # Pulse-Eight OneIP HDMI-over-IP device
|
|
122
|
+
device.is_video_matrix # neo video matrix
|
|
87
123
|
device.is_audio_matrix # audio-only matrix
|
|
88
|
-
device.is_amp # audio amplifier
|
|
124
|
+
device.is_amp # ProAmp8 audio amplifier
|
|
89
125
|
device.is_oneip_tx # OneIP transmitter
|
|
90
126
|
device.is_oneip_rx # OneIP receiver
|
|
91
127
|
device.is_oneip_tz # OneIP transceiver
|
|
92
128
|
device.is_oneip_multiviewer # OneIP multiviewer
|
|
129
|
+
device.supports_video_wall # sink can crop its source to a video wall window
|
|
130
|
+
# (advertised ~1s after boot by the v2ipwall module,
|
|
131
|
+
# and never withdrawn once advertised)
|
|
93
132
|
|
|
94
133
|
# iterate bays
|
|
95
134
|
for port, bay in device.bays.items():
|
|
@@ -304,24 +343,28 @@ log = await device.get_log()
|
|
|
304
343
|
result = await device.get_api("system/status")
|
|
305
344
|
```
|
|
306
345
|
|
|
307
|
-
## OneIP
|
|
346
|
+
## Pulse-Eight OneIP Devices
|
|
308
347
|
|
|
309
|
-
OneIP devices expose additional streaming properties:
|
|
348
|
+
[Pulse-Eight OneIP](https://www.pulse-eight.com/p/248/oneip-tx) HDMI-over-IP devices expose additional streaming properties:
|
|
310
349
|
|
|
311
350
|
```python
|
|
312
|
-
#
|
|
351
|
+
# stream source addresses
|
|
313
352
|
if device.is_v2ip and device.v2ip_sources:
|
|
314
353
|
for source in device.v2ip_sources:
|
|
315
354
|
print(f"Video: {source.video.ip}:{source.video.port}")
|
|
316
355
|
print(f"Audio: {source.audio.ip}:{source.audio.port}")
|
|
317
356
|
|
|
318
|
-
#
|
|
357
|
+
# stream details (encoder/decoder config)
|
|
319
358
|
if device.v2ip_details:
|
|
320
359
|
details = device.v2ip_details
|
|
321
360
|
print(f"Video: {details.video}")
|
|
322
|
-
print(f"TX rate: {details.tx_rate}")
|
|
361
|
+
print(f"TX rate: {details.tx_rate}") # None when the sender offered no rate
|
|
362
|
+
|
|
363
|
+
# per-stream DSCP marking (None from peers that predate it)
|
|
364
|
+
if details.dscp and details.dscp.complete:
|
|
365
|
+
print(f"DSCP: {details.dscp}") # video / audio / anc, 0..63
|
|
323
366
|
|
|
324
|
-
#
|
|
367
|
+
# streaming statistics
|
|
325
368
|
await device.read_stats(enable=True) # start collecting
|
|
326
369
|
# ... later ...
|
|
327
370
|
stats = device.v2ip_stats
|
|
@@ -336,9 +379,9 @@ if device.v2ip_firmware_versions:
|
|
|
336
379
|
print(f"{fw_type}: {fw.version}")
|
|
337
380
|
```
|
|
338
381
|
|
|
339
|
-
## OneIP Multiviewer
|
|
382
|
+
## Pulse-Eight OneIP Multiviewer
|
|
340
383
|
|
|
341
|
-
Control
|
|
384
|
+
Control [OneIP Multiviewer](https://www.pulse-eight.com/p/248/oneip-tx)-specific settings:
|
|
342
385
|
|
|
343
386
|
```python
|
|
344
387
|
from mx_remote import (
|
|
@@ -357,6 +357,31 @@ class V2IPStreamSources:
|
|
|
357
357
|
def arc(self) -> V2IPStreamSource|None:
|
|
358
358
|
''' audio return stream source '''
|
|
359
359
|
|
|
360
|
+
@property
|
|
361
|
+
def valid(self) -> bool:
|
|
362
|
+
"""
|
|
363
|
+
True when this entry carries a usable address.
|
|
364
|
+
|
|
365
|
+
Video and anc must both be multicast with a non-zero port; audio is
|
|
366
|
+
optional and rides along. A sender without MXR_FEATURE_CONFIG_INITIALISED
|
|
367
|
+
can put stack contents in bay 0's addresses under its own uid, which
|
|
368
|
+
rarely satisfy both halves of this test.
|
|
369
|
+
"""
|
|
370
|
+
return v2ip_av_source_valid(self.video, self.anc)
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def cleared(self) -> bool:
|
|
374
|
+
"""
|
|
375
|
+
True when every stream is zeroed, which reports that the source is gone.
|
|
376
|
+
|
|
377
|
+
Distinct from an entry that is neither valid nor cleared, which is
|
|
378
|
+
malformed. Treating a cleared entry as unusable discards the only signal
|
|
379
|
+
that a source went away.
|
|
380
|
+
"""
|
|
381
|
+
return (v2ip_stream_cleared(self.video)
|
|
382
|
+
and v2ip_stream_cleared(self.audio)
|
|
383
|
+
and v2ip_stream_cleared(self.anc))
|
|
384
|
+
|
|
360
385
|
class V2IPStreamSourcesList(list[V2IPStreamSources]):
|
|
361
386
|
''' list of V2IP sources '''
|
|
362
387
|
|
|
@@ -1183,27 +1208,154 @@ class DeviceV2IPScalingSettings(ABC):
|
|
|
1183
1208
|
@property
|
|
1184
1209
|
@abstractmethod
|
|
1185
1210
|
def mode(self) -> int:
|
|
1186
|
-
|
|
1211
|
+
'''Scaling mode, carried only when flags has MXR_SCALING_FLAG_MODE_VALID'''
|
|
1187
1212
|
|
|
1188
1213
|
@property
|
|
1189
1214
|
@abstractmethod
|
|
1190
1215
|
def refresh(self) -> int:
|
|
1191
|
-
|
|
1216
|
+
'''Refresh rate paired with mode, under the same MXR_SCALING_FLAG_MODE_VALID marker'''
|
|
1192
1217
|
|
|
1193
1218
|
@property
|
|
1194
1219
|
@abstractmethod
|
|
1195
1220
|
def flags(self) -> int:
|
|
1196
|
-
|
|
1221
|
+
'''mxr_scaling_config.flags.
|
|
1222
|
+
|
|
1223
|
+
MXR_SCALING_FLAG_MODE_VALID and MXR_SCALING_FLAG_OPTIONS_VALID mark
|
|
1224
|
+
which half a sender meant to send, and are separately valid; a sender
|
|
1225
|
+
setting neither is offering no scaling at all. MXR_SCALING_FLAG_AUTO_SCALING
|
|
1226
|
+
is the only option bit with a defined meaning.
|
|
1227
|
+
'''
|
|
1228
|
+
|
|
1229
|
+
def v2ip_stream_valid(stream:'V2IPStreamSource|None') -> bool:
|
|
1230
|
+
"""
|
|
1231
|
+
True when a stream source carries an address a frame actually meant to send.
|
|
1232
|
+
|
|
1233
|
+
Mirrors mxr_v2ip_stream_valid(): a multicast ip and a non-zero port. A
|
|
1234
|
+
controller write that offers no addresses leaves the block zeroed, which
|
|
1235
|
+
fails both tests.
|
|
1236
|
+
"""
|
|
1237
|
+
if (stream is None):
|
|
1238
|
+
return False
|
|
1239
|
+
try:
|
|
1240
|
+
return ipaddress.IPv4Address(stream.ip).is_multicast and (stream.port != 0)
|
|
1241
|
+
except ValueError:
|
|
1242
|
+
return False
|
|
1243
|
+
|
|
1244
|
+
def v2ip_av_source_valid(video:'V2IPStreamSource|None', anc:'V2IPStreamSource|None') -> bool:
|
|
1245
|
+
"""
|
|
1246
|
+
True when an av source block carries addresses a frame meant to send.
|
|
1247
|
+
|
|
1248
|
+
Mirrors mxr_v2ip_av_source_valid(): video and anc must both be valid, audio
|
|
1249
|
+
is optional and is carried along with them.
|
|
1250
|
+
"""
|
|
1251
|
+
return v2ip_stream_valid(video) and v2ip_stream_valid(anc)
|
|
1252
|
+
|
|
1253
|
+
def v2ip_stream_cleared(stream:'V2IPStreamSource|None') -> bool:
|
|
1254
|
+
"""True when a stream source is zeroed, which is how "no source" is spelled."""
|
|
1255
|
+
return (stream is not None) and (stream.ip == '0.0.0.0') and (stream.port == 0)
|
|
1256
|
+
|
|
1257
|
+
class V2IPScalingSettings(DeviceV2IPScalingSettings):
|
|
1258
|
+
"""Plain holder for a merged mxr_scaling_config."""
|
|
1259
|
+
def __init__(self, mode:int, refresh:int, flags:int) -> None:
|
|
1260
|
+
self._mode = mode
|
|
1261
|
+
self._refresh = refresh
|
|
1262
|
+
self._flags = flags
|
|
1263
|
+
|
|
1264
|
+
@property
|
|
1265
|
+
def mode(self) -> int:
|
|
1266
|
+
return self._mode
|
|
1267
|
+
|
|
1268
|
+
@property
|
|
1269
|
+
def refresh(self) -> int:
|
|
1270
|
+
return self._refresh
|
|
1271
|
+
|
|
1272
|
+
@property
|
|
1273
|
+
def flags(self) -> int:
|
|
1274
|
+
return self._flags
|
|
1275
|
+
|
|
1276
|
+
def __str__(self) -> str:
|
|
1277
|
+
return f"mode:{self._mode} refresh:{self._refresh} flags:{self._flags:02X}"
|
|
1278
|
+
|
|
1279
|
+
def __repr__(self) -> str:
|
|
1280
|
+
return str(self)
|
|
1281
|
+
|
|
1282
|
+
class V2IPDscpConfig:
|
|
1283
|
+
"""
|
|
1284
|
+
Per-stream DSCP marking carried in a V2IP device configuration.
|
|
1285
|
+
|
|
1286
|
+
DSCP 0 (CS0) is a legal marking, so each wire byte carries a set bit
|
|
1287
|
+
alongside its value and a stream whose byte is unset reads back as None
|
|
1288
|
+
here. Firmware treats the marking as all-or-nothing: it only applies one
|
|
1289
|
+
when all three streams carry a value, and otherwise falls back to CS2
|
|
1290
|
+
(``V2IP_DSCP_DEFAULT``) - ``complete`` reports which case a frame is in.
|
|
1291
|
+
"""
|
|
1292
|
+
def __init__(self, video:int|None, audio:int|None, anc:int|None) -> None:
|
|
1293
|
+
self._video = video
|
|
1294
|
+
self._audio = audio
|
|
1295
|
+
self._anc = anc
|
|
1296
|
+
|
|
1297
|
+
@property
|
|
1298
|
+
def video(self) -> int|None:
|
|
1299
|
+
return self._video
|
|
1300
|
+
|
|
1301
|
+
@property
|
|
1302
|
+
def audio(self) -> int|None:
|
|
1303
|
+
return self._audio
|
|
1304
|
+
|
|
1305
|
+
@property
|
|
1306
|
+
def anc(self) -> int|None:
|
|
1307
|
+
return self._anc
|
|
1308
|
+
|
|
1309
|
+
@property
|
|
1310
|
+
def carried(self) -> bool:
|
|
1311
|
+
"""
|
|
1312
|
+
True when this frame carried a marking at all.
|
|
1313
|
+
|
|
1314
|
+
Gated on the video byte alone, matching the firmware cache, which stores
|
|
1315
|
+
all three bytes verbatim. Not ``complete``: a partial set does overwrite
|
|
1316
|
+
a peer's marking, so treating it as absent would report a marking the
|
|
1317
|
+
peer does not hold.
|
|
1318
|
+
"""
|
|
1319
|
+
return (self._video is not None)
|
|
1320
|
+
|
|
1321
|
+
@property
|
|
1322
|
+
def complete(self) -> bool:
|
|
1323
|
+
"""
|
|
1324
|
+
True when all three streams carry a marking.
|
|
1325
|
+
|
|
1326
|
+
Whether a marking is applied, as opposed to carried: firmware falls back
|
|
1327
|
+
to CS2 on all three streams unless every byte is set, so a partial set is
|
|
1328
|
+
cached and reported but never applied.
|
|
1329
|
+
"""
|
|
1330
|
+
return (self._video is not None) and (self._audio is not None) and (self._anc is not None)
|
|
1331
|
+
|
|
1332
|
+
def __eq__(self, other:Any) -> bool:
|
|
1333
|
+
if not isinstance(other, V2IPDscpConfig):
|
|
1334
|
+
return False
|
|
1335
|
+
return (self._video == other._video) and (self._audio == other._audio) and (self._anc == other._anc)
|
|
1336
|
+
|
|
1337
|
+
def __str__(self) -> str:
|
|
1338
|
+
if not self.carried:
|
|
1339
|
+
return "no marking"
|
|
1340
|
+
def one(v:int|None) -> str:
|
|
1341
|
+
return str(v) if (v is not None) else "unset"
|
|
1342
|
+
rv = f"video:{one(self._video)} audio:{one(self._audio)} anc:{one(self._anc)}"
|
|
1343
|
+
# a partial set is cached and reported, but firmware will not apply it
|
|
1344
|
+
return rv if self.complete else (rv + " (partial, not applied)")
|
|
1345
|
+
|
|
1346
|
+
def __repr__(self) -> str:
|
|
1347
|
+
return str(self)
|
|
1197
1348
|
|
|
1198
1349
|
class DeviceV2IPDetails:
|
|
1199
1350
|
""" V2IP stream source details for a device """
|
|
1200
|
-
def __init__(self, video:V2IPStreamSource|None, audio:V2IPStreamSource|None, anc:V2IPStreamSource|None, arc:V2IPStreamSource|None, tx_rate:int|None, scaling:DeviceV2IPScalingSettings|None) -> None:
|
|
1351
|
+
def __init__(self, video:V2IPStreamSource|None, audio:V2IPStreamSource|None, anc:V2IPStreamSource|None, arc:V2IPStreamSource|None, tx_rate:int|None, scaling:DeviceV2IPScalingSettings|None, dscp:V2IPDscpConfig|None=None) -> None:
|
|
1201
1352
|
self._video = video
|
|
1202
1353
|
self._audio = audio
|
|
1203
1354
|
self._anc = anc
|
|
1204
1355
|
self._arc = arc
|
|
1205
1356
|
self._tx_rate = tx_rate
|
|
1206
1357
|
self._scaling = scaling
|
|
1358
|
+
self._dscp = dscp
|
|
1207
1359
|
|
|
1208
1360
|
@property
|
|
1209
1361
|
def has_config(self) -> bool:
|
|
@@ -1227,12 +1379,81 @@ class DeviceV2IPDetails:
|
|
|
1227
1379
|
|
|
1228
1380
|
@property
|
|
1229
1381
|
def tx_rate(self) -> int|None:
|
|
1382
|
+
'''Source rate this device transmits at, or None if none is known.
|
|
1383
|
+
|
|
1384
|
+
Only a rate within V2IP_SOURCE_RATE_MIN..MAX is one a sender meant to
|
|
1385
|
+
send; anything else is an absent offer rather than a rate, and merge()
|
|
1386
|
+
keeps the previously known value instead of caching what was on the wire.
|
|
1387
|
+
'''
|
|
1230
1388
|
return self._tx_rate
|
|
1231
1389
|
|
|
1232
1390
|
@property
|
|
1233
1391
|
def scaling(self) -> DeviceV2IPScalingSettings|None:
|
|
1234
1392
|
return self._scaling
|
|
1235
1393
|
|
|
1394
|
+
@property
|
|
1395
|
+
def dscp(self) -> V2IPDscpConfig|None:
|
|
1396
|
+
"""Per-stream DSCP marking, or None from peers that predate it."""
|
|
1397
|
+
return self._dscp
|
|
1398
|
+
|
|
1399
|
+
def merge(self, previous:'DeviceV2IPDetails|None') -> 'DeviceV2IPDetails':
|
|
1400
|
+
"""
|
|
1401
|
+
Carry forward every field this frame did not carry.
|
|
1402
|
+
|
|
1403
|
+
A controller writing one field of a peer's config leaves the others
|
|
1404
|
+
zeroed on the wire, so each is applied only behind its own validity
|
|
1405
|
+
marker. See FrameV2IPDeviceConfiguration for the markers.
|
|
1406
|
+
"""
|
|
1407
|
+
if (previous is None):
|
|
1408
|
+
return self
|
|
1409
|
+
|
|
1410
|
+
if v2ip_av_source_valid(self._video, self._anc):
|
|
1411
|
+
video, audio, anc = self._video, self._audio, self._anc
|
|
1412
|
+
else:
|
|
1413
|
+
video, audio, anc = previous._video, previous._audio, previous._anc
|
|
1414
|
+
arc = self._arc if v2ip_stream_valid(self._arc) else previous._arc
|
|
1415
|
+
tx_rate = self._tx_rate if v2ip_rate_valid(self._tx_rate) else previous._tx_rate
|
|
1416
|
+
dscp = self._dscp if ((self._dscp is not None) and self._dscp.carried) else previous._dscp
|
|
1417
|
+
scaling = self._merge_scaling(previous._scaling)
|
|
1418
|
+
|
|
1419
|
+
if (video is self._video) and (audio is self._audio) and (anc is self._anc) and (arc is self._arc) and (tx_rate == self._tx_rate) and (dscp is self._dscp) and (scaling is self._scaling):
|
|
1420
|
+
return self
|
|
1421
|
+
return DeviceV2IPDetails(video=video, audio=audio, anc=anc, arc=arc,
|
|
1422
|
+
tx_rate=tx_rate, scaling=scaling, dscp=dscp)
|
|
1423
|
+
|
|
1424
|
+
def _merge_scaling(self, previous:DeviceV2IPScalingSettings|None) -> DeviceV2IPScalingSettings|None:
|
|
1425
|
+
"""
|
|
1426
|
+
Merge a scaling config field by field, the way firmware applies it.
|
|
1427
|
+
|
|
1428
|
+
The mode/refresh pair and the options nibble are separately valid, so a
|
|
1429
|
+
write carrying only one of them must leave the other as it was.
|
|
1430
|
+
"""
|
|
1431
|
+
incoming = self._scaling
|
|
1432
|
+
if (incoming is None) or (previous is None):
|
|
1433
|
+
return incoming if (incoming is not None) else previous
|
|
1434
|
+
mode_valid = ((incoming.flags & MXR_SCALING_FLAG_MODE_VALID) != 0)
|
|
1435
|
+
options_valid = ((incoming.flags & MXR_SCALING_FLAG_OPTIONS_VALID) != 0)
|
|
1436
|
+
if not mode_valid and not options_valid:
|
|
1437
|
+
# carries no scaling at all
|
|
1438
|
+
return previous
|
|
1439
|
+
if mode_valid and options_valid:
|
|
1440
|
+
return incoming
|
|
1441
|
+
mode = incoming.mode if mode_valid else previous.mode
|
|
1442
|
+
refresh = incoming.refresh if mode_valid else previous.refresh
|
|
1443
|
+
flags = previous.flags
|
|
1444
|
+
if mode_valid:
|
|
1445
|
+
flags |= MXR_SCALING_FLAG_MODE_VALID
|
|
1446
|
+
else:
|
|
1447
|
+
# Options only. Carry AUTO_SCALING alone rather than the whole upper
|
|
1448
|
+
# nibble: bits 4..6 have no defined meaning, and on a receiver-capable
|
|
1449
|
+
# unit running firmware before the scaling-config initialisation fix
|
|
1450
|
+
# they are stack noise - the sender declares mxr_scaling_config
|
|
1451
|
+
# uninitialised and only ever |= flags onto it. Masking to the bit we
|
|
1452
|
+
# mean cannot lose information even on fixed firmware, and stops the
|
|
1453
|
+
# noise at this boundary instead of caching it as a peer's config.
|
|
1454
|
+
flags = (flags & 0x0F) | MXR_SCALING_FLAG_OPTIONS_VALID | (incoming.flags & MXR_SCALING_FLAG_AUTO_SCALING)
|
|
1455
|
+
return V2IPScalingSettings(mode=mode, refresh=refresh, flags=flags)
|
|
1456
|
+
|
|
1236
1457
|
class DeviceV2IPSink:
|
|
1237
1458
|
"""
|
|
1238
1459
|
A V2IP sink's effective multicast subscriptions and resolved audio format.
|
|
@@ -1591,6 +1812,23 @@ class DeviceBase(ABC):
|
|
|
1591
1812
|
def is_oneip_multiviewer(self) -> bool:
|
|
1592
1813
|
'''True if this device is a OneIP Multiviewer'''
|
|
1593
1814
|
|
|
1815
|
+
@property
|
|
1816
|
+
@abstractmethod
|
|
1817
|
+
def supports_video_wall(self) -> bool:
|
|
1818
|
+
'''True if this sink can crop its source to a video wall window (MXR_FEATURE_VIDEO_WALL)'''
|
|
1819
|
+
|
|
1820
|
+
@property
|
|
1821
|
+
@abstractmethod
|
|
1822
|
+
def config_initialised(self) -> bool:
|
|
1823
|
+
'''True if every configuration block this device broadcasts is fully written.
|
|
1824
|
+
|
|
1825
|
+
A device without MXR_FEATURE_CONFIG_INITIALISED builds some config
|
|
1826
|
+
payloads from stack locals it never zeroes, so these are unreliable from
|
|
1827
|
+
it: the scaling block in V2IP_DEVICE_CFG, bay 0's addresses in
|
|
1828
|
+
SYS_BAY_V2IP_SOURCES, and the padding beside the rc target in
|
|
1829
|
+
RC_SETTINGS. None of that is detectable within a frame.
|
|
1830
|
+
'''
|
|
1831
|
+
|
|
1594
1832
|
@property
|
|
1595
1833
|
@abstractmethod
|
|
1596
1834
|
def is_oneip_tz(self) -> bool:
|