mx-remote 9.3.0__tar.gz → 9.4.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 (274) hide show
  1. {mx_remote-9.3.0 → mx_remote-9.4.0}/PKG-INFO +1 -1
  2. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/oneip.md +14 -0
  3. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/Interface.py +21 -0
  4. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/Interface.pyi +9 -0
  5. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/const.py +1 -1
  6. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Constants.py +12 -0
  7. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Constants.pyi +5 -0
  8. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSinkSdp.py +56 -4
  9. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +15 -0
  10. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Device.py +102 -4
  11. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Device.pyi +8 -2
  12. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/unit.py +30 -0
  13. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2ipsdp.py +80 -2
  14. {mx_remote-9.3.0 → mx_remote-9.4.0}/.gitignore +0 -0
  15. {mx_remote-9.3.0 → mx_remote-9.4.0}/AGENTS.md +0 -0
  16. {mx_remote-9.3.0 → mx_remote-9.4.0}/CLAUDE.md +0 -0
  17. {mx_remote-9.3.0 → mx_remote-9.4.0}/LICENSE +0 -0
  18. {mx_remote-9.3.0 → mx_remote-9.4.0}/README.md +0 -0
  19. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/README.md +0 -0
  20. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/_static/brand.css +0 -0
  21. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/_static/favicon.svg +0 -0
  22. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/_static/logo-dark.svg +0 -0
  23. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/_static/logo.svg +0 -0
  24. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/api.md +0 -0
  25. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/audio.md +0 -0
  26. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/callbacks.md +0 -0
  27. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/conf.py +0 -0
  28. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/configuration.md +0 -0
  29. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/devices-and-bays.md +0 -0
  30. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/diagnostics.md +0 -0
  31. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/index.md +0 -0
  32. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/multiviewer.md +0 -0
  33. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/requirements.txt +0 -0
  34. {mx_remote-9.3.0 → mx_remote-9.4.0}/docs/routing.md +0 -0
  35. {mx_remote-9.3.0 → mx_remote-9.4.0}/hatch_build.py +0 -0
  36. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/Uid.py +0 -0
  37. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/Uid.pyi +0 -0
  38. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/__init__.py +0 -0
  39. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/__init__.pyi +0 -0
  40. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/compat.py +0 -0
  41. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/compat.pyi +0 -0
  42. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/const.pyi +0 -0
  43. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/deprecated.py +0 -0
  44. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/deprecated.pyi +0 -0
  45. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/main.py +0 -0
  46. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/main.pyi +0 -0
  47. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/BayConfig.py +0 -0
  48. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/BayConfig.pyi +0 -0
  49. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Data.py +0 -0
  50. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Data.pyi +0 -0
  51. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Factory.py +0 -0
  52. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Factory.pyi +0 -0
  53. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  54. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  55. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  56. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  57. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  58. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  59. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  60. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  61. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBase.py +0 -0
  62. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBase.pyi +0 -0
  63. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  64. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  65. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  66. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  67. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayContent.py +0 -0
  68. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  69. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayHide.py +0 -0
  70. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  71. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  72. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  73. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  74. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  75. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDebug.py +0 -0
  76. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  77. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDiscover.py +0 -0
  78. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  79. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDID.py +0 -0
  80. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  81. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  82. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  83. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  84. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  85. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  86. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  87. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  88. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  89. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHeader.py +0 -0
  90. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  91. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHello.py +0 -0
  92. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHello.pyi +0 -0
  93. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameLinks.py +0 -0
  94. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  95. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  96. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  97. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  98. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  99. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  100. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  101. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  102. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  103. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FramePing.py +0 -0
  104. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FramePing.pyi +0 -0
  105. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FramePowerChange.py +0 -0
  106. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  107. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCAction.py +0 -0
  108. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  109. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCIr.py +0 -0
  110. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  111. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCKey.py +0 -0
  112. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  113. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  114. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  115. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameReboot.py +0 -0
  116. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  117. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  118. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  119. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  120. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  121. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetName.py +0 -0
  122. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  123. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  124. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  125. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  126. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  127. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  128. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  129. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  130. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  131. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  132. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  133. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  134. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  135. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  136. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  137. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  138. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  139. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameThirdParty.py +0 -0
  140. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameThirdParty.pyi +0 -0
  141. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTime.py +0 -0
  142. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTime.pyi +0 -0
  143. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  144. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  145. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTopology.py +0 -0
  146. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  147. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTxIR.py +0 -0
  148. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  149. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  150. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  151. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  152. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  153. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  154. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  155. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  156. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  157. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  158. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  159. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  160. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  161. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  162. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  163. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  164. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  165. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  166. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  167. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  168. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  169. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPOsdSync.py +0 -0
  170. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +0 -0
  171. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  172. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  173. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  174. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  175. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  176. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  177. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  178. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  179. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  180. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  181. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  182. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  183. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  184. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  185. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  186. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  187. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  188. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  189. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  190. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  191. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolume.py +0 -0
  192. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  193. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  194. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  195. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  196. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  197. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  198. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  199. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/LinkConfig.py +0 -0
  200. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  201. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Multiviewer.py +0 -0
  202. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  203. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Svd.py +0 -0
  204. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/Svd.pyi +0 -0
  205. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPConfig.py +0 -0
  206. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  207. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPStats.py +0 -0
  208. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  209. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/__init__.py +0 -0
  210. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/__init__.pyi +0 -0
  211. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/proto/svd.csv +0 -0
  212. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/py.typed +0 -0
  213. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Bay.py +0 -0
  214. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Bay.pyi +0 -0
  215. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  216. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  217. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Link.py +0 -0
  218. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Link.pyi +0 -0
  219. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/OsdSync.py +0 -0
  220. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/OsdSync.pyi +0 -0
  221. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Remote.py +0 -0
  222. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/Remote.pyi +0 -0
  223. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/State.py +0 -0
  224. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/State.pyi +0 -0
  225. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/V2IP.py +0 -0
  226. {mx_remote-9.3.0 → mx_remote-9.4.0}/mx_remote/remote/V2IP.pyi +0 -0
  227. {mx_remote-9.3.0 → mx_remote-9.4.0}/pyproject.toml +0 -0
  228. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/audio_ir.py +0 -0
  229. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/audiolock.py +0 -0
  230. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/baycontent.py +0 -0
  231. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/capture.py +0 -0
  232. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/complete.py +0 -0
  233. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/deprecated.py +0 -0
  234. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/downmix.py +0 -0
  235. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/e2e.py +0 -0
  236. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/entry.py +0 -0
  237. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/handlers.py +0 -0
  238. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/handlers_cover.py +0 -0
  239. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/hello.py +0 -0
  240. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/idempotent.py +0 -0
  241. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/lengths.py +0 -0
  242. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/merge.py +0 -0
  243. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/mesh.py +0 -0
  244. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/meshbays.py +0 -0
  245. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/newops.py +0 -0
  246. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/offline.py +0 -0
  247. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/offsets.py +0 -0
  248. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/osdmessage.py +0 -0
  249. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/osdsync.py +0 -0
  250. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/ping.py +0 -0
  251. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/privacy.py +0 -0
  252. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/proto_stamp.py +0 -0
  253. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/protogate.py +0 -0
  254. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/rcsettings.py +0 -0
  255. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/roundtrip.py +0 -0
  256. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/routes.py +0 -0
  257. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/run.py +0 -0
  258. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/smoke.py +0 -0
  259. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/sources.py +0 -0
  260. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/staterequest.py +0 -0
  261. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/stats.py +0 -0
  262. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/structs.py +0 -0
  263. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/thirdparty.py +0 -0
  264. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/txresult.py +0 -0
  265. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/uid.py +0 -0
  266. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/unknowns.py +0 -0
  267. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2ipptp.py +0 -0
  268. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2ipscaling.py +0 -0
  269. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2ipsettings.py +0 -0
  270. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2ipsubject.py +0 -0
  271. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2iptestcard.py +0 -0
  272. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/v2ipvlan.py +0 -0
  273. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/vdetails.py +0 -0
  274. {mx_remote-9.3.0 → mx_remote-9.4.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 9.3.0
3
+ Version: 9.4.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/
@@ -432,6 +432,14 @@ application, which this client announces itself as, and only from protocol
432
432
  after its last page, so one is refused before it is sent. Nothing acknowledges
433
433
  the pages; `oneip_sink_sdp` reads what the output reports next.
434
434
 
435
+ From protocol 0x31 an output also reports the description it plays, when it
436
+ starts a stream other than by its mesh controller's command and when asked:
437
+
438
+ ```python
439
+ await sink.request_oneip_sink_description() # False below protocol 0x31
440
+ print(sink.oneip_sink_description) # None until reported, and once it plays none
441
+ ```
442
+
435
443
  ### Overlay sync
436
444
 
437
445
  A management unit can have the other units copy its overlay captions, logo
@@ -553,8 +561,14 @@ await device.set_unit_network(UnitNetConfig(
553
561
  ip='10.1.2.3', netmask='255.255.255.0', gateway='10.1.2.254', dns1='10.1.2.254'))
554
562
  await device.confirm_unit_network() # keep it, once the unit reports it
555
563
  await device.set_unit_dhcp()
564
+ await device.locate_unit(timed=True) # flash its LED for 30 s; timed=False until stopped
565
+ await device.stop_locating_unit()
556
566
  ```
557
567
 
568
+ A unit is located only once its status carries `UnitFlag.LOCATE_SUPPORTED`. Its
569
+ board-ID LED flashes and an output shows "Locate Device"; `UnitFlag.LOCATE` in
570
+ the cached status says so from the moment the command is sent.
571
+
558
572
  A changed IP configuration applies at once and reverts unless the mesh
559
573
  controller confirms it in time; `status.network_pending` says whether it still
560
574
  waits. Each command returns False without sending for a device that is not a
@@ -3355,6 +3355,15 @@ class DeviceBase(ABC):
3355
3355
  async def confirm_unit_network(self) -> bool:
3356
3356
  '''keep the unit's pending IP configuration'''
3357
3357
 
3358
+ @abstractmethod
3359
+ async def locate_unit(self, timed:bool=False) -> bool:
3360
+ '''flash the unit's board-ID LED and show "Locate Device" on its output,
3361
+ for 30 s when timed is set, until stop_locating_unit() otherwise'''
3362
+
3363
+ @abstractmethod
3364
+ async def stop_locating_unit(self) -> bool:
3365
+ '''stop locating the unit'''
3366
+
3358
3367
  @property
3359
3368
  @abstractmethod
3360
3369
  def oneip_downmix(self) -> OneIPDownmix|None:
@@ -3740,6 +3749,18 @@ class DeviceBase(ABC):
3740
3749
  async def request_oneip_sink_sdp(self) -> bool:
3741
3750
  '''ask this OneIP output what it plays from a session description now'''
3742
3751
 
3752
+ @property
3753
+ @abstractmethod
3754
+ def oneip_sink_description(self) -> str|None:
3755
+ '''The session description this OneIP output last reported playing, None
3756
+ until it has and once it reports playing none. An output reports one
3757
+ when it starts a stream other than by its mesh controller's command,
3758
+ and when asked with request_oneip_sink_description().'''
3759
+
3760
+ @abstractmethod
3761
+ async def request_oneip_sink_description(self) -> bool:
3762
+ '''ask this OneIP output for the session description it plays'''
3763
+
3743
3764
  @abstractmethod
3744
3765
  async def set_oneip_test_pattern(self, pattern:OneIPTestPattern, colour:int=0) -> bool:
3745
3766
  '''show a test pattern on this sink's output'''
@@ -1210,6 +1210,10 @@ class DeviceBase(ABC):
1210
1210
  async def set_unit_network(self, network: UnitNetConfig) -> bool: ...
1211
1211
  @abstractmethod
1212
1212
  async def confirm_unit_network(self) -> bool: ...
1213
+ @abstractmethod
1214
+ async def locate_unit(self, timed: bool = False) -> bool: ...
1215
+ @abstractmethod
1216
+ async def stop_locating_unit(self) -> bool: ...
1213
1217
  @property
1214
1218
  @abstractmethod
1215
1219
  def oneip_downmix(self) -> OneIPDownmix | None: ...
@@ -1393,6 +1397,11 @@ class DeviceBase(ABC):
1393
1397
  async def set_oneip_sdp_screen(self, screen: int) -> bool: ...
1394
1398
  @abstractmethod
1395
1399
  async def request_oneip_sink_sdp(self) -> bool: ...
1400
+ @property
1401
+ @abstractmethod
1402
+ def oneip_sink_description(self) -> str | None: ...
1403
+ @abstractmethod
1404
+ async def request_oneip_sink_description(self) -> bool: ...
1396
1405
  @abstractmethod
1397
1406
  async def set_oneip_test_pattern(self, pattern: OneIPTestPattern, colour: int = 0) -> bool: ...
1398
1407
  @abstractmethod
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '9.3.0'
17
+ VERSION = '9.4.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -684,6 +684,11 @@ class UnitFlag(IntFlag):
684
684
  '''The boot count is reported.'''
685
685
  FAN_SPEED = (1 << 15)
686
686
  '''The fan speed is reported.'''
687
+ LOCATE_SUPPORTED = (1 << 16)
688
+ '''The unit takes UnitCommandKind.LOCATE.'''
689
+ LOCATE = (1 << 17)
690
+ '''The unit is being located: its board-ID LED flashes, and an output shows
691
+ "Locate Device".'''
687
692
 
688
693
  class UnitCommandKind(IntEnum):
689
694
  '''What a unit command asks of its unit.'''
@@ -695,6 +700,8 @@ class UnitCommandKind(IntEnum):
695
700
  '''Change the IP configuration.'''
696
701
  NET_CONFIRM = 3
697
702
  '''Keep a pending IP configuration.'''
703
+ LOCATE = 4
704
+ '''Start or stop locating the unit.'''
698
705
 
699
706
  class UnitCommandFlag(IntFlag):
700
707
  '''The flags of a unit command.'''
@@ -703,6 +710,11 @@ class UnitCommandFlag(IntFlag):
703
710
  already.'''
704
711
  DHCP = (1 << 1)
705
712
  '''With NET_CONFIG: use DHCP, ignoring the addresses.'''
713
+ LOCATE_ON = (1 << 2)
714
+ '''With LOCATE: locate it until stopped. Neither this nor LOCATE_TIMED
715
+ stops it.'''
716
+ LOCATE_TIMED = (1 << 3)
717
+ '''With LOCATE: locate it for 30 s.'''
706
718
 
707
719
  UNIT_MAX_TEMPERATURES = 6
708
720
  """The most temperature sensors a unit reports."""
@@ -271,16 +271,21 @@ class UnitFlag(IntFlag):
271
271
  POWER_CHECK: Incomplete
272
272
  BOOT_COUNT: Incomplete
273
273
  FAN_SPEED: Incomplete
274
+ LOCATE_SUPPORTED: Incomplete
275
+ LOCATE: Incomplete
274
276
 
275
277
  class UnitCommandKind(IntEnum):
276
278
  CLEAR_STATUS: int
277
279
  VIDEO_PROCESSOR_UPGRADE: int
278
280
  NET_CONFIG: int
279
281
  NET_CONFIRM: int
282
+ LOCATE: int
280
283
 
281
284
  class UnitCommandFlag(IntFlag):
282
285
  FORCE: Incomplete
283
286
  DHCP: Incomplete
287
+ LOCATE_ON: Incomplete
288
+ LOCATE_TIMED: Incomplete
284
289
 
285
290
  UNIT_MAX_TEMPERATURES: int
286
291
 
@@ -7,6 +7,7 @@
7
7
  '''Protocol frame for V2IP_SINK_SDP (opcode 0x52): what a OneIP output plays
8
8
  from a session description, and the commands that hand it one.'''
9
9
 
10
+ from dataclasses import dataclass
10
11
  from functools import cached_property
11
12
  import ipaddress
12
13
  import struct
@@ -30,9 +31,11 @@ from ..Uid import MxrDeviceUid
30
31
  #
31
32
  # A page follows the header with the description's length, the page's offset
32
33
  # into it and the page's length, all u16, two reserved bytes, and at most
33
- # SDP_PAGE_LEN bytes of the description.
34
+ # SDP_PAGE_LEN bytes of the description. An output reports the description it
35
+ # plays in pages of the same shape.
34
36
  SDP_HEADER_SIZE = 20
35
37
  _STATUS_SIZE = 80
38
+ _PAGE_HEADER_SIZE = 8
36
39
 
37
40
  SDP_STATUS = 0
38
41
  '''The output's report.'''
@@ -44,10 +47,35 @@ SDP_SCREEN = 3
44
47
  '''What the output shows while it plays audio alone, in the value byte.'''
45
48
  SDP_REQUEST = 4
46
49
  '''Report now.'''
50
+ SDP_DESCRIPTION = 5
51
+ '''A page of the description an output reports playing.'''
52
+
53
+ SDP_REQUEST_DESCRIPTION = (1 << 0)
54
+ '''The bit of a request's value that asks for the description too.'''
55
+
56
+ SDP_DESCRIPTION_PROTOCOL = 0x31
57
+ '''The first protocol version whose outputs report the description they play.
58
+ Not the stamp: that is the opcode's, and an output between the two answers a
59
+ request for the description with its status alone.'''
60
+
61
+ SDP_PAGE_TIMEOUT = 5
62
+ '''How long an output may take between two pages of one description, in
63
+ seconds, before the pages it sent are dropped.'''
47
64
 
48
65
  SDP_PAGE_LEN = 1024
49
66
  '''The most bytes of a session description one page carries.'''
50
67
 
68
+ @dataclass(frozen=True)
69
+ class SdpDescriptionPage:
70
+ '''A page of the description an output reports playing.'''
71
+ total: int
72
+ '''The length of the whole description.'''
73
+ offset: int
74
+ '''Where this page's text starts in it.'''
75
+ text: bytes
76
+ timestamp: float
77
+ '''When the page arrived.'''
78
+
51
79
  def _cstr(data:bytes) -> str:
52
80
  return data.split(b'\0', 1)[0].decode('ascii', errors='replace')
53
81
 
@@ -109,14 +137,38 @@ class FrameV2IPSinkSdp(FrameBase):
109
137
  # that fills its field stops one byte short of it.
110
138
  name=_cstr(s[24:71]), layout=_cstr(s[72:79]))
111
139
 
140
+ @cached_property
141
+ def description_page(self) -> SdpDescriptionPage|None:
142
+ '''A page of the description the output reports playing, None for
143
+ another type, for a page whose header names a device other than its
144
+ sender, which only the output itself reports, and for a frame too short
145
+ for the text its page header declares.'''
146
+ p = self.payload or b''
147
+ if (self.kind != SDP_DESCRIPTION) or (self.target_uid != self.remote_id) \
148
+ or (len(p) < SDP_HEADER_SIZE + _PAGE_HEADER_SIZE):
149
+ return None
150
+ total, offset, length = struct.unpack_from('<HHH', p, SDP_HEADER_SIZE)
151
+ start = SDP_HEADER_SIZE + _PAGE_HEADER_SIZE
152
+ if (len(p) < start + length):
153
+ return None
154
+ return SdpDescriptionPage(total=total, offset=offset, text=bytes(p[start:start + length]),
155
+ timestamp=self.timestamp)
156
+
112
157
  @override
113
158
  def process(self) -> None:
114
- '''Record an output's report against the output that sent it. The other
115
- types are commands for an output, which only that output acts on.'''
116
- if ((status := self.status) is not None) and ((dev := self.remote_device) is not None):
159
+ '''Record an output's report and the pages of its description against
160
+ the output that sent them. The other types are commands for an output,
161
+ which only that output acts on.'''
162
+ if ((dev := self.remote_device) is None):
163
+ return
164
+ if ((status := self.status) is not None):
117
165
  dev.on_mxr_update(status)
166
+ elif ((page := self.description_page) is not None):
167
+ dev.on_mxr_update(page)
118
168
 
119
169
  def __str__(self) -> str:
120
170
  if (self.status is not None):
121
171
  return f"OneIP sink SDP: {self.status}"
172
+ if ((page := self.description_page) is not None):
173
+ return f"OneIP sink SDP description: bytes {page.offset} to {page.offset + len(page.text)} of {page.total}"
122
174
  return f"OneIP sink SDP command {self.kind} for {self.uid_to_user_string(self.target_uid)}"
@@ -3,6 +3,8 @@ from ..Uid import MxrDeviceUid as MxrDeviceUid
3
3
  from ..compat import override as override
4
4
  from .Constants import OneIPSdpClockMode as OneIPSdpClockMode, OneIPSdpClockState as OneIPSdpClockState, OneIPSdpEncoding as OneIPSdpEncoding, OneIPSdpFlag as OneIPSdpFlag, OneIPSdpMediaClock as OneIPSdpMediaClock, OneIPSdpRefClock as OneIPSdpRefClock
5
5
  from .FrameBase import FrameBase as FrameBase
6
+ from _typeshed import Incomplete
7
+ from dataclasses import dataclass
6
8
  from functools import cached_property
7
9
  from typing import Any
8
10
 
@@ -12,8 +14,19 @@ SDP_PAGE: int
12
14
  SDP_STOP: int
13
15
  SDP_SCREEN: int
14
16
  SDP_REQUEST: int
17
+ SDP_DESCRIPTION: int
18
+ SDP_REQUEST_DESCRIPTION: Incomplete
19
+ SDP_DESCRIPTION_PROTOCOL: int
20
+ SDP_PAGE_TIMEOUT: int
15
21
  SDP_PAGE_LEN: int
16
22
 
23
+ @dataclass(frozen=True)
24
+ class SdpDescriptionPage:
25
+ total: int
26
+ offset: int
27
+ text: bytes
28
+ timestamp: float
29
+
17
30
  class FrameV2IPSinkSdp(FrameBase):
18
31
  @staticmethod
19
32
  def construct_command(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid, kind: int, value: int = 0) -> FrameBase | None: ...
@@ -25,5 +38,7 @@ class FrameV2IPSinkSdp(FrameBase):
25
38
  def kind(self) -> int | None: ...
26
39
  @cached_property
27
40
  def status(self) -> OneIPSinkSdp | None: ...
41
+ @cached_property
42
+ def description_page(self) -> SdpDescriptionPage | None: ...
28
43
  @override
29
44
  def process(self) -> None: ...
@@ -74,6 +74,7 @@ from ..const import MXR_CONFIG_TIMEOUT
74
74
  from ..Uid import MxrDeviceUid
75
75
  from typing import Any, Callable
76
76
  from ..compat import override
77
+ from dataclasses import replace
77
78
  from datetime import datetime
78
79
  from enum import Enum
79
80
  import logging
@@ -88,13 +89,14 @@ from ..proto.Constants import (MXR_PROTOCOL_VERSION_SLOW_HELLO, ONEIP_DOWNMIX_LF
88
89
  ONEIP_IR_PROFILE_MAX, ONEIP_IR_PROFILE_NOT_SET, ONEIP_VLAN_PORT_SFP,
89
90
  ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DSCP_MAX,
90
91
  ONEIP_PTP_DOMAIN_MAX, ONEIP_SDP_MAX, OneIPPowerSaveLevel, OneIPPtpFlag, OneIPPtpMode, UnitCommandFlag,
91
- UnitCommandKind)
92
+ UnitCommandKind, UnitFlag)
92
93
  from ..proto.FrameUnitCommand import FrameUnitCommand
93
94
  from ..proto.FrameV2IPAudio import AUDIO_LAYOUT_SINCE, FrameV2IPAudio
94
95
  from ..proto.FrameV2IPDeviceConfiguration import FrameV2IPDeviceConfiguration
95
96
  from ..proto.FrameV2IPOsdSync import FrameV2IPOsdSync, OSD_KIND_MESSAGE_REQUEST, OSD_KIND_MESSAGE_SET
96
97
  from ..proto.FrameV2IPPrivacy import FrameV2IPPrivacy
97
- from ..proto.FrameV2IPSinkSdp import FrameV2IPSinkSdp, SDP_REQUEST, SDP_SCREEN, SDP_STOP
98
+ from ..proto.FrameV2IPSinkSdp import (FrameV2IPSinkSdp, SdpDescriptionPage, SDP_DESCRIPTION_PROTOCOL, SDP_PAGE_TIMEOUT,
99
+ SDP_REQUEST, SDP_REQUEST_DESCRIPTION, SDP_SCREEN, SDP_STOP)
98
100
  from ..proto.FrameV2IPTestcard import (FrameV2IPTestcard, TESTCARD_REQUEST, TESTCARD_SET,
99
101
  TESTCARD_PART_PATTERN, TESTCARD_PART_TONE, TESTCARD_PART_SYNC)
100
102
 
@@ -177,6 +179,11 @@ class Device(DeviceBase):
177
179
  self._unit_status:UnitStatus|None = None
178
180
  self._v2ip_testcard:OneIPTestcard|None = None
179
181
  self._v2ip_sink_sdp:OneIPSinkSdp|None = None
182
+ self._v2ip_sink_description:str|None = None
183
+ # The pages of a description the output is still sending: its whole
184
+ # length, the bytes received so far from offset 0, and when the last
185
+ # page arrived.
186
+ self._v2ip_sink_description_pages:tuple[int, bytes, float]|None = None
180
187
  self._v2ip_privacy:OneIPPrivacy|None = None
181
188
  self._osd_sync:OsdSync|None = None
182
189
  self._osd_sync_report:OsdSyncReport|None = None
@@ -1076,9 +1083,18 @@ class Device(DeviceBase):
1076
1083
  self.callbacks.on_osd_message_changed(self, data)
1077
1084
  self.call_callbacks()
1078
1085
  elif isinstance(data, OneIPSinkSdp):
1079
- if (self._v2ip_sink_sdp != data):
1080
- self._v2ip_sink_sdp = data
1086
+ changed = (self._v2ip_sink_sdp != data)
1087
+ self._v2ip_sink_sdp = data
1088
+ if not data.active:
1089
+ # A description is reported when a stream starts, so one that
1090
+ # has stopped leaves none behind it.
1091
+ self._v2ip_sink_description_pages = None
1092
+ changed |= (self._v2ip_sink_description is not None)
1093
+ self._v2ip_sink_description = None
1094
+ if changed:
1081
1095
  self.call_callbacks()
1096
+ elif isinstance(data, SdpDescriptionPage):
1097
+ self._on_sink_description_page(data)
1082
1098
  elif isinstance(data, OneIPPrivacy):
1083
1099
  if (self._v2ip_privacy != data):
1084
1100
  self._v2ip_privacy = data
@@ -1275,6 +1291,39 @@ class Device(DeviceBase):
1275
1291
  upgrade_unit_video_processor() apply.'''
1276
1292
  return self._unit_command(UnitCommandKind.NET_CONFIRM)
1277
1293
 
1294
+ async def locate_unit(self, timed:bool=False) -> bool:
1295
+ '''Locate this unit: its board-ID LED flashes, and an output shows
1296
+ "Locate Device" on its display. For 30 s when timed is set, until
1297
+ stop_locating_unit() otherwise.
1298
+
1299
+ Refused until the unit has reported its status, and for one whose report
1300
+ lacks UnitFlag.LOCATE_SUPPORTED. The cached status takes UnitFlag.LOCATE
1301
+ at once, as the mesh controller's copy does; the unit reports its own
1302
+ within 2 s, and again when a timed locate ends. Otherwise as
1303
+ upgrade_unit_video_processor().
1304
+ '''
1305
+ return self._locate(UnitCommandFlag.LOCATE_TIMED if timed else UnitCommandFlag.LOCATE_ON)
1306
+
1307
+ async def stop_locating_unit(self) -> bool:
1308
+ '''Stop locating this unit. As locate_unit().'''
1309
+ return self._locate(UnitCommandFlag(0))
1310
+
1311
+ def _locate(self, flags:UnitCommandFlag) -> bool:
1312
+ '''The one send behind the locate methods.'''
1313
+ if ((status := self._unit_status) is None):
1314
+ _LOGGER.warning(f"not locating {self}: it has not reported its status")
1315
+ return False
1316
+ if (UnitFlag.LOCATE_SUPPORTED not in status.flags):
1317
+ _LOGGER.warning(f"not locating {self}: it cannot be located")
1318
+ return False
1319
+ if not self._unit_command(UnitCommandKind.LOCATE, flags):
1320
+ return False
1321
+ located = int(status.flags) & ~int(UnitFlag.LOCATE)
1322
+ if flags:
1323
+ located |= int(UnitFlag.LOCATE)
1324
+ self.on_mxr_update(replace(status, flags=UnitFlag(located)))
1325
+ return True
1326
+
1278
1327
  def _unit_command(self, command:UnitCommandKind, flags:UnitCommandFlag=UnitCommandFlag(0),
1279
1328
  network:UnitNetConfig|None=None) -> bool:
1280
1329
  '''The one send behind the unit command methods.'''
@@ -1888,6 +1937,55 @@ class Device(DeviceBase):
1888
1937
  play_oneip_sdp().'''
1889
1938
  return self._send_sink_sdp_command(SDP_REQUEST, 0)
1890
1939
 
1940
+ @property
1941
+ @override
1942
+ def oneip_sink_description(self) -> str|None:
1943
+ return self._v2ip_sink_description
1944
+
1945
+ async def request_oneip_sink_description(self) -> bool:
1946
+ '''Ask this OneIP output what it plays from a session description and for
1947
+ the description itself, which oneip_sink_description then reads. An
1948
+ output that plays none sends its status alone.
1949
+
1950
+ Refused below protocol 0x31, where an output ignores the ask for the
1951
+ description. Otherwise as play_oneip_sdp().'''
1952
+ if (self.protocol != 0) and (self.protocol < SDP_DESCRIPTION_PROTOCOL):
1953
+ _LOGGER.warning(f"not asking {self} for its session description: protocol {self.protocol:#04x} "
1954
+ f"predates it")
1955
+ return False
1956
+ return self._send_sink_sdp_command(SDP_REQUEST, SDP_REQUEST_DESCRIPTION)
1957
+
1958
+ def _on_sink_description_page(self, page:SdpDescriptionPage) -> None:
1959
+ '''Put together the description this output reports playing, one page
1960
+ at a time, as a unit puts together the pages it is sent.
1961
+
1962
+ A page at offset 0 starts a description of 1 to ONEIP_SDP_MAX bytes.
1963
+ Every later page has to continue it: at the offset received so far, for
1964
+ the same length, within SDP_PAGE_TIMEOUT of the page before. Anything
1965
+ else drops the pages received, and the description is kept once its
1966
+ last byte is in.'''
1967
+ pending = self._v2ip_sink_description_pages
1968
+ self._v2ip_sink_description_pages = None
1969
+ if (page.offset == 0):
1970
+ if not (1 <= page.total <= ONEIP_SDP_MAX):
1971
+ return
1972
+ data = b''
1973
+ elif (pending is not None) and (pending[0] == page.total) and (len(pending[1]) == page.offset) \
1974
+ and (page.timestamp - pending[2] <= SDP_PAGE_TIMEOUT):
1975
+ data = pending[1]
1976
+ else:
1977
+ return
1978
+ if (len(data) + len(page.text) > page.total):
1979
+ return
1980
+ data += page.text
1981
+ if (len(data) < page.total):
1982
+ self._v2ip_sink_description_pages = (page.total, data, page.timestamp)
1983
+ return
1984
+ description = data.decode('utf-8', errors='replace')
1985
+ if (self._v2ip_sink_description != description):
1986
+ self._v2ip_sink_description = description
1987
+ self.call_callbacks()
1988
+
1891
1989
  @property
1892
1990
  @override
1893
1991
  def oneip_privacy(self) -> OneIPPrivacy|None:
@@ -3,7 +3,7 @@ from ..Uid import MxrDeviceUid as MxrDeviceUid
3
3
  from ..compat import override as override
4
4
  from ..const import MXR_CONFIG_TIMEOUT as MXR_CONFIG_TIMEOUT
5
5
  from ..proto.BayConfig import BayConfig as BayConfig
6
- from ..proto.Constants import BayFeaturesMask as BayFeaturesMask, DeviceFeature as DeviceFeature, MXR_PROTOCOL_VERSION_SLOW_HELLO as MXR_PROTOCOL_VERSION_SLOW_HELLO, 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, ONEIP_DEVICE_SETTINGS_BLOCK as ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DEVICE_SETTING_SWITCHES as ONEIP_DEVICE_SETTING_SWITCHES, ONEIP_DOWNMIX_LFE_MAX as ONEIP_DOWNMIX_LFE_MAX, ONEIP_DSCP_MAX as ONEIP_DSCP_MAX, ONEIP_IR_PROFILE_MAX as ONEIP_IR_PROFILE_MAX, ONEIP_IR_PROFILE_NOT_SET as ONEIP_IR_PROFILE_NOT_SET, ONEIP_PTP_DOMAIN_MAX as ONEIP_PTP_DOMAIN_MAX, ONEIP_SDP_MAX as ONEIP_SDP_MAX, ONEIP_VLAN_PORT_SFP as ONEIP_VLAN_PORT_SFP, OSD_MESSAGE_TEXT_MAX as OSD_MESSAGE_TEXT_MAX, OneIPDeviceSetting as OneIPDeviceSetting, OneIPPowerSaveLevel as OneIPPowerSaveLevel, OneIPPtpFlag as OneIPPtpFlag, OneIPPtpMode as OneIPPtpMode, OneIPTestPattern as OneIPTestPattern, OneIPToneMode as OneIPToneMode, OneIPVideoProcessorFeature as OneIPVideoProcessorFeature, OneIPVlanFlag as OneIPVlanFlag, OsdMessageFlag as OsdMessageFlag, OsdMessagePriority as OsdMessagePriority, UnitCommandFlag as UnitCommandFlag, UnitCommandKind as UnitCommandKind
6
+ from ..proto.Constants import BayFeaturesMask as BayFeaturesMask, DeviceFeature as DeviceFeature, MXR_PROTOCOL_VERSION_SLOW_HELLO as MXR_PROTOCOL_VERSION_SLOW_HELLO, 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, ONEIP_DEVICE_SETTINGS_BLOCK as ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DEVICE_SETTING_SWITCHES as ONEIP_DEVICE_SETTING_SWITCHES, ONEIP_DOWNMIX_LFE_MAX as ONEIP_DOWNMIX_LFE_MAX, ONEIP_DSCP_MAX as ONEIP_DSCP_MAX, ONEIP_IR_PROFILE_MAX as ONEIP_IR_PROFILE_MAX, ONEIP_IR_PROFILE_NOT_SET as ONEIP_IR_PROFILE_NOT_SET, ONEIP_PTP_DOMAIN_MAX as ONEIP_PTP_DOMAIN_MAX, ONEIP_SDP_MAX as ONEIP_SDP_MAX, ONEIP_VLAN_PORT_SFP as ONEIP_VLAN_PORT_SFP, OSD_MESSAGE_TEXT_MAX as OSD_MESSAGE_TEXT_MAX, OneIPDeviceSetting as OneIPDeviceSetting, OneIPPowerSaveLevel as OneIPPowerSaveLevel, OneIPPtpFlag as OneIPPtpFlag, OneIPPtpMode as OneIPPtpMode, OneIPTestPattern as OneIPTestPattern, OneIPToneMode as OneIPToneMode, OneIPVideoProcessorFeature as OneIPVideoProcessorFeature, OneIPVlanFlag as OneIPVlanFlag, OsdMessageFlag as OsdMessageFlag, OsdMessagePriority as OsdMessagePriority, UnitCommandFlag as UnitCommandFlag, UnitCommandKind as UnitCommandKind, UnitFlag as UnitFlag
7
7
  from ..proto.FrameBase import FrameBase as FrameBase
8
8
  from ..proto.FrameFirmwareVersion import FirmwareType as FirmwareType, FirmwareVersion as FirmwareVersion
9
9
  from ..proto.FrameHello import FrameHello as FrameHello
@@ -20,7 +20,7 @@ from ..proto.FrameV2IPDeviceConfiguration import FrameV2IPDeviceConfiguration as
20
20
  from ..proto.FrameV2IPMultiviewer import FrameV2IPMultiviewer as FrameV2IPMultiviewer, V2IPMultiviewerConfig as V2IPMultiviewerConfig
21
21
  from ..proto.FrameV2IPOsdSync import FrameV2IPOsdSync as FrameV2IPOsdSync, OSD_KIND_MESSAGE_REQUEST as OSD_KIND_MESSAGE_REQUEST, OSD_KIND_MESSAGE_SET as OSD_KIND_MESSAGE_SET
22
22
  from ..proto.FrameV2IPPrivacy import FrameV2IPPrivacy as FrameV2IPPrivacy
23
- from ..proto.FrameV2IPSinkSdp import FrameV2IPSinkSdp as FrameV2IPSinkSdp, SDP_REQUEST as SDP_REQUEST, SDP_SCREEN as SDP_SCREEN, SDP_STOP as SDP_STOP
23
+ from ..proto.FrameV2IPSinkSdp import FrameV2IPSinkSdp as FrameV2IPSinkSdp, SDP_DESCRIPTION_PROTOCOL as SDP_DESCRIPTION_PROTOCOL, SDP_PAGE_TIMEOUT as SDP_PAGE_TIMEOUT, SDP_REQUEST as SDP_REQUEST, SDP_REQUEST_DESCRIPTION as SDP_REQUEST_DESCRIPTION, SDP_SCREEN as SDP_SCREEN, SDP_STOP as SDP_STOP, SdpDescriptionPage as SdpDescriptionPage
24
24
  from ..proto.FrameV2IPStats import FrameV2IPStats as FrameV2IPStats
25
25
  from ..proto.FrameV2IPTestcard import FrameV2IPTestcard as FrameV2IPTestcard, TESTCARD_PART_PATTERN as TESTCARD_PART_PATTERN, TESTCARD_PART_SYNC as TESTCARD_PART_SYNC, TESTCARD_PART_TONE as TESTCARD_PART_TONE, TESTCARD_REQUEST as TESTCARD_REQUEST, TESTCARD_SET as TESTCARD_SET
26
26
  from ..proto.Multiviewer import MultiviewerAspectRatio as MultiviewerAspectRatio, MultiviewerBoolSetting as MultiviewerBoolSetting, MultiviewerConfig as MultiviewerConfig, MultiviewerEDIDTemplate as MultiviewerEDIDTemplate, MultiviewerHDCPMode as MultiviewerHDCPMode, MultiviewerITCMode as MultiviewerITCMode, MultiviewerOutputMode as MultiviewerOutputMode, MultiviewerPipPosition as MultiviewerPipPosition, MultiviewerPipSize as MultiviewerPipSize, MultiviewerSource as MultiviewerSource, MultiviewerViewMode as MultiviewerViewMode
@@ -254,6 +254,8 @@ class Device(DeviceBase):
254
254
  async def set_unit_dhcp(self) -> bool: ...
255
255
  async def set_unit_network(self, network: UnitNetConfig) -> bool: ...
256
256
  async def confirm_unit_network(self) -> bool: ...
257
+ async def locate_unit(self, timed: bool = False) -> bool: ...
258
+ async def stop_locating_unit(self) -> bool: ...
257
259
  async def ping(self) -> bool: ...
258
260
  async def mesh_promote(self) -> bool: ...
259
261
  async def auto_assign_oneip_source_addresses(self) -> bool: ...
@@ -301,6 +303,10 @@ class Device(DeviceBase):
301
303
  async def request_oneip_sink_sdp(self) -> bool: ...
302
304
  @property
303
305
  @override
306
+ def oneip_sink_description(self) -> str | None: ...
307
+ async def request_oneip_sink_description(self) -> bool: ...
308
+ @property
309
+ @override
304
310
  def oneip_privacy(self) -> OneIPPrivacy | None: ...
305
311
  async def request_oneip_privacy(self) -> bool: ...
306
312
  def __eq__(self, other: Any) -> bool: ...
@@ -180,6 +180,36 @@ for bad in (UnitNetConfig('0.0.0.0', '255.255.255.0'), UnitNetConfig('239.1.2.3'
180
180
  refused(unit.set_unit_network(bad), f'network [{bad}]')
181
181
  print('refusals : old units, non-units and networks a unit cannot hold')
182
182
 
183
+ # A unit says it can be located in bit 16 of its status flags, and that it is
184
+ # being located in bit 17.
185
+ def report_flags(flags):
186
+ return struct.pack('<I', flags) + report()[4:]
187
+ found = hello(uid(0x23), 0x2C, UNIT)
188
+ rx(uid(0x23), 0x4F, report_flags(1 << 16))
189
+ assert found.unit_status.flags == UnitFlag.LOCATE_SUPPORTED, found.unit_status.flags
190
+ rx(uid(0x23), 0x4F, report_flags(1 << 17))
191
+ assert found.unit_status.flags == UnitFlag.LOCATE, found.unit_status.flags
192
+
193
+ # A unit is located for 30 s or until stopped, only once it has reported that it
194
+ # can be, and the cached status says so at once.
195
+ located = hello(uid(0x24), 0x2C, UNIT)
196
+ refused(located.locate_unit(timed=True), 'a unit that has not reported its status')
197
+ rx(uid(0x24), 0x4F, report_flags(int(UnitFlag.FAN_SPEED)))
198
+ refused(located.locate_unit(timed=True), 'a unit that cannot be located')
199
+ refused(located.stop_locating_unit(), 'a unit that cannot be located')
200
+ rx(uid(0x24), 0x4F, report_flags(int(UnitFlag.LOCATE_SUPPORTED | UnitFlag.FAN_SPEED) | (1 << 30)))
201
+ assert call(located.locate_unit(timed=True)) is True
202
+ p = command()
203
+ assert p[:16] == uid(0x24) and p[16:18] == bytes([4, 0x08]), p.hex()
204
+ assert UnitFlag.LOCATE in located.unit_status.flags, 'the cached status was not marked located'
205
+ assert call(located.locate_unit()) is True
206
+ assert command()[16:18] == bytes([4, 0x04]), 'until stopped'
207
+ assert call(located.stop_locating_unit()) is True
208
+ assert command()[16:18] == bytes([4, 0x00]), 'stop'
209
+ assert located.unit_status.flags == UnitFlag(int(UnitFlag.LOCATE_SUPPORTED | UnitFlag.FAN_SPEED) | (1 << 30)), \
210
+ f'a stop clears the flag and nothing else: {located.unit_status.flags!r}'
211
+ print('locate : timed, until stopped and stop, once the unit says it can be')
212
+
183
213
  # -------------------------------------------------- a command seen on the wire
184
214
 
185
215
  # Decoded for the log; a command too short to hold its fields carries none.
@@ -37,10 +37,10 @@ def name(s, sz=16):
37
37
  b = s.encode('ascii')[:sz]
38
38
  return b + bytes(sz - len(b))
39
39
 
40
- def rx(sender, opcode, payload=b'', protocol=0x2E):
40
+ def rx(sender, opcode, payload=b'', protocol=0x2E, when=0.0):
41
41
  frame = bytearray(create_mxr_frame(sender, opcode, payload))
42
42
  frame[2] = protocol
43
- mx.process_frame(0.0, bytes(frame), ADDR)
43
+ mx.process_frame(when, bytes(frame), ADDR)
44
44
 
45
45
  def hello(sender, protocol, features):
46
46
  rx(sender, 0x00, struct.pack('<H', protocol) + name('ONEIP') + name('P8SN%08d' % sender[0])
@@ -114,4 +114,82 @@ refused(old.request_oneip_sink_sdp(), 'an output below 0x2E')
114
114
  refused(out.set_oneip_sdp_screen(256), 'a screen past the field')
115
115
  print('refusals : nothing an output ignores is sent')
116
116
 
117
+ # ------------------------------------------------------------------- description
118
+ def page(header_uid, total, offset, text):
119
+ '''One page of the description an output reports playing, its reserved
120
+ bytes set.'''
121
+ return (header_uid + bytes([5, 0xA5, 0x5A, 0xC3]) + struct.pack('<HHH', total, offset, len(text))
122
+ + bytes([0x96, 0x69]) + text)
123
+
124
+ # Put together from its pages and kept once its last byte is in, and a status
125
+ # playing nothing clears it.
126
+ desc = hello(uid(0x20), 0x31, DeviceFeature.ONEIP_SINK)
127
+ sdp = bytes(ord('a') + i % 26 for i in range(1500))
128
+ rx(uid(0x20), 0x52, page(uid(0x20), 1500, 0, sdp[:1024]))
129
+ assert desc.oneip_sink_description is None, 'kept early'
130
+ rx(uid(0x20), 0x52, page(uid(0x20), 1500, 1024, sdp[1024:]))
131
+ assert desc.oneip_sink_description == sdp.decode(), desc.oneip_sink_description
132
+ playing = bytearray(report(uid(0x20)))
133
+ rx(uid(0x20), 0x52, bytes(playing))
134
+ assert desc.oneip_sink_description == sdp.decode(), 'a status playing something cleared it'
135
+ playing[20] &= ~int(OneIPSdpFlag.ACTIVE)
136
+ rx(uid(0x20), 0x52, bytes(playing))
137
+ assert desc.oneip_sink_description is None, 'a status playing nothing left it'
138
+ # A status playing nothing also drops the pages of one still arriving.
139
+ rx(uid(0x20), 0x52, page(uid(0x20), 4, 0, b'ab'))
140
+ rx(uid(0x20), 0x52, bytes(playing))
141
+ rx(uid(0x20), 0x52, page(uid(0x20), 4, 2, b'cd'))
142
+ assert desc.oneip_sink_description is None, 'pages from before a stop were kept'
143
+ print('description : put together from its pages, and cleared by a status playing nothing')
144
+
145
+ # A page that does not continue the description drops the pages received, and a
146
+ # description is started only at offset 0, by the output itself, for 1 to
147
+ # ONEIP_SDP_MAX bytes. Each case would complete a description if the rule it
148
+ # breaks were not checked, so none passes on the back of another's drop.
149
+ desc = hello(uid(0x21), 0x31, DeviceFeature.ONEIP_SINK)
150
+ me = uid(0x21)
151
+ short = page(me, 1, 0, b'ab')[:-1]
152
+ for frames in ((page(me, 6, 0, b'ab'), page(me, 6, 4, b'cdef'), page(me, 6, 2, b'cdef')), # out of order
153
+ (page(me, 6, 0, b'ab'), page(me, 4, 2, b'cd')), # another length
154
+ (page(me, 4, 0, b'ab'), page(me, 4, 2, b'cde')), # past the end
155
+ (page(me, 1, 1, b'b'),), # not at 0
156
+ (short,), # text cut short
157
+ (page(me, ONEIP_SDP_MAX + 1, 0, b'ab'),), # too long
158
+ (page(me, 0, 0, b''),), # empty
159
+ (page(uid(0x77), 2, 0, b'ab'),), # someone else's
160
+ (page(me, 2, 0, b'ab')[:25],), # page header cut short
161
+ (me + bytes([1]) + page(me, 2, 0, b'ab')[17:],)): # a page handed to it
162
+ for f in frames:
163
+ rx(me, 0x52, f)
164
+ assert desc.oneip_sink_description is None, frames
165
+ # Restarting at 0 starts afresh.
166
+ for f in (page(me, 4, 0, b'xx'), page(me, 4, 0, b'ab'), page(me, 4, 2, b'cd')):
167
+ rx(me, 0x52, f)
168
+ assert desc.oneip_sink_description == 'abcd', desc.oneip_sink_description
169
+ longest = hello(uid(0x22), 0x31, DeviceFeature.ONEIP_SINK)
170
+ rx(uid(0x22), 0x52, page(uid(0x22), ONEIP_SDP_MAX, 0, b'z' * 1024))
171
+ rx(uid(0x22), 0x52, page(uid(0x22), ONEIP_SDP_MAX, 1024, b'z' * 1024))
172
+ assert longest.oneip_sink_description == 'z' * ONEIP_SDP_MAX
173
+ print('pages : only pages that continue a description from offset 0 are kept')
174
+
175
+ # Pages more than 5 s apart are dropped.
176
+ slow = hello(uid(0x23), 0x31, DeviceFeature.ONEIP_SINK)
177
+ rx(uid(0x23), 0x52, page(uid(0x23), 4, 0, b'ab'), when=100.0)
178
+ rx(uid(0x23), 0x52, page(uid(0x23), 4, 2, b'cd'), when=105.001)
179
+ assert slow.oneip_sink_description is None, 'a page after 5 s was kept'
180
+ rx(uid(0x23), 0x52, page(uid(0x23), 4, 0, b'ab'), when=100.0)
181
+ rx(uid(0x23), 0x52, page(uid(0x23), 4, 2, b'cd'), when=105.0)
182
+ assert slow.oneip_sink_description == 'abcd', slow.oneip_sink_description
183
+ print('timeout : pages at most 5 s apart')
184
+
185
+ # Asking for it sets bit 0 of the request's value, at the opcode's stamp, and is
186
+ # refused below 0x31, where an output ignores the bit.
187
+ sent.clear()
188
+ assert call(desc.request_oneip_sink_description()) is True
189
+ assert int.from_bytes(sent[-1][20:22], 'little') == 0x52 and sent[-1][2] == 0x2E, sent[-1][:24].hex()
190
+ assert sent[-1][24:] == uid(0x21) + bytes([4, 1, 0, 0]), sent[-1][24:].hex()
191
+ refused(hello(uid(0x24), 0x30, DeviceFeature.ONEIP_SINK).request_oneip_sink_description(), 'an output below 0x31')
192
+ refused(src.request_oneip_sink_description(), 'a device that is not a OneIP output')
193
+ print('request : bit 0 of the value, refused below 0x31')
194
+
117
195
  print('ALL OK')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes