mx-remote 5.9.0__tar.gz → 5.10.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.9.0 → mx_remote-5.10.0}/PKG-INFO +1 -1
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/api.md +17 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/oneip.md +37 -4
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Interface.py +145 -6
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Interface.pyi +28 -1
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/const.py +9 -1
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/const.pyi +1 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Constants.py +53 -2
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Constants.pyi +17 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameLinks.py +1 -5
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +89 -5
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +6 -2
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Device.py +156 -29
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Device.pyi +10 -3
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Remote.py +13 -5
- mx_remote-5.10.0/tests/capture.py +118 -0
- mx_remote-5.10.0/tests/complete.py +218 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/handlers_cover.py +18 -0
- mx_remote-5.10.0/tests/offline.py +107 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/run.py +2 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/smoke.py +1 -1
- mx_remote-5.10.0/tests/v2ipsettings.py +228 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/v2ipsubject.py +30 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/wirefix.py +4 -3
- mx_remote-5.9.0/tests/capture.py +0 -50
- mx_remote-5.9.0/tests/complete.py +0 -105
- {mx_remote-5.9.0 → mx_remote-5.10.0}/.gitignore +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/AGENTS.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/CLAUDE.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/LICENSE +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/README.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/README.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/brand.css +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/favicon.svg +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/logo-dark.svg +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/logo.svg +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/audio.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/callbacks.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/conf.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/configuration.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/devices-and-bays.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/diagnostics.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/index.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/multiviewer.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/requirements.txt +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/routing.md +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/hatch_build.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Uid.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/__init__.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/__init__.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/compat.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/compat.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/main.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/main.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Factory.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfig.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHello.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCIr.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Svd.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Svd.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPConfig.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPStats.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPStats.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/py.typed +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Bay.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Bay.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Remote.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/pyproject.toml +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/audio_ir.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/e2e.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/entry.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/handlers.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/hello.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/idempotent.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/lengths.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/merge.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/newops.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/offsets.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/proto_stamp.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/protogate.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/rcsettings.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/roundtrip.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/sources.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/stats.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/structs.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/txresult.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/unknowns.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/v2ipscaling.py +0 -0
- {mx_remote-5.9.0 → mx_remote-5.10.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.10.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://opdenkamp.github.io/mx-remote/
|
|
@@ -57,3 +57,20 @@ The key and action codes a bay reports, and the target a key was aimed at.
|
|
|
57
57
|
|
|
58
58
|
.. autodata:: mx_remote.proto.Constants.MXR_PROTOCOL_VERSION
|
|
59
59
|
```
|
|
60
|
+
|
|
61
|
+
## V2IP device settings
|
|
62
|
+
|
|
63
|
+
The settings a V2IP device reports and a controller writes, each behind its own
|
|
64
|
+
bit, and the range of the infrared profiles.
|
|
65
|
+
|
|
66
|
+
```{eval-rst}
|
|
67
|
+
.. autoclass:: mx_remote.proto.Constants.V2IPDeviceSetting
|
|
68
|
+
:members:
|
|
69
|
+
:undoc-members:
|
|
70
|
+
|
|
71
|
+
.. autodata:: mx_remote.proto.Constants.V2IP_DEVICE_SETTING_SWITCHES
|
|
72
|
+
|
|
73
|
+
.. autodata:: mx_remote.proto.Constants.V2IP_IR_PROFILE_NOT_SET
|
|
74
|
+
|
|
75
|
+
.. autodata:: mx_remote.proto.Constants.V2IP_IR_PROFILE_MAX
|
|
76
|
+
```
|
|
@@ -141,16 +141,15 @@ expect `device.v2ip_sink` to read empty for a moment afterwards - see
|
|
|
141
141
|
|
|
142
142
|
`device.v2ip_sink` addresses that read as unset mean "no route, or the sink
|
|
143
143
|
could not work one out", never "definitely not subscribed". This is the one part
|
|
144
|
-
of a device configuration with no validity marker of its own, so a
|
|
145
|
-
nothing to say sends zeros and
|
|
144
|
+
of a device configuration with no validity marker of its own, so a sink with
|
|
145
|
+
nothing to say sends zeros and the library stores them. A sender leaves it
|
|
146
146
|
empty whenever its own stream configuration does not resolve, which covers more
|
|
147
147
|
than having no route: a selected source whose record has not arrived yet, the
|
|
148
148
|
state after a restart at either end, missing audio bay configuration, or a
|
|
149
149
|
stream failing its validity check.
|
|
150
150
|
|
|
151
151
|
Expect that reading rather than guarding against it. Any scaling change rebuilds
|
|
152
|
-
and rebroadcasts the block,
|
|
153
|
-
however it was requested, so an empty reading turns up most often during exactly
|
|
152
|
+
and rebroadcasts the block, so an empty reading turns up most often during exactly
|
|
154
153
|
the no-signal troubleshooting that prompted the change. A device's periodic
|
|
155
154
|
report puts a real route back within a minute of it having one, so wait one out
|
|
156
155
|
rather than treat the first empty reading as an answer.
|
|
@@ -159,6 +158,40 @@ The library caches an empty reading rather than dropping it. A sink that has
|
|
|
159
158
|
genuinely lost its route sends the same zeros, and so does every report after
|
|
160
159
|
it, so refusing them would hold a route that nothing later could clear.
|
|
161
160
|
|
|
161
|
+
Only the device's own report sets the block. A controller writing another
|
|
162
|
+
device's configuration sends it zeroed, and the library ignores that copy.
|
|
163
|
+
|
|
164
|
+
## Device settings
|
|
165
|
+
|
|
166
|
+
A V2IP device reports its settings in its configuration: decoder auto-disable,
|
|
167
|
+
HDMI off without signal, IR modulation, the status and network LEDs, quiet fan,
|
|
168
|
+
CEC combo keys, and the infrared profiles. Each sits behind its own bit, so a
|
|
169
|
+
device reports only the settings it has, and a write changes one without
|
|
170
|
+
restating the rest.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from mx_remote import V2IPDeviceSetting
|
|
174
|
+
|
|
175
|
+
settings = device.v2ip_settings # None until the device has reported
|
|
176
|
+
print(settings.get(V2IPDeviceSetting.FAN_QUIET)) # None: the device lacks it
|
|
177
|
+
print(settings.ir_profile, settings.ir_profile_sink)
|
|
178
|
+
|
|
179
|
+
await device.set_v2ip_setting(V2IPDeviceSetting.STATUS_LED, False)
|
|
180
|
+
await device.set_v2ip_ir_profile(2)
|
|
181
|
+
await device.set_v2ip_sink_ir_profile(mx_remote.V2IP_IR_PROFILE_NOT_SET) # follow the global port
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A write returns False, without sending anything, for whatever the device would
|
|
185
|
+
ignore in silence: a setting it has not reported, a profile out of range, or a
|
|
186
|
+
device that has reported no settings at all. So a True is never a change that
|
|
187
|
+
does not happen, though nothing acknowledges it either: the device answers by
|
|
188
|
+
reporting its settings, and until then `v2ip_settings` reads back what was
|
|
189
|
+
written.
|
|
190
|
+
|
|
191
|
+
A write from another controller is cached only as far as the device takes it.
|
|
192
|
+
The list of stored infrared profiles is the device's own and is never taken
|
|
193
|
+
from a write.
|
|
194
|
+
|
|
162
195
|
## Mesh and firmware
|
|
163
196
|
|
|
164
197
|
```python
|
|
@@ -1757,8 +1757,8 @@ class DeviceV2IPSink:
|
|
|
1757
1757
|
|
|
1758
1758
|
**An address that reads as unset means "no route, or the sink could not work
|
|
1759
1759
|
one out", never "definitely not subscribed".** This block is the one part of
|
|
1760
|
-
a device configuration with no validity marker of its own, so a
|
|
1761
|
-
nothing to say sends zeros and
|
|
1760
|
+
a device configuration with no validity marker of its own, so a sink with
|
|
1761
|
+
nothing to say sends zeros and they are stored. A sender leaves it
|
|
1762
1762
|
empty whenever its own stream configuration does not resolve, which covers
|
|
1763
1763
|
more than having no route: a selected source whose record has not arrived
|
|
1764
1764
|
yet, the state after a restart at either end, missing audio bay
|
|
@@ -1766,8 +1766,7 @@ class DeviceV2IPSink:
|
|
|
1766
1766
|
second gate of its own, so it can be absent while the addresses are not.
|
|
1767
1767
|
|
|
1768
1768
|
Expect that reading rather than guarding against it. Any scaling change makes
|
|
1769
|
-
the device rebuild and rebroadcast this block,
|
|
1770
|
-
bay sends it zeroed however it was requested, so an empty reading turns up
|
|
1769
|
+
the device rebuild and rebroadcast this block, so an empty reading turns up
|
|
1771
1770
|
most often during exactly the no-signal troubleshooting that prompted the
|
|
1772
1771
|
change. A device's periodic report puts a real route back within a minute of
|
|
1773
1772
|
it having one, so a reader that needs certainty should wait one out rather
|
|
@@ -1776,6 +1775,9 @@ class DeviceV2IPSink:
|
|
|
1776
1775
|
An empty reading is cached rather than dropped on purpose. A sink that has
|
|
1777
1776
|
genuinely lost its route sends the same zeros, and so does every report after
|
|
1778
1777
|
it, so refusing them would hold a route that nothing later could clear.
|
|
1778
|
+
|
|
1779
|
+
Only the device's own report sets this. A controller writing another
|
|
1780
|
+
device's configuration sends the block zeroed, and that frame is ignored.
|
|
1779
1781
|
"""
|
|
1780
1782
|
def __init__(self, addresses:V2IPStreamSources|None, audio_fmt:V2IPAudioFormat|None) -> None:
|
|
1781
1783
|
self._addresses = addresses
|
|
@@ -1792,6 +1794,122 @@ class DeviceV2IPSink:
|
|
|
1792
1794
|
def __str__(self) -> str:
|
|
1793
1795
|
return f"addresses=[{self._addresses}] audio_fmt={self._audio_fmt}"
|
|
1794
1796
|
|
|
1797
|
+
class V2IPDeviceSettings:
|
|
1798
|
+
"""
|
|
1799
|
+
The device settings of a V2IP unit, as it reports them and as its controller
|
|
1800
|
+
changes them.
|
|
1801
|
+
|
|
1802
|
+
Each setting is carried only behind its bit in ``valid``, so a frame changes
|
|
1803
|
+
one setting without restating the others, and a device reports only the
|
|
1804
|
+
settings it has. Read a setting through ``get()`` and the profile
|
|
1805
|
+
properties, which answer None for one the device has not reported.
|
|
1806
|
+
"""
|
|
1807
|
+
def __init__(self, valid:V2IPDeviceSetting=V2IPDeviceSetting(0),
|
|
1808
|
+
flags:V2IPDeviceSetting=V2IPDeviceSetting(0), ir_profiles:int=0,
|
|
1809
|
+
ir_profile:int=0, ir_profile_sink:int=0) -> None:
|
|
1810
|
+
self._valid = V2IPDeviceSetting(valid)
|
|
1811
|
+
self._flags = V2IPDeviceSetting(flags)
|
|
1812
|
+
self._ir_profiles = ir_profiles
|
|
1813
|
+
self._ir_profile = ir_profile
|
|
1814
|
+
self._ir_profile_sink = ir_profile_sink
|
|
1815
|
+
|
|
1816
|
+
@property
|
|
1817
|
+
def valid(self) -> V2IPDeviceSetting:
|
|
1818
|
+
"""The settings reported so far."""
|
|
1819
|
+
return self._valid
|
|
1820
|
+
|
|
1821
|
+
@property
|
|
1822
|
+
def flags(self) -> V2IPDeviceSetting:
|
|
1823
|
+
"""The values of the on/off settings among ``valid``."""
|
|
1824
|
+
return self._flags
|
|
1825
|
+
|
|
1826
|
+
def get(self, setting:V2IPDeviceSetting) -> bool|None:
|
|
1827
|
+
"""Whether an on/off setting is on, None while the device has not reported it."""
|
|
1828
|
+
if ((self._valid & setting) != setting):
|
|
1829
|
+
return None
|
|
1830
|
+
return ((self._flags & setting) == setting)
|
|
1831
|
+
|
|
1832
|
+
@property
|
|
1833
|
+
def ir_profile(self) -> int|None:
|
|
1834
|
+
"""The global infrared port's profile, None while it is not reported."""
|
|
1835
|
+
if (V2IPDeviceSetting.IR_PROFILE not in self._valid):
|
|
1836
|
+
return None
|
|
1837
|
+
return self._ir_profile
|
|
1838
|
+
|
|
1839
|
+
@property
|
|
1840
|
+
def ir_profile_sink(self) -> int|None:
|
|
1841
|
+
"""The output infrared port's profile, None while it is not reported.
|
|
1842
|
+
V2IP_IR_PROFILE_NOT_SET means the port follows the global one."""
|
|
1843
|
+
if (V2IPDeviceSetting.IR_PROFILE_SINK not in self._valid):
|
|
1844
|
+
return None
|
|
1845
|
+
return self._ir_profile_sink
|
|
1846
|
+
|
|
1847
|
+
@property
|
|
1848
|
+
def stored_ir_profiles(self) -> int|None:
|
|
1849
|
+
"""The infrared profiles stored on the device, bit n for profile n, None
|
|
1850
|
+
while it is not reported."""
|
|
1851
|
+
if (V2IPDeviceSetting.IR_PROFILES not in self._valid):
|
|
1852
|
+
return None
|
|
1853
|
+
return self._ir_profiles
|
|
1854
|
+
|
|
1855
|
+
def merge(self, previous:'V2IPDeviceSettings|None') -> 'V2IPDeviceSettings':
|
|
1856
|
+
"""
|
|
1857
|
+
Fold this received block onto the cached one.
|
|
1858
|
+
|
|
1859
|
+
Each bit in this block's ``valid`` replaces its own setting and leaves
|
|
1860
|
+
the others alone, so a write about one setting does not clear what was
|
|
1861
|
+
known about the rest.
|
|
1862
|
+
"""
|
|
1863
|
+
if (previous is None):
|
|
1864
|
+
previous = V2IPDeviceSettings()
|
|
1865
|
+
valid = self._valid
|
|
1866
|
+
return V2IPDeviceSettings(
|
|
1867
|
+
valid=(previous._valid | valid),
|
|
1868
|
+
flags=((previous._flags & ~valid) | (self._flags & valid)),
|
|
1869
|
+
ir_profiles=(self._ir_profiles if (V2IPDeviceSetting.IR_PROFILES in valid)
|
|
1870
|
+
else previous._ir_profiles),
|
|
1871
|
+
ir_profile=(self._ir_profile if (V2IPDeviceSetting.IR_PROFILE in valid)
|
|
1872
|
+
else previous._ir_profile),
|
|
1873
|
+
ir_profile_sink=(self._ir_profile_sink if (V2IPDeviceSetting.IR_PROFILE_SINK in valid)
|
|
1874
|
+
else previous._ir_profile_sink))
|
|
1875
|
+
|
|
1876
|
+
def as_applied_to(self, reported:V2IPDeviceSetting) -> 'V2IPDeviceSettings':
|
|
1877
|
+
"""
|
|
1878
|
+
This block limited to what a device takes from a write about it.
|
|
1879
|
+
|
|
1880
|
+
A device applies a setting only if it has it, a profile only within its
|
|
1881
|
+
range, and never the list of stored profiles, which only it knows.
|
|
1882
|
+
"""
|
|
1883
|
+
valid = (self._valid & reported) & ~V2IPDeviceSetting.IR_PROFILES
|
|
1884
|
+
if not (0 <= self._ir_profile < V2IP_IR_PROFILE_MAX):
|
|
1885
|
+
valid &= ~V2IPDeviceSetting.IR_PROFILE
|
|
1886
|
+
if not (V2IP_IR_PROFILE_NOT_SET <= self._ir_profile_sink < V2IP_IR_PROFILE_MAX):
|
|
1887
|
+
valid &= ~V2IPDeviceSetting.IR_PROFILE_SINK
|
|
1888
|
+
return V2IPDeviceSettings(valid=valid, flags=self._flags, ir_profiles=self._ir_profiles,
|
|
1889
|
+
ir_profile=self._ir_profile, ir_profile_sink=self._ir_profile_sink)
|
|
1890
|
+
|
|
1891
|
+
def __eq__(self, other:Any) -> bool:
|
|
1892
|
+
if not isinstance(other, V2IPDeviceSettings):
|
|
1893
|
+
return NotImplemented
|
|
1894
|
+
return ((self._valid, self._flags, self._ir_profiles, self._ir_profile, self._ir_profile_sink)
|
|
1895
|
+
== (other._valid, other._flags, other._ir_profiles, other._ir_profile, other._ir_profile_sink))
|
|
1896
|
+
|
|
1897
|
+
def __ne__(self, other:Any) -> bool:
|
|
1898
|
+
result = self.__eq__(other)
|
|
1899
|
+
return result if (result is NotImplemented) else (not result)
|
|
1900
|
+
|
|
1901
|
+
def __str__(self) -> str:
|
|
1902
|
+
parts = [f"{setting.name}={'on' if self.get(setting) else 'off'}"
|
|
1903
|
+
for setting in V2IPDeviceSetting
|
|
1904
|
+
if (setting in V2IP_DEVICE_SETTING_SWITCHES) and (self.get(setting) is not None)]
|
|
1905
|
+
if (self.ir_profile is not None):
|
|
1906
|
+
parts.append(f"ir_profile={self.ir_profile}")
|
|
1907
|
+
if (self.ir_profile_sink is not None):
|
|
1908
|
+
parts.append(f"ir_profile_sink={self.ir_profile_sink}")
|
|
1909
|
+
if (self.stored_ir_profiles is not None):
|
|
1910
|
+
parts.append(f"ir_profiles={self.stored_ir_profiles:#x}")
|
|
1911
|
+
return " ".join(parts)
|
|
1912
|
+
|
|
1795
1913
|
def __repr__(self) -> str:
|
|
1796
1914
|
return str(self)
|
|
1797
1915
|
|
|
@@ -2101,6 +2219,15 @@ class DeviceBase(ABC):
|
|
|
2101
2219
|
def v2ip_sink(self) -> DeviceV2IPSink|None:
|
|
2102
2220
|
'''V2IP sink-side multicast subscriptions and resolved audio format (None until first announced)'''
|
|
2103
2221
|
|
|
2222
|
+
@property
|
|
2223
|
+
@abstractmethod
|
|
2224
|
+
def v2ip_settings(self) -> V2IPDeviceSettings|None:
|
|
2225
|
+
'''The V2IP device settings, None until the device has reported any.
|
|
2226
|
+
|
|
2227
|
+
A device reports only the settings it has, so one missing from valid
|
|
2228
|
+
once the rest have arrived is one the device does not have.
|
|
2229
|
+
'''
|
|
2230
|
+
|
|
2104
2231
|
@property
|
|
2105
2232
|
@abstractmethod
|
|
2106
2233
|
def v2ip_source_local(self) -> V2IPStreamSources|None:
|
|
@@ -2275,8 +2402,8 @@ class DeviceBase(ABC):
|
|
|
2275
2402
|
'''merge one frame of this device's V2IP source list into the cached list'''
|
|
2276
2403
|
|
|
2277
2404
|
@abstractmethod
|
|
2278
|
-
def
|
|
2279
|
-
'''internal callback:
|
|
2405
|
+
def note_link_config(self) -> None:
|
|
2406
|
+
'''internal callback: a page of the link configuration was received'''
|
|
2280
2407
|
|
|
2281
2408
|
@abstractmethod
|
|
2282
2409
|
def note_bay_config(self) -> None:
|
|
@@ -2322,6 +2449,18 @@ class DeviceBase(ABC):
|
|
|
2322
2449
|
async def clear_v2ip_output_mode(self) -> bool:
|
|
2323
2450
|
'''clear the output format this sink is configured to scale to'''
|
|
2324
2451
|
|
|
2452
|
+
@abstractmethod
|
|
2453
|
+
async def set_v2ip_setting(self, setting:V2IPDeviceSetting, enabled:bool) -> bool:
|
|
2454
|
+
'''switch on/off V2IP device settings, all to the same value'''
|
|
2455
|
+
|
|
2456
|
+
@abstractmethod
|
|
2457
|
+
async def set_v2ip_ir_profile(self, profile:int) -> bool:
|
|
2458
|
+
'''set the infrared profile of the device's global infrared port'''
|
|
2459
|
+
|
|
2460
|
+
@abstractmethod
|
|
2461
|
+
async def set_v2ip_sink_ir_profile(self, profile:int) -> bool:
|
|
2462
|
+
'''set the infrared profile of the device's output infrared port'''
|
|
2463
|
+
|
|
2325
2464
|
@abstractmethod
|
|
2326
2465
|
async def get_log(self) -> str|None:
|
|
2327
2466
|
'''read the log from the device and return it as string'''
|
|
@@ -656,6 +656,24 @@ class DeviceV2IPSink:
|
|
|
656
656
|
@property
|
|
657
657
|
def audio_fmt(self) -> V2IPAudioFormat | None: ...
|
|
658
658
|
|
|
659
|
+
class V2IPDeviceSettings:
|
|
660
|
+
def __init__(self, valid: V2IPDeviceSetting = ..., flags: V2IPDeviceSetting = ..., ir_profiles: int = 0, ir_profile: int = 0, ir_profile_sink: int = 0) -> None: ...
|
|
661
|
+
@property
|
|
662
|
+
def valid(self) -> V2IPDeviceSetting: ...
|
|
663
|
+
@property
|
|
664
|
+
def flags(self) -> V2IPDeviceSetting: ...
|
|
665
|
+
def get(self, setting: V2IPDeviceSetting) -> bool | None: ...
|
|
666
|
+
@property
|
|
667
|
+
def ir_profile(self) -> int | None: ...
|
|
668
|
+
@property
|
|
669
|
+
def ir_profile_sink(self) -> int | None: ...
|
|
670
|
+
@property
|
|
671
|
+
def stored_ir_profiles(self) -> int | None: ...
|
|
672
|
+
def merge(self, previous: V2IPDeviceSettings | None) -> V2IPDeviceSettings: ...
|
|
673
|
+
def as_applied_to(self, reported: V2IPDeviceSetting) -> V2IPDeviceSettings: ...
|
|
674
|
+
def __eq__(self, other: Any) -> bool: ...
|
|
675
|
+
def __ne__(self, other: Any) -> bool: ...
|
|
676
|
+
|
|
659
677
|
class UtpLinkErrorStatus(ABC):
|
|
660
678
|
@property
|
|
661
679
|
@abstractmethod
|
|
@@ -838,6 +856,9 @@ class DeviceBase(ABC):
|
|
|
838
856
|
def v2ip_sink(self) -> DeviceV2IPSink | None: ...
|
|
839
857
|
@property
|
|
840
858
|
@abstractmethod
|
|
859
|
+
def v2ip_settings(self) -> V2IPDeviceSettings | None: ...
|
|
860
|
+
@property
|
|
861
|
+
@abstractmethod
|
|
841
862
|
def v2ip_source_local(self) -> V2IPStreamSources | None: ...
|
|
842
863
|
@property
|
|
843
864
|
@abstractmethod
|
|
@@ -916,7 +937,7 @@ class DeviceBase(ABC):
|
|
|
916
937
|
@abstractmethod
|
|
917
938
|
def merge_v2ip_sources(self, first: int, total: int, page: V2IPStreamSourcesList) -> None: ...
|
|
918
939
|
@abstractmethod
|
|
919
|
-
def
|
|
940
|
+
def note_link_config(self) -> None: ...
|
|
920
941
|
@abstractmethod
|
|
921
942
|
def note_bay_config(self) -> None: ...
|
|
922
943
|
@abstractmethod
|
|
@@ -940,6 +961,12 @@ class DeviceBase(ABC):
|
|
|
940
961
|
@abstractmethod
|
|
941
962
|
async def clear_v2ip_output_mode(self) -> bool: ...
|
|
942
963
|
@abstractmethod
|
|
964
|
+
async def set_v2ip_setting(self, setting: V2IPDeviceSetting, enabled: bool) -> bool: ...
|
|
965
|
+
@abstractmethod
|
|
966
|
+
async def set_v2ip_ir_profile(self, profile: int) -> bool: ...
|
|
967
|
+
@abstractmethod
|
|
968
|
+
async def set_v2ip_sink_ir_profile(self, profile: int) -> bool: ...
|
|
969
|
+
@abstractmethod
|
|
943
970
|
async def get_log(self) -> str | None: ...
|
|
944
971
|
@abstractmethod
|
|
945
972
|
def on_mxr_update(self, data: object) -> None: ...
|
|
@@ -14,7 +14,7 @@ Default communication modes:
|
|
|
14
14
|
"""
|
|
15
15
|
import os
|
|
16
16
|
|
|
17
|
-
VERSION = '5.
|
|
17
|
+
VERSION = '5.10.0'
|
|
18
18
|
__version__ = VERSION
|
|
19
19
|
|
|
20
20
|
MX_BCAST_UDP_PORT = 8811
|
|
@@ -37,6 +37,14 @@ re-drawn after every hello that is sent, so devices announcing on the same mesh
|
|
|
37
37
|
not converge into synchronised bursts.
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
+
MXR_CONFIG_TIMEOUT = 15
|
|
41
|
+
"""How long a device is given to finish describing itself, in seconds.
|
|
42
|
+
|
|
43
|
+
Past this a device is reported as fully described without its link
|
|
44
|
+
configuration, and keeps being asked for the rest. It never stands in for the
|
|
45
|
+
bay configuration - see ``Device.has_bays``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
40
48
|
V2IP_UDP_PORT_VIDEO = 50020
|
|
41
49
|
"""Default destination UDP port for a V2IP video stream (firmware ``V2IP_UDP_PORT_VIDEO``)."""
|
|
42
50
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
from enum import IntEnum, IntFlag
|
|
11
11
|
from typing import TypeVar
|
|
12
12
|
|
|
13
|
-
MXR_PROTOCOL_VERSION =
|
|
13
|
+
MXR_PROTOCOL_VERSION = 0x2A
|
|
14
14
|
"""Highest mx_remote protocol version this library understands.
|
|
15
15
|
|
|
16
16
|
Mirrors the firmware's own protocol version. Bump
|
|
@@ -21,7 +21,14 @@ frame.
|
|
|
21
21
|
This is also a receive ceiling: Remote.process_frame drops any frame stamped
|
|
22
22
|
above it, unparsed. So it has to reach the stamp of every layout revision
|
|
23
23
|
decoded below, or that revision goes silent on the mesh rather than noisy -
|
|
24
|
-
0x29 is what a MatrixOS 10.12.46 unit stamps on V2IP_STATS.
|
|
24
|
+
0x29 is what a MatrixOS 10.12.46 unit stamps on V2IP_STATS.
|
|
25
|
+
|
|
26
|
+
0x2A is also what MatrixOS reads a hello by: a device on 0x2A or later is given
|
|
27
|
+
60s to hello again rather than 15s, and a unit only slows its own hello to one
|
|
28
|
+
every 20-30s once every device it knows is on 0x2A."""
|
|
29
|
+
|
|
30
|
+
MXR_PROTOCOL_VERSION_SLOW_HELLO = 0x2A
|
|
31
|
+
"""First protocol version whose devices may hello only every 20-30s."""
|
|
25
32
|
|
|
26
33
|
MXR_OPCODE_VERSIONS: dict[int, int] = {
|
|
27
34
|
0x00: 0x01, # SYS_HELLO
|
|
@@ -258,6 +265,50 @@ class V2IPFpgaFeature(IntFlag):
|
|
|
258
265
|
SINK_STREAM_INFO = (1 << 5)
|
|
259
266
|
'''Reports information about the stream its sink receives.'''
|
|
260
267
|
|
|
268
|
+
class V2IPDeviceSetting(IntFlag):
|
|
269
|
+
'''The device settings a V2IP configuration can carry, each on its own bit.
|
|
270
|
+
|
|
271
|
+
The same bits serve as the settings a frame carries and as the values of the
|
|
272
|
+
on/off ones among them. The last three carry a value elsewhere in the block
|
|
273
|
+
and have no on/off value of their own.
|
|
274
|
+
|
|
275
|
+
A device reports every setting it has, so one it never reports is one it
|
|
276
|
+
does not have.'''
|
|
277
|
+
SINK_CHECK_POWER = (1 << 0)
|
|
278
|
+
'''Disables the decoder while the display is off.'''
|
|
279
|
+
SINK_OFF_NO_SIGNAL = (1 << 1)
|
|
280
|
+
'''Disables the HDMI output while there is no signal.'''
|
|
281
|
+
IR_TX_MODULATED = (1 << 2)
|
|
282
|
+
'''Sends infrared modulated.'''
|
|
283
|
+
STATUS_LED = (1 << 3)
|
|
284
|
+
'''Lights the status LED.'''
|
|
285
|
+
NETWORK_LED = (1 << 4)
|
|
286
|
+
'''Lights the network port LEDs.'''
|
|
287
|
+
FAN_QUIET = (1 << 5)
|
|
288
|
+
'''Runs the fan in quiet mode.'''
|
|
289
|
+
CEC_COMBO_KEYS = (1 << 6)
|
|
290
|
+
'''Accepts CEC combo keys.'''
|
|
291
|
+
CEC_COMBO_INPUT = (1 << 7)
|
|
292
|
+
'''Accepts CEC combo keys for the device's own input.'''
|
|
293
|
+
IR_PROFILE = (1 << 8)
|
|
294
|
+
'''The infrared profile of the device's global infrared port.'''
|
|
295
|
+
IR_PROFILE_SINK = (1 << 9)
|
|
296
|
+
'''The infrared profile of the output's infrared port.'''
|
|
297
|
+
IR_PROFILES = (1 << 10)
|
|
298
|
+
'''The infrared profiles stored on the device. Reported by the device
|
|
299
|
+
itself and never written.'''
|
|
300
|
+
|
|
301
|
+
V2IP_DEVICE_SETTING_SWITCHES = V2IPDeviceSetting(0xFF)
|
|
302
|
+
"""The settings that are on or off, as opposed to carrying a value."""
|
|
303
|
+
|
|
304
|
+
V2IP_IR_PROFILE_NOT_SET = -1
|
|
305
|
+
"""The infrared profile an output's port reports when it follows the device's
|
|
306
|
+
global one."""
|
|
307
|
+
|
|
308
|
+
V2IP_IR_PROFILE_MAX = 18
|
|
309
|
+
"""One past the highest infrared profile: the default profile is 0, the Atlona
|
|
310
|
+
profile 1, and user-defined profiles 1 to 16 are 2 to 17."""
|
|
311
|
+
|
|
261
312
|
BAY_FEATURE_DOLBY_IN_POS = 24
|
|
262
313
|
|
|
263
314
|
class BayFeaturesMask(IntFlag):
|
|
@@ -2,6 +2,7 @@ from _typeshed import Incomplete
|
|
|
2
2
|
from enum import IntEnum, IntFlag
|
|
3
3
|
|
|
4
4
|
MXR_PROTOCOL_VERSION: int
|
|
5
|
+
MXR_PROTOCOL_VERSION_SLOW_HELLO: int
|
|
5
6
|
MXR_OPCODE_VERSIONS: dict[int, int]
|
|
6
7
|
MXR_CFG_SIGNAL_STATUS_LEN: int
|
|
7
8
|
MXR_DEVICE_NAME_LEN: int
|
|
@@ -68,6 +69,22 @@ class V2IPFpgaFeature(IntFlag):
|
|
|
68
69
|
SINK_STATE: Incomplete
|
|
69
70
|
SINK_STREAM_INFO: Incomplete
|
|
70
71
|
|
|
72
|
+
class V2IPDeviceSetting(IntFlag):
|
|
73
|
+
SINK_CHECK_POWER: Incomplete
|
|
74
|
+
SINK_OFF_NO_SIGNAL: Incomplete
|
|
75
|
+
IR_TX_MODULATED: Incomplete
|
|
76
|
+
STATUS_LED: Incomplete
|
|
77
|
+
NETWORK_LED: Incomplete
|
|
78
|
+
FAN_QUIET: Incomplete
|
|
79
|
+
CEC_COMBO_KEYS: Incomplete
|
|
80
|
+
CEC_COMBO_INPUT: Incomplete
|
|
81
|
+
IR_PROFILE: Incomplete
|
|
82
|
+
IR_PROFILE_SINK: Incomplete
|
|
83
|
+
IR_PROFILES: Incomplete
|
|
84
|
+
|
|
85
|
+
V2IP_DEVICE_SETTING_SWITCHES: Incomplete
|
|
86
|
+
V2IP_IR_PROFILE_NOT_SET: int
|
|
87
|
+
V2IP_IR_PROFILE_MAX: int
|
|
71
88
|
BAY_FEATURE_DOLBY_IN_POS: int
|
|
72
89
|
|
|
73
90
|
class BayFeaturesMask(IntFlag):
|
|
@@ -44,11 +44,7 @@ class FrameLinks(FrameBase):
|
|
|
44
44
|
return
|
|
45
45
|
for link in self.links:
|
|
46
46
|
link.process()
|
|
47
|
-
|
|
48
|
-
# this client has not seen is dropped rather than held, which is
|
|
49
|
-
# what a device does with the same frame, and the device re-sends.
|
|
50
|
-
if (link.remote_bay is not None):
|
|
51
|
-
dev.note_link_record(link.remote_port)
|
|
47
|
+
dev.note_link_config()
|
|
52
48
|
|
|
53
49
|
def __str__(self) -> str:
|
|
54
50
|
return f"{self.remote_device} links config page: {len(self.links)} links"
|
|
@@ -12,8 +12,10 @@ from .FrameBase import FrameBase
|
|
|
12
12
|
from .FrameHeader import FrameHeader
|
|
13
13
|
from ..Uid import MxrDeviceUid
|
|
14
14
|
from ..Interface import (DeviceBase, DeviceRegistry, DeviceV2IPDetails, DeviceV2IPScalingSettings,
|
|
15
|
-
DeviceV2IPSink, V2IPAudioFormat, V2IPDscpConfig,
|
|
16
|
-
|
|
15
|
+
DeviceV2IPSink, V2IPAudioFormat, V2IPDeviceSettings, V2IPDscpConfig,
|
|
16
|
+
V2IPStreamSource)
|
|
17
|
+
from .Constants import (V2IPDeviceSetting, V2IPFpgaFeature, MXR_SCALING_FLAG_AUTO_SCALING,
|
|
18
|
+
MXR_SCALING_FLAG_MODE_VALID,
|
|
17
19
|
MXR_SCALING_FLAG_OPTIONS_VALID, MXR_SCALING_FLAG_OPTIONS2_VALID,
|
|
18
20
|
MXR_SCALING_OPTIONS2_SETTINGS, MxrSignalType, v2ip_dscp_value,
|
|
19
21
|
v2ip_rate_valid)
|
|
@@ -30,6 +32,8 @@ from .V2IPConfig import V2IPStreamSourceImpl, parse_v2ip_av_source
|
|
|
30
32
|
# 88..112 v2ip_av_source sink, zero when no route is active
|
|
31
33
|
# 112..120 v2ip_audio_format sink_audio_fmt
|
|
32
34
|
# 120..128 u64 video processor feature mask
|
|
35
|
+
# 128..144 device settings: u32 valid, u32 flags, u32 stored ir profiles,
|
|
36
|
+
# s8 ir_profile, s8 ir_profile_sink, 2 reserved
|
|
33
37
|
#
|
|
34
38
|
# Each of those blocks was appended behind what came before it, leaving every
|
|
35
39
|
# offset ahead of it where it was, so the length is what says whether one is
|
|
@@ -52,7 +56,8 @@ from .V2IPConfig import V2IPStreamSourceImpl, parse_v2ip_av_source
|
|
|
52
56
|
# DeviceV2IPDetails.merge() carries the unmarked fields forward. Replacing the
|
|
53
57
|
# cache wholesale reports a peer's addresses as 0.0.0.0 the moment a controller
|
|
54
58
|
# writes anything else. The sink trailer is the exception: present or absent by
|
|
55
|
-
# length, with no marker.
|
|
59
|
+
# length, with no marker, and taken only from a device describing itself.
|
|
60
|
+
# The settings block marks each setting with its own bit in valid.
|
|
56
61
|
#
|
|
57
62
|
# Trust the scaling block only from a peer whose hello carries
|
|
58
63
|
# MXR_FEATURE_CONFIG_INITIALISED. Without it the sender may have built those
|
|
@@ -80,6 +85,9 @@ _SINK_OFFSET = _BASE_SIZE
|
|
|
80
85
|
_SINK_AUDIO_OFFSET = _BASE_SIZE + 24
|
|
81
86
|
_CODEC_OFFSET = _WITH_OPTIONS_SIZE
|
|
82
87
|
_CODEC_SIZE = 8
|
|
88
|
+
_SETTINGS_OFFSET = _CODEC_OFFSET + _CODEC_SIZE
|
|
89
|
+
_SETTINGS_SIZE = 16
|
|
90
|
+
_WITH_SETTINGS_SIZE = _SETTINGS_OFFSET + _SETTINGS_SIZE
|
|
83
91
|
|
|
84
92
|
class V2IPDeviceOptions:
|
|
85
93
|
'''Parsed V2IP device options (TX rate and per-stream DSCP marking).'''
|
|
@@ -236,6 +244,46 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
236
244
|
payload += bytes(_BASE_SIZE - len(payload))
|
|
237
245
|
return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
|
|
238
246
|
|
|
247
|
+
@staticmethod
|
|
248
|
+
def construct_settings(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid,
|
|
249
|
+
settings:V2IPDeviceSettings) -> FrameBase|None:
|
|
250
|
+
'''Build the 0x3C write that changes a device's settings and nothing else.
|
|
251
|
+
|
|
252
|
+
144 bytes: the configuration as construct_scaling() lays it out but with
|
|
253
|
+
no scaling validity bit, then the options trailer - the sink block at
|
|
254
|
+
88..120, the processor's feature word at 120..128 and the settings at
|
|
255
|
+
128..144. The settings are the last block, so a frame that carries them
|
|
256
|
+
carries the two before them too.
|
|
257
|
+
|
|
258
|
+
**Both of those go out zeroed.** A device leaves the feature word zero on
|
|
259
|
+
a frame about another device, and a receiver takes the sink block only
|
|
260
|
+
from a device describing itself - except on a receiver that predates the
|
|
261
|
+
settings block, which copies it into its record of the target until the
|
|
262
|
+
target next reports. That is the frame a controlling device sends for
|
|
263
|
+
the same change.
|
|
264
|
+
|
|
265
|
+
Only the settings named in settings.valid are carried; the device keeps
|
|
266
|
+
the rest.
|
|
267
|
+
'''
|
|
268
|
+
payload = bytearray(target_uid.byte_value)
|
|
269
|
+
if (len(payload) != 16):
|
|
270
|
+
raise ValueError(f"invalid uid length: {len(payload)}")
|
|
271
|
+
# source: three stream slots, left zeroed so the encoder keeps its own.
|
|
272
|
+
payload += bytes(40 - len(payload))
|
|
273
|
+
payload.append(_RATE_UNSET)
|
|
274
|
+
# No dscp, no audio return, a scaling block without a validity bit and a
|
|
275
|
+
# tiling window whose zero uid says none is carried; then the zeroed
|
|
276
|
+
# sink block and feature word.
|
|
277
|
+
payload += bytes(_SETTINGS_OFFSET - len(payload))
|
|
278
|
+
payload += int(settings.valid).to_bytes(4, 'little')
|
|
279
|
+
payload += int(settings.flags).to_bytes(4, 'little')
|
|
280
|
+
# A value not named in valid is not read, so it goes out as zero.
|
|
281
|
+
payload += (settings.stored_ir_profiles or 0).to_bytes(4, 'little')
|
|
282
|
+
payload += (settings.ir_profile or 0).to_bytes(1, 'little', signed=True)
|
|
283
|
+
payload += (settings.ir_profile_sink or 0).to_bytes(1, 'little', signed=True)
|
|
284
|
+
payload += bytes(_WITH_SETTINGS_SIZE - len(payload))
|
|
285
|
+
return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
|
|
286
|
+
|
|
239
287
|
@property
|
|
240
288
|
def target_uid(self) -> MxrDeviceUid|None:
|
|
241
289
|
return self.payload_uuid(idx=0)
|
|
@@ -312,14 +360,47 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
312
360
|
@cached_property
|
|
313
361
|
def sink(self) -> DeviceV2IPSink|None:
|
|
314
362
|
'''Sink-side state a longer frame appends to the configuration; None when
|
|
315
|
-
the frame stops in front of it.
|
|
363
|
+
the frame stops in front of it.
|
|
364
|
+
|
|
365
|
+
Read only from a frame a device sent about itself. A controller writing
|
|
366
|
+
another device's configuration sends the block zeroed, because it has
|
|
367
|
+
nothing to say about that sink's subscription, and caching those zeros
|
|
368
|
+
would clear the route on every unrelated write.
|
|
369
|
+
'''
|
|
316
370
|
if (self.payload is None) or (len(self.payload) < _WITH_OPTIONS_SIZE):
|
|
317
371
|
return None
|
|
372
|
+
if not self.target_self:
|
|
373
|
+
return None
|
|
318
374
|
return DeviceV2IPSink(
|
|
319
375
|
addresses=parse_v2ip_av_source(self.payload, _SINK_OFFSET),
|
|
320
376
|
audio_fmt=V2IPAudioFormat.from_bytes(self.payload[_SINK_AUDIO_OFFSET:_SINK_AUDIO_OFFSET + 8]),
|
|
321
377
|
)
|
|
322
378
|
|
|
379
|
+
@cached_property
|
|
380
|
+
def settings(self) -> V2IPDeviceSettings|None:
|
|
381
|
+
'''The device settings block, as the subject will hold it; None when the
|
|
382
|
+
frame stops in front of it or nothing would act on it.
|
|
383
|
+
|
|
384
|
+
A device reports every setting it has. A controller sends only the ones
|
|
385
|
+
it changes, and the device applies only those it has, so a write is
|
|
386
|
+
limited here to what the device will take from it and no further.
|
|
387
|
+
'''
|
|
388
|
+
if (self.payload is None) or (len(self.payload) < _WITH_SETTINGS_SIZE):
|
|
389
|
+
return None
|
|
390
|
+
if ((dev := self.subject_device) is None):
|
|
391
|
+
return None
|
|
392
|
+
block = self.payload[_SETTINGS_OFFSET:_WITH_SETTINGS_SIZE]
|
|
393
|
+
frame = V2IPDeviceSettings(
|
|
394
|
+
valid=V2IPDeviceSetting(int.from_bytes(block[0:4], 'little')),
|
|
395
|
+
flags=V2IPDeviceSetting(int.from_bytes(block[4:8], 'little')),
|
|
396
|
+
ir_profiles=int.from_bytes(block[8:12], 'little'),
|
|
397
|
+
ir_profile=int.from_bytes(block[12:13], 'little', signed=True),
|
|
398
|
+
ir_profile_sink=int.from_bytes(block[13:14], 'little', signed=True))
|
|
399
|
+
if self.target_self:
|
|
400
|
+
return frame
|
|
401
|
+
reported = dev.v2ip_settings
|
|
402
|
+
return frame.as_applied_to(reported.valid if (reported is not None) else V2IPDeviceSetting(0))
|
|
403
|
+
|
|
323
404
|
def process(self) -> None:
|
|
324
405
|
'''Update the cache of the device this configuration names, not of its sender.'''
|
|
325
406
|
if ((dev := self.subject_device) is None):
|
|
@@ -329,8 +410,11 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
329
410
|
dev.on_mxr_update(sink)
|
|
330
411
|
if ((features := self.video_processor_features) is not None):
|
|
331
412
|
dev.on_mxr_update(features)
|
|
413
|
+
if ((settings := self.settings) is not None):
|
|
414
|
+
dev.on_mxr_update(settings)
|
|
332
415
|
|
|
333
416
|
def __str__(self) -> str:
|
|
334
417
|
sink_str = f" sink=[{self.sink}]" if (self.sink is not None) else ""
|
|
335
418
|
fpga_str = f" fpga={self.video_processor_features}" if (self.video_processor_features is not None) else ""
|
|
336
|
-
|
|
419
|
+
settings_str = f" settings=[{self.settings}]" if (self.settings is not None) else ""
|
|
420
|
+
return f"V2IP device configuration self={self.target_self} {self.video} {self.audio} {self.anc} {self.arc} options={self.options}{sink_str}{fpga_str}{settings_str}"
|