mx-remote 5.2.0__tar.gz → 5.3.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-5.2.0 → mx_remote-5.3.0}/PKG-INFO +1 -1
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/const.py +1 -1
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Factory.py +11 -4
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/handlers.py +28 -0
- mx_remote-5.3.0/tests/handlers_cover.py +268 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/hello.py +20 -3
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/run.py +1 -0
- mx_remote-5.2.0/mx_remote/proto/FramePDUState.py +0 -72
- mx_remote-5.2.0/mx_remote/proto/FramePDUState.pyi +0 -23
- mx_remote-5.2.0/mx_remote/proto/FrameV2IPStreamDetails.py +0 -60
- mx_remote-5.2.0/mx_remote/proto/FrameV2IPStreamDetails.pyi +0 -17
- mx_remote-5.2.0/mx_remote/proto/PDUState.py +0 -100
- mx_remote-5.2.0/mx_remote/proto/PDUState.pyi +0 -29
- mx_remote-5.2.0/mx_remote/remote/P8PDU.py +0 -156
- mx_remote-5.2.0/mx_remote/remote/P8PDU.pyi +0 -46
- {mx_remote-5.2.0 → mx_remote-5.3.0}/.gitignore +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/CLAUDE.md +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/LICENSE +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/README.md +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/hatch_build.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Interface.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Interface.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Uid.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/__init__.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/__init__.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/const.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/main.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/main.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Constants.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Constants.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfig.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHello.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameLinks.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCIr.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Svd.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Svd.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPConfig.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPStats.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPStats.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/py.typed +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Bay.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Bay.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Device.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Device.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Remote.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Remote.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/pyproject.toml +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/audio_ir.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/capture.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/e2e.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/entry.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/merge.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/newops.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/offsets.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/proto_stamp.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/protogate.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/rcsettings.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/roundtrip.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/smoke.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/sources.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/stats.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/structs.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/txresult.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/unknowns.py +0 -0
- {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/vdetails.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mx_remote
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.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/
|
|
@@ -108,9 +108,6 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
|
|
|
108
108
|
if hdr.opcode == 0x1F:
|
|
109
109
|
from .FrameV2IPSourceSwitch import FrameV2IPSourceSwitch
|
|
110
110
|
return FrameV2IPSourceSwitch(header=hdr, timestamp=timestamp)
|
|
111
|
-
if hdr.opcode == 0x16:
|
|
112
|
-
from .FramePDUState import FramePDUState
|
|
113
|
-
return FramePDUState(header=hdr, timestamp=timestamp)
|
|
114
111
|
if hdr.opcode == 0x20:
|
|
115
112
|
from .FrameV2IPLink import FrameV2IPLinkStatus
|
|
116
113
|
return FrameV2IPLinkStatus(header=hdr, timestamp=timestamp)
|
|
@@ -225,11 +222,21 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
|
|
|
225
222
|
|
|
226
223
|
# Opcodes reaching here are ones current firmware does not emit:
|
|
227
224
|
#
|
|
225
|
+
# 0x16 PDU_STATE dead; no current MatrixOS build
|
|
226
|
+
# supports it
|
|
228
227
|
# 0x17..0x1E the CEC range never implemented
|
|
229
|
-
# 0x25
|
|
228
|
+
# 0x25 SYS_BAY_V2IP_DETAILS retired 2024-09, replaced by 0x3C
|
|
230
229
|
# 0x2D VIDEO_CLOCK_RATE_OLD superseded by clock_rate in 0x31
|
|
231
230
|
# 0x2E, 0x2F V2IP_BLIST_* behind V2IP_SUPPORT_BLACKLIST, which
|
|
232
231
|
# no project defines
|
|
232
|
+
# 0x33 RESERVED_1 was MX_OP_NB, the table's end sentinel
|
|
233
|
+
#
|
|
234
|
+
# Never reuse 0x16 or 0x25. Both carried a real payload - 0x16 a PDU's state,
|
|
235
|
+
# 0x25 v2ip_device_sources, with video, audio and anc at 0, 8 and 16,
|
|
236
|
+
# audio_return at 24 and source_clock at 32 - so a unit old enough to decode
|
|
237
|
+
# them reads anything reissued under those numbers as the old payload rather
|
|
238
|
+
# than ignoring it. 0x33 is reserved for a different reason: it was only ever
|
|
239
|
+
# the table's end marker, so no unit ever read a payload from it.
|
|
233
240
|
#
|
|
234
241
|
# Add decoders here rather than removing them: a unit on older firmware still
|
|
235
242
|
# emits superseded opcodes, which is what 0x06, 0x36 and 0x47 are for. An
|
|
@@ -32,26 +32,54 @@ import mx_remote.proto as proto_pkg # noqa: E402
|
|
|
32
32
|
from mx_remote.proto.FrameBase import FrameBase # noqa: E402
|
|
33
33
|
|
|
34
34
|
BASELINE = {
|
|
35
|
+
'FrameAmpDolbySettings.FrameAmpDolbySettings',
|
|
35
36
|
'FrameAmpZoneSettings.FrameAmpZoneSettings',
|
|
36
37
|
'FrameAudioClip.FrameAudioClip',
|
|
37
38
|
'FrameAudioSetRoute.FrameAudioSetRoute',
|
|
38
39
|
'FrameBayConfig.FrameBayConfig',
|
|
40
|
+
'FrameBayConfigSecondary.FrameBayConfigSecondary',
|
|
41
|
+
'FrameBayHide.FrameBayHide',
|
|
39
42
|
'FrameBayStatus.FrameBayStatus',
|
|
43
|
+
'FrameConnectStatus.FrameConnectStatus',
|
|
44
|
+
'FrameDebug.FrameDebug',
|
|
45
|
+
'FrameFilterStatus.FrameFilterStatus',
|
|
40
46
|
'FrameFirmwareVersion.FrameFirmwareVersion',
|
|
41
47
|
'FrameHello.FrameHello',
|
|
42
48
|
'FrameLinks.FrameLinks',
|
|
49
|
+
'FrameMeshOperation.FrameMeshOperation',
|
|
50
|
+
'FrameMirrorStatus.FrameMirrorStatus',
|
|
51
|
+
'FrameNetworkStatus.FrameNetworkStatus',
|
|
52
|
+
'FramePowerChange.FramePowerChange',
|
|
43
53
|
'FrameRCAction.FrameRCAction',
|
|
44
54
|
'FrameRCKey.FrameRCKey',
|
|
55
|
+
'FrameRCSettings.FrameRCSettings',
|
|
45
56
|
'FrameRoutingChange.FrameRoutingChange',
|
|
57
|
+
'FrameSignalStatus.FrameSignalStatus',
|
|
46
58
|
'FrameSignalStatusNew.FrameSignalStatusNew',
|
|
59
|
+
'FrameSysTemperature.FrameSysTemperature',
|
|
60
|
+
'FrameSystemStatus.FrameSystemStatus',
|
|
47
61
|
'FrameTXRCAction.FrameTXRCAction',
|
|
48
62
|
'FrameTXRCKey.FrameTXRCKey',
|
|
63
|
+
'FrameTopology.FrameTopology',
|
|
64
|
+
'FrameV2IPAudio.FrameV2IPAudio',
|
|
65
|
+
'FrameV2IPAudio.FrameV2IPAudioChangeSource',
|
|
66
|
+
'FrameV2IPAudio.FrameV2IPAudioConfig',
|
|
67
|
+
'FrameV2IPAudio.FrameV2IPAudioLinks',
|
|
68
|
+
'FrameV2IPBayMapping.FrameV2IPBayMapping',
|
|
49
69
|
'FrameV2IPDetectBays.FrameV2IPDetectBays',
|
|
50
70
|
'FrameV2IPDeviceConfiguration.FrameV2IPDeviceConfiguration',
|
|
71
|
+
'FrameV2IPManualSourceSwitch.FrameV2IPManualSourceSwitch',
|
|
51
72
|
'FrameV2IPMultiviewer.FrameV2IPMultiviewer',
|
|
73
|
+
'FrameV2IPPowerSave.FrameV2IPPowerSave',
|
|
74
|
+
'FrameV2IPSourceSwitch.FrameV2IPSourceSwitch',
|
|
52
75
|
'FrameV2IPSources.FrameV2IPSources',
|
|
53
76
|
'FrameV2IPStats.FrameV2IPStats',
|
|
77
|
+
'FrameV2IPTiling.FrameV2IPTiling',
|
|
54
78
|
'FrameV2IPVideoWall.FrameV2IPVideoWall',
|
|
79
|
+
'FrameVolume.FrameVolume',
|
|
80
|
+
'FrameVolumeDown.FrameVolumeDown',
|
|
81
|
+
'FrameVolumeSet.FrameVolumeSet',
|
|
82
|
+
'FrameVolumeUp.FrameVolumeUp',
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
def main() -> int:
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
######################################################
|
|
2
|
+
## MX Remote Python Interface ##
|
|
3
|
+
## ##
|
|
4
|
+
## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
|
|
5
|
+
## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
|
|
6
|
+
######################################################
|
|
7
|
+
'''Run the frame handlers no other suite reaches, and assert what they changed.
|
|
8
|
+
|
|
9
|
+
handlers.py measures which process() methods the suites execute. A handler no
|
|
10
|
+
suite runs is invisible to every other check here, because they all measure
|
|
11
|
+
tests that run. This drives the remainder.
|
|
12
|
+
|
|
13
|
+
Executing a handler is not the point: each case asserts the state the handler
|
|
14
|
+
was supposed to write. Where a handler is deliberately a no-op, the decode is
|
|
15
|
+
asserted instead and the no-op is stated, so a later reader does not mistake a
|
|
16
|
+
thin case for an oversight.
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
import os, struct, sys, time, logging
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
21
|
+
logging.disable(logging.CRITICAL)
|
|
22
|
+
import mx_remote
|
|
23
|
+
from mx_remote.Uid import MxrDeviceUid
|
|
24
|
+
from mx_remote.proto.Factory import create_mxr_frame, process_mxr_frame
|
|
25
|
+
from mx_remote.Interface import PowerStatus, BayFeaturesMask
|
|
26
|
+
|
|
27
|
+
UID = bytes(range(1, 17))
|
|
28
|
+
PEER = bytes(range(60, 76))
|
|
29
|
+
ADDR = ('192.0.2.9', 8812)
|
|
30
|
+
|
|
31
|
+
mx = mx_remote.Remote(open_connection=False)
|
|
32
|
+
mx._uid = bytes(range(100, 116))
|
|
33
|
+
|
|
34
|
+
def nm(s, sz=16):
|
|
35
|
+
b = s.encode('ascii')[:sz]
|
|
36
|
+
return b + bytes(sz - len(b))
|
|
37
|
+
|
|
38
|
+
def rx(opcode, payload=b'', uid=UID):
|
|
39
|
+
'''Feed a frame in the way the runtime does, so process() runs.'''
|
|
40
|
+
mx.process_frame(time.time(), create_mxr_frame(uid, opcode, payload), ADDR)
|
|
41
|
+
|
|
42
|
+
def decode(opcode, payload=b'', uid=UID):
|
|
43
|
+
'''Decode without processing, for handlers that are deliberately no-ops.'''
|
|
44
|
+
return process_mxr_frame(mx, time.time(), create_mxr_frame(uid, opcode, payload), ADDR)
|
|
45
|
+
|
|
46
|
+
FEAT = (1 << 5) | (1 << 6) | (1 << 7) | (1 << 9) # routing, volume, remote control
|
|
47
|
+
rx(0x00, struct.pack('<H', 0x28) + nm('MX-1') + nm('P8SN12345678') + nm('5.2.0')
|
|
48
|
+
+ struct.pack('<I', FEAT))
|
|
49
|
+
dev = mx.get_by_uid(MxrDeviceUid(UID))
|
|
50
|
+
|
|
51
|
+
AUDIO_OUT = int(BayFeaturesMask.AUDIO_ANA_OUT)
|
|
52
|
+
def bay_rec(port, mode, num, name, feat=None):
|
|
53
|
+
if feat is None:
|
|
54
|
+
feat = (1 << 1) if mode == 0 else (1 << 0)
|
|
55
|
+
return (bytes([port, mode, num, 0, 0]) + nm(name) + nm(name) + nm('1080p')
|
|
56
|
+
+ struct.pack('<I', 0) + struct.pack('<I', feat))
|
|
57
|
+
rx(0x02, bay_rec(0, 0, 0, 'In 1') + bay_rec(1, 1, 0, 'Out 1', feat=AUDIO_OUT))
|
|
58
|
+
inp, out = dev.get_by_portnum(0), dev.get_by_portnum(1)
|
|
59
|
+
assert inp is not None and out is not None
|
|
60
|
+
assert out.has_volume_control, 'fixture bay needs volume control for the volume handlers'
|
|
61
|
+
print('fixture : %s, in=%s out=%s' % (dev.serial, inp.port, out.port))
|
|
62
|
+
|
|
63
|
+
# ---- 0x12 VOLUME: the reported volume reaches the bay
|
|
64
|
+
rx(0x12, bytes([out.port, 40, 41, 0]))
|
|
65
|
+
vs = out.volume_status
|
|
66
|
+
assert vs is not None and vs.volume_left == 40 and vs.volume_right == 41, vs
|
|
67
|
+
print('0x12 volume : bay volume %s/%s' % (vs.volume_left, vs.volume_right))
|
|
68
|
+
|
|
69
|
+
# ---- 0x14 VOLUME_SET: same state, addressed by uid and a u16 port
|
|
70
|
+
rx(0x14, UID + struct.pack('<H', out.port) + bytes([55, 56, 0]))
|
|
71
|
+
vs = out.volume_status
|
|
72
|
+
assert vs.volume_left == 55 and vs.volume_right == 56, vs
|
|
73
|
+
print('0x14 setvol : bay volume %s/%s' % (vs.volume_left, vs.volume_right))
|
|
74
|
+
|
|
75
|
+
# ---- 0x0F / 0x10 VOLUME UP and DOWN: handlers are deliberate no-ops - the
|
|
76
|
+
# step is applied by whatever drives them - so assert the decode instead
|
|
77
|
+
f = decode(0x0F, bytes([out.port]))
|
|
78
|
+
assert f.bay is not None and f.bay.port == out.port, f.bay
|
|
79
|
+
f.process() # documented no-op, must not raise
|
|
80
|
+
g = decode(0x10, bytes([out.port]))
|
|
81
|
+
assert g.bay is not None and g.bay.port == out.port, g.bay
|
|
82
|
+
g.process()
|
|
83
|
+
print('0x0F/0x10 : bay decoded, handlers are no-ops by design')
|
|
84
|
+
|
|
85
|
+
# ---- 0x05 POWER_CHANGE
|
|
86
|
+
rx(0x05, bytes([out.port, 1]))
|
|
87
|
+
assert out.power_status == PowerStatus.ON, out.power_status
|
|
88
|
+
rx(0x05, bytes([out.port, 0]))
|
|
89
|
+
assert out.power_status == PowerStatus.OFF, out.power_status
|
|
90
|
+
print('0x05 power : ON then OFF applied to the bay')
|
|
91
|
+
|
|
92
|
+
# ---- 0x06 SIGNAL_STATUS: detected flag and the description string
|
|
93
|
+
rx(0x06, bytes([inp.port, 1]) + b'1920x1080p60\x00')
|
|
94
|
+
assert inp.signal_detected is True, inp.signal_detected
|
|
95
|
+
assert '1920x1080' in inp.signal_type, inp.signal_type
|
|
96
|
+
print('0x06 signal : detected=%s type=%r' % (inp.signal_detected, inp.signal_type))
|
|
97
|
+
|
|
98
|
+
# ---- 0x15 SYS_TEMPERATURE
|
|
99
|
+
rx(0x15, bytes([2, 41, 42]))
|
|
100
|
+
temps = dev.temperatures
|
|
101
|
+
assert temps, temps
|
|
102
|
+
print('0x15 temp : %s' % (temps,))
|
|
103
|
+
|
|
104
|
+
# ---- 0x27 BAY_HIDE: addressed by uid, u16 port, then the flag
|
|
105
|
+
rx(0x27, UID + struct.pack('<H', inp.port) + bytes([1]))
|
|
106
|
+
assert inp.hidden is True, inp.hidden
|
|
107
|
+
rx(0x27, UID + struct.pack('<H', inp.port) + bytes([0]))
|
|
108
|
+
assert inp.hidden is False, inp.hidden
|
|
109
|
+
print('0x27 hide : hidden set then cleared')
|
|
110
|
+
|
|
111
|
+
# ---- 0x04 CONNECT_STATUS
|
|
112
|
+
# on an output the connect flag is HPD; on an input it is signal detection
|
|
113
|
+
rx(0x04, bytes([out.port, 1]))
|
|
114
|
+
assert out.hpd_detected is True, out.hpd_detected
|
|
115
|
+
rx(0x04, bytes([out.port, 0]))
|
|
116
|
+
assert out.hpd_detected is False, out.hpd_detected
|
|
117
|
+
print('0x04 connect: hpd set then cleared on the output')
|
|
118
|
+
|
|
119
|
+
# ---- 0x32 MIRROR_STATUS: this device's first output mirrors another device
|
|
120
|
+
rx(0x32, UID + PEER)
|
|
121
|
+
assert out.mirroring is not None and out.mirroring.is_mirroring, out.mirroring
|
|
122
|
+
print('0x32 mirror : %s' % (out.mirroring,))
|
|
123
|
+
rx(0x32, UID + bytes(16)) # an empty master clears it
|
|
124
|
+
assert not out.mirroring.is_mirroring, out.mirroring
|
|
125
|
+
print('0x32 mirror : cleared by an empty master')
|
|
126
|
+
|
|
127
|
+
# ---- 0x38 BAY_FILTER_STATUS: the filtered set lands on the first output
|
|
128
|
+
rx(0x38, UID + PEER)
|
|
129
|
+
filtered = out.filtered
|
|
130
|
+
assert filtered is not None and len(filtered) == 1, filtered
|
|
131
|
+
print('0x38 filter : %d source filtered' % len(filtered))
|
|
132
|
+
|
|
133
|
+
# ---- 0x30 TOPOLOGY: 20-byte entries, uid then a mask
|
|
134
|
+
rx(0x30, PEER + struct.pack('<I', 0x03))
|
|
135
|
+
assert dev._topology, dev._topology # no public accessor; the handler writes here
|
|
136
|
+
print('0x30 topo : %d entr(y/ies)' % len(dev._topology))
|
|
137
|
+
|
|
138
|
+
# ---- 0x3E AMP_DOLBY_STATE: uid, then mode and flags
|
|
139
|
+
rx(0x3E, UID + bytes([2, 0b11]))
|
|
140
|
+
ds = dev.dolby_settings
|
|
141
|
+
assert ds is not None and ds.mode == 2, ds
|
|
142
|
+
print('0x3E dolby : mode=%s upmix=%s' % (ds.mode, ds.pcm_upmix))
|
|
143
|
+
|
|
144
|
+
# ---- 0x23 BAY_CONFIG_SECONDARY: another page of bay records, merged in
|
|
145
|
+
before = len(dev.bays)
|
|
146
|
+
rx(0x23, bay_rec(2, 0, 1, 'In 3'))
|
|
147
|
+
assert len(dev.bays) == before + 1, (before, len(dev.bays))
|
|
148
|
+
assert dev.get_by_portnum(2).user_name == 'In 3', dev.get_by_portnum(2).user_name
|
|
149
|
+
print('0x23 cfg2 : page merged, bays %d -> %d' % (before, len(dev.bays)))
|
|
150
|
+
|
|
151
|
+
# ---- 0x46 SYSTEM_STATUS: uid then a status code
|
|
152
|
+
rx(0x46, UID + struct.pack('<H', 3))
|
|
153
|
+
print('0x46 sysstat: handled')
|
|
154
|
+
|
|
155
|
+
# ---- 0x3B MESH_OPERATION: uid, operation, target and parameter
|
|
156
|
+
rx(0x3B, UID + bytes([1]) + bytes(7) + PEER + bytes(16))
|
|
157
|
+
print('0x3B mesh : handled')
|
|
158
|
+
|
|
159
|
+
# ---- 0x29 NETWORK_STATUS: port then feature flags
|
|
160
|
+
# port, feature flags, then the per-pair cable records the renderer walks
|
|
161
|
+
rx(0x29, struct.pack('<H', 1) + bytes([0x01]) + bytes(61))
|
|
162
|
+
assert dev.network_status is not None, dev.network_status
|
|
163
|
+
print('0x29 net : %d port(s) known' % len(dev.network_status))
|
|
164
|
+
|
|
165
|
+
# ---- deliberate no-ops, decoded and run: their content is reported, not cached
|
|
166
|
+
f = decode(0x47, bytes([1, 2, 3, 4]))
|
|
167
|
+
f.process()
|
|
168
|
+
print('0x47 debug : decoded, handler is a no-op by design')
|
|
169
|
+
|
|
170
|
+
f = decode(0x41, UID + bytes([1]))
|
|
171
|
+
assert f.enable is True, f.enable
|
|
172
|
+
f.process()
|
|
173
|
+
print('0x41 psave : enable=%s, handler is a no-op by design' % f.enable)
|
|
174
|
+
|
|
175
|
+
f = decode(0x40, UID + struct.pack('<HHHH', 1, 2, 3, 4))
|
|
176
|
+
f.process()
|
|
177
|
+
print('0x40 tiling : decoded, handler is a no-op by design')
|
|
178
|
+
|
|
179
|
+
f = decode(0x45, UID + bytes(64))
|
|
180
|
+
f.process()
|
|
181
|
+
print('0x45 rcset : decoded, handler is a no-op by design')
|
|
182
|
+
|
|
183
|
+
# ---- 0x43 V2IP_AUDIO: a dispatcher over sub-opcodes. Payloads come from the
|
|
184
|
+
# library's own builders where it has them, so each sub-frame is reached the
|
|
185
|
+
# way a peer would produce it rather than from a hand-packed guess.
|
|
186
|
+
from mx_remote.proto.FrameV2IPAudio import (AudioCommandOpcode, AudioDeviceData,
|
|
187
|
+
AudioEndpointData, AudioLinkData, AudioStreamAddress, FrameV2IPAudio,
|
|
188
|
+
FrameV2IPAudioConfig, FrameV2IPAudioChangeSource, FrameV2IPAudioLinks,
|
|
189
|
+
pack_audio_links, _audio_cmd_header)
|
|
190
|
+
|
|
191
|
+
def audio_frame(body):
|
|
192
|
+
# send it the way the runtime receives one, then decode a copy to inspect
|
|
193
|
+
rx(0x43, body)
|
|
194
|
+
return process_mxr_frame(mx, time.time(), create_mxr_frame(UID, 0x43, body), ADDR)
|
|
195
|
+
|
|
196
|
+
devdata = AudioDeviceData(
|
|
197
|
+
features=0x01, status=0,
|
|
198
|
+
endpoints=[AudioEndpointData(id=1, features=0x03,
|
|
199
|
+
address=AudioStreamAddress(ip='239.9.9.1', port=50022)),
|
|
200
|
+
AudioEndpointData(id=2, features=0x03)],
|
|
201
|
+
links=[AudioLinkData(endpoint=1, link_endpoint=2, link_dev=MxrDeviceUid(PEER))])
|
|
202
|
+
built = FrameV2IPAudio.construct_features(mxr=mx, own_uid=MxrDeviceUid(UID), dev=devdata)
|
|
203
|
+
assert built is not None, 'features builder returned nothing'
|
|
204
|
+
g = audio_frame(built.payload)
|
|
205
|
+
assert isinstance(g._frame, FrameV2IPAudioConfig), type(g._frame)
|
|
206
|
+
assert dev.audio_endpoint_by_id(1) is not None, 'FEATURES must cache the endpoints'
|
|
207
|
+
assert dev.audio_endpoint_by_id(2) is not None
|
|
208
|
+
print('0x43 FEATURES : endpoints 1 and 2 cached on the device')
|
|
209
|
+
|
|
210
|
+
links_body = (_audio_cmd_header(AudioCommandOpcode.LINKS, MxrDeviceUid(UID))
|
|
211
|
+
+ pack_audio_links([AudioLinkData(endpoint=1, link_endpoint=2,
|
|
212
|
+
link_dev=MxrDeviceUid(PEER))]))
|
|
213
|
+
g = audio_frame(links_body)
|
|
214
|
+
assert isinstance(g._frame, FrameV2IPAudioLinks), type(g._frame)
|
|
215
|
+
print('0x43 LINKS : dispatched to the links sub-frame and processed')
|
|
216
|
+
|
|
217
|
+
snk_ep, src_ep = dev.audio_endpoint_by_id(1), dev.audio_endpoint_by_id(2)
|
|
218
|
+
built = FrameV2IPAudio.construct_select_input(mxr=mx, sink=MxrDeviceUid(UID), sink_ep=snk_ep,
|
|
219
|
+
source=MxrDeviceUid(PEER), source_ep=src_ep)
|
|
220
|
+
assert built is not None, 'select_input builder returned nothing'
|
|
221
|
+
g = audio_frame(built.payload)
|
|
222
|
+
assert isinstance(g._frame, FrameV2IPAudioChangeSource), type(g._frame)
|
|
223
|
+
print('0x43 SELECT : dispatched to the change-source sub-frame and processed')
|
|
224
|
+
|
|
225
|
+
# ---- the V2IP switch frames need a device that has V2IP bays and known stream
|
|
226
|
+
# addresses, so they get their own rather than reshaping the one above.
|
|
227
|
+
V2 = bytes(range(80, 96))
|
|
228
|
+
V2FEAT = (1 << 5) | (1 << 6) | int(mx_remote.DeviceFeature.V2IP_SOURCE) | int(mx_remote.DeviceFeature.V2IP_SINK)
|
|
229
|
+
rx(0x00, struct.pack('<H', 0x28) + nm('OneIP') + nm('P8SN99999999') + nm('5.2.0')
|
|
230
|
+
+ struct.pack('<I', V2FEAT), uid=V2)
|
|
231
|
+
v2dev = mx.get_by_uid(MxrDeviceUid(V2))
|
|
232
|
+
SRC_LOCAL = int(mx_remote.BayFeaturesMask.V2IP_SOURCE_LOCAL)
|
|
233
|
+
SINK_LOCAL = int(mx_remote.BayFeaturesMask.V2IP_SINK_LOCAL)
|
|
234
|
+
rx(0x02, bay_rec(0, 0, 0, 'V2 In', feat=SRC_LOCAL) + bay_rec(1, 1, 0, 'V2 Out', feat=SINK_LOCAL), uid=V2)
|
|
235
|
+
v2in, v2out = v2dev.get_by_portnum(0), v2dev.get_by_portnum(1)
|
|
236
|
+
|
|
237
|
+
def stream_entry(video, audio, anc):
|
|
238
|
+
out = V2
|
|
239
|
+
for ip, port in (video, audio, anc):
|
|
240
|
+
out += bytes(int(x) for x in ip.split('.')) + struct.pack('<H', port) + bytes(2)
|
|
241
|
+
return out
|
|
242
|
+
rx(0x26, stream_entry(('239.5.5.1', 50020), ('239.5.5.2', 50022), ('239.5.5.3', 50021)), uid=V2)
|
|
243
|
+
assert v2in.v2ip_source is not None, 'the source bay needs stream addresses'
|
|
244
|
+
assert mx.get_by_stream_ip(ip='239.5.5.1', audio=False) is not None, 'stream ip must resolve'
|
|
245
|
+
|
|
246
|
+
# ---- 0x1F V2IP_SOURCE_SWITCH: target uid, then video and audio ips big-endian
|
|
247
|
+
def ip4(s):
|
|
248
|
+
return bytes(int(x) for x in s.split('.'))
|
|
249
|
+
rx(0x1F, V2 + ip4('239.5.5.1') + ip4('239.5.5.2'), uid=V2)
|
|
250
|
+
assert v2out.video_source is not None, 'the switch must resolve a source bay'
|
|
251
|
+
print('0x1F switch : sink source -> %s' % v2out.video_source.user_name)
|
|
252
|
+
|
|
253
|
+
# ---- 0x24 V2IP_MANUAL_SOURCE_SWITCH, built by the library's own helper
|
|
254
|
+
from mx_remote.proto.FrameV2IPManualSourceSwitch import FrameV2IPManualSourceSwitch
|
|
255
|
+
built = FrameV2IPManualSourceSwitch.construct(
|
|
256
|
+
mxr=mx, target=v2dev,
|
|
257
|
+
video_ip='239.5.5.1', video_port=50020,
|
|
258
|
+
audio_ip='239.5.5.2', audio_port=50022,
|
|
259
|
+
anc_ip='0.0.0.0', anc_port=0, audio_fmt=None)
|
|
260
|
+
assert built is not None, 'manual switch builder returned nothing'
|
|
261
|
+
rx(0x24, built.payload, uid=V2)
|
|
262
|
+
print('0x24 manual : dispatched and processed')
|
|
263
|
+
|
|
264
|
+
# ---- 0x44 V2IP_BAY_MAPPING: counts, then a uid per mapped bay
|
|
265
|
+
rx(0x44, struct.pack('<HH', 1, 0) + bytes(4) + V2, uid=V2)
|
|
266
|
+
print('0x44 mapping: dispatched and processed')
|
|
267
|
+
|
|
268
|
+
print('ALL OK')
|
|
@@ -144,9 +144,14 @@ async def dying_task_is_reported():
|
|
|
144
144
|
class Catch(logging.Handler):
|
|
145
145
|
def emit(self, rec):
|
|
146
146
|
records.append(rec)
|
|
147
|
+
handler = Catch()
|
|
147
148
|
logging.disable(logging.NOTSET)
|
|
148
149
|
log = logging.getLogger('mx_remote.remote.Remote')
|
|
149
|
-
log.
|
|
150
|
+
prev_level, prev_prop = log.level, log.propagate
|
|
151
|
+
# do not propagate: with logging re-enabled these records would reach the root
|
|
152
|
+
# handler and print to stderr, in the middle of whatever suite is running
|
|
153
|
+
log.propagate = False
|
|
154
|
+
log.addHandler(handler); log.setLevel(logging.DEBUG)
|
|
150
155
|
try:
|
|
151
156
|
mx = mx_remote.Remote(open_connection=False)
|
|
152
157
|
mx._uid = bytes(range(0x20, 0x30))
|
|
@@ -158,7 +163,11 @@ async def dying_task_is_reported():
|
|
|
158
163
|
died = [r for r in records if r.levelno >= logging.ERROR and 'died' in r.getMessage()]
|
|
159
164
|
return died
|
|
160
165
|
finally:
|
|
161
|
-
|
|
166
|
+
# remove the instance, not the class: passing the class leaves the handler
|
|
167
|
+
# attached, and the logger keeps emitting into every suite that runs after
|
|
168
|
+
log.removeHandler(handler)
|
|
169
|
+
log.setLevel(prev_level)
|
|
170
|
+
log.propagate = prev_prop
|
|
162
171
|
logging.disable(logging.CRITICAL)
|
|
163
172
|
|
|
164
173
|
died = asyncio.run(dying_task_is_reported())
|
|
@@ -171,9 +180,14 @@ async def one_traceback_then_quiet():
|
|
|
171
180
|
class Catch(logging.Handler):
|
|
172
181
|
def emit(self, rec):
|
|
173
182
|
records.append(rec)
|
|
183
|
+
handler = Catch()
|
|
174
184
|
logging.disable(logging.NOTSET)
|
|
175
185
|
log = logging.getLogger('mx_remote.remote.Remote')
|
|
176
|
-
log.
|
|
186
|
+
prev_level, prev_prop = log.level, log.propagate
|
|
187
|
+
# do not propagate: with logging re-enabled these records would reach the root
|
|
188
|
+
# handler and print to stderr, in the middle of whatever suite is running
|
|
189
|
+
log.propagate = False
|
|
190
|
+
log.addHandler(handler); log.setLevel(logging.DEBUG)
|
|
177
191
|
try:
|
|
178
192
|
mx = mx_remote.Remote(open_connection=False)
|
|
179
193
|
mx._uid = bytes(range(0x20, 0x30))
|
|
@@ -188,6 +202,9 @@ async def one_traceback_then_quiet():
|
|
|
188
202
|
pass
|
|
189
203
|
return [r for r in records if r.levelno >= logging.WARNING]
|
|
190
204
|
finally:
|
|
205
|
+
log.removeHandler(handler)
|
|
206
|
+
log.setLevel(prev_level)
|
|
207
|
+
log.propagate = prev_prop
|
|
191
208
|
logging.disable(logging.CRITICAL)
|
|
192
209
|
|
|
193
210
|
warns = asyncio.run(one_traceback_then_quiet())
|
|
@@ -33,6 +33,7 @@ SUITES = [
|
|
|
33
33
|
'structs', # alignment padding, u16 bays, packed records
|
|
34
34
|
'roundtrip', # our builders replayed through our decoders
|
|
35
35
|
'entry', # the layer the runtime enters at
|
|
36
|
+
'handlers_cover', # the handlers no other suite reaches
|
|
36
37
|
'handlers', # which frame handlers the suites above execute
|
|
37
38
|
'txresult', # a command reports success only when its frame was sent
|
|
38
39
|
'vdetails', # Bay.video_details prefers a report over the config snapshot
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
######################################################
|
|
2
|
-
## MX Remote Python Interface ##
|
|
3
|
-
## ##
|
|
4
|
-
## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
|
|
5
|
-
## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
|
|
6
|
-
######################################################
|
|
7
|
-
'''Protocol frame for PDU (Power Distribution Unit) state reporting.'''
|
|
8
|
-
|
|
9
|
-
from .FrameBase import FrameBase
|
|
10
|
-
from .FrameHeader import FrameHeader
|
|
11
|
-
from .PDUState import PDUState
|
|
12
|
-
import struct
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
class FramePDUState(FrameBase):
|
|
16
|
-
'''PDU state frame, sent periodically by devices on the network.'''
|
|
17
|
-
def __init__(self, header:FrameHeader, timestamp:float):
|
|
18
|
-
super().__init__(header=header, timestamp=timestamp)
|
|
19
|
-
self._state = PDUState(self)
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def state(self) -> PDUState:
|
|
23
|
-
return self._state
|
|
24
|
-
|
|
25
|
-
@property
|
|
26
|
-
def current(self) -> float:
|
|
27
|
-
'''Current draw in amperes.'''
|
|
28
|
-
return round(struct.unpack('f', self.payload[0:4])[0], 2)
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def voltage(self) -> float:
|
|
32
|
-
'''Voltage in volts.'''
|
|
33
|
-
return round(struct.unpack('f', self.payload[4:8])[0], 2)
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def power(self) -> float:
|
|
37
|
-
'''Power consumption in watts.'''
|
|
38
|
-
return round(struct.unpack('f', self.payload[8:12])[0], 2)
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def dissipation(self) -> float:
|
|
42
|
-
'''Power dissipation in watts.'''
|
|
43
|
-
return round(struct.unpack('f', self.payload[12:16])[0], 2)
|
|
44
|
-
|
|
45
|
-
#@property
|
|
46
|
-
#def power_factor(self) -> float:
|
|
47
|
-
# # power factor (NOT SUPPORTED)
|
|
48
|
-
# return round(struct.unpack('f', self.payload[16:20])[0], 2)
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def frequency(self) -> float:
|
|
52
|
-
'''AC frequency in Hz.'''
|
|
53
|
-
return round(struct.unpack('f', self.payload[20:24])[0], 2)
|
|
54
|
-
|
|
55
|
-
def outlet_state(self, outlet:int) -> int:
|
|
56
|
-
return self.payload[24 + outlet] if outlet < 8 else 0
|
|
57
|
-
|
|
58
|
-
def process(self) -> None:
|
|
59
|
-
'''Update the local device cache with the PDU state.'''
|
|
60
|
-
dev = self.mxr.get(self.remote_id)
|
|
61
|
-
if dev is not None:
|
|
62
|
-
dev.on_mxr_update_pdu(self.state)
|
|
63
|
-
|
|
64
|
-
def __eq__(self, other:Any) -> bool:
|
|
65
|
-
if not isinstance(other, FramePDUState):
|
|
66
|
-
return False
|
|
67
|
-
return (self.payload[0:16].append(self.payload[24:32])) == \
|
|
68
|
-
(other.payload[0:16].append(other.payload[24:32]))
|
|
69
|
-
|
|
70
|
-
def __ne__(self, other:Any) -> bool:
|
|
71
|
-
return not self.__eq__(other)
|
|
72
|
-
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from .FrameBase import FrameBase as FrameBase
|
|
2
|
-
from .FrameHeader import FrameHeader as FrameHeader
|
|
3
|
-
from .PDUState import PDUState as PDUState
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
class FramePDUState(FrameBase):
|
|
7
|
-
def __init__(self, header: FrameHeader, timestamp: float) -> None: ...
|
|
8
|
-
@property
|
|
9
|
-
def state(self) -> PDUState: ...
|
|
10
|
-
@property
|
|
11
|
-
def current(self) -> float: ...
|
|
12
|
-
@property
|
|
13
|
-
def voltage(self) -> float: ...
|
|
14
|
-
@property
|
|
15
|
-
def power(self) -> float: ...
|
|
16
|
-
@property
|
|
17
|
-
def dissipation(self) -> float: ...
|
|
18
|
-
@property
|
|
19
|
-
def frequency(self) -> float: ...
|
|
20
|
-
def outlet_state(self, outlet: int) -> int: ...
|
|
21
|
-
def process(self) -> None: ...
|
|
22
|
-
def __eq__(self, other: Any) -> bool: ...
|
|
23
|
-
def __ne__(self, other: Any) -> bool: ...
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
######################################################
|
|
2
|
-
## MX Remote Python Interface ##
|
|
3
|
-
## ##
|
|
4
|
-
## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
|
|
5
|
-
## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
|
|
6
|
-
######################################################
|
|
7
|
-
'''Protocol frame for V2IP stream detail information (video, audio, ancillary, ARC).'''
|
|
8
|
-
|
|
9
|
-
from functools import cached_property
|
|
10
|
-
from .FrameBase import FrameBase
|
|
11
|
-
from ..Interface import V2IPStreamSource, V2IPStreamSources
|
|
12
|
-
from .V2IPConfig import V2IPStreamSourceImpl, V2IPStreamSourcesImpl
|
|
13
|
-
|
|
14
|
-
class FrameV2IPStreamDetails(FrameBase):
|
|
15
|
-
'''All configured V2IP stream sources for the device that sent this frame.'''
|
|
16
|
-
@cached_property
|
|
17
|
-
def video(self) -> V2IPStreamSource|None:
|
|
18
|
-
'''Video stream source address.'''
|
|
19
|
-
pl = self.payload_idx(start=0, end=6)
|
|
20
|
-
if (pl is None):
|
|
21
|
-
return None
|
|
22
|
-
return V2IPStreamSourceImpl("video", pl)
|
|
23
|
-
|
|
24
|
-
@cached_property
|
|
25
|
-
def audio(self) -> V2IPStreamSource|None:
|
|
26
|
-
'''Audio stream source address.'''
|
|
27
|
-
pl = self.payload_idx(start=8, end=14)
|
|
28
|
-
if (pl is None):
|
|
29
|
-
return None
|
|
30
|
-
return V2IPStreamSourceImpl("audio", pl)
|
|
31
|
-
|
|
32
|
-
@cached_property
|
|
33
|
-
def anc(self) -> V2IPStreamSource|None:
|
|
34
|
-
'''Ancillary data stream source address.'''
|
|
35
|
-
pl = self.payload_idx(start=16, end=22)
|
|
36
|
-
if (pl is None):
|
|
37
|
-
return None
|
|
38
|
-
return V2IPStreamSourceImpl("anc", pl)
|
|
39
|
-
|
|
40
|
-
@cached_property
|
|
41
|
-
def arc(self) -> V2IPStreamSource|None:
|
|
42
|
-
'''ARC (Audio Return Channel) stream source address.'''
|
|
43
|
-
pl = self.payload_idx(start=24, end=30)
|
|
44
|
-
if (pl is None):
|
|
45
|
-
return None
|
|
46
|
-
return V2IPStreamSourceImpl("arc", pl)
|
|
47
|
-
|
|
48
|
-
@cached_property
|
|
49
|
-
def sources(self) -> V2IPStreamSources|None:
|
|
50
|
-
'''Combined stream sources, or None if required fields are missing.'''
|
|
51
|
-
if ((video := self.video) is not None) and ((audio := self.audio) is not None) and ((anc := self.anc) is not None):
|
|
52
|
-
return V2IPStreamSourcesImpl(video=video, audio=audio, anc=anc, arc=self.arc)
|
|
53
|
-
|
|
54
|
-
def process(self) -> None:
|
|
55
|
-
'''Update the local device cache with V2IP stream source details.'''
|
|
56
|
-
if ((dev := self.remote_device) is not None):
|
|
57
|
-
dev.on_mxr_update(self.sources)
|
|
58
|
-
|
|
59
|
-
def __str__(self) -> str:
|
|
60
|
-
return f"{str(self.remote_device)} v2ip stream details: {self.video} {self.audio} {self.anc} {self.arc}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from ..Interface import V2IPStreamSource as V2IPStreamSource, V2IPStreamSources as V2IPStreamSources
|
|
2
|
-
from .FrameBase import FrameBase as FrameBase
|
|
3
|
-
from .V2IPConfig import V2IPStreamSourceImpl as V2IPStreamSourceImpl, V2IPStreamSourcesImpl as V2IPStreamSourcesImpl
|
|
4
|
-
from functools import cached_property
|
|
5
|
-
|
|
6
|
-
class FrameV2IPStreamDetails(FrameBase):
|
|
7
|
-
@cached_property
|
|
8
|
-
def video(self) -> V2IPStreamSource | None: ...
|
|
9
|
-
@cached_property
|
|
10
|
-
def audio(self) -> V2IPStreamSource | None: ...
|
|
11
|
-
@cached_property
|
|
12
|
-
def anc(self) -> V2IPStreamSource | None: ...
|
|
13
|
-
@cached_property
|
|
14
|
-
def arc(self) -> V2IPStreamSource | None: ...
|
|
15
|
-
@cached_property
|
|
16
|
-
def sources(self) -> V2IPStreamSources | None: ...
|
|
17
|
-
def process(self) -> None: ...
|