mx-remote 5.5.1__tar.gz → 5.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. {mx_remote-5.5.1 → mx_remote-5.7.0}/.gitignore +1 -0
  2. {mx_remote-5.5.1 → mx_remote-5.7.0}/PKG-INFO +1 -1
  3. mx_remote-5.7.0/docs/oneip.md +177 -0
  4. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Interface.py +159 -1
  5. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Interface.pyi +31 -1
  6. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/const.py +1 -1
  7. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Constants.py +50 -2
  8. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Constants.pyi +7 -0
  9. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFactoryReset.py +21 -6
  10. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHello.py +14 -1
  11. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPAudio.py +12 -6
  12. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +104 -4
  13. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +7 -2
  14. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPStats.py +51 -3
  15. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPStats.pyi +5 -1
  16. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Svd.py +4 -0
  17. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Svd.pyi +2 -0
  18. mx_remote-5.7.0/mx_remote/proto/V2IPStats.py +521 -0
  19. mx_remote-5.7.0/mx_remote/proto/V2IPStats.pyi +116 -0
  20. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Device.py +146 -1
  21. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Device.pyi +15 -2
  22. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/audio_ir.py +16 -4
  23. mx_remote-5.7.0/tests/lengths.py +107 -0
  24. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/run.py +3 -0
  25. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/smoke.py +1 -1
  26. mx_remote-5.7.0/tests/stats.py +432 -0
  27. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/unknowns.py +25 -0
  28. mx_remote-5.7.0/tests/v2ipscaling.py +161 -0
  29. mx_remote-5.7.0/tests/v2ipsubject.py +124 -0
  30. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/wirefix.py +28 -0
  31. mx_remote-5.5.1/docs/oneip.md +0 -43
  32. mx_remote-5.5.1/mx_remote/proto/V2IPStats.py +0 -190
  33. mx_remote-5.5.1/mx_remote/proto/V2IPStats.pyi +0 -54
  34. mx_remote-5.5.1/tests/stats.py +0 -73
  35. {mx_remote-5.5.1 → mx_remote-5.7.0}/AGENTS.md +0 -0
  36. {mx_remote-5.5.1 → mx_remote-5.7.0}/CLAUDE.md +0 -0
  37. {mx_remote-5.5.1 → mx_remote-5.7.0}/LICENSE +0 -0
  38. {mx_remote-5.5.1 → mx_remote-5.7.0}/README.md +0 -0
  39. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/README.md +0 -0
  40. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/brand.css +0 -0
  41. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/favicon.svg +0 -0
  42. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/logo-dark.svg +0 -0
  43. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/_static/logo.svg +0 -0
  44. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/api.md +0 -0
  45. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/audio.md +0 -0
  46. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/callbacks.md +0 -0
  47. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/conf.py +0 -0
  48. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/configuration.md +0 -0
  49. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/devices-and-bays.md +0 -0
  50. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/diagnostics.md +0 -0
  51. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/index.md +0 -0
  52. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/multiviewer.md +0 -0
  53. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/requirements.txt +0 -0
  54. {mx_remote-5.5.1 → mx_remote-5.7.0}/docs/routing.md +0 -0
  55. {mx_remote-5.5.1 → mx_remote-5.7.0}/hatch_build.py +0 -0
  56. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Uid.py +0 -0
  57. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/Uid.pyi +0 -0
  58. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/__init__.py +0 -0
  59. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/__init__.pyi +0 -0
  60. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/compat.py +0 -0
  61. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/compat.pyi +0 -0
  62. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/const.pyi +0 -0
  63. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/main.py +0 -0
  64. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/main.pyi +0 -0
  65. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/BayConfig.py +0 -0
  66. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/BayConfig.pyi +0 -0
  67. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Data.py +0 -0
  68. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Data.pyi +0 -0
  69. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Factory.py +0 -0
  70. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Factory.pyi +0 -0
  71. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  72. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  73. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  74. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  75. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  76. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  77. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  78. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  79. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBase.py +0 -0
  80. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBase.pyi +0 -0
  81. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  82. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  83. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  84. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  85. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayHide.py +0 -0
  86. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  87. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  88. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  89. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  90. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  91. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDebug.py +0 -0
  92. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  93. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDiscover.py +0 -0
  94. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  95. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDID.py +0 -0
  96. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  97. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  98. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  99. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  100. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  101. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  102. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  103. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  104. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHeader.py +0 -0
  105. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  106. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameHello.pyi +0 -0
  107. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameLinks.py +0 -0
  108. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  109. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  110. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  111. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  112. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  113. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  114. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  115. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  116. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  117. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FramePowerChange.py +0 -0
  118. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  119. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCAction.py +0 -0
  120. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  121. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCIr.py +0 -0
  122. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  123. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCKey.py +0 -0
  124. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  125. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  126. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  127. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameReboot.py +0 -0
  128. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  129. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  130. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  131. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  132. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  133. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetName.py +0 -0
  134. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  135. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  136. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  137. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  138. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  139. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  140. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  141. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  142. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  143. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  144. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  145. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  146. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  147. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  148. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  149. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  150. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  151. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTopology.py +0 -0
  152. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  153. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTxIR.py +0 -0
  154. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  155. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  156. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  157. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  158. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  159. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  160. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  161. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  162. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  163. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  164. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  165. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  166. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  167. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  168. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  169. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  170. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  171. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  172. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  173. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  174. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  175. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  176. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  177. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  178. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  179. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  180. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolume.py +0 -0
  181. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  182. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  183. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  184. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  185. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  186. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  187. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  188. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/LinkConfig.py +0 -0
  189. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  190. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Multiviewer.py +0 -0
  191. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  192. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/V2IPConfig.py +0 -0
  193. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  194. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/__init__.py +0 -0
  195. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/__init__.pyi +0 -0
  196. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/proto/svd.csv +0 -0
  197. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/py.typed +0 -0
  198. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Bay.py +0 -0
  199. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Bay.pyi +0 -0
  200. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  201. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  202. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Link.py +0 -0
  203. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Link.pyi +0 -0
  204. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Remote.py +0 -0
  205. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/Remote.pyi +0 -0
  206. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/State.py +0 -0
  207. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/State.pyi +0 -0
  208. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/V2IP.py +0 -0
  209. {mx_remote-5.5.1 → mx_remote-5.7.0}/mx_remote/remote/V2IP.pyi +0 -0
  210. {mx_remote-5.5.1 → mx_remote-5.7.0}/pyproject.toml +0 -0
  211. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/capture.py +0 -0
  212. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/e2e.py +0 -0
  213. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/entry.py +0 -0
  214. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/handlers.py +0 -0
  215. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/handlers_cover.py +0 -0
  216. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/hello.py +0 -0
  217. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/idempotent.py +0 -0
  218. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/merge.py +0 -0
  219. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/newops.py +0 -0
  220. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/offsets.py +0 -0
  221. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/proto_stamp.py +0 -0
  222. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/protogate.py +0 -0
  223. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/rcsettings.py +0 -0
  224. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/roundtrip.py +0 -0
  225. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/sources.py +0 -0
  226. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/structs.py +0 -0
  227. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/txresult.py +0 -0
  228. {mx_remote-5.5.1 → mx_remote-5.7.0}/tests/vdetails.py +0 -0
@@ -1,5 +1,6 @@
1
1
  __pycache__
2
2
  /build
3
3
  /dist
4
+ /site
4
5
  *.egg-info
5
6
  *.pyi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 5.5.1
3
+ Version: 5.7.0
4
4
  Summary: Python 3 library for interfacing with MX Remote compatible devices
5
5
  Project-URL: Homepage, https://github.com/opdenkamp/mx-remote/
6
6
  Project-URL: Documentation, https://opdenkamp.github.io/mx-remote/
@@ -0,0 +1,177 @@
1
+ # OneIP and V2IP
2
+
3
+ Streaming endpoints, stream sources and statistics.
4
+
5
+ ## Pulse-Eight OneIP Devices
6
+
7
+ [Pulse-Eight OneIP](https://www.pulse-eight.com/p/248/oneip-tx) HDMI-over-IP devices expose additional streaming properties:
8
+
9
+ ```python
10
+ # stream source addresses
11
+ if device.is_v2ip and device.v2ip_sources:
12
+ for source in device.v2ip_sources:
13
+ print(f"Video: {source.video.ip}:{source.video.port}")
14
+ print(f"Audio: {source.audio.ip}:{source.audio.port}")
15
+
16
+ # stream details (encoder/decoder config)
17
+ # A configuration frame names its subject in the payload, so a controller's
18
+ # write for a transceiver reaches the transceiver's record - the controller's
19
+ # own stays empty, which is what the device always meant.
20
+ if device.v2ip_details:
21
+ details = device.v2ip_details
22
+ print(f"Video: {details.video}")
23
+ print(f"TX rate: {details.tx_rate}") # None when the sender offered no rate
24
+
25
+ # per-stream DSCP marking (None from peers that predate it)
26
+ if details.dscp and details.dscp.complete:
27
+ print(f"DSCP: {details.dscp}") # video / audio / anc, 0..63
28
+
29
+ # streaming statistics
30
+ # The subscription lapses 60s after the request and there is no free-running
31
+ # mode, so call this again inside the minute to keep the 1Hz reports coming.
32
+ await device.read_stats(enable=True) # start collecting
33
+ # ... later ...
34
+ stats = device.v2ip_stats
35
+
36
+ # what the sink's decoder recovered from the codestream it is being given.
37
+ # Three answers, and they mean different things:
38
+ if stats.decoder is None:
39
+ pass # sender predates MatrixOS 10.12.46
40
+ elif stats.decoder.reading is None:
41
+ pass # decoder has never answered
42
+ else:
43
+ reading = stats.decoder.reading
44
+ # Geometry is what says a picture was recovered. `format` never does: with
45
+ # no stream it reads RGB, which is indistinguishable from a real RGB source.
46
+ if reading.recovered:
47
+ print(f"{reading.width}x{reading.height} {reading.format}")
48
+ # `reason` for display, `causes` for logic. Every true cause sets its bit
49
+ # in flags; which one keeps `reason` is a fixed priority in the video
50
+ # processor, not the numbering. TX_BRIDGE_UNLOCKED ranks below every
51
+ # input-side cause, so a repeating pipeline restart shows as bit 9 in
52
+ # `causes` while `reason` names the input cause instead.
53
+ print(f"{reading.reason}") # None for a cause this build cannot name
54
+ print(f"{reading.causes}") # raw reason values, lowest first
55
+ ```
56
+
57
+ ### Trusting a decoder reading after a switch
58
+
59
+ The values are read off the video processor every 2s and reported every 1s,
60
+ latched between reads, so roughly every other report repeats a reading already
61
+ seen — a frame arriving says nothing about freshness. `reading.updates` counts
62
+ readings actually **stored**, so it stays still while a processor is stalled
63
+ rather than implying a refresh. It is monotonic, never reset, and wraps at
64
+ 65535 (~36h).
65
+
66
+ After changing what a sink is pointed at, wait for `updates` to advance by
67
+ **two** before trusting the geometry. It ticks when the reply lands, not when
68
+ the query is sent, so a single tick can carry an answer read fractionally
69
+ before the switch.
70
+
71
+ ```python
72
+ before = device.v2ip_stats.decoder.reading.updates
73
+ await sink.switch_source(...)
74
+ # ... wait until reading.updates - before >= 2 (mod 65536) ...
75
+ ```
76
+
77
+ Colour depth is deliberately absent: the video processor answers depth from a
78
+ driver constant rather than from the codestream, so it was withheld rather than
79
+ shipped as a constant that looks like a measurement. Assert depth at the
80
+ encoder's input bay instead.
81
+
82
+ ### What this block cannot tell you
83
+
84
+ **Whether a sink is off on purpose.** A sink someone deliberately disabled
85
+ reports `NO_PACKETS`, indefinitely — the same cause as a sink that should be
86
+ receiving and is not. There is no enablement field here, so read that from
87
+ `MXR_OP_V2IP_DEVICE_CFG` or the device's HTTP status. `IDLE` sounds like the
88
+ answer and is not: it is effectively unreachable in shipping firmware.
89
+
90
+ **Whether a single sample is representative.** `reason` during a teardown is a
91
+ sequence, not a state — a measured one passed through `SWITCH_PENDING` before
92
+ settling on `NO_PACKETS` six seconds later. Use `updates` to tell a fresh
93
+ reading from a repeat, and note it cannot see a `TX_BRIDGE_UNLOCKED` flag
94
+ carried forward across a format change: a value held over is a stored reading
95
+ like any other.
96
+
97
+ ## Sink output scaling
98
+
99
+ A sink scales for two independent reasons: automatic scaling, and a configured
100
+ output mode. Each is written behind its own validity bit, so moving one leaves
101
+ the other where it is.
102
+
103
+ ```python
104
+ # what the sink reports. Trust this block only from a device whose hello carries
105
+ # CONFIG_INITIALISED - firmware without it builds the block over uninitialised
106
+ # stack, where the valid bit itself is noise.
107
+ scaling = device.v2ip_details.scaling
108
+ print(scaling.auto_scaling) # None when the sender did not say
109
+ print(scaling.configured_mode) # (signal type, refresh), None when it has none
110
+
111
+ # a mode is given as a depth and a colour space, never as a packed signal-type
112
+ # word read back off the wire: a sink with no mode reports the word carrying the
113
+ # index for "no depth", which a receiver decodes to zero and drops in silence.
114
+ mode = mx_remote.V2IPOutputMode(svd=16, depth=12,
115
+ colour=mx_remote.VideoColourSpace.YUV422, refresh=60)
116
+
117
+ await device.set_v2ip_auto_scaling(False) # turn it off before setting a mode
118
+ await device.set_v2ip_output_mode(mode)
119
+ await device.set_v2ip_auto_scaling(True) # back on, if that is what you want
120
+
121
+ await device.clear_v2ip_output_mode() # the only way to say "no mode"
122
+ ```
123
+
124
+ **Set the mode with automatic scaling off.** A sink scaling automatically
125
+ refuses a mode whose format the attached display does not list, and refuses it
126
+ in silence. Setting the mode first and turning automatic scaling back on
127
+ afterwards is the order that survives, because the mode is checked while
128
+ automatic scaling is still off.
129
+
130
+ Nothing acknowledges any of these. `set_v2ip_output_mode` returns False for a
131
+ mode no sink would take, but a True only says the frame went out: the sink
132
+ weighs the format against the display's EDID and against what its own output
133
+ stage can produce, and refuses silently either way. Read `v2ip_details.scaling`
134
+ back on the device's next report to learn what it did.
135
+
136
+ A scaling change makes the device rebuild and rebroadcast its sink block, so
137
+ expect `device.v2ip_sink` to read empty for a moment afterwards - see
138
+ [what an empty sink block means](#what-an-empty-sink-block-means).
139
+
140
+ ## What an empty sink block means
141
+
142
+ `device.v2ip_sink` addresses that read as unset mean "no route, or the sink
143
+ could not work one out", never "definitely not subscribed". This is the one part
144
+ of a device configuration with no validity marker of its own, so a sender with
145
+ nothing to say sends zeros and every receiver stores them. A sender leaves it
146
+ empty whenever its own stream configuration does not resolve, which covers more
147
+ than having no route: a selected source whose record has not arrived yet, the
148
+ state after a restart at either end, missing audio bay configuration, or a
149
+ stream failing its validity check.
150
+
151
+ Expect that reading rather than guarding against it. Any scaling change rebuilds
152
+ and rebroadcasts the block, and a write aimed at a remote bay sends it zeroed
153
+ however it was requested, so an empty reading turns up most often during exactly
154
+ the no-signal troubleshooting that prompted the change. A device's periodic
155
+ report puts a real route back within a minute of it having one, so wait one out
156
+ rather than treat the first empty reading as an answer.
157
+
158
+ The library caches an empty reading rather than dropping it. A sink that has
159
+ genuinely lost its route sends the same zeros, and so does every report after
160
+ it, so refusing them would hold a route that nothing later could clear.
161
+
162
+ ## Mesh and firmware
163
+
164
+ ```python
165
+ # mesh operations
166
+ await device.mesh_promote() # promote to mesh master
167
+ await device.mesh_remove() # remove from mesh
168
+
169
+ # firmware versions
170
+ if device.v2ip_firmware_versions:
171
+ for fw_type, fw in device.v2ip_firmware_versions.items():
172
+ print(f"{fw_type}: {fw.version}")
173
+ ```
174
+
175
+ ---
176
+
177
+ [Documentation index](README.md) | [Project README](https://github.com/opdenkamp/mx-remote#readme)
@@ -28,7 +28,7 @@ from .proto.Multiviewer import (
28
28
  MultiviewerITCMode,
29
29
  MultiviewerHDCPMode,
30
30
  )
31
- from .proto.Svd import SvdMap
31
+ from .proto.Svd import SvdMap, lookup_svd
32
32
  from typing import Any, Callable
33
33
  from .Uid import MxrDeviceUid, MxrBayUid
34
34
 
@@ -1390,6 +1390,28 @@ class DeviceV2IPScalingSettings(ABC):
1390
1390
  is the only option bit with a defined meaning.
1391
1391
  '''
1392
1392
 
1393
+ @property
1394
+ def configured_mode(self) -> 'tuple[MxrSignalType, int]|None':
1395
+ '''The mode and refresh rate this sink scales to, None when it has none.
1396
+
1397
+ The two are distinct on the wire: a sink with no mode configured leaves
1398
+ MXR_SCALING_FLAG_MODE_VALID clear, and never sets it over a zero mode.
1399
+
1400
+ Trust it only from a peer whose hello carries CONFIG_INITIALISED
1401
+ (DeviceBase.config_initialised). Firmware without that builds this block
1402
+ over uninitialised stack, where the valid bit itself is noise.
1403
+ '''
1404
+ if ((self.flags & MXR_SCALING_FLAG_MODE_VALID) == 0):
1405
+ return None
1406
+ return (MxrSignalType(self.mode.to_bytes(2, 'little')), self.refresh)
1407
+
1408
+ @property
1409
+ def auto_scaling(self) -> bool|None:
1410
+ '''Whether the output scales automatically, None when the sender did not say.'''
1411
+ if ((self.flags & MXR_SCALING_FLAG_OPTIONS_VALID) == 0):
1412
+ return None
1413
+ return ((self.flags & MXR_SCALING_FLAG_AUTO_SCALING) != 0)
1414
+
1393
1415
  def v2ip_stream_valid(stream:'V2IPStreamSource|None') -> bool:
1394
1416
  """
1395
1417
  True when a stream source carries an address a frame actually meant to send.
@@ -1418,6 +1440,72 @@ def v2ip_stream_cleared(stream:'V2IPStreamSource|None') -> bool:
1418
1440
  """True when a stream source is zeroed, which is how "no source" is spelled."""
1419
1441
  return (stream is not None) and (stream.ip == '0.0.0.0') and (stream.port == 0)
1420
1442
 
1443
+ class V2IPOutputMode:
1444
+ """
1445
+ The output format to scale a V2IP sink to.
1446
+
1447
+ Built from a depth and a colour space rather than from a packed
1448
+ mxr_signal_type word, so the word a caller sends cannot carry the unset bpp
1449
+ index a sink reports while it has no mode configured.
1450
+ """
1451
+ def __init__(self, svd:int, depth:int, colour:VideoColourSpace, refresh:int) -> None:
1452
+ self.svd = svd
1453
+ """CTA-861 short video descriptor to output."""
1454
+ self.depth = depth
1455
+ """Bit depth: 8, 10 or 12."""
1456
+ self.colour = colour
1457
+ """Colour space to output."""
1458
+ self.refresh = refresh
1459
+ """Refresh rate in Hz, V2IP_SCALING_REFRESH_MIN to V2IP_SCALING_REFRESH_MAX."""
1460
+
1461
+ def validate(self) -> str|None:
1462
+ """
1463
+ Why a sink will refuse this mode, or None when it will take it.
1464
+
1465
+ Checked here because a sink checks it and then says nothing: every value
1466
+ this rejects is one the receiver decodes cleanly and drops, leaving a
1467
+ caller with a send that succeeded and a setting that did not move.
1468
+
1469
+ Passing is not a guarantee. A sink also weighs the format against the
1470
+ EDID of the display attached to it and against what its own clock and
1471
+ output stage can produce, and none of that is knowable from here.
1472
+ """
1473
+ if (self.svd == 0):
1474
+ return "svd 0 is how a mode is cleared, not a mode to set"
1475
+ if (lookup_svd(self.svd) is None):
1476
+ return "the svd names no known video descriptor"
1477
+ if (mxr_sig_bpp_index(self.depth) is None):
1478
+ return "a V2IP output stage takes 8, 10 or 12 bits per pixel"
1479
+ if (self.colour not in (VideoColourSpace.RGB, VideoColourSpace.YUV444,
1480
+ VideoColourSpace.YUV422, VideoColourSpace.YUV420)):
1481
+ return "the colour space names none of RGB, 4:4:4, 4:2:2 or 4:2:0"
1482
+ if not (V2IP_SCALING_REFRESH_MIN <= self.refresh <= V2IP_SCALING_REFRESH_MAX):
1483
+ return f"the refresh rate is outside {V2IP_SCALING_REFRESH_MIN}..{V2IP_SCALING_REFRESH_MAX}Hz"
1484
+ return None
1485
+
1486
+ @property
1487
+ def signal_type(self) -> MxrSignalType:
1488
+ """
1489
+ The packed signal type a scaling write carries for this mode.
1490
+
1491
+ Call validate() first: an unvalidated depth packs as the index for
1492
+ "no depth", which a receiver drops.
1493
+ """
1494
+ return MxrSignalType.from_parts(svd=self.svd, colour=int(self.colour),
1495
+ bpp_index=(mxr_sig_bpp_index(self.depth) or 0))
1496
+
1497
+ def __eq__(self, other:Any) -> bool:
1498
+ if not isinstance(other, V2IPOutputMode):
1499
+ return NotImplemented
1500
+ return (self.svd == other.svd) and (self.depth == other.depth) \
1501
+ and (self.colour == other.colour) and (self.refresh == other.refresh)
1502
+
1503
+ def __str__(self) -> str:
1504
+ return f"svd {self.svd}, colour {self.colour}, {self.depth}bpp, {self.refresh}Hz"
1505
+
1506
+ def __repr__(self) -> str:
1507
+ return str(self)
1508
+
1421
1509
  class V2IPScalingSettings(DeviceV2IPScalingSettings):
1422
1510
  """Plain holder for a merged mxr_scaling_config."""
1423
1511
  def __init__(self, mode:int, refresh:int, flags:int) -> None:
@@ -1627,6 +1715,28 @@ class DeviceV2IPSink:
1627
1715
  when present, each stream may still carry an all-zero ip when that stream is
1628
1716
  inactive. ``audio_fmt`` carries the resolved sink audio format (zero fields
1629
1717
  fall back to firmware defaults of 48kHz / 2 channels).
1718
+
1719
+ **An address that reads as unset means "no route, or the sink could not work
1720
+ one out", never "definitely not subscribed".** This block is the one part of
1721
+ a device configuration with no validity marker of its own, so a sender with
1722
+ nothing to say sends zeros and every receiver stores them. A sender leaves it
1723
+ empty whenever its own stream configuration does not resolve, which covers
1724
+ more than having no route: a selected source whose record has not arrived
1725
+ yet, the state after a restart at either end, missing audio bay
1726
+ configuration, or a stream failing its validity check. ``audio_fmt`` has a
1727
+ second gate of its own, so it can be absent while the addresses are not.
1728
+
1729
+ Expect that reading rather than guarding against it. Any scaling change makes
1730
+ the device rebuild and rebroadcast this block, and a write aimed at a remote
1731
+ bay sends it zeroed however it was requested, so an empty reading turns up
1732
+ most often during exactly the no-signal troubleshooting that prompted the
1733
+ change. A device's periodic report puts a real route back within a minute of
1734
+ it having one, so a reader that needs certainty should wait one out rather
1735
+ than treat the first empty reading as an answer.
1736
+
1737
+ An empty reading is cached rather than dropped on purpose. A sink that has
1738
+ genuinely lost its route sends the same zeros, and so does every report after
1739
+ it, so refusing them would hold a route that nothing later could clear.
1630
1740
  """
1631
1741
  def __init__(self, addresses:V2IPStreamSources|None, audio_fmt:V2IPAudioFormat|None) -> None:
1632
1742
  self._addresses = addresses
@@ -1956,6 +2066,17 @@ class DeviceBase(ABC):
1956
2066
  def mesh_master(self, master:MxrDeviceUid) -> None:
1957
2067
  '''Change the master device of this device'''
1958
2068
 
2069
+ @property
2070
+ @abstractmethod
2071
+ def mesh_master_uid(self) -> MxrDeviceUid|None:
2072
+ '''UID of the mesh controller this device reports, None while it has named none.
2073
+
2074
+ Distinct from mesh_master, which falls back to the device itself. A
2075
+ device that has named no controller is not a device that follows nobody:
2076
+ a client that has just started knows nothing about anyone for as long as
2077
+ a broadcast period.
2078
+ '''
2079
+
1959
2080
  @property
1960
2081
  @abstractmethod
1961
2082
  def protocol(self) -> int:
@@ -1978,6 +2099,31 @@ class DeviceBase(ABC):
1978
2099
  def is_mesh_master(self) -> bool:
1979
2100
  '''True if this device is the master device of a V2IP mesh'''
1980
2101
 
2102
+ @property
2103
+ @abstractmethod
2104
+ def is_management(self) -> bool:
2105
+ '''True when a receiver would let this device write another device's configuration.
2106
+
2107
+ Two bits, because the two kinds of writer announce themselves
2108
+ differently and neither implies the other. MANAGER belongs to external
2109
+ management applications - anything driving these devices that is not one
2110
+ of them, this library included - and no device firmware ever sets it on
2111
+ itself, so testing it alone would refuse every write a controller makes.
2112
+ A device that is controlling its mesh announces MESH_MASTER instead.
2113
+
2114
+ The bits have a hole a caller has to close elsewhere: a device sets
2115
+ MESH_MASTER only while it is both the controller and has bays mapped, so
2116
+ one promoted before it has any carries neither bit while still being its
2117
+ mesh's controller. What covers that window is the controller uid the
2118
+ devices in that mesh report, which is their question rather than this
2119
+ device's - see mesh_master_uid.
2120
+ '''
2121
+
2122
+ @property
2123
+ @abstractmethod
2124
+ def is_v2ip_sink(self) -> bool:
2125
+ '''True if this device decodes a V2IP stream, which is what carries the scaling settings'''
2126
+
1981
2127
  @property
1982
2128
  @abstractmethod
1983
2129
  def is_mesh_member(self) -> bool:
@@ -2106,6 +2252,18 @@ class DeviceBase(ABC):
2106
2252
  async def read_stats(self, enable:bool) -> bool:
2107
2253
  '''start or stop dumping stats'''
2108
2254
 
2255
+ @abstractmethod
2256
+ async def set_v2ip_auto_scaling(self, enabled:bool) -> bool:
2257
+ '''turn this sink's automatic scaling on or off'''
2258
+
2259
+ @abstractmethod
2260
+ async def set_v2ip_output_mode(self, mode:'V2IPOutputMode') -> bool:
2261
+ '''set the output format this sink scales to'''
2262
+
2263
+ @abstractmethod
2264
+ async def clear_v2ip_output_mode(self) -> bool:
2265
+ '''clear the output format this sink is configured to scale to'''
2266
+
2109
2267
  @abstractmethod
2110
2268
  async def get_log(self) -> str|None:
2111
2269
  '''read the log from the device and return it as string'''
@@ -4,7 +4,7 @@ from .Uid import MxrBayUid as MxrBayUid, MxrDeviceUid as MxrDeviceUid
4
4
  from .proto import RCKey as RCKey
5
5
  from .proto.Data import VolumeMuteStatus as VolumeMuteStatus
6
6
  from .proto.Multiviewer import MultiviewerAspectRatio as MultiviewerAspectRatio, MultiviewerBoolSetting as MultiviewerBoolSetting, MultiviewerEDIDTemplate as MultiviewerEDIDTemplate, MultiviewerHDCPMode as MultiviewerHDCPMode, MultiviewerITCMode as MultiviewerITCMode, MultiviewerOutputMode as MultiviewerOutputMode, MultiviewerPipPosition as MultiviewerPipPosition, MultiviewerPipSize as MultiviewerPipSize, MultiviewerSource as MultiviewerSource, MultiviewerViewMode as MultiviewerViewMode
7
- from .proto.Svd import SvdMap as SvdMap
7
+ from .proto.Svd import SvdMap as SvdMap, lookup_svd as lookup_svd
8
8
  from .proto.V2IPStats import V2IPDeviceStats as V2IPDeviceStats
9
9
  from _typeshed import Incomplete
10
10
  from abc import ABC, abstractmethod
@@ -582,11 +582,26 @@ class DeviceV2IPScalingSettings(ABC):
582
582
  @property
583
583
  @abstractmethod
584
584
  def flags(self) -> int: ...
585
+ @property
586
+ def configured_mode(self) -> tuple[MxrSignalType, int] | None: ...
587
+ @property
588
+ def auto_scaling(self) -> bool | None: ...
585
589
 
586
590
  def v2ip_stream_valid(stream: V2IPStreamSource | None) -> bool: ...
587
591
  def v2ip_av_source_valid(video: V2IPStreamSource | None, anc: V2IPStreamSource | None) -> bool: ...
588
592
  def v2ip_stream_cleared(stream: V2IPStreamSource | None) -> bool: ...
589
593
 
594
+ class V2IPOutputMode:
595
+ svd: Incomplete
596
+ depth: Incomplete
597
+ colour: Incomplete
598
+ refresh: Incomplete
599
+ def __init__(self, svd: int, depth: int, colour: VideoColourSpace, refresh: int) -> None: ...
600
+ def validate(self) -> str | None: ...
601
+ @property
602
+ def signal_type(self) -> MxrSignalType: ...
603
+ def __eq__(self, other: Any) -> bool: ...
604
+
590
605
  class V2IPScalingSettings(DeviceV2IPScalingSettings):
591
606
  def __init__(self, mode: int, refresh: int, flags: int) -> None: ...
592
607
  @property
@@ -825,6 +840,9 @@ class DeviceBase(ABC):
825
840
  def mesh_master(self, master: MxrDeviceUid) -> None: ...
826
841
  @property
827
842
  @abstractmethod
843
+ def mesh_master_uid(self) -> MxrDeviceUid | None: ...
844
+ @property
845
+ @abstractmethod
828
846
  def protocol(self) -> int: ...
829
847
  @abstractmethod
830
848
  def supports_opcode(self, opcode: int) -> bool: ...
@@ -833,6 +851,12 @@ class DeviceBase(ABC):
833
851
  def is_mesh_master(self) -> bool: ...
834
852
  @property
835
853
  @abstractmethod
854
+ def is_management(self) -> bool: ...
855
+ @property
856
+ @abstractmethod
857
+ def is_v2ip_sink(self) -> bool: ...
858
+ @property
859
+ @abstractmethod
836
860
  def is_mesh_member(self) -> bool: ...
837
861
  @property
838
862
  @abstractmethod
@@ -899,6 +923,12 @@ class DeviceBase(ABC):
899
923
  @abstractmethod
900
924
  async def read_stats(self, enable: bool) -> bool: ...
901
925
  @abstractmethod
926
+ async def set_v2ip_auto_scaling(self, enabled: bool) -> bool: ...
927
+ @abstractmethod
928
+ async def set_v2ip_output_mode(self, mode: V2IPOutputMode) -> bool: ...
929
+ @abstractmethod
930
+ async def clear_v2ip_output_mode(self) -> bool: ...
931
+ @abstractmethod
902
932
  async def get_log(self) -> str | None: ...
903
933
  @abstractmethod
904
934
  def on_mxr_update(self, data: object) -> None: ...
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '5.5.1'
17
+ VERSION = '5.7.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -10,13 +10,18 @@
10
10
  from enum import IntEnum, IntFlag
11
11
  from typing import TypeVar
12
12
 
13
- MXR_PROTOCOL_VERSION = 0x28
13
+ MXR_PROTOCOL_VERSION = 0x29
14
14
  """Highest mx_remote protocol version this library understands.
15
15
 
16
16
  Mirrors the firmware's own protocol version. Bump
17
17
  when adding RX support for an opcode revision; transmitters stamp the
18
18
  per-opcode minimum from MXR_OPCODE_VERSIONS so older peers still parse the
19
- frame."""
19
+ frame.
20
+
21
+ This is also a receive ceiling: Remote.process_frame drops any frame stamped
22
+ above it, unparsed. So it has to reach the stamp of every layout revision
23
+ decoded below, or that revision goes silent on the mesh rather than noisy -
24
+ 0x29 is what a MatrixOS 10.12.46 unit stamps on V2IP_STATS."""
20
25
 
21
26
  MXR_OPCODE_VERSIONS: dict[int, int] = {
22
27
  0x00: 0x01, # SYS_HELLO
@@ -141,6 +146,12 @@ MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
141
146
  """mxr_scaling_config.flags. The mode/refresh pair and the options nibble are
142
147
  separately valid: a sender carrying neither flag is offering no scaling at all."""
143
148
 
149
+ V2IP_SCALING_REFRESH_MIN = 24
150
+ V2IP_SCALING_REFRESH_MAX = 120
151
+ """Refresh rate range a V2IP output stage accepts, in Hz. A receiver replaces
152
+ anything outside it with 50 rather than refusing the write, so a 0 here asks
153
+ for 50Hz instead of asking for nothing."""
154
+
144
155
  MXR_V2IP_DSCP_SET = 0x80
145
156
  """DSCP 0 (CS0) is a legal marking, so a zero byte cannot mean "absent": each
146
157
  dscp byte in a V2IP_DEVICE_CFG options word carries this bit alongside its
@@ -374,6 +385,23 @@ def mxr_sig_bpp_get(bpp: int) -> int:
374
385
  bit depth is a common trap. Returns 0 for both unknown and unset.'''
375
386
  return _MXR_SIG_BPP_VALUES.get(bpp, MXR_SIG_BPP_UNKNOWN)
376
387
 
388
+ _MXR_SIG_BPP_WRITABLE_INDICES: dict[int, int] = {
389
+ 8: 1,
390
+ 10: 2,
391
+ 12: 3,
392
+ }
393
+
394
+ def mxr_sig_bpp_index(depth: int) -> int|None:
395
+ '''Map a bit depth to the mxr_signal_type bpp *index* that stands for it.
396
+
397
+ Only the three depths a V2IP output stage accepts are here. Index 4 names
398
+ 16bpp, which mxr_sig_bpp_get() reads back from a device, but the output
399
+ stage refuses it - offering it as something to write would build a frame
400
+ that is decoded cleanly and then dropped in silence.
401
+
402
+ Returns None for a depth no index names.'''
403
+ return _MXR_SIG_BPP_WRITABLE_INDICES.get(depth)
404
+
377
405
  class MxrSignalType:
378
406
  '''The 2-byte mxr_signal_type carried in scaling configs and bay signal reports.
379
407
 
@@ -386,6 +414,26 @@ class MxrSignalType:
386
414
  self._svd = int(data[0])
387
415
  self._flags = int(data[1])
388
416
 
417
+ @staticmethod
418
+ def from_parts(svd: int, colour: int, bpp_index: int) -> 'MxrSignalType':
419
+ '''Build the word a scaling write carries, from the fields it consumes.
420
+
421
+ non_int is left clear: the receiving struct carries the bit and the
422
+ apply path does not read it.
423
+
424
+ Building rather than editing is the point. A sink with no mode
425
+ configured reports the word with the unset bpp index in it, so a caller
426
+ that read that word back and filled in an svd would send an index no
427
+ depth uses, which the receiver decodes to zero and rejects without
428
+ answering.'''
429
+ flags = (colour & 0xF) | ((bpp_index & 0x7) << 5)
430
+ return MxrSignalType(bytes([svd & 0xFF, flags]))
431
+
432
+ @property
433
+ def byte_value(self) -> bytes:
434
+ '''The two bytes as they sit on the wire.'''
435
+ return bytes([self._svd, self._flags])
436
+
389
437
  @property
390
438
  def value(self) -> int:
391
439
  '''Raw 16-bit value.'''
@@ -18,6 +18,8 @@ def v2ip_rate_valid(rate: int | None) -> bool: ...
18
18
  MXR_SCALING_FLAG_MODE_VALID: Incomplete
19
19
  MXR_SCALING_FLAG_OPTIONS_VALID: Incomplete
20
20
  MXR_SCALING_FLAG_AUTO_SCALING: Incomplete
21
+ V2IP_SCALING_REFRESH_MIN: int
22
+ V2IP_SCALING_REFRESH_MAX: int
21
23
  MXR_V2IP_DSCP_SET: int
22
24
  V2IP_DSCP_MAX: int
23
25
  V2IP_DSCP_DEFAULT: int
@@ -108,9 +110,14 @@ MXR_SIG_BPP_UNKNOWN: int
108
110
  MXR_SIG_BPP_UNSET: int
109
111
 
110
112
  def mxr_sig_bpp_get(bpp: int) -> int: ...
113
+ def mxr_sig_bpp_index(depth: int) -> int | None: ...
111
114
 
112
115
  class MxrSignalType:
113
116
  def __init__(self, data: bytes) -> None: ...
117
+ @staticmethod
118
+ def from_parts(svd: int, colour: int, bpp_index: int) -> MxrSignalType: ...
119
+ @property
120
+ def byte_value(self) -> bytes: ...
114
121
  @property
115
122
  def value(self) -> int: ...
116
123
  @property
@@ -7,10 +7,15 @@
7
7
  '''Protocol frame for SYS_FACTORY_RESET (opcode 0x3A).
8
8
 
9
9
  A mesh controller or management app broadcasts this to factory-reset peers.
10
- Payload variants the firmware accepts:
11
- empty controller-only target (just the sender)
12
- 1 byte = 0xFF broadcast to all peers (controller/management only)
13
- 16 bytes single target uid
10
+ Payload forms:
11
+ empty controller-only target (just the sender)
12
+ >=1, byte 0xFF broadcast to all peers (controller/management only)
13
+ >=16 single target uid
14
+
15
+ Each length is a minimum and they are tested longest first. An update that
16
+ appends a field leaves the fields ahead of it where they are, so an exact
17
+ length would read a grown request as a shorter form - here, as one addressing
18
+ nobody.
14
19
  '''
15
20
 
16
21
  from functools import cached_property
@@ -22,11 +27,21 @@ class FrameFactoryReset(FrameBase):
22
27
 
23
28
  @cached_property
24
29
  def is_broadcast_all(self) -> bool:
25
- return (len(self) == 1) and (self.payload_u8(0) == 0xFF)
30
+ '''Whether this asks every peer to reset.
31
+
32
+ Tested after the uid form, which is longer: a uid whose first byte is
33
+ 0xFF would otherwise read as a broadcast.'''
34
+ return (self.target_uid is None) and (len(self) >= 1) and (self.payload_u8(0) == 0xFF)
26
35
 
27
36
  @cached_property
28
37
  def target_uid(self) -> MxrDeviceUid | None:
29
- if (len(self) == 16):
38
+ '''Uid of the single peer being reset, None when the request names none.
39
+
40
+ A minimum rather than an exact length. A sender that appends a field
41
+ leaves the uid where it is, so requiring 16 exactly would read a grown
42
+ request as one addressing nobody - which is the sender resetting itself,
43
+ a different request altogether.'''
44
+ if (len(self) >= 16):
30
45
  return self.payload_uuid(0)
31
46
  return None
32
47
 
@@ -19,7 +19,20 @@ class FrameHello(FrameBase):
19
19
  '''Hello frame, sent by devices to advertise themselves on the network.'''
20
20
  @staticmethod
21
21
  def construct(mxr:DeviceRegistry) -> FrameBase|None:
22
- '''Build a hello frame for transmission to advertise this client.'''
22
+ '''Build a hello frame for transmission to advertise this client.
23
+
24
+ Never append a field to this payload. Receivers read a payload length as
25
+ a minimum everywhere except the hello, which they check exactly, so a
26
+ hello that grew is discarded rather than read up to the part they know.
27
+ That costs more than the field: this is how the client stops being
28
+ unknown, and a receiver that drops it never registers the sender and
29
+ ignores everything that follows as coming from a stranger.
30
+
31
+ It holds however many receivers are later fixed to accept a longer
32
+ hello, because the ones already deployed are the ones deciding whether
33
+ this client is seen at all. Nothing on the receive side needs the rule -
34
+ FrameHello reads its fields by offset with no length gate, so a peer may
35
+ grow its own.'''
23
36
  payload = [ (MXR_PROTOCOL_VERSION & 0xFF), ((MXR_PROTOCOL_VERSION >> 8) & 0xFF) ]
24
37
  payload = append_payload_str(payload=payload, value=mxr.name, sz=16)
25
38
  payload = append_payload_str(payload=payload, value="P9SN00000000", sz=16)