mx-remote 5.5.0__tar.gz → 5.6.0__tar.gz

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