mx-remote 8.0.2__tar.gz → 8.1.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-8.0.2 → mx_remote-8.1.0}/PKG-INFO +1 -1
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/api.md +12 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/oneip.md +25 -1
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/Interface.py +64 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/Interface.pyi +24 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/const.py +1 -1
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Constants.py +9 -3
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Constants.pyi +1 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Factory.py +3 -0
- mx_remote-8.1.0/mx_remote/proto/FrameV2IPPrivacy.py +83 -0
- mx_remote-8.1.0/mx_remote/proto/FrameV2IPPrivacy.pyi +22 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Device.py +25 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Device.pyi +6 -1
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Remote.py +11 -1
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Remote.pyi +2 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/handlers.py +1 -0
- mx_remote-8.1.0/tests/privacy.py +112 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/proto_stamp.py +1 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/run.py +1 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/smoke.py +1 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2ipsettings.py +10 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/.gitignore +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/AGENTS.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/CLAUDE.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/LICENSE +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/README.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/README.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/_static/brand.css +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/_static/favicon.svg +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/_static/logo-dark.svg +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/_static/logo.svg +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/audio.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/callbacks.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/conf.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/configuration.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/devices-and-bays.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/diagnostics.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/index.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/multiviewer.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/requirements.txt +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/docs/routing.md +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/hatch_build.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/Uid.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/Uid.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/__init__.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/__init__.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/compat.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/compat.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/const.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/deprecated.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/deprecated.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/main.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/main.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/BayConfig.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/BayConfig.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Data.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Data.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Factory.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAudioClip.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBase.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBase.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayConfig.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayContent.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayHide.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameDebug.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameDebug.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameDiscover.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameEDID.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameEDID.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameHeader.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameHeader.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameHello.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameHello.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameLinks.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameLinks.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FramePing.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FramePing.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FramePowerChange.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCAction.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCIr.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCKey.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCSettings.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameReboot.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameReboot.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetName.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetName.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetRoute.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTime.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTime.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTimeZone.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTopology.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTopology.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTxIR.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPOsdSync.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSinkSdp.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolume.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolume.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/LinkConfig.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/LinkConfig.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Multiviewer.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Multiviewer.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Svd.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/Svd.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/V2IPConfig.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/V2IPStats.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/V2IPStats.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/__init__.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/__init__.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/proto/svd.csv +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/py.typed +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Bay.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Bay.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/ConnectionAsync.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Link.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/Link.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/OsdSync.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/OsdSync.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/State.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/State.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/V2IP.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/mx_remote/remote/V2IP.pyi +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/pyproject.toml +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/audio_ir.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/audiolock.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/baycontent.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/capture.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/complete.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/deprecated.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/e2e.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/entry.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/handlers_cover.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/hello.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/idempotent.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/lengths.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/merge.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/mesh.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/meshbays.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/newops.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/offline.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/offsets.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/osdsync.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/ping.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/protogate.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/rcsettings.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/roundtrip.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/routes.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/sources.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/staterequest.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/stats.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/structs.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/txresult.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/unit.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/unknowns.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2ipptp.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2ipscaling.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2ipsdp.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2ipsubject.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2iptestcard.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/v2ipvlan.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/vdetails.py +0 -0
- {mx_remote-8.0.2 → mx_remote-8.1.0}/tests/wirefix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mx_remote
|
|
3
|
-
Version: 8.0
|
|
3
|
+
Version: 8.1.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/
|
|
@@ -240,6 +240,18 @@ What an output plays from a session description, and how it is timed.
|
|
|
240
240
|
.. autodata:: mx_remote.proto.Constants.ONEIP_SDP_MAX
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
## OneIP stream encryption
|
|
244
|
+
|
|
245
|
+
How a source unit encrypts its streams, as it reports it.
|
|
246
|
+
|
|
247
|
+
```{eval-rst}
|
|
248
|
+
.. autoclass:: mx_remote.OneIPPrivacy
|
|
249
|
+
:members:
|
|
250
|
+
|
|
251
|
+
.. autoclass:: mx_remote.OneIPPrivacyStream
|
|
252
|
+
:members:
|
|
253
|
+
```
|
|
254
|
+
|
|
243
255
|
## OneIP test card
|
|
244
256
|
|
|
245
257
|
A sink's test pattern, tone and lip-sync flash, and the ranges it accepts.
|
|
@@ -247,6 +247,10 @@ that takes a power save level switches it off, so a level write, this client's
|
|
|
247
247
|
or another sender's, clears it unless the same frame states it. Firmware
|
|
248
248
|
without the bit never reports it, and the switch is refused there.
|
|
249
249
|
|
|
250
|
+
`OneIPDeviceSetting.CLOUD` is a switch for whether a device connects to the
|
|
251
|
+
cloud service. A device that does not report it connects regardless, and the
|
|
252
|
+
switch is refused there.
|
|
253
|
+
|
|
250
254
|
### PTP
|
|
251
255
|
|
|
252
256
|
A device's PTP mode, domain and grandmaster priority1 are settings like the
|
|
@@ -408,10 +412,30 @@ await mx.clear_osd_sync() # stop answering for it
|
|
|
408
412
|
Each unit asks for the settings it lacks and takes only those, so this client
|
|
409
413
|
holds the content and answers until it is replaced or cleared, announcing
|
|
410
414
|
itself meanwhile as the mesh's overlay sync manager: a unit waking from power
|
|
411
|
-
save then takes this content rather than the controller's.
|
|
415
|
+
save then takes this content rather than the controller's. The controller
|
|
416
|
+
still answers a unit that asks for settings without naming whose sync they are
|
|
417
|
+
from, as an older overlay module does, with its own sync, which replaces this
|
|
418
|
+
client's on that unit. Logo images are
|
|
412
419
|
served over HTTP from this client, on `logo_port` or a free port. Content a unit
|
|
413
420
|
would refuse is refused before anything is sent; see `send_osd_sync`.
|
|
414
421
|
|
|
422
|
+
### Stream encryption
|
|
423
|
+
|
|
424
|
+
A source unit from protocol 0x30 encrypts its video, audio and ancillary
|
|
425
|
+
streams, and reports what a receiver needs to decrypt them whenever that
|
|
426
|
+
changes and at least once a minute. The key itself never travels: every unit of
|
|
427
|
+
a mesh holds the same one, and this library holds none.
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
await source.request_oneip_privacy() # report now
|
|
431
|
+
await mx.request_all_oneip_privacy() # every source unit of the mesh
|
|
432
|
+
|
|
433
|
+
print(source.oneip_privacy) # None until the unit has reported
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
A request to a unit that is not a source, or that is below protocol 0x30, is
|
|
437
|
+
refused before it is sent.
|
|
438
|
+
|
|
415
439
|
### Every device at once
|
|
416
440
|
|
|
417
441
|
One broadcast changes settings on every OneIP device of the mesh. Each device
|
|
@@ -2471,6 +2471,59 @@ class OneIPSinkSdp:
|
|
|
2471
2471
|
return (f"'{self.name}' {self.group}:{self.port} {encoding} {self.channels}ch {self.rate_hz}Hz"
|
|
2472
2472
|
f"{' audio only' if (OneIPSdpFlag.AUDIO_ONLY in self.flags) else ''}")
|
|
2473
2473
|
|
|
2474
|
+
@dataclass(frozen=True)
|
|
2475
|
+
class OneIPPrivacyStream:
|
|
2476
|
+
"""
|
|
2477
|
+
How one of a OneIP source unit's streams is encrypted, as the unit reports
|
|
2478
|
+
it: the values of the stream's ``a=privacy`` and ``a=extmap`` session
|
|
2479
|
+
description attributes.
|
|
2480
|
+
|
|
2481
|
+
A receiver needs these to decrypt the stream. The key itself never travels:
|
|
2482
|
+
every unit of a mesh holds the same one. This library holds no key and
|
|
2483
|
+
decrypts nothing.
|
|
2484
|
+
"""
|
|
2485
|
+
group: str = '0.0.0.0'
|
|
2486
|
+
"""The stream's destination address."""
|
|
2487
|
+
port: int = 0
|
|
2488
|
+
"""The stream's UDP port."""
|
|
2489
|
+
version: int = 0
|
|
2490
|
+
"""Steps whenever the parameters change."""
|
|
2491
|
+
on: bool = False
|
|
2492
|
+
"""Whether the stream is encrypted. Without it the stream goes out in the
|
|
2493
|
+
clear, and the fields below mean nothing."""
|
|
2494
|
+
full_id: int = 0
|
|
2495
|
+
"""The RTP header extension id of the full IV counter."""
|
|
2496
|
+
short_id: int = 0
|
|
2497
|
+
"""The RTP header extension id of the short IV counter."""
|
|
2498
|
+
iv: bytes = bytes(8)
|
|
2499
|
+
"""The initialisation vector, 8 bytes."""
|
|
2500
|
+
key_generator: bytes = bytes(16)
|
|
2501
|
+
"""The key generator, 16 bytes."""
|
|
2502
|
+
key_id: bytes = bytes(8)
|
|
2503
|
+
"""Names the mesh's key, 8 bytes. A receiver holding another drops the stream."""
|
|
2504
|
+
key_version: int = 0
|
|
2505
|
+
"""The key version."""
|
|
2506
|
+
|
|
2507
|
+
def __str__(self) -> str:
|
|
2508
|
+
if not self.on:
|
|
2509
|
+
return f"{self.group}:{self.port} clear"
|
|
2510
|
+
return f"{self.group}:{self.port} encrypted, key {self.key_id.hex()} v{self.key_version}"
|
|
2511
|
+
|
|
2512
|
+
@dataclass(frozen=True)
|
|
2513
|
+
class OneIPPrivacy:
|
|
2514
|
+
"""
|
|
2515
|
+
How a OneIP source unit's streams are encrypted, as it last reported it. A
|
|
2516
|
+
unit reports this whenever it changes, at least once a minute, and when
|
|
2517
|
+
asked. A unit that is in no mesh sends in the clear.
|
|
2518
|
+
"""
|
|
2519
|
+
video: OneIPPrivacyStream
|
|
2520
|
+
audio: OneIPPrivacyStream
|
|
2521
|
+
anc: OneIPPrivacyStream
|
|
2522
|
+
"""The ancillary data stream."""
|
|
2523
|
+
|
|
2524
|
+
def __str__(self) -> str:
|
|
2525
|
+
return f"video {self.video}, audio {self.audio}, anc {self.anc}"
|
|
2526
|
+
|
|
2474
2527
|
@dataclass(frozen=True)
|
|
2475
2528
|
class OneIPTestTone:
|
|
2476
2529
|
"""A OneIP sink's test tone."""
|
|
@@ -3459,6 +3512,17 @@ class DeviceBase(ABC):
|
|
|
3459
3512
|
reported it, None until it has. It reports when that changes and at
|
|
3460
3513
|
least once a minute.'''
|
|
3461
3514
|
|
|
3515
|
+
@property
|
|
3516
|
+
@abstractmethod
|
|
3517
|
+
def oneip_privacy(self) -> OneIPPrivacy|None:
|
|
3518
|
+
'''How this OneIP source unit's streams are encrypted, as it last
|
|
3519
|
+
reported it, None until it has. It reports when that changes and at
|
|
3520
|
+
least once a minute.'''
|
|
3521
|
+
|
|
3522
|
+
@abstractmethod
|
|
3523
|
+
async def request_oneip_privacy(self) -> bool:
|
|
3524
|
+
'''ask this OneIP source unit how its streams are encrypted now'''
|
|
3525
|
+
|
|
3462
3526
|
@abstractmethod
|
|
3463
3527
|
async def play_oneip_sdp(self, sdp:str) -> bool:
|
|
3464
3528
|
'''have this OneIP output play the stream a session description describes'''
|
|
@@ -891,6 +891,25 @@ class OneIPSinkSdp:
|
|
|
891
891
|
@property
|
|
892
892
|
def active(self) -> bool: ...
|
|
893
893
|
|
|
894
|
+
@dataclass(frozen=True)
|
|
895
|
+
class OneIPPrivacyStream:
|
|
896
|
+
group: str = ...
|
|
897
|
+
port: int = ...
|
|
898
|
+
version: int = ...
|
|
899
|
+
on: bool = ...
|
|
900
|
+
full_id: int = ...
|
|
901
|
+
short_id: int = ...
|
|
902
|
+
iv: bytes = ...
|
|
903
|
+
key_generator: bytes = ...
|
|
904
|
+
key_id: bytes = ...
|
|
905
|
+
key_version: int = ...
|
|
906
|
+
|
|
907
|
+
@dataclass(frozen=True)
|
|
908
|
+
class OneIPPrivacy:
|
|
909
|
+
video: OneIPPrivacyStream
|
|
910
|
+
audio: OneIPPrivacyStream
|
|
911
|
+
anc: OneIPPrivacyStream
|
|
912
|
+
|
|
894
913
|
@dataclass(frozen=True)
|
|
895
914
|
class OneIPTestTone:
|
|
896
915
|
mode: OneIPToneMode | int = ...
|
|
@@ -1306,6 +1325,11 @@ class DeviceBase(ABC):
|
|
|
1306
1325
|
@property
|
|
1307
1326
|
@abstractmethod
|
|
1308
1327
|
def oneip_sink_sdp(self) -> OneIPSinkSdp | None: ...
|
|
1328
|
+
@property
|
|
1329
|
+
@abstractmethod
|
|
1330
|
+
def oneip_privacy(self) -> OneIPPrivacy | None: ...
|
|
1331
|
+
@abstractmethod
|
|
1332
|
+
async def request_oneip_privacy(self) -> bool: ...
|
|
1309
1333
|
@abstractmethod
|
|
1310
1334
|
async def play_oneip_sdp(self, sdp: str) -> bool: ...
|
|
1311
1335
|
@abstractmethod
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
from enum import IntEnum, IntFlag
|
|
11
11
|
from typing import TypeVar
|
|
12
12
|
|
|
13
|
-
MXR_PROTOCOL_VERSION =
|
|
13
|
+
MXR_PROTOCOL_VERSION = 0x30
|
|
14
14
|
"""Highest mx_remote protocol version this library understands.
|
|
15
15
|
|
|
16
16
|
Mirrors the firmware's own protocol version. Bump
|
|
@@ -31,7 +31,8 @@ every 20-30s once every device it knows is on 0x2A.
|
|
|
31
31
|
0x2B.
|
|
32
32
|
|
|
33
33
|
0x2E is the row of V2IP_SINK_SDP, so an output's session description report
|
|
34
|
-
arrives stamped 0x2E,
|
|
34
|
+
arrives stamped 0x2E, 0x2F the row of V2IP_OSD_SYNC and 0x30 the row of
|
|
35
|
+
V2IP_PRIVACY."""
|
|
35
36
|
|
|
36
37
|
MXR_PROTOCOL_VERSION_SLOW_HELLO = 0x2A
|
|
37
38
|
"""First protocol version whose devices may hello only every 20-30s."""
|
|
@@ -121,6 +122,7 @@ MXR_OPCODE_VERSIONS: dict[int, int] = {
|
|
|
121
122
|
0x51: 0x2D, # BAY_CONTENT
|
|
122
123
|
0x52: 0x2E, # V2IP_SINK_SDP
|
|
123
124
|
0x53: 0x2F, # V2IP_OSD_SYNC
|
|
125
|
+
0x54: 0x30, # V2IP_PRIVACY
|
|
124
126
|
}
|
|
125
127
|
"""Per-opcode minimum compatible protocol version, mirroring the firmware's
|
|
126
128
|
own opcode table. Transmitters stamp the
|
|
@@ -374,9 +376,13 @@ class OneIPDeviceSetting(IntFlag):
|
|
|
374
376
|
STREAM_TTL = (1 << 29)
|
|
375
377
|
'''The TTL of the device's streams, video, audio and ancillary alike.
|
|
376
378
|
Reported only by a device with an encoder.'''
|
|
379
|
+
CLOUD = (1 << 30)
|
|
380
|
+
'''Connects to the cloud service. A device that does not report it
|
|
381
|
+
connects regardless.'''
|
|
377
382
|
|
|
378
383
|
ONEIP_DEVICE_SETTING_SWITCHES = OneIPDeviceSetting(0xFF) | OneIPDeviceSetting.AES67 \
|
|
379
|
-
| OneIPDeviceSetting.SAP_LISTEN | OneIPDeviceSetting.SAP_ANNOUNCE | OneIPDeviceSetting.POWER_SAVE_DISABLED
|
|
384
|
+
| OneIPDeviceSetting.SAP_LISTEN | OneIPDeviceSetting.SAP_ANNOUNCE | OneIPDeviceSetting.POWER_SAVE_DISABLED \
|
|
385
|
+
| OneIPDeviceSetting.CLOUD
|
|
380
386
|
"""The settings that are on or off, as opposed to carrying a value, and that
|
|
381
387
|
can be written."""
|
|
382
388
|
|
|
@@ -246,6 +246,9 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
|
|
|
246
246
|
if hdr.opcode == 0x53:
|
|
247
247
|
from .FrameV2IPOsdSync import FrameV2IPOsdSync
|
|
248
248
|
return FrameV2IPOsdSync(header=hdr, timestamp=timestamp)
|
|
249
|
+
if hdr.opcode == 0x54:
|
|
250
|
+
from .FrameV2IPPrivacy import FrameV2IPPrivacy
|
|
251
|
+
return FrameV2IPPrivacy(header=hdr, timestamp=timestamp)
|
|
249
252
|
if hdr.opcode == 0x50:
|
|
250
253
|
from .FrameUnitCommand import FrameUnitCommand
|
|
251
254
|
return FrameUnitCommand(header=hdr, timestamp=timestamp)
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
'''Protocol frame for V2IP_PRIVACY (opcode 0x54): how a OneIP source unit
|
|
8
|
+
encrypts its streams, and the request that asks it.'''
|
|
9
|
+
|
|
10
|
+
from functools import cached_property
|
|
11
|
+
import ipaddress
|
|
12
|
+
import struct
|
|
13
|
+
from typing import Any
|
|
14
|
+
from ..compat import override
|
|
15
|
+
from .FrameBase import FrameBase
|
|
16
|
+
from ..Interface import DeviceRegistry, OneIPPrivacy, OneIPPrivacyStream
|
|
17
|
+
from ..Uid import MxrDeviceUid
|
|
18
|
+
|
|
19
|
+
# Every frame opens with a uid, the frame type at 16, the count of streams that
|
|
20
|
+
# follow at 17 and two reserved bytes.
|
|
21
|
+
#
|
|
22
|
+
# A report follows with its streams, video, audio and ancillary data first, each
|
|
23
|
+
# PRIVACY_STREAM_SIZE bytes: the IPv4 group at 0, u16 port at 4, the version at
|
|
24
|
+
# 6, a reserved byte, then whether it is encrypted at 8, the two header extension
|
|
25
|
+
# ids at 9 and 10, the 8-byte IV at 11, the 16-byte key generator at 19, the
|
|
26
|
+
# 8-byte key id at 35 and u32 key version at 43, unaligned, and a reserved byte.
|
|
27
|
+
PRIVACY_HEADER_SIZE = 20
|
|
28
|
+
PRIVACY_STREAM_SIZE = 48
|
|
29
|
+
PRIVACY_STREAMS = 3
|
|
30
|
+
|
|
31
|
+
PRIVACY_REPORT = 0
|
|
32
|
+
'''A source unit's report.'''
|
|
33
|
+
PRIVACY_REQUEST = 1
|
|
34
|
+
'''Report now.'''
|
|
35
|
+
|
|
36
|
+
def _stream(s:bytes) -> OneIPPrivacyStream:
|
|
37
|
+
port, = struct.unpack_from('<H', s, 4)
|
|
38
|
+
key_version, = struct.unpack_from('<I', s, 43)
|
|
39
|
+
return OneIPPrivacyStream(group=str(ipaddress.IPv4Address(s[0:4])), port=port, version=s[6],
|
|
40
|
+
on=(s[8] != 0), full_id=s[9], short_id=s[10], iv=bytes(s[11:19]),
|
|
41
|
+
key_generator=bytes(s[19:35]), key_id=bytes(s[35:43]),
|
|
42
|
+
key_version=key_version)
|
|
43
|
+
|
|
44
|
+
class FrameV2IPPrivacy(FrameBase):
|
|
45
|
+
'''A OneIP source unit's report of how it encrypts its streams, or a request
|
|
46
|
+
for one.'''
|
|
47
|
+
@staticmethod
|
|
48
|
+
def construct_request(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid|None) -> FrameBase|None:
|
|
49
|
+
'''A request naming the source unit asked, or the zero uid for every one.'''
|
|
50
|
+
uid = target_uid.byte_value if (target_uid is not None) else bytes(16)
|
|
51
|
+
payload = uid + bytes([PRIVACY_REQUEST, 0, 0, 0])
|
|
52
|
+
return FrameBase.construct_base(target=target, mxr=mxr, opcode=0x54, payload=payload)
|
|
53
|
+
|
|
54
|
+
@cached_property
|
|
55
|
+
def kind(self) -> int|None:
|
|
56
|
+
'''The frame type, None for a frame shorter than its header.'''
|
|
57
|
+
if (len(self.payload or b'') < PRIVACY_HEADER_SIZE):
|
|
58
|
+
return None
|
|
59
|
+
return self.payload_u8(16)
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def privacy(self) -> OneIPPrivacy|None:
|
|
63
|
+
'''What the unit reports, None for a request and for a report missing
|
|
64
|
+
any of the three streams.'''
|
|
65
|
+
p = self.payload or b''
|
|
66
|
+
if (self.kind != PRIVACY_REPORT) or (p[17] < PRIVACY_STREAMS) \
|
|
67
|
+
or (len(p) < PRIVACY_HEADER_SIZE + PRIVACY_STREAMS * PRIVACY_STREAM_SIZE):
|
|
68
|
+
return None
|
|
69
|
+
at = lambda i: _stream(p[PRIVACY_HEADER_SIZE + i * PRIVACY_STREAM_SIZE:])
|
|
70
|
+
return OneIPPrivacy(video=at(0), audio=at(1), anc=at(2))
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
def process(self) -> None:
|
|
74
|
+
'''Record a report against the unit that sent it, whatever uid its
|
|
75
|
+
header names, as units file it. A request is for a unit with an encoder
|
|
76
|
+
to answer, which this client is not.'''
|
|
77
|
+
if ((privacy := self.privacy) is not None) and ((dev := self.remote_device) is not None):
|
|
78
|
+
dev.on_mxr_update(privacy)
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
if (self.privacy is not None):
|
|
82
|
+
return f"OneIP privacy: {self.privacy}"
|
|
83
|
+
return f"OneIP privacy request for {self.uid_to_user_string(self.payload_uuid(0))}"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from ..Interface import DeviceRegistry as DeviceRegistry, OneIPPrivacy as OneIPPrivacy, OneIPPrivacyStream as OneIPPrivacyStream
|
|
2
|
+
from ..Uid import MxrDeviceUid as MxrDeviceUid
|
|
3
|
+
from ..compat import override as override
|
|
4
|
+
from .FrameBase import FrameBase as FrameBase
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
PRIVACY_HEADER_SIZE: int
|
|
9
|
+
PRIVACY_STREAM_SIZE: int
|
|
10
|
+
PRIVACY_STREAMS: int
|
|
11
|
+
PRIVACY_REPORT: int
|
|
12
|
+
PRIVACY_REQUEST: int
|
|
13
|
+
|
|
14
|
+
class FrameV2IPPrivacy(FrameBase):
|
|
15
|
+
@staticmethod
|
|
16
|
+
def construct_request(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid | None) -> FrameBase | None: ...
|
|
17
|
+
@cached_property
|
|
18
|
+
def kind(self) -> int | None: ...
|
|
19
|
+
@cached_property
|
|
20
|
+
def privacy(self) -> OneIPPrivacy | None: ...
|
|
21
|
+
@override
|
|
22
|
+
def process(self) -> None: ...
|
|
@@ -31,6 +31,7 @@ from ..Interface import (
|
|
|
31
31
|
OneIPVlan,
|
|
32
32
|
OneIPTestcard,
|
|
33
33
|
OneIPSinkSdp,
|
|
34
|
+
OneIPPrivacy,
|
|
34
35
|
OsdSync,
|
|
35
36
|
OsdSyncReport,
|
|
36
37
|
OneIPTestSync,
|
|
@@ -89,6 +90,7 @@ from ..proto.Constants import (MXR_PROTOCOL_VERSION_SLOW_HELLO, BayFeaturesMask,
|
|
|
89
90
|
from ..proto.FrameUnitCommand import FrameUnitCommand
|
|
90
91
|
from ..proto.FrameV2IPAudio import AUDIO_LAYOUT_SINCE, FrameV2IPAudio
|
|
91
92
|
from ..proto.FrameV2IPDeviceConfiguration import FrameV2IPDeviceConfiguration
|
|
93
|
+
from ..proto.FrameV2IPPrivacy import FrameV2IPPrivacy
|
|
92
94
|
from ..proto.FrameV2IPSinkSdp import FrameV2IPSinkSdp, SDP_REQUEST, SDP_SCREEN, SDP_STOP
|
|
93
95
|
from ..proto.FrameV2IPTestcard import (FrameV2IPTestcard, TESTCARD_REQUEST, TESTCARD_SET,
|
|
94
96
|
TESTCARD_PART_PATTERN, TESTCARD_PART_TONE, TESTCARD_PART_SYNC)
|
|
@@ -171,6 +173,7 @@ class Device(DeviceBase):
|
|
|
171
173
|
self._unit_status:UnitStatus|None = None
|
|
172
174
|
self._v2ip_testcard:OneIPTestcard|None = None
|
|
173
175
|
self._v2ip_sink_sdp:OneIPSinkSdp|None = None
|
|
176
|
+
self._v2ip_privacy:OneIPPrivacy|None = None
|
|
174
177
|
self._osd_sync:OsdSync|None = None
|
|
175
178
|
self._osd_sync_report:OsdSyncReport|None = None
|
|
176
179
|
self._time_zone:TimeZone|None = None
|
|
@@ -1060,6 +1063,10 @@ class Device(DeviceBase):
|
|
|
1060
1063
|
if (self._v2ip_sink_sdp != data):
|
|
1061
1064
|
self._v2ip_sink_sdp = data
|
|
1062
1065
|
self.call_callbacks()
|
|
1066
|
+
elif isinstance(data, OneIPPrivacy):
|
|
1067
|
+
if (self._v2ip_privacy != data):
|
|
1068
|
+
self._v2ip_privacy = data
|
|
1069
|
+
self.call_callbacks()
|
|
1063
1070
|
elif isinstance(data, TimeZone):
|
|
1064
1071
|
if (self._time_zone != data):
|
|
1065
1072
|
self._time_zone = data
|
|
@@ -1744,6 +1751,24 @@ class Device(DeviceBase):
|
|
|
1744
1751
|
play_oneip_sdp().'''
|
|
1745
1752
|
return self._send_sink_sdp_command(SDP_REQUEST, 0)
|
|
1746
1753
|
|
|
1754
|
+
@property
|
|
1755
|
+
@override
|
|
1756
|
+
def oneip_privacy(self) -> OneIPPrivacy|None:
|
|
1757
|
+
return self._v2ip_privacy
|
|
1758
|
+
|
|
1759
|
+
async def request_oneip_privacy(self) -> bool:
|
|
1760
|
+
'''Ask this OneIP source unit how its streams are encrypted now, rather
|
|
1761
|
+
than at its next report, which comes at least once a minute and whenever
|
|
1762
|
+
they change. Refused for a device that is not a OneIP source, and for
|
|
1763
|
+
one below protocol 0x30, which predates the report.'''
|
|
1764
|
+
if (self.features is None) or (DeviceFeature.ONEIP_SOURCE not in self.features):
|
|
1765
|
+
_LOGGER.warning(f"not asking {self} how its streams are encrypted: it is not a OneIP source")
|
|
1766
|
+
return False
|
|
1767
|
+
frame = FrameV2IPPrivacy.construct_request(mxr=self.registry, target=self, target_uid=self.remote_id)
|
|
1768
|
+
if (frame is None):
|
|
1769
|
+
return False
|
|
1770
|
+
return self.registry.transmit(frame.frame) == len(frame.frame)
|
|
1771
|
+
|
|
1747
1772
|
def _send_sink_sdp_command(self, kind:int, value:int) -> bool:
|
|
1748
1773
|
'''The one send behind the session description commands.'''
|
|
1749
1774
|
if not self.is_oneip_sink:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ..Interface import AmpDolbySettings as AmpDolbySettings, AudioChangeSource as AudioChangeSource, AudioEndpoint as AudioEndpoint, AudioEndpoints as AudioEndpoints, AudioLinks as AudioLinks, BayBase as BayBase, DeviceBase as DeviceBase, DeviceClock as DeviceClock, DeviceOneIPDetails as DeviceOneIPDetails, DeviceOneIPScalingSettings as DeviceOneIPScalingSettings, DeviceOneIPSink as DeviceOneIPSink, DeviceRegistry as DeviceRegistry, DeviceStatus as DeviceStatus, Multiviewer as Multiviewer, MxrCallbacks as MxrCallbacks, OneIPDeviceSettings as OneIPDeviceSettings, OneIPOutputMode as OneIPOutputMode, OneIPPowerSaveSchedule as OneIPPowerSaveSchedule, OneIPPtpMessages as OneIPPtpMessages, OneIPPtpState as OneIPPtpState, OneIPScalingSettings as OneIPScalingSettings, OneIPSinkSdp as OneIPSinkSdp, OneIPStreamSources as OneIPStreamSources, OneIPStreamSourcesList as OneIPStreamSourcesList, OneIPTestSync as OneIPTestSync, OneIPTestTone as OneIPTestTone, OneIPTestcard as OneIPTestcard, OneIPVlan as OneIPVlan, OsdSync as OsdSync, OsdSyncReport as OsdSyncReport, SystemTemperature as SystemTemperature, TimeZone as TimeZone, UnitNetConfig as UnitNetConfig, UnitStatus as UnitStatus
|
|
1
|
+
from ..Interface import AmpDolbySettings as AmpDolbySettings, AudioChangeSource as AudioChangeSource, AudioEndpoint as AudioEndpoint, AudioEndpoints as AudioEndpoints, AudioLinks as AudioLinks, BayBase as BayBase, DeviceBase as DeviceBase, DeviceClock as DeviceClock, DeviceOneIPDetails as DeviceOneIPDetails, DeviceOneIPScalingSettings as DeviceOneIPScalingSettings, DeviceOneIPSink as DeviceOneIPSink, DeviceRegistry as DeviceRegistry, DeviceStatus as DeviceStatus, Multiviewer as Multiviewer, MxrCallbacks as MxrCallbacks, OneIPDeviceSettings as OneIPDeviceSettings, OneIPOutputMode as OneIPOutputMode, OneIPPowerSaveSchedule as OneIPPowerSaveSchedule, OneIPPrivacy as OneIPPrivacy, OneIPPtpMessages as OneIPPtpMessages, OneIPPtpState as OneIPPtpState, OneIPScalingSettings as OneIPScalingSettings, OneIPSinkSdp as OneIPSinkSdp, OneIPStreamSources as OneIPStreamSources, OneIPStreamSourcesList as OneIPStreamSourcesList, OneIPTestSync as OneIPTestSync, OneIPTestTone as OneIPTestTone, OneIPTestcard as OneIPTestcard, OneIPVlan as OneIPVlan, OsdSync as OsdSync, OsdSyncReport as OsdSyncReport, SystemTemperature as SystemTemperature, TimeZone as TimeZone, UnitNetConfig as UnitNetConfig, UnitStatus as UnitStatus
|
|
2
2
|
from ..Uid import MxrDeviceUid as MxrDeviceUid
|
|
3
3
|
from ..compat import override as override
|
|
4
4
|
from ..const import MXR_CONFIG_TIMEOUT as MXR_CONFIG_TIMEOUT
|
|
@@ -18,6 +18,7 @@ from ..proto.FrameV2IPAudio import AUDIO_LAYOUT_SINCE as AUDIO_LAYOUT_SINCE, Fra
|
|
|
18
18
|
from ..proto.FrameV2IPBayMapping import FrameV2IPBayMapping as FrameV2IPBayMapping
|
|
19
19
|
from ..proto.FrameV2IPDeviceConfiguration import FrameV2IPDeviceConfiguration as FrameV2IPDeviceConfiguration
|
|
20
20
|
from ..proto.FrameV2IPMultiviewer import FrameV2IPMultiviewer as FrameV2IPMultiviewer, V2IPMultiviewerConfig as V2IPMultiviewerConfig
|
|
21
|
+
from ..proto.FrameV2IPPrivacy import FrameV2IPPrivacy as FrameV2IPPrivacy
|
|
21
22
|
from ..proto.FrameV2IPSinkSdp import FrameV2IPSinkSdp as FrameV2IPSinkSdp, SDP_REQUEST as SDP_REQUEST, SDP_SCREEN as SDP_SCREEN, SDP_STOP as SDP_STOP
|
|
22
23
|
from ..proto.FrameV2IPStats import FrameV2IPStats as FrameV2IPStats
|
|
23
24
|
from ..proto.FrameV2IPTestcard import FrameV2IPTestcard as FrameV2IPTestcard, TESTCARD_PART_PATTERN as TESTCARD_PART_PATTERN, TESTCARD_PART_SYNC as TESTCARD_PART_SYNC, TESTCARD_PART_TONE as TESTCARD_PART_TONE, TESTCARD_REQUEST as TESTCARD_REQUEST, TESTCARD_SET as TESTCARD_SET
|
|
@@ -286,6 +287,10 @@ class Device(DeviceBase):
|
|
|
286
287
|
async def stop_oneip_sdp(self) -> bool: ...
|
|
287
288
|
async def set_oneip_sdp_screen(self, screen: int) -> bool: ...
|
|
288
289
|
async def request_oneip_sink_sdp(self) -> bool: ...
|
|
290
|
+
@property
|
|
291
|
+
@override
|
|
292
|
+
def oneip_privacy(self) -> OneIPPrivacy | None: ...
|
|
293
|
+
async def request_oneip_privacy(self) -> bool: ...
|
|
289
294
|
def __eq__(self, other: Any) -> bool: ...
|
|
290
295
|
|
|
291
296
|
class MultiviewerImpl(Multiviewer):
|
|
@@ -31,6 +31,7 @@ from ..proto.FramePing import FramePing
|
|
|
31
31
|
from ..proto.FrameBase import FrameBase
|
|
32
32
|
from ..proto.FrameTime import FrameTime
|
|
33
33
|
from ..proto.FrameTimeZone import FrameTimeZone
|
|
34
|
+
from ..proto.FrameV2IPPrivacy import FrameV2IPPrivacy
|
|
34
35
|
from ..proto.FrameV2IPSettingsAll import FrameV2IPSettingsAll
|
|
35
36
|
from ..proto.FrameV2IPOsdSync import FrameV2IPOsdSync
|
|
36
37
|
from .OsdSync import LogoServer, OsdSender, PASS_GAP, osd_content_refusal
|
|
@@ -529,6 +530,12 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
|
|
|
529
530
|
return False
|
|
530
531
|
return self._send_to_all(FrameV2IPSettingsAll.construct(mxr=self, settings=settings))
|
|
531
532
|
|
|
533
|
+
async def request_all_oneip_privacy(self) -> bool:
|
|
534
|
+
'''Ask every OneIP source unit of the mesh how its streams are
|
|
535
|
+
encrypted, as Device.request_oneip_privacy() asks one. A unit below
|
|
536
|
+
protocol 0x30 ignores it.'''
|
|
537
|
+
return self._send_to_all(FrameV2IPPrivacy.construct_request(mxr=self, target=None, target_uid=None))
|
|
538
|
+
|
|
532
539
|
async def set_mesh_time_zone(self, zone:str, rule:str) -> bool:
|
|
533
540
|
'''Set the time zone of every device that hears it.
|
|
534
541
|
|
|
@@ -692,7 +699,10 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
|
|
|
692
699
|
it announces itself as the mesh's overlay sync manager, so a unit waking
|
|
693
700
|
from power save takes this content too, rather than the controller's.
|
|
694
701
|
The controller can still send its own over it; the last sync a unit
|
|
695
|
-
took is what it shows.
|
|
702
|
+
took is what it shows. It does so whenever a unit asks for settings
|
|
703
|
+
without naming whose sync they are from, as an older overlay module
|
|
704
|
+
does, and nothing this client sends prevents that. Each unit's
|
|
705
|
+
osd_sync_report says how it went.
|
|
696
706
|
|
|
697
707
|
Refused for content a unit would refuse: copying nothing, more than
|
|
698
708
|
OSD_SYNC_RECORDS_MAX settings, a caption font over OSD_SYNC_FONT_MAX or
|
|
@@ -12,6 +12,7 @@ from ..proto.FramePing import FramePing as FramePing
|
|
|
12
12
|
from ..proto.FrameTime import FrameTime as FrameTime
|
|
13
13
|
from ..proto.FrameTimeZone import FrameTimeZone as FrameTimeZone
|
|
14
14
|
from ..proto.FrameV2IPOsdSync import FrameV2IPOsdSync as FrameV2IPOsdSync
|
|
15
|
+
from ..proto.FrameV2IPPrivacy import FrameV2IPPrivacy as FrameV2IPPrivacy
|
|
15
16
|
from ..proto.FrameV2IPSettingsAll import FrameV2IPSettingsAll as FrameV2IPSettingsAll
|
|
16
17
|
from ..proto.Svd import SvdMap as SvdMap
|
|
17
18
|
from .ConnectionAsync import ConnectionAsync as ConnectionAsync
|
|
@@ -72,6 +73,7 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
|
|
|
72
73
|
def transmit(self, data: bytes) -> int: ...
|
|
73
74
|
def tx_discover(self) -> int: ...
|
|
74
75
|
async def set_all_oneip_device_settings(self, settings: OneIPDeviceSettings) -> bool: ...
|
|
76
|
+
async def request_all_oneip_privacy(self) -> bool: ...
|
|
75
77
|
async def set_mesh_time_zone(self, zone: str, rule: str) -> bool: ...
|
|
76
78
|
async def clear_mesh_time_zone(self) -> bool: ...
|
|
77
79
|
async def set_mesh_time(self, when: datetime | None = None) -> bool: ...
|
|
@@ -81,6 +81,7 @@ BASELINE = {
|
|
|
81
81
|
'FrameV2IPManualSourceSwitch.FrameV2IPManualSourceSwitch',
|
|
82
82
|
'FrameV2IPMultiviewer.FrameV2IPMultiviewer',
|
|
83
83
|
'FrameV2IPPowerSave.FrameV2IPPowerSave',
|
|
84
|
+
'FrameV2IPPrivacy.FrameV2IPPrivacy',
|
|
84
85
|
'FrameV2IPSinkSdp.FrameV2IPSinkSdp',
|
|
85
86
|
'FrameV2IPSourceSwitch.FrameV2IPSourceSwitch',
|
|
86
87
|
'FrameV2IPOsdSync.FrameV2IPOsdSync',
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
'''How a OneIP source unit encrypts its streams (0x54), and asking it.
|
|
8
|
+
|
|
9
|
+
Every field of every stream is distinct from the others and from stream to
|
|
10
|
+
stream, with a port and a key version too wide for a narrower read and the
|
|
11
|
+
reserved bytes set, so a field read at a neighbour's offset shows.
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
import asyncio, os, struct, sys, logging
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
16
|
+
logging.disable(logging.CRITICAL)
|
|
17
|
+
import mx_remote
|
|
18
|
+
from mx_remote import DeviceFeature, MxrDeviceUid, OneIPPrivacyStream
|
|
19
|
+
from mx_remote.proto.Factory import create_mxr_frame
|
|
20
|
+
|
|
21
|
+
ADDR = ('192.0.2.9', 8812)
|
|
22
|
+
mx = mx_remote.Remote(open_connection=False)
|
|
23
|
+
mx._uid = bytes(range(0x20, 0x30))
|
|
24
|
+
sent = []
|
|
25
|
+
mx.transmit = lambda data: (sent.append(bytes(data)), len(data))[1]
|
|
26
|
+
|
|
27
|
+
def uid(n):
|
|
28
|
+
return bytes([n]) * 16
|
|
29
|
+
|
|
30
|
+
def name(s, sz=16):
|
|
31
|
+
b = s.encode('ascii')[:sz]
|
|
32
|
+
return b + bytes(sz - len(b))
|
|
33
|
+
|
|
34
|
+
def rx(sender, opcode, payload=b'', protocol=0x30):
|
|
35
|
+
frame = bytearray(create_mxr_frame(sender, opcode, payload))
|
|
36
|
+
frame[2] = protocol
|
|
37
|
+
mx.process_frame(0.0, bytes(frame), ADDR)
|
|
38
|
+
|
|
39
|
+
def hello(sender, protocol, features):
|
|
40
|
+
rx(sender, 0x00, struct.pack('<H', protocol) + name('ONEIP') + name('P8SN%08d' % sender[0])
|
|
41
|
+
+ name('5.0.0') + struct.pack('<I', int(features)), protocol=0x01)
|
|
42
|
+
return mx.get_by_uid(MxrDeviceUid(sender))
|
|
43
|
+
|
|
44
|
+
def call(coro):
|
|
45
|
+
return asyncio.run(coro)
|
|
46
|
+
|
|
47
|
+
def stream(n):
|
|
48
|
+
'''Stream n; stream 2 is in the clear.'''
|
|
49
|
+
s = bytes([239, 0, 0, n]) + struct.pack('<H', 5000 + n * 0x101) + bytes([0x10 + n, 0xEE])
|
|
50
|
+
s += bytes([int(n != 2), 0x20 + n, 0x30 + n])
|
|
51
|
+
s += bytes(0x40 + n * 16 + i for i in range(8))
|
|
52
|
+
s += bytes(0x80 + n * 16 + i for i in range(16))
|
|
53
|
+
s += bytes(0xC0 + n * 8 + i for i in range(8))
|
|
54
|
+
s += struct.pack('<I', 0x01020304 * n) + bytes([0xEE])
|
|
55
|
+
assert len(s) == 48, len(s)
|
|
56
|
+
return s
|
|
57
|
+
|
|
58
|
+
def expected(n):
|
|
59
|
+
return OneIPPrivacyStream(group=f'239.0.0.{n}', port=5000 + n * 0x101, version=0x10 + n, on=(n != 2),
|
|
60
|
+
full_id=0x20 + n, short_id=0x30 + n,
|
|
61
|
+
iv=bytes(0x40 + n * 16 + i for i in range(8)),
|
|
62
|
+
key_generator=bytes(0x80 + n * 16 + i for i in range(16)),
|
|
63
|
+
key_id=bytes(0xC0 + n * 8 + i for i in range(8)),
|
|
64
|
+
key_version=0x01020304 * n)
|
|
65
|
+
|
|
66
|
+
def report(header_uid, count=3):
|
|
67
|
+
return header_uid + bytes([0, count, 0xA5, 0x5A]) + b''.join(stream(n) for n in range(1, count + 1))
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------- reading
|
|
70
|
+
# A report is filed under its sender, whatever uid its header names.
|
|
71
|
+
src = hello(uid(0x10), 0x30, DeviceFeature.ONEIP_SOURCE)
|
|
72
|
+
assert src.oneip_privacy is None
|
|
73
|
+
rx(uid(0x10), 0x54, report(uid(0x11)))
|
|
74
|
+
p = src.oneip_privacy
|
|
75
|
+
assert p is not None, 'no report was kept'
|
|
76
|
+
assert (p.video, p.audio, p.anc) == (expected(1), expected(2), expected(3)), p
|
|
77
|
+
assert mx.get_by_uid(MxrDeviceUid(uid(0x11))) is None
|
|
78
|
+
print('decode :', p)
|
|
79
|
+
|
|
80
|
+
# Only a whole report of three streams is kept; a longer one is read for its
|
|
81
|
+
# first three.
|
|
82
|
+
other = hello(uid(0x12), 0x30, DeviceFeature.ONEIP_SOURCE)
|
|
83
|
+
whole = report(uid(0x12))
|
|
84
|
+
request = whole[:16] + bytes([1]) + whole[17:]
|
|
85
|
+
two = whole[:17] + bytes([2]) + whole[18:]
|
|
86
|
+
for what, frame in (('a request', request), ('a count of two', two), ('a frame a byte short', whole[:-1])):
|
|
87
|
+
rx(uid(0x12), 0x54, frame)
|
|
88
|
+
assert other.oneip_privacy is None, f'{what} was kept'
|
|
89
|
+
rx(uid(0x12), 0x54, report(uid(0x12), count=4))
|
|
90
|
+
assert other.oneip_privacy is not None and other.oneip_privacy.anc == expected(3), 'a longer report was dropped'
|
|
91
|
+
print('whole : only a report of all three streams is kept')
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------- asking
|
|
94
|
+
sent.clear()
|
|
95
|
+
assert call(src.request_oneip_privacy()) is True
|
|
96
|
+
frame = sent[-1]
|
|
97
|
+
assert int.from_bytes(frame[20:22], 'little') == 0x54 and frame[2] == 0x30, frame[:24].hex()
|
|
98
|
+
assert frame[24:] == uid(0x10) + bytes([1, 0, 0, 0]), frame[24:].hex()
|
|
99
|
+
sent.clear()
|
|
100
|
+
assert call(mx.request_all_oneip_privacy()) is True
|
|
101
|
+
assert sent[-1][24:] == bytes(16) + bytes([1, 0, 0, 0]) and sent[-1][2] == 0x30, sent[-1].hex()
|
|
102
|
+
print('request : names the unit asked, or the zero uid for every one, stamped 0x30')
|
|
103
|
+
|
|
104
|
+
def refused(coro, why):
|
|
105
|
+
n = len(sent)
|
|
106
|
+
assert call(coro) is False, why
|
|
107
|
+
assert len(sent) == n, f'{why}: refused, but transmitted anyway'
|
|
108
|
+
refused(hello(uid(0x13), 0x2F, DeviceFeature.ONEIP_SOURCE).request_oneip_privacy(), 'a source below 0x30')
|
|
109
|
+
refused(hello(uid(0x14), 0x30, DeviceFeature.ONEIP_SINK).request_oneip_privacy(), 'a unit that is not a source')
|
|
110
|
+
print('refusals : nothing a unit would ignore is sent')
|
|
111
|
+
|
|
112
|
+
print('ALL OK')
|
|
@@ -40,6 +40,7 @@ ABOVE_CAP = {
|
|
|
40
40
|
0x50: 'SYS_UNIT_COMMAND',
|
|
41
41
|
0x52: 'V2IP_SINK_SDP',
|
|
42
42
|
0x53: 'V2IP_OSD_SYNC',
|
|
43
|
+
0x54: 'V2IP_PRIVACY',
|
|
43
44
|
}
|
|
44
45
|
root = pathlib.Path(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'mx_remote', 'proto'))
|
|
45
46
|
# Calls only. The pattern also matches the def in FrameBase, which carries a
|
|
@@ -58,6 +58,7 @@ SUITES = [
|
|
|
58
58
|
'routes', # what a OneIP output is routed to, as the frames a unit acts on move it
|
|
59
59
|
'meshbays', # the bays a unit lists for other units, from its mappings
|
|
60
60
|
'v2ipsdp', # what a OneIP output plays from a session description
|
|
61
|
+
'privacy', # how a OneIP source unit encrypts its streams
|
|
61
62
|
'baycontent', # what the source on an input is showing
|
|
62
63
|
'osdsync', # overlay sync, watched and sent
|
|
63
64
|
'deprecated', # the V2IP names 5.11.0 exported still resolve, with a warning
|
|
@@ -11,6 +11,7 @@ print('version', mx_remote.const.VERSION, 'proto', hex(MXR_PROTOCOL_VERSION))
|
|
|
11
11
|
# handles is stamped at.
|
|
12
12
|
assert MXR_PROTOCOL_VERSION == max(MXR_OPCODE_VERSIONS.values()), hex(MXR_PROTOCOL_VERSION)
|
|
13
13
|
assert MXR_OPCODE_VERSIONS[0x52] == 0x2E and MXR_OPCODE_VERSIONS[0x53] == 0x2F # V2IP_SINK_SDP, V2IP_OSD_SYNC
|
|
14
|
+
assert MXR_OPCODE_VERSIONS[0x54] == 0x30 # V2IP_PRIVACY
|
|
14
15
|
assert MXR_OPCODE_VERSIONS[0x49] == 0x28 and MXR_OPCODE_VERSIONS[0x3C] == 0x11
|
|
15
16
|
assert MXR_OPCODE_VERSIONS[0x4F] == 0x2C and MXR_OPCODE_VERSIONS[0x50] == 0x2C # SYS_UNIT_STATUS, SYS_UNIT_COMMAND
|
|
16
17
|
assert DeviceFeature.VIDEO_WALL == (1 << 24)
|