mx-remote 5.6.0__tar.gz → 5.7.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.6.0 → mx_remote-5.7.0}/PKG-INFO +1 -1
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/oneip.md +68 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/Interface.py +159 -1
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/Interface.pyi +31 -1
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/const.py +1 -1
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Constants.py +43 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Constants.pyi +7 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +104 -4
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +7 -2
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Svd.py +4 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Svd.pyi +2 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Device.py +146 -1
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Device.pyi +15 -2
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/run.py +2 -0
- mx_remote-5.7.0/tests/v2ipscaling.py +161 -0
- mx_remote-5.7.0/tests/v2ipsubject.py +124 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/.gitignore +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/AGENTS.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/CLAUDE.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/LICENSE +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/README.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/README.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/_static/brand.css +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/_static/favicon.svg +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/_static/logo-dark.svg +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/_static/logo.svg +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/api.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/audio.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/callbacks.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/conf.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/configuration.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/devices-and-bays.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/diagnostics.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/index.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/multiviewer.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/requirements.txt +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/docs/routing.md +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/hatch_build.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/Uid.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/__init__.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/__init__.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/compat.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/compat.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/const.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/main.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/main.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Factory.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfig.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameHello.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameLinks.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCIr.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/V2IPConfig.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/V2IPStats.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/V2IPStats.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/py.typed +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Bay.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Bay.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Remote.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/Remote.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/pyproject.toml +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/audio_ir.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/capture.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/e2e.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/entry.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/handlers.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/handlers_cover.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/hello.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/idempotent.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/lengths.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/merge.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/newops.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/offsets.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/proto_stamp.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/protogate.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/rcsettings.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/roundtrip.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/smoke.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/sources.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/stats.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/structs.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/txresult.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/unknowns.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/vdetails.py +0 -0
- {mx_remote-5.6.0 → mx_remote-5.7.0}/tests/wirefix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mx_remote
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.7.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/
|
|
@@ -14,6 +14,9 @@ if device.is_v2ip and device.v2ip_sources:
|
|
|
14
14
|
print(f"Audio: {source.audio.ip}:{source.audio.port}")
|
|
15
15
|
|
|
16
16
|
# stream details (encoder/decoder config)
|
|
17
|
+
# A configuration frame names its subject in the payload, so a controller's
|
|
18
|
+
# write for a transceiver reaches the transceiver's record - the controller's
|
|
19
|
+
# own stays empty, which is what the device always meant.
|
|
17
20
|
if device.v2ip_details:
|
|
18
21
|
details = device.v2ip_details
|
|
19
22
|
print(f"Video: {details.video}")
|
|
@@ -91,6 +94,71 @@ reading from a repeat, and note it cannot see a `TX_BRIDGE_UNLOCKED` flag
|
|
|
91
94
|
carried forward across a format change: a value held over is a stored reading
|
|
92
95
|
like any other.
|
|
93
96
|
|
|
97
|
+
## Sink output scaling
|
|
98
|
+
|
|
99
|
+
A sink scales for two independent reasons: automatic scaling, and a configured
|
|
100
|
+
output mode. Each is written behind its own validity bit, so moving one leaves
|
|
101
|
+
the other where it is.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# what the sink reports. Trust this block only from a device whose hello carries
|
|
105
|
+
# CONFIG_INITIALISED - firmware without it builds the block over uninitialised
|
|
106
|
+
# stack, where the valid bit itself is noise.
|
|
107
|
+
scaling = device.v2ip_details.scaling
|
|
108
|
+
print(scaling.auto_scaling) # None when the sender did not say
|
|
109
|
+
print(scaling.configured_mode) # (signal type, refresh), None when it has none
|
|
110
|
+
|
|
111
|
+
# a mode is given as a depth and a colour space, never as a packed signal-type
|
|
112
|
+
# word read back off the wire: a sink with no mode reports the word carrying the
|
|
113
|
+
# index for "no depth", which a receiver decodes to zero and drops in silence.
|
|
114
|
+
mode = mx_remote.V2IPOutputMode(svd=16, depth=12,
|
|
115
|
+
colour=mx_remote.VideoColourSpace.YUV422, refresh=60)
|
|
116
|
+
|
|
117
|
+
await device.set_v2ip_auto_scaling(False) # turn it off before setting a mode
|
|
118
|
+
await device.set_v2ip_output_mode(mode)
|
|
119
|
+
await device.set_v2ip_auto_scaling(True) # back on, if that is what you want
|
|
120
|
+
|
|
121
|
+
await device.clear_v2ip_output_mode() # the only way to say "no mode"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Set the mode with automatic scaling off.** A sink scaling automatically
|
|
125
|
+
refuses a mode whose format the attached display does not list, and refuses it
|
|
126
|
+
in silence. Setting the mode first and turning automatic scaling back on
|
|
127
|
+
afterwards is the order that survives, because the mode is checked while
|
|
128
|
+
automatic scaling is still off.
|
|
129
|
+
|
|
130
|
+
Nothing acknowledges any of these. `set_v2ip_output_mode` returns False for a
|
|
131
|
+
mode no sink would take, but a True only says the frame went out: the sink
|
|
132
|
+
weighs the format against the display's EDID and against what its own output
|
|
133
|
+
stage can produce, and refuses silently either way. Read `v2ip_details.scaling`
|
|
134
|
+
back on the device's next report to learn what it did.
|
|
135
|
+
|
|
136
|
+
A scaling change makes the device rebuild and rebroadcast its sink block, so
|
|
137
|
+
expect `device.v2ip_sink` to read empty for a moment afterwards - see
|
|
138
|
+
[what an empty sink block means](#what-an-empty-sink-block-means).
|
|
139
|
+
|
|
140
|
+
## What an empty sink block means
|
|
141
|
+
|
|
142
|
+
`device.v2ip_sink` addresses that read as unset mean "no route, or the sink
|
|
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 sender with
|
|
145
|
+
nothing to say sends zeros and every receiver stores them. A sender leaves it
|
|
146
|
+
empty whenever its own stream configuration does not resolve, which covers more
|
|
147
|
+
than having no route: a selected source whose record has not arrived yet, the
|
|
148
|
+
state after a restart at either end, missing audio bay configuration, or a
|
|
149
|
+
stream failing its validity check.
|
|
150
|
+
|
|
151
|
+
Expect that reading rather than guarding against it. Any scaling change rebuilds
|
|
152
|
+
and rebroadcasts the block, and a write aimed at a remote bay sends it zeroed
|
|
153
|
+
however it was requested, so an empty reading turns up most often during exactly
|
|
154
|
+
the no-signal troubleshooting that prompted the change. A device's periodic
|
|
155
|
+
report puts a real route back within a minute of it having one, so wait one out
|
|
156
|
+
rather than treat the first empty reading as an answer.
|
|
157
|
+
|
|
158
|
+
The library caches an empty reading rather than dropping it. A sink that has
|
|
159
|
+
genuinely lost its route sends the same zeros, and so does every report after
|
|
160
|
+
it, so refusing them would hold a route that nothing later could clear.
|
|
161
|
+
|
|
94
162
|
## Mesh and firmware
|
|
95
163
|
|
|
96
164
|
```python
|
|
@@ -28,7 +28,7 @@ from .proto.Multiviewer import (
|
|
|
28
28
|
MultiviewerITCMode,
|
|
29
29
|
MultiviewerHDCPMode,
|
|
30
30
|
)
|
|
31
|
-
from .proto.Svd import SvdMap
|
|
31
|
+
from .proto.Svd import SvdMap, lookup_svd
|
|
32
32
|
from typing import Any, Callable
|
|
33
33
|
from .Uid import MxrDeviceUid, MxrBayUid
|
|
34
34
|
|
|
@@ -1390,6 +1390,28 @@ class DeviceV2IPScalingSettings(ABC):
|
|
|
1390
1390
|
is the only option bit with a defined meaning.
|
|
1391
1391
|
'''
|
|
1392
1392
|
|
|
1393
|
+
@property
|
|
1394
|
+
def configured_mode(self) -> 'tuple[MxrSignalType, int]|None':
|
|
1395
|
+
'''The mode and refresh rate this sink scales to, None when it has none.
|
|
1396
|
+
|
|
1397
|
+
The two are distinct on the wire: a sink with no mode configured leaves
|
|
1398
|
+
MXR_SCALING_FLAG_MODE_VALID clear, and never sets it over a zero mode.
|
|
1399
|
+
|
|
1400
|
+
Trust it only from a peer whose hello carries CONFIG_INITIALISED
|
|
1401
|
+
(DeviceBase.config_initialised). Firmware without that builds this block
|
|
1402
|
+
over uninitialised stack, where the valid bit itself is noise.
|
|
1403
|
+
'''
|
|
1404
|
+
if ((self.flags & MXR_SCALING_FLAG_MODE_VALID) == 0):
|
|
1405
|
+
return None
|
|
1406
|
+
return (MxrSignalType(self.mode.to_bytes(2, 'little')), self.refresh)
|
|
1407
|
+
|
|
1408
|
+
@property
|
|
1409
|
+
def auto_scaling(self) -> bool|None:
|
|
1410
|
+
'''Whether the output scales automatically, None when the sender did not say.'''
|
|
1411
|
+
if ((self.flags & MXR_SCALING_FLAG_OPTIONS_VALID) == 0):
|
|
1412
|
+
return None
|
|
1413
|
+
return ((self.flags & MXR_SCALING_FLAG_AUTO_SCALING) != 0)
|
|
1414
|
+
|
|
1393
1415
|
def v2ip_stream_valid(stream:'V2IPStreamSource|None') -> bool:
|
|
1394
1416
|
"""
|
|
1395
1417
|
True when a stream source carries an address a frame actually meant to send.
|
|
@@ -1418,6 +1440,72 @@ def v2ip_stream_cleared(stream:'V2IPStreamSource|None') -> bool:
|
|
|
1418
1440
|
"""True when a stream source is zeroed, which is how "no source" is spelled."""
|
|
1419
1441
|
return (stream is not None) and (stream.ip == '0.0.0.0') and (stream.port == 0)
|
|
1420
1442
|
|
|
1443
|
+
class V2IPOutputMode:
|
|
1444
|
+
"""
|
|
1445
|
+
The output format to scale a V2IP sink to.
|
|
1446
|
+
|
|
1447
|
+
Built from a depth and a colour space rather than from a packed
|
|
1448
|
+
mxr_signal_type word, so the word a caller sends cannot carry the unset bpp
|
|
1449
|
+
index a sink reports while it has no mode configured.
|
|
1450
|
+
"""
|
|
1451
|
+
def __init__(self, svd:int, depth:int, colour:VideoColourSpace, refresh:int) -> None:
|
|
1452
|
+
self.svd = svd
|
|
1453
|
+
"""CTA-861 short video descriptor to output."""
|
|
1454
|
+
self.depth = depth
|
|
1455
|
+
"""Bit depth: 8, 10 or 12."""
|
|
1456
|
+
self.colour = colour
|
|
1457
|
+
"""Colour space to output."""
|
|
1458
|
+
self.refresh = refresh
|
|
1459
|
+
"""Refresh rate in Hz, V2IP_SCALING_REFRESH_MIN to V2IP_SCALING_REFRESH_MAX."""
|
|
1460
|
+
|
|
1461
|
+
def validate(self) -> str|None:
|
|
1462
|
+
"""
|
|
1463
|
+
Why a sink will refuse this mode, or None when it will take it.
|
|
1464
|
+
|
|
1465
|
+
Checked here because a sink checks it and then says nothing: every value
|
|
1466
|
+
this rejects is one the receiver decodes cleanly and drops, leaving a
|
|
1467
|
+
caller with a send that succeeded and a setting that did not move.
|
|
1468
|
+
|
|
1469
|
+
Passing is not a guarantee. A sink also weighs the format against the
|
|
1470
|
+
EDID of the display attached to it and against what its own clock and
|
|
1471
|
+
output stage can produce, and none of that is knowable from here.
|
|
1472
|
+
"""
|
|
1473
|
+
if (self.svd == 0):
|
|
1474
|
+
return "svd 0 is how a mode is cleared, not a mode to set"
|
|
1475
|
+
if (lookup_svd(self.svd) is None):
|
|
1476
|
+
return "the svd names no known video descriptor"
|
|
1477
|
+
if (mxr_sig_bpp_index(self.depth) is None):
|
|
1478
|
+
return "a V2IP output stage takes 8, 10 or 12 bits per pixel"
|
|
1479
|
+
if (self.colour not in (VideoColourSpace.RGB, VideoColourSpace.YUV444,
|
|
1480
|
+
VideoColourSpace.YUV422, VideoColourSpace.YUV420)):
|
|
1481
|
+
return "the colour space names none of RGB, 4:4:4, 4:2:2 or 4:2:0"
|
|
1482
|
+
if not (V2IP_SCALING_REFRESH_MIN <= self.refresh <= V2IP_SCALING_REFRESH_MAX):
|
|
1483
|
+
return f"the refresh rate is outside {V2IP_SCALING_REFRESH_MIN}..{V2IP_SCALING_REFRESH_MAX}Hz"
|
|
1484
|
+
return None
|
|
1485
|
+
|
|
1486
|
+
@property
|
|
1487
|
+
def signal_type(self) -> MxrSignalType:
|
|
1488
|
+
"""
|
|
1489
|
+
The packed signal type a scaling write carries for this mode.
|
|
1490
|
+
|
|
1491
|
+
Call validate() first: an unvalidated depth packs as the index for
|
|
1492
|
+
"no depth", which a receiver drops.
|
|
1493
|
+
"""
|
|
1494
|
+
return MxrSignalType.from_parts(svd=self.svd, colour=int(self.colour),
|
|
1495
|
+
bpp_index=(mxr_sig_bpp_index(self.depth) or 0))
|
|
1496
|
+
|
|
1497
|
+
def __eq__(self, other:Any) -> bool:
|
|
1498
|
+
if not isinstance(other, V2IPOutputMode):
|
|
1499
|
+
return NotImplemented
|
|
1500
|
+
return (self.svd == other.svd) and (self.depth == other.depth) \
|
|
1501
|
+
and (self.colour == other.colour) and (self.refresh == other.refresh)
|
|
1502
|
+
|
|
1503
|
+
def __str__(self) -> str:
|
|
1504
|
+
return f"svd {self.svd}, colour {self.colour}, {self.depth}bpp, {self.refresh}Hz"
|
|
1505
|
+
|
|
1506
|
+
def __repr__(self) -> str:
|
|
1507
|
+
return str(self)
|
|
1508
|
+
|
|
1421
1509
|
class V2IPScalingSettings(DeviceV2IPScalingSettings):
|
|
1422
1510
|
"""Plain holder for a merged mxr_scaling_config."""
|
|
1423
1511
|
def __init__(self, mode:int, refresh:int, flags:int) -> None:
|
|
@@ -1627,6 +1715,28 @@ class DeviceV2IPSink:
|
|
|
1627
1715
|
when present, each stream may still carry an all-zero ip when that stream is
|
|
1628
1716
|
inactive. ``audio_fmt`` carries the resolved sink audio format (zero fields
|
|
1629
1717
|
fall back to firmware defaults of 48kHz / 2 channels).
|
|
1718
|
+
|
|
1719
|
+
**An address that reads as unset means "no route, or the sink could not work
|
|
1720
|
+
one out", never "definitely not subscribed".** This block is the one part of
|
|
1721
|
+
a device configuration with no validity marker of its own, so a sender with
|
|
1722
|
+
nothing to say sends zeros and every receiver stores them. A sender leaves it
|
|
1723
|
+
empty whenever its own stream configuration does not resolve, which covers
|
|
1724
|
+
more than having no route: a selected source whose record has not arrived
|
|
1725
|
+
yet, the state after a restart at either end, missing audio bay
|
|
1726
|
+
configuration, or a stream failing its validity check. ``audio_fmt`` has a
|
|
1727
|
+
second gate of its own, so it can be absent while the addresses are not.
|
|
1728
|
+
|
|
1729
|
+
Expect that reading rather than guarding against it. Any scaling change makes
|
|
1730
|
+
the device rebuild and rebroadcast this block, and a write aimed at a remote
|
|
1731
|
+
bay sends it zeroed however it was requested, so an empty reading turns up
|
|
1732
|
+
most often during exactly the no-signal troubleshooting that prompted the
|
|
1733
|
+
change. A device's periodic report puts a real route back within a minute of
|
|
1734
|
+
it having one, so a reader that needs certainty should wait one out rather
|
|
1735
|
+
than treat the first empty reading as an answer.
|
|
1736
|
+
|
|
1737
|
+
An empty reading is cached rather than dropped on purpose. A sink that has
|
|
1738
|
+
genuinely lost its route sends the same zeros, and so does every report after
|
|
1739
|
+
it, so refusing them would hold a route that nothing later could clear.
|
|
1630
1740
|
"""
|
|
1631
1741
|
def __init__(self, addresses:V2IPStreamSources|None, audio_fmt:V2IPAudioFormat|None) -> None:
|
|
1632
1742
|
self._addresses = addresses
|
|
@@ -1956,6 +2066,17 @@ class DeviceBase(ABC):
|
|
|
1956
2066
|
def mesh_master(self, master:MxrDeviceUid) -> None:
|
|
1957
2067
|
'''Change the master device of this device'''
|
|
1958
2068
|
|
|
2069
|
+
@property
|
|
2070
|
+
@abstractmethod
|
|
2071
|
+
def mesh_master_uid(self) -> MxrDeviceUid|None:
|
|
2072
|
+
'''UID of the mesh controller this device reports, None while it has named none.
|
|
2073
|
+
|
|
2074
|
+
Distinct from mesh_master, which falls back to the device itself. A
|
|
2075
|
+
device that has named no controller is not a device that follows nobody:
|
|
2076
|
+
a client that has just started knows nothing about anyone for as long as
|
|
2077
|
+
a broadcast period.
|
|
2078
|
+
'''
|
|
2079
|
+
|
|
1959
2080
|
@property
|
|
1960
2081
|
@abstractmethod
|
|
1961
2082
|
def protocol(self) -> int:
|
|
@@ -1978,6 +2099,31 @@ class DeviceBase(ABC):
|
|
|
1978
2099
|
def is_mesh_master(self) -> bool:
|
|
1979
2100
|
'''True if this device is the master device of a V2IP mesh'''
|
|
1980
2101
|
|
|
2102
|
+
@property
|
|
2103
|
+
@abstractmethod
|
|
2104
|
+
def is_management(self) -> bool:
|
|
2105
|
+
'''True when a receiver would let this device write another device's configuration.
|
|
2106
|
+
|
|
2107
|
+
Two bits, because the two kinds of writer announce themselves
|
|
2108
|
+
differently and neither implies the other. MANAGER belongs to external
|
|
2109
|
+
management applications - anything driving these devices that is not one
|
|
2110
|
+
of them, this library included - and no device firmware ever sets it on
|
|
2111
|
+
itself, so testing it alone would refuse every write a controller makes.
|
|
2112
|
+
A device that is controlling its mesh announces MESH_MASTER instead.
|
|
2113
|
+
|
|
2114
|
+
The bits have a hole a caller has to close elsewhere: a device sets
|
|
2115
|
+
MESH_MASTER only while it is both the controller and has bays mapped, so
|
|
2116
|
+
one promoted before it has any carries neither bit while still being its
|
|
2117
|
+
mesh's controller. What covers that window is the controller uid the
|
|
2118
|
+
devices in that mesh report, which is their question rather than this
|
|
2119
|
+
device's - see mesh_master_uid.
|
|
2120
|
+
'''
|
|
2121
|
+
|
|
2122
|
+
@property
|
|
2123
|
+
@abstractmethod
|
|
2124
|
+
def is_v2ip_sink(self) -> bool:
|
|
2125
|
+
'''True if this device decodes a V2IP stream, which is what carries the scaling settings'''
|
|
2126
|
+
|
|
1981
2127
|
@property
|
|
1982
2128
|
@abstractmethod
|
|
1983
2129
|
def is_mesh_member(self) -> bool:
|
|
@@ -2106,6 +2252,18 @@ class DeviceBase(ABC):
|
|
|
2106
2252
|
async def read_stats(self, enable:bool) -> bool:
|
|
2107
2253
|
'''start or stop dumping stats'''
|
|
2108
2254
|
|
|
2255
|
+
@abstractmethod
|
|
2256
|
+
async def set_v2ip_auto_scaling(self, enabled:bool) -> bool:
|
|
2257
|
+
'''turn this sink's automatic scaling on or off'''
|
|
2258
|
+
|
|
2259
|
+
@abstractmethod
|
|
2260
|
+
async def set_v2ip_output_mode(self, mode:'V2IPOutputMode') -> bool:
|
|
2261
|
+
'''set the output format this sink scales to'''
|
|
2262
|
+
|
|
2263
|
+
@abstractmethod
|
|
2264
|
+
async def clear_v2ip_output_mode(self) -> bool:
|
|
2265
|
+
'''clear the output format this sink is configured to scale to'''
|
|
2266
|
+
|
|
2109
2267
|
@abstractmethod
|
|
2110
2268
|
async def get_log(self) -> str|None:
|
|
2111
2269
|
'''read the log from the device and return it as string'''
|
|
@@ -4,7 +4,7 @@ from .Uid import MxrBayUid as MxrBayUid, MxrDeviceUid as MxrDeviceUid
|
|
|
4
4
|
from .proto import RCKey as RCKey
|
|
5
5
|
from .proto.Data import VolumeMuteStatus as VolumeMuteStatus
|
|
6
6
|
from .proto.Multiviewer import MultiviewerAspectRatio as MultiviewerAspectRatio, MultiviewerBoolSetting as MultiviewerBoolSetting, MultiviewerEDIDTemplate as MultiviewerEDIDTemplate, MultiviewerHDCPMode as MultiviewerHDCPMode, MultiviewerITCMode as MultiviewerITCMode, MultiviewerOutputMode as MultiviewerOutputMode, MultiviewerPipPosition as MultiviewerPipPosition, MultiviewerPipSize as MultiviewerPipSize, MultiviewerSource as MultiviewerSource, MultiviewerViewMode as MultiviewerViewMode
|
|
7
|
-
from .proto.Svd import SvdMap as SvdMap
|
|
7
|
+
from .proto.Svd import SvdMap as SvdMap, lookup_svd as lookup_svd
|
|
8
8
|
from .proto.V2IPStats import V2IPDeviceStats as V2IPDeviceStats
|
|
9
9
|
from _typeshed import Incomplete
|
|
10
10
|
from abc import ABC, abstractmethod
|
|
@@ -582,11 +582,26 @@ class DeviceV2IPScalingSettings(ABC):
|
|
|
582
582
|
@property
|
|
583
583
|
@abstractmethod
|
|
584
584
|
def flags(self) -> int: ...
|
|
585
|
+
@property
|
|
586
|
+
def configured_mode(self) -> tuple[MxrSignalType, int] | None: ...
|
|
587
|
+
@property
|
|
588
|
+
def auto_scaling(self) -> bool | None: ...
|
|
585
589
|
|
|
586
590
|
def v2ip_stream_valid(stream: V2IPStreamSource | None) -> bool: ...
|
|
587
591
|
def v2ip_av_source_valid(video: V2IPStreamSource | None, anc: V2IPStreamSource | None) -> bool: ...
|
|
588
592
|
def v2ip_stream_cleared(stream: V2IPStreamSource | None) -> bool: ...
|
|
589
593
|
|
|
594
|
+
class V2IPOutputMode:
|
|
595
|
+
svd: Incomplete
|
|
596
|
+
depth: Incomplete
|
|
597
|
+
colour: Incomplete
|
|
598
|
+
refresh: Incomplete
|
|
599
|
+
def __init__(self, svd: int, depth: int, colour: VideoColourSpace, refresh: int) -> None: ...
|
|
600
|
+
def validate(self) -> str | None: ...
|
|
601
|
+
@property
|
|
602
|
+
def signal_type(self) -> MxrSignalType: ...
|
|
603
|
+
def __eq__(self, other: Any) -> bool: ...
|
|
604
|
+
|
|
590
605
|
class V2IPScalingSettings(DeviceV2IPScalingSettings):
|
|
591
606
|
def __init__(self, mode: int, refresh: int, flags: int) -> None: ...
|
|
592
607
|
@property
|
|
@@ -825,6 +840,9 @@ class DeviceBase(ABC):
|
|
|
825
840
|
def mesh_master(self, master: MxrDeviceUid) -> None: ...
|
|
826
841
|
@property
|
|
827
842
|
@abstractmethod
|
|
843
|
+
def mesh_master_uid(self) -> MxrDeviceUid | None: ...
|
|
844
|
+
@property
|
|
845
|
+
@abstractmethod
|
|
828
846
|
def protocol(self) -> int: ...
|
|
829
847
|
@abstractmethod
|
|
830
848
|
def supports_opcode(self, opcode: int) -> bool: ...
|
|
@@ -833,6 +851,12 @@ class DeviceBase(ABC):
|
|
|
833
851
|
def is_mesh_master(self) -> bool: ...
|
|
834
852
|
@property
|
|
835
853
|
@abstractmethod
|
|
854
|
+
def is_management(self) -> bool: ...
|
|
855
|
+
@property
|
|
856
|
+
@abstractmethod
|
|
857
|
+
def is_v2ip_sink(self) -> bool: ...
|
|
858
|
+
@property
|
|
859
|
+
@abstractmethod
|
|
836
860
|
def is_mesh_member(self) -> bool: ...
|
|
837
861
|
@property
|
|
838
862
|
@abstractmethod
|
|
@@ -899,6 +923,12 @@ class DeviceBase(ABC):
|
|
|
899
923
|
@abstractmethod
|
|
900
924
|
async def read_stats(self, enable: bool) -> bool: ...
|
|
901
925
|
@abstractmethod
|
|
926
|
+
async def set_v2ip_auto_scaling(self, enabled: bool) -> bool: ...
|
|
927
|
+
@abstractmethod
|
|
928
|
+
async def set_v2ip_output_mode(self, mode: V2IPOutputMode) -> bool: ...
|
|
929
|
+
@abstractmethod
|
|
930
|
+
async def clear_v2ip_output_mode(self) -> bool: ...
|
|
931
|
+
@abstractmethod
|
|
902
932
|
async def get_log(self) -> str | None: ...
|
|
903
933
|
@abstractmethod
|
|
904
934
|
def on_mxr_update(self, data: object) -> None: ...
|
|
@@ -146,6 +146,12 @@ MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
|
|
|
146
146
|
"""mxr_scaling_config.flags. The mode/refresh pair and the options nibble are
|
|
147
147
|
separately valid: a sender carrying neither flag is offering no scaling at all."""
|
|
148
148
|
|
|
149
|
+
V2IP_SCALING_REFRESH_MIN = 24
|
|
150
|
+
V2IP_SCALING_REFRESH_MAX = 120
|
|
151
|
+
"""Refresh rate range a V2IP output stage accepts, in Hz. A receiver replaces
|
|
152
|
+
anything outside it with 50 rather than refusing the write, so a 0 here asks
|
|
153
|
+
for 50Hz instead of asking for nothing."""
|
|
154
|
+
|
|
149
155
|
MXR_V2IP_DSCP_SET = 0x80
|
|
150
156
|
"""DSCP 0 (CS0) is a legal marking, so a zero byte cannot mean "absent": each
|
|
151
157
|
dscp byte in a V2IP_DEVICE_CFG options word carries this bit alongside its
|
|
@@ -379,6 +385,23 @@ def mxr_sig_bpp_get(bpp: int) -> int:
|
|
|
379
385
|
bit depth is a common trap. Returns 0 for both unknown and unset.'''
|
|
380
386
|
return _MXR_SIG_BPP_VALUES.get(bpp, MXR_SIG_BPP_UNKNOWN)
|
|
381
387
|
|
|
388
|
+
_MXR_SIG_BPP_WRITABLE_INDICES: dict[int, int] = {
|
|
389
|
+
8: 1,
|
|
390
|
+
10: 2,
|
|
391
|
+
12: 3,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
def mxr_sig_bpp_index(depth: int) -> int|None:
|
|
395
|
+
'''Map a bit depth to the mxr_signal_type bpp *index* that stands for it.
|
|
396
|
+
|
|
397
|
+
Only the three depths a V2IP output stage accepts are here. Index 4 names
|
|
398
|
+
16bpp, which mxr_sig_bpp_get() reads back from a device, but the output
|
|
399
|
+
stage refuses it - offering it as something to write would build a frame
|
|
400
|
+
that is decoded cleanly and then dropped in silence.
|
|
401
|
+
|
|
402
|
+
Returns None for a depth no index names.'''
|
|
403
|
+
return _MXR_SIG_BPP_WRITABLE_INDICES.get(depth)
|
|
404
|
+
|
|
382
405
|
class MxrSignalType:
|
|
383
406
|
'''The 2-byte mxr_signal_type carried in scaling configs and bay signal reports.
|
|
384
407
|
|
|
@@ -391,6 +414,26 @@ class MxrSignalType:
|
|
|
391
414
|
self._svd = int(data[0])
|
|
392
415
|
self._flags = int(data[1])
|
|
393
416
|
|
|
417
|
+
@staticmethod
|
|
418
|
+
def from_parts(svd: int, colour: int, bpp_index: int) -> 'MxrSignalType':
|
|
419
|
+
'''Build the word a scaling write carries, from the fields it consumes.
|
|
420
|
+
|
|
421
|
+
non_int is left clear: the receiving struct carries the bit and the
|
|
422
|
+
apply path does not read it.
|
|
423
|
+
|
|
424
|
+
Building rather than editing is the point. A sink with no mode
|
|
425
|
+
configured reports the word with the unset bpp index in it, so a caller
|
|
426
|
+
that read that word back and filled in an svd would send an index no
|
|
427
|
+
depth uses, which the receiver decodes to zero and rejects without
|
|
428
|
+
answering.'''
|
|
429
|
+
flags = (colour & 0xF) | ((bpp_index & 0x7) << 5)
|
|
430
|
+
return MxrSignalType(bytes([svd & 0xFF, flags]))
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def byte_value(self) -> bytes:
|
|
434
|
+
'''The two bytes as they sit on the wire.'''
|
|
435
|
+
return bytes([self._svd, self._flags])
|
|
436
|
+
|
|
394
437
|
@property
|
|
395
438
|
def value(self) -> int:
|
|
396
439
|
'''Raw 16-bit value.'''
|
|
@@ -18,6 +18,8 @@ def v2ip_rate_valid(rate: int | None) -> bool: ...
|
|
|
18
18
|
MXR_SCALING_FLAG_MODE_VALID: Incomplete
|
|
19
19
|
MXR_SCALING_FLAG_OPTIONS_VALID: Incomplete
|
|
20
20
|
MXR_SCALING_FLAG_AUTO_SCALING: Incomplete
|
|
21
|
+
V2IP_SCALING_REFRESH_MIN: int
|
|
22
|
+
V2IP_SCALING_REFRESH_MAX: int
|
|
21
23
|
MXR_V2IP_DSCP_SET: int
|
|
22
24
|
V2IP_DSCP_MAX: int
|
|
23
25
|
V2IP_DSCP_DEFAULT: int
|
|
@@ -108,9 +110,14 @@ MXR_SIG_BPP_UNKNOWN: int
|
|
|
108
110
|
MXR_SIG_BPP_UNSET: int
|
|
109
111
|
|
|
110
112
|
def mxr_sig_bpp_get(bpp: int) -> int: ...
|
|
113
|
+
def mxr_sig_bpp_index(depth: int) -> int | None: ...
|
|
111
114
|
|
|
112
115
|
class MxrSignalType:
|
|
113
116
|
def __init__(self, data: bytes) -> None: ...
|
|
117
|
+
@staticmethod
|
|
118
|
+
def from_parts(svd: int, colour: int, bpp_index: int) -> MxrSignalType: ...
|
|
119
|
+
@property
|
|
120
|
+
def byte_value(self) -> bytes: ...
|
|
114
121
|
@property
|
|
115
122
|
def value(self) -> int: ...
|
|
116
123
|
@property
|
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
'''Protocol frame for V2IP device configuration (stream addresses, scaling, options).'''
|
|
8
8
|
|
|
9
9
|
from functools import cached_property
|
|
10
|
+
from typing import Any
|
|
10
11
|
from .FrameBase import FrameBase
|
|
11
12
|
from .FrameHeader import FrameHeader
|
|
12
13
|
from ..Uid import MxrDeviceUid
|
|
13
|
-
from ..Interface import
|
|
14
|
+
from ..Interface import (DeviceBase, DeviceRegistry, DeviceV2IPDetails, DeviceV2IPScalingSettings,
|
|
15
|
+
DeviceV2IPSink, V2IPAudioFormat, V2IPDscpConfig, V2IPStreamSource)
|
|
14
16
|
from .Constants import (MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID,
|
|
15
|
-
MXR_SCALING_FLAG_OPTIONS_VALID, v2ip_dscp_value,
|
|
17
|
+
MXR_SCALING_FLAG_OPTIONS_VALID, MxrSignalType, v2ip_dscp_value,
|
|
18
|
+
v2ip_rate_valid)
|
|
16
19
|
from .V2IPConfig import V2IPStreamSourceImpl, parse_v2ip_av_source
|
|
17
20
|
|
|
18
21
|
# v2ip_device_config_update wire layout (little-endian, ALIGN(8) per inner struct):
|
|
@@ -120,6 +123,15 @@ class V2IPScalingSettingsImpl(DeviceV2IPScalingSettings):
|
|
|
120
123
|
def flags(self) -> int:
|
|
121
124
|
return self._flags
|
|
122
125
|
|
|
126
|
+
_OPCODE = 0x3C
|
|
127
|
+
|
|
128
|
+
_RATE_UNSET = 0xFF
|
|
129
|
+
'''The tx_rate a frame that is not setting a rate carries.
|
|
130
|
+
|
|
131
|
+
The field's valid range ends at V2IP_SOURCE_RATE_MAX, and a receiver drops an
|
|
132
|
+
out-of-range rate and keeps the one it had. A plain zero would ask for a rate of
|
|
133
|
+
zero.'''
|
|
134
|
+
|
|
123
135
|
class FrameV2IPDeviceConfiguration(FrameBase):
|
|
124
136
|
'''V2IP device configuration with stream addresses and scaling settings.'''
|
|
125
137
|
def __init__(self, header:FrameHeader, timestamp:float):
|
|
@@ -133,6 +145,57 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
133
145
|
self.arc = V2IPStreamSourceImpl("arc", self.payload[48:54])
|
|
134
146
|
self.scaling = V2IPScalingSettingsImpl(self.payload[56:61])
|
|
135
147
|
|
|
148
|
+
@staticmethod
|
|
149
|
+
def construct_scaling(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid,
|
|
150
|
+
mode:MxrSignalType, refresh:int, flags:int) -> FrameBase|None:
|
|
151
|
+
'''Build the 0x3C write that moves one sink's scaling block and nothing else.
|
|
152
|
+
|
|
153
|
+
88 bytes, which is both the receiver's minimum and the whole of
|
|
154
|
+
v2ip_device_config_update: uid 0..16, source 16..40, the options word at
|
|
155
|
+
40, audio return 48..56, scaling 56..64, tiling 64..88.
|
|
156
|
+
|
|
157
|
+
**88 rather than the 120-byte form.** The longer form appends a sink
|
|
158
|
+
block, and a receiver copies that block into its record for the target
|
|
159
|
+
with no validity test of its own - unlike the source, rate, marking,
|
|
160
|
+
scaling and tiling fields, which each sit behind one. This frame is a
|
|
161
|
+
broadcast, so every device on the network runs that copy, not just the
|
|
162
|
+
addressee: sending the long form with the block zeroed would replace the
|
|
163
|
+
whole network's idea of where the target's sink is subscribed, as a side
|
|
164
|
+
effect of setting one scaling flag. At 88 the block is absent rather
|
|
165
|
+
than zeroed and nothing reads it.
|
|
166
|
+
|
|
167
|
+
**The source block at 16..40 must stay zeroed**, and does. A receiver
|
|
168
|
+
hands this frame's addresses to its encoder unconditionally, on every
|
|
169
|
+
frame it applies rather than only on the ones that carry addresses; what
|
|
170
|
+
stops a scaling write from repointing the encoder is that the call
|
|
171
|
+
refuses a video address which is not multicast. Zero is not multicast.
|
|
172
|
+
Anything that is, written here, would move a transceiver's stream.
|
|
173
|
+
|
|
174
|
+
Which halves of the scaling block a receiver reads is chosen by the
|
|
175
|
+
validity bits in flags, not by this layout: the mode and refresh are
|
|
176
|
+
read behind MXR_SCALING_FLAG_MODE_VALID and the options behind
|
|
177
|
+
MXR_SCALING_FLAG_OPTIONS_VALID, so a write that carries neither bit
|
|
178
|
+
lands as a no-op rather than as a request to zero the settings.
|
|
179
|
+
'''
|
|
180
|
+
payload = bytearray(target_uid.byte_value)
|
|
181
|
+
if (len(payload) != 16):
|
|
182
|
+
raise ValueError(f"invalid uid length: {len(payload)}")
|
|
183
|
+
# source: three stream slots, left zeroed so the encoder keeps its own.
|
|
184
|
+
payload += bytes(40 - len(payload))
|
|
185
|
+
payload.append(_RATE_UNSET)
|
|
186
|
+
# Three dscp bytes with no MXR_V2IP_DSCP_SET bit, so no marking is
|
|
187
|
+
# applied, then the padding that aligns the audio-return slot, then the
|
|
188
|
+
# audio return itself, zeroed, which reads as carrying no address.
|
|
189
|
+
payload += bytes(56 - len(payload))
|
|
190
|
+
payload += mode.byte_value
|
|
191
|
+
payload += refresh.to_bytes(2, 'little')
|
|
192
|
+
payload.append(flags & 0xFF)
|
|
193
|
+
# The scaling struct is 8-aligned, so its five bytes of fields are
|
|
194
|
+
# followed by three of padding; then the tiling window, whose zero uid
|
|
195
|
+
# is what says no window is carried.
|
|
196
|
+
payload += bytes(_BASE_SIZE - len(payload))
|
|
197
|
+
return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
|
|
198
|
+
|
|
136
199
|
@property
|
|
137
200
|
def target_uid(self) -> MxrDeviceUid|None:
|
|
138
201
|
return self.payload_uuid(idx=0)
|
|
@@ -141,6 +204,43 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
141
204
|
def target_self(self) -> bool:
|
|
142
205
|
return (self.remote_id == self.target_uid)
|
|
143
206
|
|
|
207
|
+
@cached_property
|
|
208
|
+
def subject_device(self) -> DeviceBase|None:
|
|
209
|
+
'''The device this configuration is about, None when nothing would act on it.
|
|
210
|
+
|
|
211
|
+
The payload names its subject in the first sixteen bytes, and that is
|
|
212
|
+
what decides whose configuration this is - not who sent it. Equal to the
|
|
213
|
+
sender it is a device describing itself, which is what almost every one
|
|
214
|
+
of these frames is. Different, it is a write for the device it names,
|
|
215
|
+
and a receiver takes one only from a sender it treats as management.
|
|
216
|
+
Anything else is dropped rather than filed against the sender, because a
|
|
217
|
+
frame the network ignores must not move a record here.
|
|
218
|
+
'''
|
|
219
|
+
if ((subject := self.target_uid) is None):
|
|
220
|
+
return None
|
|
221
|
+
if (subject == self.remote_id):
|
|
222
|
+
return self.remote_device
|
|
223
|
+
# An unknown subject is dropped, as it is on a device: there is no
|
|
224
|
+
# record to move, and inventing one from a third party's description
|
|
225
|
+
# would create a device nothing has been heard from.
|
|
226
|
+
if ((device := self.mxr.get_by_uid(subject)) is None):
|
|
227
|
+
return None
|
|
228
|
+
sender = self.remote_device
|
|
229
|
+
if ((sender is not None) and sender.is_management):
|
|
230
|
+
return device
|
|
231
|
+
# Or the subject naming this sender as its mesh controller. Asked of the
|
|
232
|
+
# subject rather than the sender because it closes the window where a
|
|
233
|
+
# controller has been promoted but has no bays mapped yet, and so
|
|
234
|
+
# announces neither bit while still being the controller its mesh obeys.
|
|
235
|
+
#
|
|
236
|
+
# A subject that has named no controller contributes nothing here rather
|
|
237
|
+
# than refusing: not knowing who a device follows is not knowing that it
|
|
238
|
+
# follows nobody, and a client that has just started knows nothing about
|
|
239
|
+
# anyone for as long as a broadcast period.
|
|
240
|
+
if (device.mesh_master_uid == self.remote_id):
|
|
241
|
+
return device
|
|
242
|
+
return None
|
|
243
|
+
|
|
144
244
|
@cached_property
|
|
145
245
|
def details(self) -> DeviceV2IPDetails:
|
|
146
246
|
return DeviceV2IPDetails(video=self.video, audio=self.audio, anc=self.anc, arc=self.arc, tx_rate=self.options.tx_rate, scaling=self.scaling, dscp=self.options.dscp)
|
|
@@ -156,8 +256,8 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
156
256
|
)
|
|
157
257
|
|
|
158
258
|
def process(self) -> None:
|
|
159
|
-
'''Update the
|
|
160
|
-
if ((dev := self.
|
|
259
|
+
'''Update the cache of the device this configuration names, not of its sender.'''
|
|
260
|
+
if ((dev := self.subject_device) is None):
|
|
161
261
|
return
|
|
162
262
|
dev.on_mxr_update(self.details)
|
|
163
263
|
if ((sink := self.sink) is not None):
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from ..Interface import DeviceV2IPDetails as DeviceV2IPDetails, DeviceV2IPScalingSettings as DeviceV2IPScalingSettings, DeviceV2IPSink as DeviceV2IPSink, V2IPAudioFormat as V2IPAudioFormat, V2IPDscpConfig as V2IPDscpConfig, V2IPStreamSource as V2IPStreamSource
|
|
1
|
+
from ..Interface import DeviceBase as DeviceBase, DeviceRegistry as DeviceRegistry, DeviceV2IPDetails as DeviceV2IPDetails, DeviceV2IPScalingSettings as DeviceV2IPScalingSettings, DeviceV2IPSink as DeviceV2IPSink, V2IPAudioFormat as V2IPAudioFormat, V2IPDscpConfig as V2IPDscpConfig, V2IPStreamSource as V2IPStreamSource
|
|
2
2
|
from ..Uid import MxrDeviceUid as MxrDeviceUid
|
|
3
|
-
from .Constants import MXR_SCALING_FLAG_AUTO_SCALING as MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID as MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS_VALID as MXR_SCALING_FLAG_OPTIONS_VALID, v2ip_dscp_value as v2ip_dscp_value, v2ip_rate_valid as v2ip_rate_valid
|
|
3
|
+
from .Constants import MXR_SCALING_FLAG_AUTO_SCALING as MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID as MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS_VALID as MXR_SCALING_FLAG_OPTIONS_VALID, MxrSignalType as MxrSignalType, v2ip_dscp_value as v2ip_dscp_value, v2ip_rate_valid as v2ip_rate_valid
|
|
4
4
|
from .FrameBase import FrameBase as FrameBase
|
|
5
5
|
from .FrameHeader import FrameHeader as FrameHeader
|
|
6
6
|
from .V2IPConfig import V2IPStreamSourceImpl as V2IPStreamSourceImpl, parse_v2ip_av_source as parse_v2ip_av_source
|
|
7
7
|
from _typeshed import Incomplete
|
|
8
8
|
from functools import cached_property
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
class V2IPDeviceOptions:
|
|
11
12
|
def __init__(self, data: bytes) -> None: ...
|
|
@@ -33,11 +34,15 @@ class FrameV2IPDeviceConfiguration(FrameBase):
|
|
|
33
34
|
arc: Incomplete
|
|
34
35
|
scaling: Incomplete
|
|
35
36
|
def __init__(self, header: FrameHeader, timestamp: float) -> None: ...
|
|
37
|
+
@staticmethod
|
|
38
|
+
def construct_scaling(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid, mode: MxrSignalType, refresh: int, flags: int) -> FrameBase | None: ...
|
|
36
39
|
@property
|
|
37
40
|
def target_uid(self) -> MxrDeviceUid | None: ...
|
|
38
41
|
@property
|
|
39
42
|
def target_self(self) -> bool: ...
|
|
40
43
|
@cached_property
|
|
44
|
+
def subject_device(self) -> DeviceBase | None: ...
|
|
45
|
+
@cached_property
|
|
41
46
|
def details(self) -> DeviceV2IPDetails: ...
|
|
42
47
|
@cached_property
|
|
43
48
|
def sink(self) -> DeviceV2IPSink | None: ...
|
|
@@ -72,6 +72,10 @@ def _load_svd_csv() -> dict[int, Svd]:
|
|
|
72
72
|
|
|
73
73
|
_SVD_DATA: dict[int, Svd] = _load_svd_csv()
|
|
74
74
|
|
|
75
|
+
def lookup_svd(id:int) -> Svd|None:
|
|
76
|
+
'''The video descriptor an svd number names, or None when none does.'''
|
|
77
|
+
return _SVD_DATA.get(id)
|
|
78
|
+
|
|
75
79
|
class SvdMap:
|
|
76
80
|
'''Lookup map of SVD entries keyed by ID.'''
|
|
77
81
|
|