mx-remote 5.9.0__tar.gz → 5.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {mx_remote-5.9.0 → mx_remote-5.9.1}/PKG-INFO +1 -1
  2. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/Interface.py +2 -2
  3. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/Interface.pyi +1 -1
  4. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/const.py +9 -1
  5. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/const.pyi +1 -0
  6. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameLinks.py +1 -5
  7. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Device.py +50 -23
  8. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Device.pyi +2 -1
  9. mx_remote-5.9.1/tests/complete.py +218 -0
  10. mx_remote-5.9.0/tests/complete.py +0 -105
  11. {mx_remote-5.9.0 → mx_remote-5.9.1}/.gitignore +0 -0
  12. {mx_remote-5.9.0 → mx_remote-5.9.1}/AGENTS.md +0 -0
  13. {mx_remote-5.9.0 → mx_remote-5.9.1}/CLAUDE.md +0 -0
  14. {mx_remote-5.9.0 → mx_remote-5.9.1}/LICENSE +0 -0
  15. {mx_remote-5.9.0 → mx_remote-5.9.1}/README.md +0 -0
  16. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/README.md +0 -0
  17. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/_static/brand.css +0 -0
  18. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/_static/favicon.svg +0 -0
  19. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/_static/logo-dark.svg +0 -0
  20. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/_static/logo.svg +0 -0
  21. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/api.md +0 -0
  22. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/audio.md +0 -0
  23. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/callbacks.md +0 -0
  24. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/conf.py +0 -0
  25. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/configuration.md +0 -0
  26. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/devices-and-bays.md +0 -0
  27. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/diagnostics.md +0 -0
  28. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/index.md +0 -0
  29. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/multiviewer.md +0 -0
  30. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/oneip.md +0 -0
  31. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/requirements.txt +0 -0
  32. {mx_remote-5.9.0 → mx_remote-5.9.1}/docs/routing.md +0 -0
  33. {mx_remote-5.9.0 → mx_remote-5.9.1}/hatch_build.py +0 -0
  34. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/Uid.py +0 -0
  35. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/Uid.pyi +0 -0
  36. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/__init__.py +0 -0
  37. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/__init__.pyi +0 -0
  38. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/compat.py +0 -0
  39. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/compat.pyi +0 -0
  40. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/main.py +0 -0
  41. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/main.pyi +0 -0
  42. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/BayConfig.py +0 -0
  43. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/BayConfig.pyi +0 -0
  44. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Constants.py +0 -0
  45. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Constants.pyi +0 -0
  46. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Data.py +0 -0
  47. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Data.pyi +0 -0
  48. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Factory.py +0 -0
  49. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Factory.pyi +0 -0
  50. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  51. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  52. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  53. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  54. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioClip.py +0 -0
  55. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  56. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  57. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  58. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBase.py +0 -0
  59. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBase.pyi +0 -0
  60. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfig.py +0 -0
  61. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  62. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  63. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  64. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayHide.py +0 -0
  65. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayHide.pyi +0 -0
  66. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayStatus.py +0 -0
  67. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  68. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameConnectStatus.py +0 -0
  69. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  70. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDebug.py +0 -0
  71. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDebug.pyi +0 -0
  72. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDiscover.py +0 -0
  73. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameDiscover.pyi +0 -0
  74. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDID.py +0 -0
  75. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDID.pyi +0 -0
  76. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  77. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  78. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFactoryReset.py +0 -0
  79. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  80. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFilterStatus.py +0 -0
  81. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  82. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  83. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  84. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHeader.py +0 -0
  85. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHeader.pyi +0 -0
  86. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHello.py +0 -0
  87. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameHello.pyi +0 -0
  88. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameLinks.pyi +0 -0
  89. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMeshOperation.py +0 -0
  90. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  91. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  92. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  93. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  94. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  95. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  96. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  97. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FramePowerChange.py +0 -0
  98. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FramePowerChange.pyi +0 -0
  99. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCAction.py +0 -0
  100. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCAction.pyi +0 -0
  101. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCIr.py +0 -0
  102. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCIr.pyi +0 -0
  103. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCKey.py +0 -0
  104. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCKey.pyi +0 -0
  105. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCSettings.py +0 -0
  106. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  107. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameReboot.py +0 -0
  108. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameReboot.pyi +0 -0
  109. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRoutingChange.py +0 -0
  110. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  111. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetInstaller.py +0 -0
  112. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  113. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetName.py +0 -0
  114. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetName.pyi +0 -0
  115. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetRoute.py +0 -0
  116. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  117. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetupStatus.py +0 -0
  118. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  119. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatus.py +0 -0
  120. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  121. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  122. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  123. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSysTemperature.py +0 -0
  124. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  125. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSystemStatus.py +0 -0
  126. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  127. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCAction.py +0 -0
  128. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  129. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCKey.py +0 -0
  130. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  131. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTopology.py +0 -0
  132. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTopology.pyi +0 -0
  133. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTxIR.py +0 -0
  134. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameTxIR.pyi +0 -0
  135. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  136. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  137. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  138. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  139. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  140. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  141. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  142. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  143. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  144. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  145. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPLink.py +0 -0
  146. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  147. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  148. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  149. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  150. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  151. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  152. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  153. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  154. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  155. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  156. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  157. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSources.py +0 -0
  158. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  159. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPStats.py +0 -0
  160. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  161. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  162. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  163. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  164. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  165. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolume.py +0 -0
  166. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolume.pyi +0 -0
  167. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeDown.py +0 -0
  168. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  169. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeSet.py +0 -0
  170. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  171. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeUp.py +0 -0
  172. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  173. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/LinkConfig.py +0 -0
  174. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/LinkConfig.pyi +0 -0
  175. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Multiviewer.py +0 -0
  176. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Multiviewer.pyi +0 -0
  177. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Svd.py +0 -0
  178. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/Svd.pyi +0 -0
  179. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPConfig.py +0 -0
  180. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPConfig.pyi +0 -0
  181. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPStats.py +0 -0
  182. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/V2IPStats.pyi +0 -0
  183. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/__init__.py +0 -0
  184. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/__init__.pyi +0 -0
  185. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/proto/svd.csv +0 -0
  186. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/py.typed +0 -0
  187. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Bay.py +0 -0
  188. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Bay.pyi +0 -0
  189. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/ConnectionAsync.py +0 -0
  190. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  191. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Link.py +0 -0
  192. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Link.pyi +0 -0
  193. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Remote.py +0 -0
  194. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/Remote.pyi +0 -0
  195. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/State.py +0 -0
  196. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/State.pyi +0 -0
  197. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/V2IP.py +0 -0
  198. {mx_remote-5.9.0 → mx_remote-5.9.1}/mx_remote/remote/V2IP.pyi +0 -0
  199. {mx_remote-5.9.0 → mx_remote-5.9.1}/pyproject.toml +0 -0
  200. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/audio_ir.py +0 -0
  201. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/capture.py +0 -0
  202. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/e2e.py +0 -0
  203. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/entry.py +0 -0
  204. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/handlers.py +0 -0
  205. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/handlers_cover.py +0 -0
  206. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/hello.py +0 -0
  207. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/idempotent.py +0 -0
  208. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/lengths.py +0 -0
  209. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/merge.py +0 -0
  210. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/newops.py +0 -0
  211. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/offsets.py +0 -0
  212. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/proto_stamp.py +0 -0
  213. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/protogate.py +0 -0
  214. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/rcsettings.py +0 -0
  215. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/roundtrip.py +0 -0
  216. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/run.py +0 -0
  217. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/smoke.py +0 -0
  218. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/sources.py +0 -0
  219. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/stats.py +0 -0
  220. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/structs.py +0 -0
  221. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/txresult.py +0 -0
  222. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/unknowns.py +0 -0
  223. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/v2ipscaling.py +0 -0
  224. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/v2ipsubject.py +0 -0
  225. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/vdetails.py +0 -0
  226. {mx_remote-5.9.0 → mx_remote-5.9.1}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 5.9.0
3
+ Version: 5.9.1
4
4
  Summary: Python 3 library for interfacing with MX Remote compatible devices
5
5
  Project-URL: Homepage, https://github.com/opdenkamp/mx-remote/
6
6
  Project-URL: Documentation, https://opdenkamp.github.io/mx-remote/
@@ -2275,8 +2275,8 @@ class DeviceBase(ABC):
2275
2275
  '''merge one frame of this device's V2IP source list into the cached list'''
2276
2276
 
2277
2277
  @abstractmethod
2278
- def note_link_record(self, port:int) -> None:
2279
- '''internal callback: the bay on this port reported its link record'''
2278
+ def note_link_config(self) -> None:
2279
+ '''internal callback: a page of the link configuration was received'''
2280
2280
 
2281
2281
  @abstractmethod
2282
2282
  def note_bay_config(self) -> None:
@@ -916,7 +916,7 @@ class DeviceBase(ABC):
916
916
  @abstractmethod
917
917
  def merge_v2ip_sources(self, first: int, total: int, page: V2IPStreamSourcesList) -> None: ...
918
918
  @abstractmethod
919
- def note_link_record(self, port: int) -> None: ...
919
+ def note_link_config(self) -> None: ...
920
920
  @abstractmethod
921
921
  def note_bay_config(self) -> None: ...
922
922
  @abstractmethod
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '5.9.0'
17
+ VERSION = '5.9.1'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -37,6 +37,14 @@ re-drawn after every hello that is sent, so devices announcing on the same mesh
37
37
  not converge into synchronised bursts.
38
38
  """
39
39
 
40
+ MXR_CONFIG_TIMEOUT = 15
41
+ """How long a device is given to finish describing itself, in seconds.
42
+
43
+ Past this a device is reported as fully described without its link
44
+ configuration, and keeps being asked for the rest. It never stands in for the
45
+ bay configuration - see ``Device.has_bays``.
46
+ """
47
+
40
48
  V2IP_UDP_PORT_VIDEO = 50020
41
49
  """Default destination UDP port for a V2IP video stream (firmware ``V2IP_UDP_PORT_VIDEO``)."""
42
50
 
@@ -7,6 +7,7 @@ MX_MCAST_UDP_IP: str
7
7
  MX_MCAST_UDP_PORT: int
8
8
  MXR_HELLO_INTERVAL_MIN: float
9
9
  MXR_HELLO_INTERVAL_RAND: float
10
+ MXR_CONFIG_TIMEOUT: int
10
11
  V2IP_UDP_PORT_VIDEO: int
11
12
  V2IP_UDP_PORT_ANC: int
12
13
  V2IP_UDP_PORT_AUDIO: int
@@ -44,11 +44,7 @@ class FrameLinks(FrameBase):
44
44
  return
45
45
  for link in self.links:
46
46
  link.process()
47
- # Only a record that landed on a known bay counts. One naming a bay
48
- # this client has not seen is dropped rather than held, which is
49
- # what a device does with the same frame, and the device re-sends.
50
- if (link.remote_bay is not None):
51
- dev.note_link_record(link.remote_port)
47
+ dev.note_link_config()
52
48
 
53
49
  def __str__(self) -> str:
54
50
  return f"{self.remote_device} links config page: {len(self.links)} links"
@@ -51,6 +51,7 @@ from ..proto.Multiviewer import (
51
51
  MultiviewerITCMode,
52
52
  MultiviewerHDCPMode,
53
53
  )
54
+ from ..const import MXR_CONFIG_TIMEOUT
54
55
  from ..Uid import MxrDeviceUid
55
56
  from typing import Any, Callable
56
57
  from ..compat import override
@@ -76,7 +77,7 @@ class Device(DeviceBase):
76
77
  self._registry = registry
77
78
  self._hello = hello
78
79
  self._temperatures:SystemTemperature = SystemTemperature([])
79
- self._link_records:set[int] = set()
80
+ self._link_config_received = False
80
81
  self._bay_config_received = False
81
82
  self._last_ping = datetime.now()
82
83
  self._online = True
@@ -200,17 +201,11 @@ class Device(DeviceBase):
200
201
  self.call_callbacks()
201
202
 
202
203
  @override
203
- def note_link_record(self, port:int) -> None:
204
- '''Note that a bay has reported its link record.
205
-
206
- A set of ports rather than a count, because a device re-sends its
207
- configuration: counting records would let one page repeated stand in for
208
- the pages behind it, completing a list with bays in it that have never
209
- reported a link.
210
- '''
211
- if (port in self._link_records):
204
+ def note_link_config(self) -> None:
205
+ '''Note that the device has reported its link configuration.'''
206
+ if self._link_config_received:
212
207
  return
213
- self._link_records.add(port)
208
+ self._link_config_received = True
214
209
  self._check_config_complete()
215
210
 
216
211
  @override
@@ -346,14 +341,21 @@ class Device(DeviceBase):
346
341
  return not self.need_link_config
347
342
 
348
343
  def check_configuration_complete_timeout(self) -> bool:
344
+ '''Whether this device's configuration is still on course.
345
+
346
+ Called on every pass of the probe loop, which is what gives the window
347
+ in need_link_config a moment to expire in: a device that stops waiting
348
+ for its links is announced from here, since no frame need arrive for
349
+ that to become true.
350
+
351
+ False asks for another discover - something the device owes has not
352
+ arrived and waiting has not produced it.
353
+ '''
354
+ self._check_config_complete()
349
355
  if self.configuration_complete:
350
356
  # info received
351
357
  return True
352
- if ((time.time() - self._hello_received) > 15):
353
- # configuration incomplete after 15 seconds
354
- return False
355
- # waiting for the timeout to pass
356
- return True
358
+ return ((time.time() - self._hello_received) <= MXR_CONFIG_TIMEOUT)
357
359
 
358
360
  @property
359
361
  def protocol(self) -> int:
@@ -486,6 +488,13 @@ class Device(DeviceBase):
486
488
  For a V2IP device this is half the answer on its own: its bays include
487
489
  ones that live on other devices, which arrive on a frame of their own
488
490
  that configuration_complete requires separately.
491
+
492
+ Unlike the link configuration, waiting for this never times out. A bay
493
+ is what a caller names things after, and a name it assigns before this
494
+ frame arrives is one it assigned to a placeholder - persisted, reused
495
+ from then on, and undone only by editing whatever holds it. So a device
496
+ that has not sent its bays is reported as undescribed for as long as
497
+ that lasts.
489
498
  '''
490
499
  return self._bay_config_received
491
500
 
@@ -630,15 +639,33 @@ class Device(DeviceBase):
630
639
 
631
640
  @property
632
641
  def need_link_config(self) -> bool:
633
- '''Whether the device owes link records that have not arrived.
634
-
635
- One record per bay, so a record for every input and output is the whole
636
- list. The frame carries no count and no end marker, and a short page is
637
- not the last one: a sender shrinks a page under memory pressure, and an
638
- evenly divided list ends on a full one.
642
+ '''Whether the device has yet to report its link configuration.
643
+
644
+ That it reported at all is the whole of what can be established, and no
645
+ count of bays stands in for it. Two reasons, either enough on its own:
646
+
647
+ A link record describes one of the sender's own ports. Most of a V2IP
648
+ device's bays are proxies for streams that live on other devices, and
649
+ those own no record - a 14-bay transceiver reports two, for its local
650
+ input and its local output.
651
+
652
+ A list longer than one payload is cut to what fits rather than continued.
653
+ An 18-bay amplifier reports 17 records in one page and sends no second
654
+ one, so even a device whose bays are all its own never accounts for the
655
+ last of them.
656
+
657
+ Waiting for it ends after MXR_CONFIG_TIMEOUT, and the device is still
658
+ asked for the rest. Nothing a caller has already built needs revising
659
+ when the records do arrive - a link is reported per bay and read on
660
+ access, so an unreported one reads as no link, which is what a link
661
+ coming up later looks like anyway. A withheld frame therefore costs a
662
+ consumer that window rather than leaving the device permanently
663
+ undescribed, which is a state nothing on the wire distinguishes from a
664
+ device that has no bays to offer.
639
665
  '''
640
666
  if (self.is_amp or self.is_video_matrix or self.is_audio_matrix or self.is_v2ip):
641
- return len(self._link_records) < (self.nb_inputs + self.nb_outputs)
667
+ return (not self._link_config_received) \
668
+ and ((time.time() - self._hello_received) <= MXR_CONFIG_TIMEOUT)
642
669
  return False
643
670
 
644
671
  @override
@@ -1,6 +1,7 @@
1
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, DeviceRegistry as DeviceRegistry, DeviceStatus as DeviceStatus, DeviceV2IPDetails as DeviceV2IPDetails, DeviceV2IPScalingSettings as DeviceV2IPScalingSettings, DeviceV2IPSink as DeviceV2IPSink, Multiviewer as Multiviewer, MxrCallbacks as MxrCallbacks, SystemTemperature as SystemTemperature, V2IPOutputMode as V2IPOutputMode, V2IPScalingSettings as V2IPScalingSettings, V2IPStreamSources as V2IPStreamSources, V2IPStreamSourcesList as V2IPStreamSourcesList
2
2
  from ..Uid import MxrDeviceUid as MxrDeviceUid
3
3
  from ..compat import override as override
4
+ from ..const import MXR_CONFIG_TIMEOUT as MXR_CONFIG_TIMEOUT
4
5
  from ..proto.BayConfig import BayConfig as BayConfig
5
6
  from ..proto.Constants import DeviceFeature as DeviceFeature, MXR_SCALING_FLAG_AUTO_SCALING as MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID as MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS_VALID as MXR_SCALING_FLAG_OPTIONS_VALID, MxrSignalType as MxrSignalType, V2IPFpgaFeature as V2IPFpgaFeature
6
7
  from ..proto.FrameBase import FrameBase as FrameBase
@@ -49,7 +50,7 @@ class Device(DeviceBase):
49
50
  def status_message(self) -> str: ...
50
51
  def check_online(self) -> None: ...
51
52
  @override
52
- def note_link_record(self, port: int) -> None: ...
53
+ def note_link_config(self) -> None: ...
53
54
  @override
54
55
  def note_bay_config(self) -> None: ...
55
56
  @property
@@ -0,0 +1,218 @@
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
+ '''When a device counts as fully described, and what each half of that rests on.
8
+
9
+ Both halves are paged lists with nothing on the wire marking the last page: no
10
+ count, no index, no terminating frame. So neither can be tested by comparing
11
+ what arrived against what is still to come, and each rests on the list having
12
+ been sent at all.
13
+
14
+ The bays additionally need the primary list specifically, which every unit sends
15
+ whatever else it sends; the secondary one does not stand in for it.
16
+
17
+ Only the links stop being waited for. What a device withholds is reported the
18
+ same way as what it does not have, so a wait with no end hides a fault rather
19
+ than reporting one - but a bay is what a caller names things after, and a name
20
+ assigned to a placeholder outlives the frame that would have corrected it. So
21
+ the window below releases the links, and is checked against the two halves it
22
+ must not release.
23
+
24
+ The two device shapes below are the geometry of real units, and each shows why a
25
+ count of bays is not the number of link records to expect. Both are built here
26
+ from frames rather than from mock bays, because the bay a record names is looked
27
+ up in the cache the bay frames fill: a test that sets the bays directly never
28
+ exercises the path that decides whether a record counts.
29
+ '''
30
+
31
+ import logging, os, struct, sys, time
32
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+ logging.disable(logging.CRITICAL)
34
+ import mx_remote
35
+ from mx_remote.const import MXR_CONFIG_TIMEOUT
36
+ from mx_remote.Uid import MxrDeviceUid
37
+ from mx_remote.proto.Factory import create_mxr_frame
38
+
39
+ class Watching(mx_remote.MxrCallbacks):
40
+ def __init__(self, sink):
41
+ self.sink = sink
42
+ def on_device_config_complete(self, dev):
43
+ self.sink.append(dev)
44
+
45
+ completed = []
46
+ ADDR = ('192.0.2.9', 8812)
47
+ mx = mx_remote.Remote(open_connection=False, callbacks=Watching(completed))
48
+ mx._uid = bytes(range(100, 116))
49
+
50
+ # device features
51
+ VIDEO_ROUTING, AUDIO_ROUTING, VOLUME_CONTROL = (1 << 5), (1 << 6), (1 << 7)
52
+ V2IP_SOURCE, V2IP_SINK = (1 << 3), (1 << 4)
53
+ # bay features
54
+ HDMI_OUT, HDMI_IN = (1 << 0), (1 << 1)
55
+ V2IP_SOURCE_REMOTE, V2IP_SINK_REMOTE = (1 << 13), (1 << 14)
56
+
57
+ def nm(s, sz=16):
58
+ b = s.encode('ascii')[:sz]
59
+ return b + bytes(sz - len(b))
60
+ def rx(uid, op, pl=b''):
61
+ mx.process_frame(time.time(), create_mxr_frame(uid, op, pl), ADDR)
62
+ def hello(uid, name, serial, features):
63
+ rx(uid, 0x00, struct.pack('<H', 0x28) + nm(name) + nm(serial) + nm('4.7.9')
64
+ + struct.pack('<I', features))
65
+ return mx.get_by_uid(MxrDeviceUid(uid))
66
+ def bay(port, mode, num, name, features=0):
67
+ bayfeat = features | (HDMI_IN if mode == 0 else HDMI_OUT)
68
+ return bytes([port, mode, num, 0, 0]) + nm(name) + nm(name) + nm('1080p') \
69
+ + struct.pack('<I', 0) + struct.pack('<I', bayfeat)
70
+ def link(port, serial, baynm):
71
+ return bytes([port, 0]) + nm(serial) + nm(baynm) + struct.pack('<I', 1)
72
+
73
+ MATRIX = bytes(range(1, 17))
74
+ dev = hello(MATRIX, 'MX-1', 'P8SN12345678', VIDEO_ROUTING | AUDIO_ROUTING)
75
+ assert dev is not None and dev.is_video_matrix
76
+ assert not dev.configuration_complete, 'a hello describes no bays'
77
+ print('complete : a hello on its own is not a configuration')
78
+
79
+ # ---- the primary bay list, and only that one
80
+ # The secondary list registers the same bays, so after it the device knows every
81
+ # bay it will ever hear about and still has not heard the list that says so.
82
+ rx(MATRIX, 0x23, bay(0, 0, 0, 'In 1') + bay(1, 0, 1, 'In 2') + bay(2, 1, 0, 'Out 1'))
83
+ assert len(dev.bays) == 3, dev.bays
84
+ assert not dev.has_bays, 'the secondary list does not stand in for the primary one'
85
+ assert not dev.configuration_complete
86
+
87
+ rx(MATRIX, 0x02, bay(0, 0, 0, 'In 1') + bay(1, 0, 1, 'In 2'))
88
+ assert dev.has_bays, 'the primary list is what completes the bays'
89
+ assert len(dev.bays) == 3, 'a page is merged into the cache, never replaces it'
90
+ print('complete : the bays need the primary list, whatever else arrived')
91
+
92
+ # ---- the links, reported
93
+ assert dev.need_link_config, 'no link configuration has arrived'
94
+ assert not dev.configuration_complete
95
+ assert not completed, 'nothing may be announced complete before the links are in'
96
+
97
+ rx(MATRIX, 0x03, link(0, 'P8SN00000001', 'Out 1'))
98
+ assert not dev.need_link_config, 'the device has reported its links'
99
+ assert dev.configuration_complete
100
+ print('complete : the links need the list, and the list to have been sent')
101
+
102
+ # ---- announced once, at that moment
103
+ assert completed == [dev], completed
104
+ rx(MATRIX, 0x02, bay(0, 0, 0, 'In 1') + bay(1, 0, 1, 'In 2'))
105
+ rx(MATRIX, 0x03, link(0, 'P8SN00000001', 'Out 1'))
106
+ assert completed == [dev], 'a re-sent configuration must not announce completion again'
107
+ print('complete : announced once, when the last of it arrived')
108
+
109
+ # ---- a V2IP transceiver: 14 bays, two of them its own
110
+ # Its remaining bays are proxies for streams on other devices. A link record
111
+ # describes one of the sender's own ports, so those twelve own no record and a
112
+ # count of bays is one no page will ever reach.
113
+ ONEIP = bytes(range(17, 33))
114
+ tz = hello(ONEIP, 'OneIP', 'P9SN66662814', V2IP_SOURCE | V2IP_SINK | VIDEO_ROUTING)
115
+ assert tz is not None and tz.is_v2ip
116
+ bays = bay(0, 0, 0, 'Input 1')
117
+ for port in range(1, 13):
118
+ bays += bay(port, 0, port, f'Input {port + 1}', V2IP_SOURCE_REMOTE)
119
+ bays += bay(16, 1, 0, 'Output 1')
120
+ rx(ONEIP, 0x02, bays)
121
+ assert len(tz.bays) == 14, tz.bays
122
+ assert tz.nb_inputs + tz.nb_outputs == 14, 'every bay is an input or an output here'
123
+
124
+ # Its V2IP source list, which completion needs for a V2IP device in its own right.
125
+ rx(ONEIP, 0x26, bytes(40 * 13))
126
+ assert tz.v2ip_sources is not None and len(tz.v2ip_sources) == 13
127
+
128
+ assert tz.need_link_config and not tz.configuration_complete
129
+ rx(ONEIP, 0x03, link(0, 'P9SN66662903', 'Output 1') + link(16, 'P9SN66662903', 'Input 1'))
130
+ assert not tz.need_link_config, 'two records are this device\'s whole list'
131
+ assert tz.configuration_complete, 'a bay that owns no link record must not gate completion'
132
+ assert completed == [dev, tz], completed
133
+ print('complete : a V2IP device reports links for its own two bays, not its fourteen')
134
+
135
+ # ---- an amplifier: 18 bays, 17 records, no second page
136
+ # The list is cut to what fits one payload rather than continued, so the last
137
+ # bay is never accounted for on any device.
138
+ AMP = bytes(range(33, 49))
139
+ amp = hello(AMP, 'PROAMP8', 'P9SN66667000', AUDIO_ROUTING | VOLUME_CONTROL)
140
+ assert amp is not None and amp.is_amp
141
+ bays = b''.join(bay(port, 0, port, f'Input {port + 1}') for port in range(9)) \
142
+ + b''.join(bay(9 + num, 1, num, f'Output {num + 1}') for num in range(9))
143
+ rx(AMP, 0x02, bays)
144
+ assert len(amp.bays) == 18, amp.bays
145
+
146
+ records = b''.join(link(port, 'P9SN66662814', 'Output 1') for port in range(17))
147
+ assert len(records) == 646, 'the page a ProAmp8 sends, to the byte'
148
+ rx(AMP, 0x03, records)
149
+ assert not amp.need_link_config, 'the page the device sent is the list it has'
150
+ assert amp.configuration_complete, 'a bay left out of a cut list must not gate completion'
151
+ assert completed == [dev, tz, amp], completed
152
+ print('complete : a cut list is the whole of what a device reports')
153
+
154
+ # ---- waiting for the links ends, and for nothing else
155
+ # Wind the clock back rather than sleeping, and drive it through the call the
156
+ # probe loop makes: no frame arrives when a window expires, so that call is the
157
+ # only thing that can announce it.
158
+ LATE = bytes(range(49, 65))
159
+ late = hello(LATE, 'MX-2', 'P8SN22222222', VIDEO_ROUTING | AUDIO_ROUTING)
160
+ assert late.need_link_config, 'inside the window the links are still awaited'
161
+ late._hello_received -= (MXR_CONFIG_TIMEOUT + 1)
162
+ assert not late.need_link_config, 'past it they are not'
163
+
164
+ rx(LATE, 0x23, bay(0, 0, 0, 'In 1'))
165
+ assert not late.has_bays and not late.configuration_complete, \
166
+ 'the window must not stand in for the primary bay list'
167
+ assert not late.check_configuration_complete_timeout(), 'a device still owing bays is still asked'
168
+ assert late not in completed, 'and nothing may name a bay it has not described'
169
+
170
+ rx(LATE, 0x02, bay(0, 0, 0, 'In 1'))
171
+ assert late.configuration_complete, 'the bays arrived, and the links are no longer awaited'
172
+ assert late.check_configuration_complete_timeout()
173
+ assert completed == [dev, tz, amp, late], completed
174
+ print('complete : the links stop being waited for, the bays never do')
175
+
176
+ # A V2IP device owes its source list in its own right, which the window does not
177
+ # release either.
178
+ LATEV2IP = bytes(range(65, 81))
179
+ vl = hello(LATEV2IP, 'OneIP-2', 'P9SN66662905', V2IP_SOURCE | V2IP_SINK)
180
+ vl._hello_received -= (MXR_CONFIG_TIMEOUT + 1)
181
+ rx(LATEV2IP, 0x02, bay(0, 0, 0, 'Input 1') + bay(16, 1, 0, 'Output 1'))
182
+ assert vl.has_bays and not vl.need_link_config
183
+ assert not vl.configuration_complete, 'the window must not stand in for the V2IP source list'
184
+ assert not vl.check_configuration_complete_timeout()
185
+ assert vl not in completed
186
+
187
+ rx(LATEV2IP, 0x26, bytes(40 * 2))
188
+ assert vl.configuration_complete and vl.check_configuration_complete_timeout()
189
+ assert completed == [dev, tz, amp, late, vl], completed
190
+ print('complete : nor does it stand in for a V2IP source list')
191
+
192
+ # Announced by the expiry alone, driven the way the runtime drives it. No frame
193
+ # arrives when a window closes, so the probe loop is the only thing that can
194
+ # notice, and the call it makes there is the whole of the announcement path.
195
+ stuck_seen = []
196
+ runtime = mx_remote.Remote(open_connection=False, callbacks=Watching(stuck_seen))
197
+ runtime._uid = bytes(range(100, 116))
198
+ runtime.transmit = lambda data: len(data) # no socket; the loop's sends are not the subject
199
+
200
+ STUCK = bytes(range(81, 97))
201
+ def rx_runtime(op, pl):
202
+ runtime.process_frame(time.time(), create_mxr_frame(STUCK, op, pl), ADDR)
203
+ rx_runtime(0x00, struct.pack('<H', 0x28) + nm('MX-3') + nm('P8SN33333333') + nm('4.7.9')
204
+ + struct.pack('<I', VIDEO_ROUTING | AUDIO_ROUTING))
205
+ stuck = runtime.get_by_uid(MxrDeviceUid(STUCK))
206
+ rx_runtime(0x02, bay(0, 0, 0, 'In 1'))
207
+
208
+ runtime._probe_once()
209
+ assert not stuck.configuration_complete
210
+ assert stuck_seen == [], 'inside the window the loop announces nothing'
211
+
212
+ stuck._hello_received -= (MXR_CONFIG_TIMEOUT + 1)
213
+ runtime._probe_once()
214
+ assert stuck_seen == [stuck], 'the loop announces a device no further frame will complete'
215
+ print('complete : announced from the probe loop, on the expiry alone')
216
+
217
+ print()
218
+ print('ALL OK')
@@ -1,105 +0,0 @@
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
- '''When a device counts as fully described, and what each half of that rests on.
8
-
9
- Both halves are paged lists with nothing on the wire marking the last page: no
10
- count, no index, no terminating frame. So neither can be tested by comparing
11
- what arrived against what is still to come, and each needs a test of its own
12
- shape.
13
-
14
- The bays: that the primary list was sent at all, which every unit sends whatever
15
- else it sends. The links: one record per bay, so a record for every input and
16
- output is the whole list.
17
-
18
- A device re-sends its configuration, so every assertion below has to survive the
19
- same frame twice. That is what makes the link half a set of ports rather than a
20
- count - one page repeated would otherwise stand in for the pages behind it.
21
- '''
22
-
23
- import logging, os, struct, sys, time
24
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25
- logging.disable(logging.CRITICAL)
26
- import mx_remote
27
- from mx_remote.Uid import MxrDeviceUid
28
- from mx_remote.proto.Factory import create_mxr_frame
29
-
30
- completed = []
31
- class Watching(mx_remote.MxrCallbacks):
32
- def on_device_config_complete(self, dev):
33
- completed.append(dev)
34
-
35
- UID = bytes(range(1, 17))
36
- ADDR = ('192.0.2.9', 8812)
37
- mx = mx_remote.Remote(open_connection=False, callbacks=Watching())
38
- mx._uid = bytes(range(100, 116))
39
-
40
- def nm(s, sz=16):
41
- b = s.encode('ascii')[:sz]
42
- return b + bytes(sz - len(b))
43
- def rx(op, pl=b''):
44
- mx.process_frame(time.time(), create_mxr_frame(UID, op, pl), ADDR)
45
- def bay(port, mode, num, name):
46
- return bytes([port, mode, num, 0, 0]) + nm(name) + nm(name) + nm('1080p') \
47
- + struct.pack('<I', 0) + struct.pack('<I', (1 << 1) if mode == 0 else (1 << 0))
48
- def link(port, serial, baynm):
49
- return bytes([port, 0]) + nm(serial) + nm(baynm) + struct.pack('<I', 1)
50
-
51
- # A video matrix, so it is a device that reports links at all.
52
- FEAT = (1 << 5) | (1 << 6)
53
- rx(0x00, struct.pack('<H', 0x28) + nm('MX-1') + nm('P8SN12345678') + nm('4.7.9')
54
- + struct.pack('<I', FEAT))
55
- dev = mx.get_by_uid(MxrDeviceUid(UID))
56
- assert dev is not None and dev.is_video_matrix
57
- assert not dev.configuration_complete, 'a hello describes no bays'
58
- print('complete : a hello on its own is not a configuration')
59
-
60
- # ---- the primary bay list, and only that one
61
- # The secondary list registers the same bays, so after it the device knows every
62
- # bay it will ever hear about and still has not heard the list that says so.
63
- rx(0x23, bay(0, 0, 0, 'In 1') + bay(1, 0, 1, 'In 2') + bay(2, 1, 0, 'Out 1'))
64
- assert len(dev.bays) == 3, dev.bays
65
- assert not dev.has_bays, 'the secondary list does not stand in for the primary one'
66
- assert not dev.configuration_complete
67
-
68
- rx(0x02, bay(0, 0, 0, 'In 1') + bay(1, 0, 1, 'In 2'))
69
- assert dev.has_bays, 'the primary list is what completes the bays'
70
- assert len(dev.bays) == 3, 'a page is merged into the cache, never replaces it'
71
- print('complete : the bays need the primary list, whatever else arrived')
72
-
73
- # ---- one link record per bay
74
- assert dev.need_link_config, 'no record has arrived'
75
- assert not dev.configuration_complete
76
-
77
- # The same page three times. A count would reach the three records this device
78
- # owes; the ports say one bay has reported and two have not.
79
- for _ in range(3):
80
- rx(0x03, link(0, 'P8SN00000001', 'Out 1'))
81
- assert dev.need_link_config, 'a repeated page must not stand in for the pages behind it'
82
-
83
- # A record for a bay this device never advertised is not one of the three.
84
- rx(0x03, link(9, 'P8SN00000001', 'Out 1'))
85
- assert dev.need_link_config, 'a record on an unknown bay counts for nothing'
86
-
87
- rx(0x03, link(1, 'P8SN00000002', 'Out 2'))
88
- assert dev.need_link_config, 'two of three bays have reported'
89
- assert not dev.configuration_complete
90
- assert not completed, 'nothing may be announced complete while a record is missing'
91
-
92
- rx(0x03, link(2, 'P8SN00000003', 'In 1'))
93
- assert not dev.need_link_config, 'every bay has reported its link record'
94
- assert dev.configuration_complete
95
- print('complete : one record per bay, counted per bay and not per page')
96
-
97
- # ---- announced once, at that moment
98
- assert completed == [dev], completed
99
- rx(0x02, bay(0, 0, 0, 'In 1') + bay(1, 0, 1, 'In 2'))
100
- rx(0x03, link(0, 'P8SN00000001', 'Out 1'))
101
- assert completed == [dev], 'a re-sent configuration must not announce completion again'
102
- print('complete : announced once, when the last of it arrived')
103
-
104
- print()
105
- print('ALL OK')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes