mx-remote 5.5.0__tar.gz → 5.6.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.0 → mx_remote-5.6.0}/.gitignore +1 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/AGENTS.md +7 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/PKG-INFO +2 -1
- mx_remote-5.6.0/docs/oneip.md +109 -0
- mx_remote-5.6.0/mx_remote/compat.py +22 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/const.py +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Constants.py +7 -2
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameFactoryReset.py +21 -6
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameHello.py +14 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSystemStatus.py +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSystemStatus.pyi +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPAudio.py +13 -7
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPAudio.pyi +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPBayMapping.py +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPMultiviewer.py +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +1 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPStats.py +51 -3
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPStats.pyi +5 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/V2IPConfig.py +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/V2IPConfig.pyi +2 -1
- mx_remote-5.6.0/mx_remote/proto/V2IPStats.py +521 -0
- mx_remote-5.6.0/mx_remote/proto/V2IPStats.pyi +116 -0
- mx_remote-5.6.0/mx_remote/py.typed +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Bay.py +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Bay.pyi +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Device.py +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Device.pyi +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Remote.py +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Remote.pyi +2 -1
- {mx_remote-5.5.0 → mx_remote-5.6.0}/pyproject.toml +2 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/audio_ir.py +16 -4
- mx_remote-5.6.0/tests/lengths.py +107 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/run.py +1 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/smoke.py +1 -1
- mx_remote-5.6.0/tests/stats.py +432 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/unknowns.py +25 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/wirefix.py +28 -0
- mx_remote-5.5.0/docs/oneip.md +0 -43
- mx_remote-5.5.0/mx_remote/proto/V2IPStats.py +0 -190
- mx_remote-5.5.0/mx_remote/proto/V2IPStats.pyi +0 -54
- mx_remote-5.5.0/tests/stats.py +0 -73
- {mx_remote-5.5.0 → mx_remote-5.6.0}/CLAUDE.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/LICENSE +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/README.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/README.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/_static/brand.css +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/_static/favicon.svg +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/_static/logo-dark.svg +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/_static/logo.svg +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/api.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/audio.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/callbacks.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/conf.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/configuration.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/devices-and-bays.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/diagnostics.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/index.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/multiviewer.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/requirements.txt +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/docs/routing.md +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/hatch_build.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/Interface.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/Interface.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/Uid.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/__init__.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/__init__.pyi +0 -0
- /mx_remote-5.5.0/mx_remote/py.typed → /mx_remote-5.6.0/mx_remote/compat.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/const.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/main.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/main.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Constants.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Factory.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayConfig.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameLinks.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCIr.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Svd.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/Svd.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/capture.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/e2e.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/entry.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/handlers.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/handlers_cover.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/hello.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/idempotent.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/merge.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/newops.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/offsets.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/proto_stamp.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/protogate.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/rcsettings.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/roundtrip.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/sources.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/structs.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/txresult.py +0 -0
- {mx_remote-5.5.0 → mx_remote-5.6.0}/tests/vdetails.py +0 -0
|
@@ -73,6 +73,13 @@ A client's uid is persisted at `~/.mxr-uid`, one file per account, and
|
|
|
73
73
|
account are a single identity to the mesh and each discards everything the other
|
|
74
74
|
sends, so a second one needs its own `uid_path`.
|
|
75
75
|
|
|
76
|
+
### Python versions
|
|
77
|
+
`requires-python` is 3.11, and `.github/workflows/tests.yml` runs the suites on
|
|
78
|
+
3.11, 3.12 and 3.13 so that floor is checked rather than assumed. A name that
|
|
79
|
+
only exists on a newer version goes through `mx_remote/compat.py`, the one place
|
|
80
|
+
that tests `sys.version_info`, and its fallback needs a dependency marked
|
|
81
|
+
`python_version < '<the version that added it>'` in `pyproject.toml`.
|
|
82
|
+
|
|
76
83
|
### Versioning
|
|
77
84
|
Version is single-sourced in `mx_remote/const.py` (`VERSION = '...'`); `pyproject.toml`
|
|
78
85
|
reads it via regex, so bump it there only. Release commits follow the convention
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mx_remote
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.6.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/
|
|
@@ -28,6 +28,7 @@ Requires-Python: >=3.11
|
|
|
28
28
|
Requires-Dist: aiofiles>=24.1.0
|
|
29
29
|
Requires-Dist: aiohttp>=3.10.5
|
|
30
30
|
Requires-Dist: netifaces>=0.11.0
|
|
31
|
+
Requires-Dist: typing-extensions>=4.4; python_version < '3.12'
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
|
|
33
34
|
# MX Remote - Python Client for Pulse-Eight MatrixOS devices
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
if device.v2ip_details:
|
|
18
|
+
details = device.v2ip_details
|
|
19
|
+
print(f"Video: {details.video}")
|
|
20
|
+
print(f"TX rate: {details.tx_rate}") # None when the sender offered no rate
|
|
21
|
+
|
|
22
|
+
# per-stream DSCP marking (None from peers that predate it)
|
|
23
|
+
if details.dscp and details.dscp.complete:
|
|
24
|
+
print(f"DSCP: {details.dscp}") # video / audio / anc, 0..63
|
|
25
|
+
|
|
26
|
+
# streaming statistics
|
|
27
|
+
# The subscription lapses 60s after the request and there is no free-running
|
|
28
|
+
# mode, so call this again inside the minute to keep the 1Hz reports coming.
|
|
29
|
+
await device.read_stats(enable=True) # start collecting
|
|
30
|
+
# ... later ...
|
|
31
|
+
stats = device.v2ip_stats
|
|
32
|
+
|
|
33
|
+
# what the sink's decoder recovered from the codestream it is being given.
|
|
34
|
+
# Three answers, and they mean different things:
|
|
35
|
+
if stats.decoder is None:
|
|
36
|
+
pass # sender predates MatrixOS 10.12.46
|
|
37
|
+
elif stats.decoder.reading is None:
|
|
38
|
+
pass # decoder has never answered
|
|
39
|
+
else:
|
|
40
|
+
reading = stats.decoder.reading
|
|
41
|
+
# Geometry is what says a picture was recovered. `format` never does: with
|
|
42
|
+
# no stream it reads RGB, which is indistinguishable from a real RGB source.
|
|
43
|
+
if reading.recovered:
|
|
44
|
+
print(f"{reading.width}x{reading.height} {reading.format}")
|
|
45
|
+
# `reason` for display, `causes` for logic. Every true cause sets its bit
|
|
46
|
+
# in flags; which one keeps `reason` is a fixed priority in the video
|
|
47
|
+
# processor, not the numbering. TX_BRIDGE_UNLOCKED ranks below every
|
|
48
|
+
# input-side cause, so a repeating pipeline restart shows as bit 9 in
|
|
49
|
+
# `causes` while `reason` names the input cause instead.
|
|
50
|
+
print(f"{reading.reason}") # None for a cause this build cannot name
|
|
51
|
+
print(f"{reading.causes}") # raw reason values, lowest first
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Trusting a decoder reading after a switch
|
|
55
|
+
|
|
56
|
+
The values are read off the video processor every 2s and reported every 1s,
|
|
57
|
+
latched between reads, so roughly every other report repeats a reading already
|
|
58
|
+
seen — a frame arriving says nothing about freshness. `reading.updates` counts
|
|
59
|
+
readings actually **stored**, so it stays still while a processor is stalled
|
|
60
|
+
rather than implying a refresh. It is monotonic, never reset, and wraps at
|
|
61
|
+
65535 (~36h).
|
|
62
|
+
|
|
63
|
+
After changing what a sink is pointed at, wait for `updates` to advance by
|
|
64
|
+
**two** before trusting the geometry. It ticks when the reply lands, not when
|
|
65
|
+
the query is sent, so a single tick can carry an answer read fractionally
|
|
66
|
+
before the switch.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
before = device.v2ip_stats.decoder.reading.updates
|
|
70
|
+
await sink.switch_source(...)
|
|
71
|
+
# ... wait until reading.updates - before >= 2 (mod 65536) ...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Colour depth is deliberately absent: the video processor answers depth from a
|
|
75
|
+
driver constant rather than from the codestream, so it was withheld rather than
|
|
76
|
+
shipped as a constant that looks like a measurement. Assert depth at the
|
|
77
|
+
encoder's input bay instead.
|
|
78
|
+
|
|
79
|
+
### What this block cannot tell you
|
|
80
|
+
|
|
81
|
+
**Whether a sink is off on purpose.** A sink someone deliberately disabled
|
|
82
|
+
reports `NO_PACKETS`, indefinitely — the same cause as a sink that should be
|
|
83
|
+
receiving and is not. There is no enablement field here, so read that from
|
|
84
|
+
`MXR_OP_V2IP_DEVICE_CFG` or the device's HTTP status. `IDLE` sounds like the
|
|
85
|
+
answer and is not: it is effectively unreachable in shipping firmware.
|
|
86
|
+
|
|
87
|
+
**Whether a single sample is representative.** `reason` during a teardown is a
|
|
88
|
+
sequence, not a state — a measured one passed through `SWITCH_PENDING` before
|
|
89
|
+
settling on `NO_PACKETS` six seconds later. Use `updates` to tell a fresh
|
|
90
|
+
reading from a repeat, and note it cannot see a `TX_BRIDGE_UNLOCKED` flag
|
|
91
|
+
carried forward across a format change: a value held over is a stored reading
|
|
92
|
+
like any other.
|
|
93
|
+
|
|
94
|
+
## Mesh and firmware
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# mesh operations
|
|
98
|
+
await device.mesh_promote() # promote to mesh master
|
|
99
|
+
await device.mesh_remove() # remove from mesh
|
|
100
|
+
|
|
101
|
+
# firmware versions
|
|
102
|
+
if device.v2ip_firmware_versions:
|
|
103
|
+
for fw_type, fw in device.v2ip_firmware_versions.items():
|
|
104
|
+
print(f"{fw_type}: {fw.version}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
[Documentation index](README.md) | [Project README](https://github.com/opdenkamp/mx-remote#readme)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
######################################################
|
|
2
|
+
## MX Remote Python Interface ##
|
|
3
|
+
## ##
|
|
4
|
+
## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
|
|
5
|
+
## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
|
|
6
|
+
######################################################
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Names that live in different modules across the Python versions this package
|
|
10
|
+
supports. Import them from here so the version test is written once.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
if sys.version_info >= (3, 12):
|
|
16
|
+
from typing import override
|
|
17
|
+
else:
|
|
18
|
+
# typing.override arrived in 3.12; typing_extensions carries the same
|
|
19
|
+
# decorator further back, and is a dependency only below that version.
|
|
20
|
+
from typing_extensions import override
|
|
21
|
+
|
|
22
|
+
__all__ = ['override']
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -10,7 +10,7 @@ from enum import IntEnum
|
|
|
10
10
|
from functools import cached_property
|
|
11
11
|
import logging
|
|
12
12
|
import struct
|
|
13
|
-
from
|
|
13
|
+
from ..compat import override
|
|
14
14
|
|
|
15
15
|
from .FrameHeader import FrameHeader
|
|
16
16
|
from .FrameBase import FrameBase
|
|
@@ -434,15 +434,21 @@ class AudioConfig:
|
|
|
434
434
|
# SELECT_INPUT payload, as construct() below writes it:
|
|
435
435
|
# 0..2 u16 sub-opcode
|
|
436
436
|
# 2..4 padding
|
|
437
|
-
# 4..20
|
|
438
|
-
# 20..36 target uid
|
|
437
|
+
# 4..20 device uid (the hop this frame is addressed to)
|
|
438
|
+
# 20..36 target uid (the sink, whose endpoint changes source)
|
|
439
439
|
# 36..52 source uid
|
|
440
440
|
# 52..54 u16 target endpoint id
|
|
441
441
|
# 54..56 u16 source endpoint id
|
|
442
442
|
#
|
|
443
|
-
#
|
|
444
|
-
#
|
|
445
|
-
#
|
|
443
|
+
# Read the route from the body alone. The uid at 4 names whichever device is
|
|
444
|
+
# addressed for the hop that carried this frame, which is the sink only when the
|
|
445
|
+
# route has one hop - so taking the sink from there mislabels every longer
|
|
446
|
+
# route, even though a single-hop command puts the same uid in both.
|
|
447
|
+
#
|
|
448
|
+
# The firmware struct calls the body's first uid the source and its second the
|
|
449
|
+
# target, the reverse of what they hold, so the field names cannot settle the
|
|
450
|
+
# orientation either. Nothing on the wire tells a swapped reading from a correct
|
|
451
|
+
# one; the test vector is what holds it.
|
|
446
452
|
|
|
447
453
|
class AudioChangeSourceImpl(AudioChangeSource):
|
|
448
454
|
'''Parsed audio source change request with source and target identifiers.'''
|
|
@@ -456,7 +462,7 @@ class AudioChangeSourceImpl(AudioChangeSource):
|
|
|
456
462
|
|
|
457
463
|
@property
|
|
458
464
|
def target_uid(self) -> MxrDeviceUid|None:
|
|
459
|
-
'''Device whose endpoint is changing source
|
|
465
|
+
'''Device whose endpoint is changing source, read from the body.'''
|
|
460
466
|
return self.data.payload_uuid(idx=20)
|
|
461
467
|
|
|
462
468
|
@property
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from ..Interface import AudioChangeSource as AudioChangeSource, AudioEndpoint as AudioEndpoint, AudioEndpoints as AudioEndpoints, AudioFeatures as AudioFeatures, AudioLink as AudioLink, AudioLinks as AudioLinks, DeviceRegistry as DeviceRegistry, MxrDeviceUid as MxrDeviceUid, V2IPStreamSource as V2IPStreamSource
|
|
2
|
+
from ..compat import override as override
|
|
2
3
|
from .FrameBase import FrameBase as FrameBase
|
|
3
4
|
from .FrameHeader import FrameHeader as FrameHeader
|
|
4
5
|
from _typeshed import Incomplete
|
|
5
6
|
from enum import IntEnum
|
|
6
7
|
from functools import cached_property
|
|
7
|
-
from typing import override
|
|
8
8
|
|
|
9
9
|
V2IP_AUDIO_EP_ID_NONE: int
|
|
10
10
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from ..Interface import MxrDeviceUid as MxrDeviceUid
|
|
2
|
+
from ..compat import override as override
|
|
2
3
|
from .FrameBase import FrameBase as FrameBase
|
|
3
4
|
from functools import cached_property
|
|
4
|
-
from typing import override
|
|
5
5
|
|
|
6
6
|
class FrameV2IPBayMapping(FrameBase):
|
|
7
7
|
@cached_property
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from functools import cached_property
|
|
11
|
-
from
|
|
11
|
+
from ..compat import override
|
|
12
12
|
from .FrameBase import FrameBase
|
|
13
13
|
from .Constants import decode_enum
|
|
14
14
|
from ..Interface import MxrDeviceUid, DeviceBase, DeviceRegistry
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from .Multiviewer import *
|
|
2
2
|
from ..Interface import DeviceBase as DeviceBase, DeviceRegistry as DeviceRegistry, MxrDeviceUid as MxrDeviceUid
|
|
3
|
+
from ..compat import override as override
|
|
3
4
|
from .Constants import decode_enum as decode_enum
|
|
4
5
|
from .FrameBase import FrameBase as FrameBase
|
|
5
6
|
from enum import Enum as Enum
|
|
6
7
|
from functools import cached_property
|
|
7
|
-
from typing import override
|
|
8
8
|
|
|
9
9
|
class V2IPMultiviewerConfig(FrameBase, MultiviewerConfig):
|
|
10
10
|
@property
|
|
@@ -9,22 +9,46 @@
|
|
|
9
9
|
from functools import cached_property
|
|
10
10
|
import warnings
|
|
11
11
|
from .FrameBase import FrameBase
|
|
12
|
-
from .V2IPStats import V2IPRxStats, V2IPTxStats, V2IPDeviceStats
|
|
12
|
+
from .V2IPStats import (V2IPRxStats, V2IPTxStats, V2IPDeviceStats, V2IPDecoderDetail,
|
|
13
|
+
V2IP_DECODER_DETAIL_OFFSET, V2IP_DECODER_PROTOCOL, V2IP_STATS_COUNTERS_LEN,
|
|
14
|
+
V2IP_STATS_FULL_LEN)
|
|
13
15
|
from ..Interface import DeviceBase, DeviceRegistry
|
|
14
16
|
|
|
15
17
|
class FrameV2IPStats(FrameBase):
|
|
16
18
|
'''V2IP encoder/decoder statistics report.'''
|
|
17
19
|
@staticmethod
|
|
18
20
|
def construct(registry:DeviceRegistry, device:DeviceBase, enable:bool) -> FrameBase|None:
|
|
19
|
-
'''Build a stats enable/disable request frame for transmission.
|
|
21
|
+
'''Build a stats enable/disable request frame for transmission.
|
|
22
|
+
|
|
23
|
+
There is no free-running mode: a device reports only while a
|
|
24
|
+
subscription is live, and the subscription lapses 60s after this frame.
|
|
25
|
+
Send it again inside the minute to keep the 1Hz reports coming.
|
|
26
|
+
|
|
27
|
+
The 17-byte form is the only one a device acts on; any other length is
|
|
28
|
+
ignored.'''
|
|
20
29
|
payload = device.remote_id.byte_value
|
|
21
30
|
payload += bytes([1]) if enable else bytes([0])
|
|
22
31
|
return FrameBase.construct_base(mxr=registry, opcode=0x3F, payload=payload)
|
|
23
32
|
|
|
24
33
|
@cached_property
|
|
25
34
|
def is_request(self) -> bool:
|
|
35
|
+
'''Whether this is the enable/disable request, which carries no counters.'''
|
|
26
36
|
return (self.payload is not None) and (len(self.payload) == 17)
|
|
27
37
|
|
|
38
|
+
@cached_property
|
|
39
|
+
def is_report(self) -> bool:
|
|
40
|
+
'''Whether the payload is long enough to hold the counter blocks.
|
|
41
|
+
|
|
42
|
+
The length is the only thing separating the two forms this opcode
|
|
43
|
+
carries: a request is a uid and a flag, far short of the counters.
|
|
44
|
+
|
|
45
|
+
Everything between the two forms is a truncated report, and reading one
|
|
46
|
+
raises: payload_idx clamps a slice to what arrived rather than failing,
|
|
47
|
+
so each block below is handed a buffer too short and refuses it. That
|
|
48
|
+
reaches str(self) as readily as process(), and the receive path renders
|
|
49
|
+
every frame it decodes to its debug log before processing it.'''
|
|
50
|
+
return (self.payload is not None) and (len(self.payload) >= V2IP_STATS_COUNTERS_LEN)
|
|
51
|
+
|
|
28
52
|
@cached_property
|
|
29
53
|
def stats_enabled(self) -> bool:
|
|
30
54
|
pl = self.payload_bool(16)
|
|
@@ -60,6 +84,27 @@ class FrameV2IPStats(FrameBase):
|
|
|
60
84
|
raise Exception("invalid FrameV2IPStats size")
|
|
61
85
|
return V2IPRxStats(pl)
|
|
62
86
|
|
|
87
|
+
@cached_property
|
|
88
|
+
def decoder(self) -> V2IPDecoderDetail|None:
|
|
89
|
+
'''What the sink's decoder recovered, None from a sender that predates it.
|
|
90
|
+
|
|
91
|
+
Recognised by the frame's stamp and its length together. The length
|
|
92
|
+
says the payload is long enough to hold the block; the stamp says those
|
|
93
|
+
bytes are that block rather than some later growth this client has no
|
|
94
|
+
name for. A sender below V2IP_DECODER_PROTOCOL appended no such block,
|
|
95
|
+
so reading its tail would invent a reason, a geometry and a fault word.
|
|
96
|
+
Its counters are still read.
|
|
97
|
+
|
|
98
|
+
Neither answers whether a reserved field has been spent: a firmware that
|
|
99
|
+
gives the block's reserved byte a meaning still stamps this version and
|
|
100
|
+
still sends 152 bytes.'''
|
|
101
|
+
if (self.protocol < V2IP_DECODER_PROTOCOL):
|
|
102
|
+
return None
|
|
103
|
+
pl = self.payload_idx(start=V2IP_DECODER_DETAIL_OFFSET, end=V2IP_STATS_FULL_LEN)
|
|
104
|
+
if (pl is None) or (len(pl) < (V2IP_STATS_FULL_LEN - V2IP_DECODER_DETAIL_OFFSET)):
|
|
105
|
+
return None
|
|
106
|
+
return V2IPDecoderDetail(pl)
|
|
107
|
+
|
|
63
108
|
@cached_property
|
|
64
109
|
def stats(self) -> V2IPDeviceStats:
|
|
65
110
|
rv = V2IPDeviceStats()
|
|
@@ -67,14 +112,17 @@ class FrameV2IPStats(FrameBase):
|
|
|
67
112
|
rv.tx_per_minute = self.tx_per_minute
|
|
68
113
|
rv.rx = self.rx
|
|
69
114
|
rv.rx_per_minute = self.rx_per_minute
|
|
115
|
+
rv.decoder = self.decoder
|
|
70
116
|
return rv
|
|
71
117
|
|
|
72
118
|
def process(self) -> None:
|
|
73
119
|
'''Update the local device cache with V2IP statistics.'''
|
|
74
|
-
if
|
|
120
|
+
if self.is_report and ((dev := self.remote_device) is not None):
|
|
75
121
|
dev.on_mxr_update(self.stats)
|
|
76
122
|
|
|
77
123
|
def __str__(self) -> str:
|
|
78
124
|
if self.is_request:
|
|
79
125
|
return f"{str(self.remote_device)} v2ip stats request: {self.stats_enabled}"
|
|
126
|
+
if not self.is_report:
|
|
127
|
+
return f"{str(self.remote_device)} v2ip stats: {len(self)} bytes, too short to read"
|
|
80
128
|
return f"{str(self.remote_device)} v2ip stats: {self.stats}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from ..Interface import DeviceBase as DeviceBase, DeviceRegistry as DeviceRegistry
|
|
2
2
|
from .FrameBase import FrameBase as FrameBase
|
|
3
|
-
from .V2IPStats import V2IPDeviceStats as V2IPDeviceStats, V2IPRxStats as V2IPRxStats, V2IPTxStats as V2IPTxStats
|
|
3
|
+
from .V2IPStats import V2IPDecoderDetail as V2IPDecoderDetail, V2IPDeviceStats as V2IPDeviceStats, V2IPRxStats as V2IPRxStats, V2IPTxStats as V2IPTxStats, V2IP_DECODER_DETAIL_OFFSET as V2IP_DECODER_DETAIL_OFFSET, V2IP_DECODER_PROTOCOL as V2IP_DECODER_PROTOCOL, V2IP_STATS_COUNTERS_LEN as V2IP_STATS_COUNTERS_LEN, V2IP_STATS_FULL_LEN as V2IP_STATS_FULL_LEN
|
|
4
4
|
from functools import cached_property
|
|
5
5
|
|
|
6
6
|
class FrameV2IPStats(FrameBase):
|
|
@@ -9,6 +9,8 @@ class FrameV2IPStats(FrameBase):
|
|
|
9
9
|
@cached_property
|
|
10
10
|
def is_request(self) -> bool: ...
|
|
11
11
|
@cached_property
|
|
12
|
+
def is_report(self) -> bool: ...
|
|
13
|
+
@cached_property
|
|
12
14
|
def stats_enabled(self) -> bool: ...
|
|
13
15
|
@cached_property
|
|
14
16
|
def tx(self) -> V2IPTxStats: ...
|
|
@@ -19,5 +21,7 @@ class FrameV2IPStats(FrameBase):
|
|
|
19
21
|
@cached_property
|
|
20
22
|
def rx_per_minute(self) -> V2IPRxStats: ...
|
|
21
23
|
@cached_property
|
|
24
|
+
def decoder(self) -> V2IPDecoderDetail | None: ...
|
|
25
|
+
@cached_property
|
|
22
26
|
def stats(self) -> V2IPDeviceStats: ...
|
|
23
27
|
def process(self) -> None: ...
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
######################################################
|
|
7
7
|
'''Video-over-IP stream source configuration and parsing.'''
|
|
8
8
|
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
10
|
+
from ..compat import override
|
|
10
11
|
from ..Uid import MxrDeviceUid
|
|
11
12
|
from ..Interface import V2IPStreamSource, V2IPStreamSources, V2IPAudioFormat
|
|
12
13
|
import socket
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from ..Interface import V2IPAudioFormat as V2IPAudioFormat, V2IPStreamSource as V2IPStreamSource, V2IPStreamSources as V2IPStreamSources
|
|
2
2
|
from ..Uid import MxrDeviceUid as MxrDeviceUid
|
|
3
|
+
from ..compat import override as override
|
|
3
4
|
from _typeshed import Incomplete
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
class V2IPStreamSourceImpl(V2IPStreamSource):
|
|
7
8
|
def __init__(self, label: str, data: bytes) -> None: ...
|