mx-remote 5.5.1__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.5.1 → mx_remote-5.7.0}/.gitignore +1 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/PKG-INFO +1 -1
- mx_remote-5.7.0/docs/oneip.md +177 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Interface.py +159 -1
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Interface.pyi +31 -1
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/const.py +1 -1
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Constants.py +50 -2
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Constants.pyi +7 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFactoryReset.py +21 -6
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHello.py +14 -1
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPAudio.py +12 -6
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +104 -4
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +7 -2
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPStats.py +51 -3
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPStats.pyi +5 -1
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Svd.py +4 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Svd.pyi +2 -0
- mx_remote-5.7.0/mx_remote/proto/V2IPStats.py +521 -0
- mx_remote-5.7.0/mx_remote/proto/V2IPStats.pyi +116 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Device.py +146 -1
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Device.pyi +15 -2
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/audio_ir.py +16 -4
- mx_remote-5.7.0/tests/lengths.py +107 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/run.py +3 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/smoke.py +1 -1
- mx_remote-5.7.0/tests/stats.py +432 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/unknowns.py +25 -0
- mx_remote-5.7.0/tests/v2ipscaling.py +161 -0
- mx_remote-5.7.0/tests/v2ipsubject.py +124 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/wirefix.py +28 -0
- mx_remote-5.5.1/docs/oneip.md +0 -43
- mx_remote-5.5.1/mx_remote/proto/V2IPStats.py +0 -190
- mx_remote-5.5.1/mx_remote/proto/V2IPStats.pyi +0 -54
- mx_remote-5.5.1/tests/stats.py +0 -73
- {mx_remote-5.5.1 → mx_remote-5.7.0}/AGENTS.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/CLAUDE.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/LICENSE +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/README.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/README.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/brand.css +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/favicon.svg +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/logo-dark.svg +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/logo.svg +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/api.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/audio.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/callbacks.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/conf.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/configuration.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/devices-and-bays.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/diagnostics.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/index.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/multiviewer.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/requirements.txt +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/routing.md +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/hatch_build.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Uid.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/__init__.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/__init__.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/compat.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/compat.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/const.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/main.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/main.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Factory.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfig.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameLinks.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCIr.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/V2IPConfig.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/py.typed +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Bay.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Bay.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Remote.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Remote.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/pyproject.toml +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/capture.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/e2e.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/entry.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/handlers.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/handlers_cover.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/hello.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/idempotent.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/merge.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/newops.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/offsets.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/proto_stamp.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/protogate.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/rcsettings.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/roundtrip.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/sources.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/structs.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/txresult.py +0 -0
- {mx_remote-5.5.1 → mx_remote-5.7.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.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/
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# OneIP and V2IP
|
|
2
|
+
|
|
3
|
+
Streaming endpoints, stream sources and statistics.
|
|
4
|
+
|
|
5
|
+
## Pulse-Eight OneIP Devices
|
|
6
|
+
|
|
7
|
+
[Pulse-Eight OneIP](https://www.pulse-eight.com/p/248/oneip-tx) HDMI-over-IP devices expose additional streaming properties:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# stream source addresses
|
|
11
|
+
if device.is_v2ip and device.v2ip_sources:
|
|
12
|
+
for source in device.v2ip_sources:
|
|
13
|
+
print(f"Video: {source.video.ip}:{source.video.port}")
|
|
14
|
+
print(f"Audio: {source.audio.ip}:{source.audio.port}")
|
|
15
|
+
|
|
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.
|
|
20
|
+
if device.v2ip_details:
|
|
21
|
+
details = device.v2ip_details
|
|
22
|
+
print(f"Video: {details.video}")
|
|
23
|
+
print(f"TX rate: {details.tx_rate}") # None when the sender offered no rate
|
|
24
|
+
|
|
25
|
+
# per-stream DSCP marking (None from peers that predate it)
|
|
26
|
+
if details.dscp and details.dscp.complete:
|
|
27
|
+
print(f"DSCP: {details.dscp}") # video / audio / anc, 0..63
|
|
28
|
+
|
|
29
|
+
# streaming statistics
|
|
30
|
+
# The subscription lapses 60s after the request and there is no free-running
|
|
31
|
+
# mode, so call this again inside the minute to keep the 1Hz reports coming.
|
|
32
|
+
await device.read_stats(enable=True) # start collecting
|
|
33
|
+
# ... later ...
|
|
34
|
+
stats = device.v2ip_stats
|
|
35
|
+
|
|
36
|
+
# what the sink's decoder recovered from the codestream it is being given.
|
|
37
|
+
# Three answers, and they mean different things:
|
|
38
|
+
if stats.decoder is None:
|
|
39
|
+
pass # sender predates MatrixOS 10.12.46
|
|
40
|
+
elif stats.decoder.reading is None:
|
|
41
|
+
pass # decoder has never answered
|
|
42
|
+
else:
|
|
43
|
+
reading = stats.decoder.reading
|
|
44
|
+
# Geometry is what says a picture was recovered. `format` never does: with
|
|
45
|
+
# no stream it reads RGB, which is indistinguishable from a real RGB source.
|
|
46
|
+
if reading.recovered:
|
|
47
|
+
print(f"{reading.width}x{reading.height} {reading.format}")
|
|
48
|
+
# `reason` for display, `causes` for logic. Every true cause sets its bit
|
|
49
|
+
# in flags; which one keeps `reason` is a fixed priority in the video
|
|
50
|
+
# processor, not the numbering. TX_BRIDGE_UNLOCKED ranks below every
|
|
51
|
+
# input-side cause, so a repeating pipeline restart shows as bit 9 in
|
|
52
|
+
# `causes` while `reason` names the input cause instead.
|
|
53
|
+
print(f"{reading.reason}") # None for a cause this build cannot name
|
|
54
|
+
print(f"{reading.causes}") # raw reason values, lowest first
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Trusting a decoder reading after a switch
|
|
58
|
+
|
|
59
|
+
The values are read off the video processor every 2s and reported every 1s,
|
|
60
|
+
latched between reads, so roughly every other report repeats a reading already
|
|
61
|
+
seen — a frame arriving says nothing about freshness. `reading.updates` counts
|
|
62
|
+
readings actually **stored**, so it stays still while a processor is stalled
|
|
63
|
+
rather than implying a refresh. It is monotonic, never reset, and wraps at
|
|
64
|
+
65535 (~36h).
|
|
65
|
+
|
|
66
|
+
After changing what a sink is pointed at, wait for `updates` to advance by
|
|
67
|
+
**two** before trusting the geometry. It ticks when the reply lands, not when
|
|
68
|
+
the query is sent, so a single tick can carry an answer read fractionally
|
|
69
|
+
before the switch.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
before = device.v2ip_stats.decoder.reading.updates
|
|
73
|
+
await sink.switch_source(...)
|
|
74
|
+
# ... wait until reading.updates - before >= 2 (mod 65536) ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Colour depth is deliberately absent: the video processor answers depth from a
|
|
78
|
+
driver constant rather than from the codestream, so it was withheld rather than
|
|
79
|
+
shipped as a constant that looks like a measurement. Assert depth at the
|
|
80
|
+
encoder's input bay instead.
|
|
81
|
+
|
|
82
|
+
### What this block cannot tell you
|
|
83
|
+
|
|
84
|
+
**Whether a sink is off on purpose.** A sink someone deliberately disabled
|
|
85
|
+
reports `NO_PACKETS`, indefinitely — the same cause as a sink that should be
|
|
86
|
+
receiving and is not. There is no enablement field here, so read that from
|
|
87
|
+
`MXR_OP_V2IP_DEVICE_CFG` or the device's HTTP status. `IDLE` sounds like the
|
|
88
|
+
answer and is not: it is effectively unreachable in shipping firmware.
|
|
89
|
+
|
|
90
|
+
**Whether a single sample is representative.** `reason` during a teardown is a
|
|
91
|
+
sequence, not a state — a measured one passed through `SWITCH_PENDING` before
|
|
92
|
+
settling on `NO_PACKETS` six seconds later. Use `updates` to tell a fresh
|
|
93
|
+
reading from a repeat, and note it cannot see a `TX_BRIDGE_UNLOCKED` flag
|
|
94
|
+
carried forward across a format change: a value held over is a stored reading
|
|
95
|
+
like any other.
|
|
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
|
+
|
|
162
|
+
## Mesh and firmware
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# mesh operations
|
|
166
|
+
await device.mesh_promote() # promote to mesh master
|
|
167
|
+
await device.mesh_remove() # remove from mesh
|
|
168
|
+
|
|
169
|
+
# firmware versions
|
|
170
|
+
if device.v2ip_firmware_versions:
|
|
171
|
+
for fw_type, fw in device.v2ip_firmware_versions.items():
|
|
172
|
+
print(f"{fw_type}: {fw.version}")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
[Documentation index](README.md) | [Project README](https://github.com/opdenkamp/mx-remote#readme)
|
|
@@ -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: ...
|
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
from enum import IntEnum, IntFlag
|
|
11
11
|
from typing import TypeVar
|
|
12
12
|
|
|
13
|
-
MXR_PROTOCOL_VERSION =
|
|
13
|
+
MXR_PROTOCOL_VERSION = 0x29
|
|
14
14
|
"""Highest mx_remote protocol version this library understands.
|
|
15
15
|
|
|
16
16
|
Mirrors the firmware's own protocol version. Bump
|
|
17
17
|
when adding RX support for an opcode revision; transmitters stamp the
|
|
18
18
|
per-opcode minimum from MXR_OPCODE_VERSIONS so older peers still parse the
|
|
19
|
-
frame.
|
|
19
|
+
frame.
|
|
20
|
+
|
|
21
|
+
This is also a receive ceiling: Remote.process_frame drops any frame stamped
|
|
22
|
+
above it, unparsed. So it has to reach the stamp of every layout revision
|
|
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."""
|
|
20
25
|
|
|
21
26
|
MXR_OPCODE_VERSIONS: dict[int, int] = {
|
|
22
27
|
0x00: 0x01, # SYS_HELLO
|
|
@@ -141,6 +146,12 @@ MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
|
|
|
141
146
|
"""mxr_scaling_config.flags. The mode/refresh pair and the options nibble are
|
|
142
147
|
separately valid: a sender carrying neither flag is offering no scaling at all."""
|
|
143
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
|
+
|
|
144
155
|
MXR_V2IP_DSCP_SET = 0x80
|
|
145
156
|
"""DSCP 0 (CS0) is a legal marking, so a zero byte cannot mean "absent": each
|
|
146
157
|
dscp byte in a V2IP_DEVICE_CFG options word carries this bit alongside its
|
|
@@ -374,6 +385,23 @@ def mxr_sig_bpp_get(bpp: int) -> int:
|
|
|
374
385
|
bit depth is a common trap. Returns 0 for both unknown and unset.'''
|
|
375
386
|
return _MXR_SIG_BPP_VALUES.get(bpp, MXR_SIG_BPP_UNKNOWN)
|
|
376
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
|
+
|
|
377
405
|
class MxrSignalType:
|
|
378
406
|
'''The 2-byte mxr_signal_type carried in scaling configs and bay signal reports.
|
|
379
407
|
|
|
@@ -386,6 +414,26 @@ class MxrSignalType:
|
|
|
386
414
|
self._svd = int(data[0])
|
|
387
415
|
self._flags = int(data[1])
|
|
388
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
|
+
|
|
389
437
|
@property
|
|
390
438
|
def value(self) -> int:
|
|
391
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,10 +7,15 @@
|
|
|
7
7
|
'''Protocol frame for SYS_FACTORY_RESET (opcode 0x3A).
|
|
8
8
|
|
|
9
9
|
A mesh controller or management app broadcasts this to factory-reset peers.
|
|
10
|
-
Payload
|
|
11
|
-
empty
|
|
12
|
-
1 byte
|
|
13
|
-
16
|
|
10
|
+
Payload forms:
|
|
11
|
+
empty controller-only target (just the sender)
|
|
12
|
+
>=1, byte 0xFF broadcast to all peers (controller/management only)
|
|
13
|
+
>=16 single target uid
|
|
14
|
+
|
|
15
|
+
Each length is a minimum and they are tested longest first. An update that
|
|
16
|
+
appends a field leaves the fields ahead of it where they are, so an exact
|
|
17
|
+
length would read a grown request as a shorter form - here, as one addressing
|
|
18
|
+
nobody.
|
|
14
19
|
'''
|
|
15
20
|
|
|
16
21
|
from functools import cached_property
|
|
@@ -22,11 +27,21 @@ class FrameFactoryReset(FrameBase):
|
|
|
22
27
|
|
|
23
28
|
@cached_property
|
|
24
29
|
def is_broadcast_all(self) -> bool:
|
|
25
|
-
|
|
30
|
+
'''Whether this asks every peer to reset.
|
|
31
|
+
|
|
32
|
+
Tested after the uid form, which is longer: a uid whose first byte is
|
|
33
|
+
0xFF would otherwise read as a broadcast.'''
|
|
34
|
+
return (self.target_uid is None) and (len(self) >= 1) and (self.payload_u8(0) == 0xFF)
|
|
26
35
|
|
|
27
36
|
@cached_property
|
|
28
37
|
def target_uid(self) -> MxrDeviceUid | None:
|
|
29
|
-
|
|
38
|
+
'''Uid of the single peer being reset, None when the request names none.
|
|
39
|
+
|
|
40
|
+
A minimum rather than an exact length. A sender that appends a field
|
|
41
|
+
leaves the uid where it is, so requiring 16 exactly would read a grown
|
|
42
|
+
request as one addressing nobody - which is the sender resetting itself,
|
|
43
|
+
a different request altogether.'''
|
|
44
|
+
if (len(self) >= 16):
|
|
30
45
|
return self.payload_uuid(0)
|
|
31
46
|
return None
|
|
32
47
|
|
|
@@ -19,7 +19,20 @@ class FrameHello(FrameBase):
|
|
|
19
19
|
'''Hello frame, sent by devices to advertise themselves on the network.'''
|
|
20
20
|
@staticmethod
|
|
21
21
|
def construct(mxr:DeviceRegistry) -> FrameBase|None:
|
|
22
|
-
'''Build a hello frame for transmission to advertise this client.
|
|
22
|
+
'''Build a hello frame for transmission to advertise this client.
|
|
23
|
+
|
|
24
|
+
Never append a field to this payload. Receivers read a payload length as
|
|
25
|
+
a minimum everywhere except the hello, which they check exactly, so a
|
|
26
|
+
hello that grew is discarded rather than read up to the part they know.
|
|
27
|
+
That costs more than the field: this is how the client stops being
|
|
28
|
+
unknown, and a receiver that drops it never registers the sender and
|
|
29
|
+
ignores everything that follows as coming from a stranger.
|
|
30
|
+
|
|
31
|
+
It holds however many receivers are later fixed to accept a longer
|
|
32
|
+
hello, because the ones already deployed are the ones deciding whether
|
|
33
|
+
this client is seen at all. Nothing on the receive side needs the rule -
|
|
34
|
+
FrameHello reads its fields by offset with no length gate, so a peer may
|
|
35
|
+
grow its own.'''
|
|
23
36
|
payload = [ (MXR_PROTOCOL_VERSION & 0xFF), ((MXR_PROTOCOL_VERSION >> 8) & 0xFF) ]
|
|
24
37
|
payload = append_payload_str(payload=payload, value=mxr.name, sz=16)
|
|
25
38
|
payload = append_payload_str(payload=payload, value="P9SN00000000", sz=16)
|