mx-remote 5.8.0__tar.gz → 5.9.1__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.8.0 → mx_remote-5.9.1}/AGENTS.md +18 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/PKG-INFO +1 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/Interface.py +78 -20
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/Interface.pyi +12 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/const.py +9 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/const.pyi +1 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Constants.py +42 -5
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Constants.pyi +12 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfig.py +1 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameLinks.py +1 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCIr.py +56 -20
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCIr.pyi +2 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatusNew.py +26 -3
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +79 -11
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +4 -2
- mx_remote-5.9.1/mx_remote/proto/FrameV2IPSources.py +112 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSources.pyi +4 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPStats.py +9 -14
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPStats.pyi +1 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPStats.py +0 -9
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPStats.pyi +0 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Device.py +168 -13
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Device.pyi +13 -2
- mx_remote-5.9.1/tests/complete.py +218 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/merge.py +57 -13
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/run.py +1 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/sources.py +47 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/stats.py +22 -24
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/unknowns.py +5 -6
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/v2ipsubject.py +71 -1
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/wirefix.py +40 -11
- mx_remote-5.8.0/mx_remote/proto/FrameV2IPSources.py +0 -54
- {mx_remote-5.8.0 → mx_remote-5.9.1}/.gitignore +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/CLAUDE.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/LICENSE +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/README.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/README.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/_static/brand.css +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/_static/favicon.svg +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/_static/logo-dark.svg +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/_static/logo.svg +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/api.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/audio.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/callbacks.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/conf.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/configuration.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/devices-and-bays.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/diagnostics.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/index.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/multiviewer.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/oneip.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/requirements.txt +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/docs/routing.md +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/hatch_build.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/Uid.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/Uid.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/__init__.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/__init__.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/compat.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/compat.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/main.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/main.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Data.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Factory.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFactoryReset.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHello.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPAudio.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Svd.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/Svd.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPConfig.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/py.typed +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Bay.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Bay.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Link.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Remote.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/Remote.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/State.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/pyproject.toml +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/audio_ir.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/capture.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/e2e.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/entry.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/handlers.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/handlers_cover.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/hello.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/idempotent.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/lengths.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/newops.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/offsets.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/proto_stamp.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/protogate.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/rcsettings.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/roundtrip.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/smoke.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/structs.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/txresult.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/v2ipscaling.py +0 -0
- {mx_remote-5.8.0 → mx_remote-5.9.1}/tests/vdetails.py +0 -0
|
@@ -213,6 +213,24 @@ selector. A payload can widen without its floor moving, and then the stamp says
|
|
|
213
213
|
nothing about which layout arrived — dispatch on payload length instead.
|
|
214
214
|
`tests/wirefix.py` covers the cases where the two come apart.
|
|
215
215
|
|
|
216
|
+
A row can also go down, which turns a version test into a silent drop. Rows are
|
|
217
|
+
not monotonic: one is lowered to bring a family back under a product's receive
|
|
218
|
+
cap, with no layout changing. A test added while a row was high then rejects
|
|
219
|
+
every frame on that opcode from every sender at once, and what it guards simply
|
|
220
|
+
stops being reported. So prefer the length, which no stamp change can
|
|
221
|
+
invalidate.
|
|
222
|
+
|
|
223
|
+
Two decoders read the stamp, and each says in place what it rests on. The mesh
|
|
224
|
+
operation gate: that struct is fixed size and what changed is what its bytes
|
|
225
|
+
mean, so no length distinguishes the forms. The network status layout selector:
|
|
226
|
+
the forms are the same length, so the stamp chooses between them rather than
|
|
227
|
+
admitting one, and getting it wrong yields a complete, plausible, wrong port
|
|
228
|
+
status rather than a lost frame.
|
|
229
|
+
|
|
230
|
+
A fixture stamped at a version no device sends is a frame that exists nowhere
|
|
231
|
+
else, and every assertion about it says nothing about the network. Stamp what a
|
|
232
|
+
device stamps, which for a report is the opcode's own row.
|
|
233
|
+
|
|
216
234
|
## Writing down a deliberate divergence
|
|
217
235
|
|
|
218
236
|
A note recording *what the firmware does* reads as *what this library should
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mx_remote
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.9.1
|
|
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/
|
|
@@ -1384,10 +1384,12 @@ class DeviceV2IPScalingSettings(ABC):
|
|
|
1384
1384
|
def flags(self) -> int:
|
|
1385
1385
|
'''mxr_scaling_config.flags.
|
|
1386
1386
|
|
|
1387
|
-
MXR_SCALING_FLAG_MODE_VALID
|
|
1388
|
-
which
|
|
1389
|
-
setting
|
|
1390
|
-
|
|
1387
|
+
MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS_VALID and
|
|
1388
|
+
MXR_SCALING_FLAG_OPTIONS2_VALID mark which part a sender meant to send,
|
|
1389
|
+
and are separately valid; a sender setting none of them is offering no
|
|
1390
|
+
scaling at all. MXR_SCALING_FLAG_AUTO_SCALING sits behind the first
|
|
1391
|
+
marker, MXR_SCALING_FLAG_MATCH_SOURCE and MXR_SCALING_FLAG_SKIP_420
|
|
1392
|
+
behind the second.
|
|
1391
1393
|
'''
|
|
1392
1394
|
|
|
1393
1395
|
@property
|
|
@@ -1412,6 +1414,37 @@ class DeviceV2IPScalingSettings(ABC):
|
|
|
1412
1414
|
return None
|
|
1413
1415
|
return ((self.flags & MXR_SCALING_FLAG_AUTO_SCALING) != 0)
|
|
1414
1416
|
|
|
1417
|
+
@property
|
|
1418
|
+
def match_source(self) -> bool|None:
|
|
1419
|
+
'''Whether the output follows its source's format, None when the device
|
|
1420
|
+
has never said.
|
|
1421
|
+
|
|
1422
|
+
Firmware with this option announces it on every configuration it sends
|
|
1423
|
+
about itself, so a device that has reported it once is known to have it.
|
|
1424
|
+
The cached block accumulates its validity bit, so a later write carrying
|
|
1425
|
+
only the first options group does not take that back.
|
|
1426
|
+
|
|
1427
|
+
Reported only from a sender announcing DeviceBase.config_initialised,
|
|
1428
|
+
which is why this needs no caveat of its own: no firmware has these
|
|
1429
|
+
options without that announcement, and one that lacks it would be
|
|
1430
|
+
reporting uninitialised stack. That is the difference from
|
|
1431
|
+
configured_mode, which is reported from any sender because a mode can be
|
|
1432
|
+
genuine on one of those.
|
|
1433
|
+
'''
|
|
1434
|
+
if ((self.flags & MXR_SCALING_FLAG_OPTIONS2_VALID) == 0):
|
|
1435
|
+
return None
|
|
1436
|
+
return ((self.flags & MXR_SCALING_FLAG_MATCH_SOURCE) != 0)
|
|
1437
|
+
|
|
1438
|
+
@property
|
|
1439
|
+
def skip_420(self) -> bool|None:
|
|
1440
|
+
'''Whether the output declines 4:2:0 rather than scaling it, None when
|
|
1441
|
+
the device has never said. Reported on the same terms as match_source,
|
|
1442
|
+
which shares its validity bit.
|
|
1443
|
+
'''
|
|
1444
|
+
if ((self.flags & MXR_SCALING_FLAG_OPTIONS2_VALID) == 0):
|
|
1445
|
+
return None
|
|
1446
|
+
return ((self.flags & MXR_SCALING_FLAG_SKIP_420) != 0)
|
|
1447
|
+
|
|
1415
1448
|
def v2ip_stream_valid(stream:'V2IPStreamSource|None') -> bool:
|
|
1416
1449
|
"""
|
|
1417
1450
|
True when a stream source carries an address a frame actually meant to send.
|
|
@@ -1677,33 +1710,39 @@ class DeviceV2IPDetails:
|
|
|
1677
1710
|
"""
|
|
1678
1711
|
Merge a scaling config field by field, the way firmware applies it.
|
|
1679
1712
|
|
|
1680
|
-
The mode/refresh pair and the options
|
|
1681
|
-
write carrying
|
|
1713
|
+
The mode/refresh pair and the two options groups are separately valid,
|
|
1714
|
+
so a write carrying one of them must leave the others as they were. Each
|
|
1715
|
+
group is replaced whole behind its own marker, which is what keeps the
|
|
1716
|
+
marker itself - and so the knowledge that the device has those options -
|
|
1717
|
+
from being taken back by a later write that carries another group.
|
|
1682
1718
|
"""
|
|
1683
1719
|
incoming = self._scaling
|
|
1684
1720
|
if (incoming is None) or (previous is None):
|
|
1685
1721
|
return incoming if (incoming is not None) else previous
|
|
1686
1722
|
mode_valid = ((incoming.flags & MXR_SCALING_FLAG_MODE_VALID) != 0)
|
|
1687
1723
|
options_valid = ((incoming.flags & MXR_SCALING_FLAG_OPTIONS_VALID) != 0)
|
|
1688
|
-
|
|
1724
|
+
options2_valid = ((incoming.flags & MXR_SCALING_FLAG_OPTIONS2_VALID) != 0)
|
|
1725
|
+
if not mode_valid and not options_valid and not options2_valid:
|
|
1689
1726
|
# carries no scaling at all
|
|
1690
1727
|
return previous
|
|
1691
|
-
if mode_valid and options_valid:
|
|
1692
|
-
return incoming
|
|
1693
1728
|
mode = incoming.mode if mode_valid else previous.mode
|
|
1694
1729
|
refresh = incoming.refresh if mode_valid else previous.refresh
|
|
1695
1730
|
flags = previous.flags
|
|
1696
1731
|
if mode_valid:
|
|
1697
1732
|
flags |= MXR_SCALING_FLAG_MODE_VALID
|
|
1698
|
-
|
|
1699
|
-
#
|
|
1700
|
-
#
|
|
1701
|
-
#
|
|
1702
|
-
#
|
|
1703
|
-
#
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1733
|
+
if options_valid:
|
|
1734
|
+
# Replace AUTO_SCALING alone rather than the whole upper nibble: on a
|
|
1735
|
+
# receiver-capable unit running firmware that leaves mxr_scaling_config
|
|
1736
|
+
# uninitialised, every other bit up there is stack noise the sender
|
|
1737
|
+
# only ever |= its flags onto, and the settings behind the second
|
|
1738
|
+
# marker are a group of their own that this write does not speak for.
|
|
1739
|
+
flags = (flags & ~MXR_SCALING_FLAG_AUTO_SCALING) | MXR_SCALING_FLAG_OPTIONS_VALID \
|
|
1740
|
+
| (incoming.flags & MXR_SCALING_FLAG_AUTO_SCALING)
|
|
1741
|
+
if options2_valid:
|
|
1742
|
+
flags = (flags & ~MXR_SCALING_OPTIONS2_SETTINGS) | MXR_SCALING_FLAG_OPTIONS2_VALID \
|
|
1743
|
+
| (incoming.flags & MXR_SCALING_OPTIONS2_SETTINGS)
|
|
1744
|
+
if (mode == incoming.mode) and (refresh == incoming.refresh) and (flags == incoming.flags):
|
|
1745
|
+
return incoming
|
|
1707
1746
|
return V2IPScalingSettings(mode=mode, refresh=refresh, flags=flags)
|
|
1708
1747
|
|
|
1709
1748
|
class DeviceV2IPSink:
|
|
@@ -2031,6 +2070,17 @@ class DeviceBase(ABC):
|
|
|
2031
2070
|
def temperatures(self) -> dict[str,int]:
|
|
2032
2071
|
''' temperature sensor reports '''
|
|
2033
2072
|
|
|
2073
|
+
@property
|
|
2074
|
+
@abstractmethod
|
|
2075
|
+
def v2ip_features(self) -> 'V2IPFpgaFeature|None':
|
|
2076
|
+
'''What this device's video processor supports, None until it has said.
|
|
2077
|
+
|
|
2078
|
+
A device reports the mask only on a configuration it sends about itself,
|
|
2079
|
+
and reports nothing at all until its processor has answered - which
|
|
2080
|
+
looks the same on the wire as a processor without any of the optional
|
|
2081
|
+
commands, so neither is read as a capability set.
|
|
2082
|
+
'''
|
|
2083
|
+
|
|
2034
2084
|
@property
|
|
2035
2085
|
@abstractmethod
|
|
2036
2086
|
def v2ip_sources(self) -> V2IPStreamSourcesList|None:
|
|
@@ -2221,8 +2271,16 @@ class DeviceBase(ABC):
|
|
|
2221
2271
|
'''system health status'''
|
|
2222
2272
|
|
|
2223
2273
|
@abstractmethod
|
|
2224
|
-
def
|
|
2225
|
-
'''
|
|
2274
|
+
def merge_v2ip_sources(self, first:int, total:int, page:'V2IPStreamSourcesList') -> None:
|
|
2275
|
+
'''merge one frame of this device's V2IP source list into the cached list'''
|
|
2276
|
+
|
|
2277
|
+
@abstractmethod
|
|
2278
|
+
def note_link_config(self) -> None:
|
|
2279
|
+
'''internal callback: a page of the link configuration was received'''
|
|
2280
|
+
|
|
2281
|
+
@abstractmethod
|
|
2282
|
+
def note_bay_config(self) -> None:
|
|
2283
|
+
'''internal callback: the primary bay configuration was received'''
|
|
2226
2284
|
|
|
2227
2285
|
@abstractmethod
|
|
2228
2286
|
async def get_api(self, uri:str) -> dict[str, Any]|None:
|
|
@@ -586,6 +586,10 @@ class DeviceV2IPScalingSettings(ABC):
|
|
|
586
586
|
def configured_mode(self) -> tuple[MxrSignalType, int] | None: ...
|
|
587
587
|
@property
|
|
588
588
|
def auto_scaling(self) -> bool | None: ...
|
|
589
|
+
@property
|
|
590
|
+
def match_source(self) -> bool | None: ...
|
|
591
|
+
@property
|
|
592
|
+
def skip_420(self) -> bool | None: ...
|
|
589
593
|
|
|
590
594
|
def v2ip_stream_valid(stream: V2IPStreamSource | None) -> bool: ...
|
|
591
595
|
def v2ip_av_source_valid(video: V2IPStreamSource | None, anc: V2IPStreamSource | None) -> bool: ...
|
|
@@ -819,6 +823,9 @@ class DeviceBase(ABC):
|
|
|
819
823
|
def temperatures(self) -> dict[str, int]: ...
|
|
820
824
|
@property
|
|
821
825
|
@abstractmethod
|
|
826
|
+
def v2ip_features(self) -> V2IPFpgaFeature | None: ...
|
|
827
|
+
@property
|
|
828
|
+
@abstractmethod
|
|
822
829
|
def v2ip_sources(self) -> V2IPStreamSourcesList | None: ...
|
|
823
830
|
@property
|
|
824
831
|
@abstractmethod
|
|
@@ -907,7 +914,11 @@ class DeviceBase(ABC):
|
|
|
907
914
|
@abstractmethod
|
|
908
915
|
def status_message(self) -> str: ...
|
|
909
916
|
@abstractmethod
|
|
910
|
-
def
|
|
917
|
+
def merge_v2ip_sources(self, first: int, total: int, page: V2IPStreamSourcesList) -> None: ...
|
|
918
|
+
@abstractmethod
|
|
919
|
+
def note_link_config(self) -> None: ...
|
|
920
|
+
@abstractmethod
|
|
921
|
+
def note_bay_config(self) -> None: ...
|
|
911
922
|
@abstractmethod
|
|
912
923
|
async def get_api(self, uri: str) -> dict[str, Any] | None: ...
|
|
913
924
|
@abstractmethod
|
|
@@ -14,7 +14,7 @@ Default communication modes:
|
|
|
14
14
|
"""
|
|
15
15
|
import os
|
|
16
16
|
|
|
17
|
-
VERSION = '5.
|
|
17
|
+
VERSION = '5.9.1'
|
|
18
18
|
__version__ = VERSION
|
|
19
19
|
|
|
20
20
|
MX_BCAST_UDP_PORT = 8811
|
|
@@ -37,6 +37,14 @@ re-drawn after every hello that is sent, so devices announcing on the same mesh
|
|
|
37
37
|
not converge into synchronised bursts.
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
+
MXR_CONFIG_TIMEOUT = 15
|
|
41
|
+
"""How long a device is given to finish describing itself, in seconds.
|
|
42
|
+
|
|
43
|
+
Past this a device is reported as fully described without its link
|
|
44
|
+
configuration, and keeps being asked for the rest. It never stands in for the
|
|
45
|
+
bay configuration - see ``Device.has_bays``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
40
48
|
V2IP_UDP_PORT_VIDEO = 50020
|
|
41
49
|
"""Default destination UDP port for a V2IP video stream (firmware ``V2IP_UDP_PORT_VIDEO``)."""
|
|
42
50
|
|
|
@@ -140,11 +140,22 @@ def v2ip_rate_valid(rate:int|None) -> bool:
|
|
|
140
140
|
'''True when a tx_rate carries an actual rate (firmware v2ip_source_rate_valid()).'''
|
|
141
141
|
return (rate is not None) and (V2IP_SOURCE_RATE_MIN <= rate <= V2IP_SOURCE_RATE_MAX)
|
|
142
142
|
|
|
143
|
-
MXR_SCALING_FLAG_MODE_VALID
|
|
144
|
-
MXR_SCALING_FLAG_OPTIONS_VALID
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
MXR_SCALING_FLAG_MODE_VALID = (1 << 0)
|
|
144
|
+
MXR_SCALING_FLAG_OPTIONS_VALID = (1 << 1)
|
|
145
|
+
MXR_SCALING_FLAG_OPTIONS2_VALID = (1 << 4)
|
|
146
|
+
MXR_SCALING_FLAG_MATCH_SOURCE = (1 << 5)
|
|
147
|
+
MXR_SCALING_FLAG_SKIP_420 = (1 << 6)
|
|
148
|
+
MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
|
|
149
|
+
"""mxr_scaling_config.flags. The mode/refresh pair and each options group are
|
|
150
|
+
separately valid: a sender carrying none of the three markers is offering no
|
|
151
|
+
scaling at all.
|
|
152
|
+
|
|
153
|
+
Firmware that has the second group sets its marker on every configuration it
|
|
154
|
+
sends about itself, so that bit doubles as the report that the device has those
|
|
155
|
+
options at all. Bits 2 and 3 have no meaning."""
|
|
156
|
+
|
|
157
|
+
MXR_SCALING_OPTIONS2_SETTINGS = (MXR_SCALING_FLAG_MATCH_SOURCE | MXR_SCALING_FLAG_SKIP_420)
|
|
158
|
+
"""The settings behind MXR_SCALING_FLAG_OPTIONS2_VALID, which carries both."""
|
|
148
159
|
|
|
149
160
|
V2IP_SCALING_REFRESH_MIN = 24
|
|
150
161
|
V2IP_SCALING_REFRESH_MAX = 120
|
|
@@ -221,6 +232,32 @@ class DeviceFeature(IntFlag):
|
|
|
221
232
|
CONFIG_INITIALISED = (1 << 25)
|
|
222
233
|
BOOT_BIT = (1 << 31)
|
|
223
234
|
|
|
235
|
+
class V2IPFpgaFeature(IntFlag):
|
|
236
|
+
'''What a V2IP device's video processor supports, as the device reports it in
|
|
237
|
+
its configuration.
|
|
238
|
+
|
|
239
|
+
Read-only, and a device's own: it fills the field in only on the frame
|
|
240
|
+
describing itself, and leaves it zero on one it sends to configure another
|
|
241
|
+
device. There is no write path.
|
|
242
|
+
|
|
243
|
+
Bits are assigned by the video processor and only ever appended, so a bit
|
|
244
|
+
this library has no name for is a later capability rather than an error. A
|
|
245
|
+
device reports no features at all until its processor answers, and an older
|
|
246
|
+
processor answers with none of the optional commands, so an empty mask is
|
|
247
|
+
never a capability set.'''
|
|
248
|
+
SOURCE_DSCP = (1 << 0)
|
|
249
|
+
'''Applies a DSCP marking to the streams it sources.'''
|
|
250
|
+
SINK_AUDIO_FORMAT = (1 << 1)
|
|
251
|
+
'''Reports the audio format arriving at its sink.'''
|
|
252
|
+
SINK_TILING_WINDOW = (1 << 2)
|
|
253
|
+
'''Places a tiling window on its sink.'''
|
|
254
|
+
SINK_OVERLAY_STATE = (1 << 3)
|
|
255
|
+
'''Reports the state of its sink's overlay.'''
|
|
256
|
+
SINK_STATE = (1 << 4)
|
|
257
|
+
'''Reports its sink's state.'''
|
|
258
|
+
SINK_STREAM_INFO = (1 << 5)
|
|
259
|
+
'''Reports information about the stream its sink receives.'''
|
|
260
|
+
|
|
224
261
|
BAY_FEATURE_DOLBY_IN_POS = 24
|
|
225
262
|
|
|
226
263
|
class BayFeaturesMask(IntFlag):
|
|
@@ -17,7 +17,11 @@ def v2ip_rate_valid(rate: int | None) -> bool: ...
|
|
|
17
17
|
|
|
18
18
|
MXR_SCALING_FLAG_MODE_VALID: Incomplete
|
|
19
19
|
MXR_SCALING_FLAG_OPTIONS_VALID: Incomplete
|
|
20
|
+
MXR_SCALING_FLAG_OPTIONS2_VALID: Incomplete
|
|
21
|
+
MXR_SCALING_FLAG_MATCH_SOURCE: Incomplete
|
|
22
|
+
MXR_SCALING_FLAG_SKIP_420: Incomplete
|
|
20
23
|
MXR_SCALING_FLAG_AUTO_SCALING: Incomplete
|
|
24
|
+
MXR_SCALING_OPTIONS2_SETTINGS = MXR_SCALING_FLAG_MATCH_SOURCE | MXR_SCALING_FLAG_SKIP_420
|
|
21
25
|
V2IP_SCALING_REFRESH_MIN: int
|
|
22
26
|
V2IP_SCALING_REFRESH_MAX: int
|
|
23
27
|
MXR_V2IP_DSCP_SET: int
|
|
@@ -56,6 +60,14 @@ class DeviceFeature(IntFlag):
|
|
|
56
60
|
CONFIG_INITIALISED: Incomplete
|
|
57
61
|
BOOT_BIT: Incomplete
|
|
58
62
|
|
|
63
|
+
class V2IPFpgaFeature(IntFlag):
|
|
64
|
+
SOURCE_DSCP: Incomplete
|
|
65
|
+
SINK_AUDIO_FORMAT: Incomplete
|
|
66
|
+
SINK_TILING_WINDOW: Incomplete
|
|
67
|
+
SINK_OVERLAY_STATE: Incomplete
|
|
68
|
+
SINK_STATE: Incomplete
|
|
69
|
+
SINK_STREAM_INFO: Incomplete
|
|
70
|
+
|
|
59
71
|
BAY_FEATURE_DOLBY_IN_POS: int
|
|
60
72
|
|
|
61
73
|
class BayFeaturesMask(IntFlag):
|
|
@@ -49,6 +49,7 @@ class FrameBayConfig(FrameBase):
|
|
|
49
49
|
for bayconfig in self.bays:
|
|
50
50
|
_LOGGER.debug(f"process {bayconfig}")
|
|
51
51
|
dev.on_mxr_update(bayconfig)
|
|
52
|
+
dev.note_bay_config()
|
|
52
53
|
|
|
53
54
|
def __str__(self) -> str:
|
|
54
55
|
return f"{self.remote_device} bay config page: {len(self.bays)} bays"
|
|
@@ -27,15 +27,48 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
27
27
|
_META_OFFSET = 12
|
|
28
28
|
_TIMINGS_OFFSET = 24
|
|
29
29
|
|
|
30
|
-
# A receiver requires one timing beyond the struct, not just the struct,
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# A receiver requires one timing beyond the struct, not just the struct, which
|
|
31
|
+
# is the firmware's own gate. It reads like an off-by-one and is not: a capture
|
|
32
|
+
# with no timing is a burst nothing blasted, with nothing in it to replay, and
|
|
33
|
+
# it decodes perfectly.
|
|
34
34
|
_MIN_SIZE = _TIMINGS_OFFSET + 2
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
# Two timing widths exist on the wire, and the header counts timings rather than
|
|
37
|
+
# bytes, so what follows the header is what says which arrived: the same count
|
|
38
|
+
# takes twice the room in the wider form, and a narrow list is exactly as long
|
|
39
|
+
# as its count, so it can never be mistaken for a wide one.
|
|
40
|
+
_TIMING_WIDE = 2
|
|
41
|
+
_TIMING_NARROW = 1
|
|
36
42
|
|
|
37
43
|
class FrameRCIr(FrameBase):
|
|
38
44
|
'''IR key press event received by a bay.'''
|
|
45
|
+
@cached_property
|
|
46
|
+
def timing_width(self) -> int|None:
|
|
47
|
+
'''Bytes per timing as this sender wrote them, None when the frame is
|
|
48
|
+
not a capture this library will read.
|
|
49
|
+
|
|
50
|
+
Tested widest first, which is what separates the two forms: a narrow
|
|
51
|
+
list is exactly as long as its count, so a wide one is never read as a
|
|
52
|
+
narrow one carrying twice as many.
|
|
53
|
+
|
|
54
|
+
The count is a declaration, not a measurement: nothing on the wire ties
|
|
55
|
+
it to the number of timings that arrived. A frame short of its own count
|
|
56
|
+
at either width is refused rather than handed to a caller that would
|
|
57
|
+
index the timings by it.
|
|
58
|
+
'''
|
|
59
|
+
pl = self.payload
|
|
60
|
+
if (pl is None) or (len(pl) < _MIN_SIZE):
|
|
61
|
+
return None
|
|
62
|
+
nb = self.payload_u16(idx=_META_OFFSET + 4)
|
|
63
|
+
if (nb is None):
|
|
64
|
+
return None
|
|
65
|
+
tail = len(pl) - _TIMINGS_OFFSET
|
|
66
|
+
if (tail >= (nb * _TIMING_WIDE)):
|
|
67
|
+
return _TIMING_WIDE
|
|
68
|
+
if (tail >= (nb * _TIMING_NARROW)):
|
|
69
|
+
return _TIMING_NARROW
|
|
70
|
+
return None
|
|
71
|
+
|
|
39
72
|
@cached_property
|
|
40
73
|
def acceptable(self) -> bool:
|
|
41
74
|
'''Whether a receiver would act on this frame at all.
|
|
@@ -43,16 +76,7 @@ class FrameRCIr(FrameBase):
|
|
|
43
76
|
Every field below reads None or empty when this is False, so a frame no
|
|
44
77
|
device blasted cannot be mistaken for a capture with nothing in it.
|
|
45
78
|
'''
|
|
46
|
-
|
|
47
|
-
if (pl is None) or (len(pl) < _MIN_SIZE):
|
|
48
|
-
return False
|
|
49
|
-
# The count is a declaration, not a measurement: nothing on the wire
|
|
50
|
-
# ties it to the number of timings that arrived. A frame claiming more
|
|
51
|
-
# than it carries is refused rather than handed to a caller that would
|
|
52
|
-
# index the timings by it.
|
|
53
|
-
nb = self.payload_u16(idx=_META_OFFSET + 4)
|
|
54
|
-
return (nb is not None) and (nb <= ((len(pl) - _TIMINGS_OFFSET) // 2)) \
|
|
55
|
-
and (self.protocol >= _ACCEPT_PROTOCOL)
|
|
79
|
+
return (self.timing_width is not None)
|
|
56
80
|
|
|
57
81
|
@cached_property
|
|
58
82
|
def bay(self) -> BayBase|None:
|
|
@@ -112,14 +136,19 @@ class FrameRCIr(FrameBase):
|
|
|
112
136
|
|
|
113
137
|
@cached_property
|
|
114
138
|
def timings(self) -> bytes:
|
|
115
|
-
'''
|
|
139
|
+
'''On/off timings appended after the struct, two bytes each.
|
|
140
|
+
|
|
141
|
+
Always that width, whichever the sender used: the narrower form older
|
|
142
|
+
devices send is widened here, so the values are the device's and only
|
|
143
|
+
the encoding is this library's. nb_timings counts the timings, so this
|
|
144
|
+
is twice that long.
|
|
116
145
|
|
|
117
146
|
A receiver drops the first timing and blasts from the second, so this
|
|
118
147
|
list is one longer than what is emitted. The first is discarded
|
|
119
148
|
outright, not treated as a leading gap: the interval before the burst
|
|
120
149
|
comes from the sender's timestamp instead.
|
|
121
150
|
|
|
122
|
-
The targeted transmit request (0x48) carries its timings
|
|
151
|
+
The targeted transmit request (0x48) carries its timings at this width,
|
|
123
152
|
so a capture replays by passing this across byte for byte, with the
|
|
124
153
|
declared count unchanged and the discarded first slot included. Its
|
|
125
154
|
header is a different struct and must be rebuilt, and its timestamp has
|
|
@@ -130,11 +159,18 @@ class FrameRCIr(FrameBase):
|
|
|
130
159
|
receiver applies its own tick length rather than the resolution
|
|
131
160
|
reported beside them.
|
|
132
161
|
'''
|
|
133
|
-
|
|
162
|
+
width = self.timing_width
|
|
163
|
+
nb = self.nb_timings
|
|
164
|
+
if (width is None) or (nb is None):
|
|
134
165
|
return bytes()
|
|
135
|
-
|
|
166
|
+
# Bounded by the timings the header counts, so a field appended behind
|
|
167
|
+
# the list does not arrive as extra timings.
|
|
168
|
+
pl = self.payload_idx(start=_TIMINGS_OFFSET, end=(_TIMINGS_OFFSET + (nb * width)))
|
|
169
|
+
if (pl is None):
|
|
136
170
|
return bytes()
|
|
137
|
-
|
|
171
|
+
if (width == _TIMING_WIDE):
|
|
172
|
+
return pl
|
|
173
|
+
return bytes([b for timing in pl for b in (timing, 0)])
|
|
138
174
|
|
|
139
175
|
def __str__(self) -> str:
|
|
140
176
|
return f"IR key press bay {self.bay} at {self.ir_timestamp} freq {self.frequency} timings {self.nb_timings}"
|
|
@@ -163,7 +163,25 @@ class FrameSignalStatusNew(FrameBase):
|
|
|
163
163
|
attributed to a bay and is dropped.
|
|
164
164
|
|
|
165
165
|
An empty payload requests a report from every device; a 16-byte payload
|
|
166
|
-
requests one from the unit it addresses.
|
|
166
|
+
requests one from the unit it addresses.
|
|
167
|
+
|
|
168
|
+
**On a decoder's output bay the signal description is a snapshot of the
|
|
169
|
+
routed source, taken when that stream last started.** Nothing refreshes it
|
|
170
|
+
while the route holds, so a source that changes format in place leaves
|
|
171
|
+
video, audio, infoframe and vsync all reading the format from before the
|
|
172
|
+
change - worse than an unfilled field, because the value stays plausible.
|
|
173
|
+
Re-routing the bay resamples them, unless the newly selected source's record
|
|
174
|
+
has not arrived yet, which leaves the previous values standing rather than
|
|
175
|
+
clearing them.
|
|
176
|
+
|
|
177
|
+
bay_status and scaling are re-stamped on every scaling change as well, so
|
|
178
|
+
they can be newer than the description beside them. clock_rate sits with
|
|
179
|
+
them but is computed from the snapshot and carries its age.
|
|
180
|
+
|
|
181
|
+
**For a decoder's current input format, read the input bay it is routed
|
|
182
|
+
to.** That record is refreshed by the source's own reports, and it is the
|
|
183
|
+
reason a decoder's own output bay can disagree with the picture on the
|
|
184
|
+
cable. '''
|
|
167
185
|
@cached_property
|
|
168
186
|
def signal_header_version(self) -> int:
|
|
169
187
|
if ((pl := self.payload_u16(0)) is not None):
|
|
@@ -236,7 +254,8 @@ class FrameSignalStatusNew(FrameBase):
|
|
|
236
254
|
|
|
237
255
|
@cached_property
|
|
238
256
|
def scaling(self) -> MxrSignalType|None:
|
|
239
|
-
'''
|
|
257
|
+
'''Format the bay is scaling to, unset while it is not scaling, or None
|
|
258
|
+
on a short report.'''
|
|
240
259
|
details = self.bay_details
|
|
241
260
|
if len(details) < 8:
|
|
242
261
|
return None
|
|
@@ -244,7 +263,11 @@ class FrameSignalStatusNew(FrameBase):
|
|
|
244
263
|
|
|
245
264
|
@cached_property
|
|
246
265
|
def clock_rate(self) -> int|None:
|
|
247
|
-
'''Video clock rate in Hz, or None on a short report.
|
|
266
|
+
'''Video clock rate in Hz, or None on a short report.
|
|
267
|
+
|
|
268
|
+
Computed rather than measured: the pixel clock of scaling while the bay
|
|
269
|
+
scales, and otherwise the signal description's own, halved for 4:2:0 and
|
|
270
|
+
scaled by colour depth.'''
|
|
248
271
|
details = self.bay_details
|
|
249
272
|
if len(details) < 12:
|
|
250
273
|
return None
|