mx-remote 9.2.0__tar.gz → 9.3.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.2.0 → mx_remote-9.3.0}/PKG-INFO +1 -1
  2. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/api.md +17 -0
  3. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/callbacks.md +25 -0
  4. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/Interface.py +69 -0
  5. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/Interface.pyi +18 -0
  6. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/const.py +1 -1
  7. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Constants.py +18 -0
  8. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Constants.pyi +5 -0
  9. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Factory.py +3 -0
  10. mx_remote-9.3.0/mx_remote/proto/FrameThirdParty.py +50 -0
  11. mx_remote-9.3.0/mx_remote/proto/FrameThirdParty.pyi +15 -0
  12. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Remote.py +20 -2
  13. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Remote.pyi +4 -2
  14. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/handlers.py +1 -0
  15. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/run.py +1 -0
  16. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/smoke.py +1 -0
  17. mx_remote-9.3.0/tests/thirdparty.py +146 -0
  18. {mx_remote-9.2.0 → mx_remote-9.3.0}/.gitignore +0 -0
  19. {mx_remote-9.2.0 → mx_remote-9.3.0}/AGENTS.md +0 -0
  20. {mx_remote-9.2.0 → mx_remote-9.3.0}/CLAUDE.md +0 -0
  21. {mx_remote-9.2.0 → mx_remote-9.3.0}/LICENSE +0 -0
  22. {mx_remote-9.2.0 → mx_remote-9.3.0}/README.md +0 -0
  23. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/README.md +0 -0
  24. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/_static/brand.css +0 -0
  25. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/_static/favicon.svg +0 -0
  26. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/_static/logo-dark.svg +0 -0
  27. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/_static/logo.svg +0 -0
  28. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/audio.md +0 -0
  29. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/conf.py +0 -0
  30. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/configuration.md +0 -0
  31. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/devices-and-bays.md +0 -0
  32. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/diagnostics.md +0 -0
  33. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/index.md +0 -0
  34. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/multiviewer.md +0 -0
  35. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/oneip.md +0 -0
  36. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/requirements.txt +0 -0
  37. {mx_remote-9.2.0 → mx_remote-9.3.0}/docs/routing.md +0 -0
  38. {mx_remote-9.2.0 → mx_remote-9.3.0}/hatch_build.py +0 -0
  39. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/Uid.py +0 -0
  40. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/Uid.pyi +0 -0
  41. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/__init__.py +0 -0
  42. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/__init__.pyi +0 -0
  43. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/compat.py +0 -0
  44. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/compat.pyi +0 -0
  45. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/const.pyi +0 -0
  46. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/deprecated.py +0 -0
  47. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/deprecated.pyi +0 -0
  48. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/main.py +0 -0
  49. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/main.pyi +0 -0
  50. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/BayConfig.py +0 -0
  51. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/BayConfig.pyi +0 -0
  52. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Data.py +0 -0
  53. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Data.pyi +0 -0
  54. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Factory.pyi +0 -0
  55. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  56. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  57. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  58. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  59. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  60. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  61. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  62. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  63. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBase.py +0 -0
  64. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBase.pyi +0 -0
  65. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  66. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  67. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  68. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  69. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayContent.py +0 -0
  70. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  71. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayHide.py +0 -0
  72. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  73. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  74. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  75. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  76. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  77. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDebug.py +0 -0
  78. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  79. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDiscover.py +0 -0
  80. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  81. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDID.py +0 -0
  82. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  83. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  84. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  85. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  86. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  87. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  88. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  89. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  90. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  91. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHeader.py +0 -0
  92. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  93. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHello.py +0 -0
  94. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHello.pyi +0 -0
  95. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameLinks.py +0 -0
  96. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  97. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  98. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  99. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  100. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  101. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  102. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  103. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  104. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  105. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FramePing.py +0 -0
  106. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FramePing.pyi +0 -0
  107. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FramePowerChange.py +0 -0
  108. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  109. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCAction.py +0 -0
  110. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  111. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCIr.py +0 -0
  112. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  113. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCKey.py +0 -0
  114. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  115. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  116. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  117. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameReboot.py +0 -0
  118. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  119. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  120. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  121. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  122. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  123. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetName.py +0 -0
  124. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  125. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  126. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  127. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  128. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  129. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  130. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  131. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  132. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  133. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  134. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  135. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  136. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  137. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  138. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  139. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  140. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  141. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTime.py +0 -0
  142. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTime.pyi +0 -0
  143. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  144. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  145. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTopology.py +0 -0
  146. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  147. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTxIR.py +0 -0
  148. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  149. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  150. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  151. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  152. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  153. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  154. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  155. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  156. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  157. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  158. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  159. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  160. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  161. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  162. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  163. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  164. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  165. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  166. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  167. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  168. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  169. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPOsdSync.py +0 -0
  170. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +0 -0
  171. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  172. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  173. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  174. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  175. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  176. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  177. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  178. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  179. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSinkSdp.py +0 -0
  180. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +0 -0
  181. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  182. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  183. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  184. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  185. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  186. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  187. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  188. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  189. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  190. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  191. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  192. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  193. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolume.py +0 -0
  194. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  195. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  196. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  197. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  198. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  199. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  200. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  201. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/LinkConfig.py +0 -0
  202. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  203. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Multiviewer.py +0 -0
  204. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  205. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Svd.py +0 -0
  206. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/Svd.pyi +0 -0
  207. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPConfig.py +0 -0
  208. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  209. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPStats.py +0 -0
  210. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  211. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/__init__.py +0 -0
  212. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/__init__.pyi +0 -0
  213. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/proto/svd.csv +0 -0
  214. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/py.typed +0 -0
  215. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Bay.py +0 -0
  216. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Bay.pyi +0 -0
  217. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  218. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  219. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Device.py +0 -0
  220. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Device.pyi +0 -0
  221. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Link.py +0 -0
  222. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/Link.pyi +0 -0
  223. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/OsdSync.py +0 -0
  224. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/OsdSync.pyi +0 -0
  225. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/State.py +0 -0
  226. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/State.pyi +0 -0
  227. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/V2IP.py +0 -0
  228. {mx_remote-9.2.0 → mx_remote-9.3.0}/mx_remote/remote/V2IP.pyi +0 -0
  229. {mx_remote-9.2.0 → mx_remote-9.3.0}/pyproject.toml +0 -0
  230. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/audio_ir.py +0 -0
  231. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/audiolock.py +0 -0
  232. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/baycontent.py +0 -0
  233. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/capture.py +0 -0
  234. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/complete.py +0 -0
  235. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/deprecated.py +0 -0
  236. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/downmix.py +0 -0
  237. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/e2e.py +0 -0
  238. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/entry.py +0 -0
  239. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/handlers_cover.py +0 -0
  240. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/hello.py +0 -0
  241. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/idempotent.py +0 -0
  242. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/lengths.py +0 -0
  243. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/merge.py +0 -0
  244. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/mesh.py +0 -0
  245. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/meshbays.py +0 -0
  246. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/newops.py +0 -0
  247. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/offline.py +0 -0
  248. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/offsets.py +0 -0
  249. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/osdmessage.py +0 -0
  250. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/osdsync.py +0 -0
  251. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/ping.py +0 -0
  252. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/privacy.py +0 -0
  253. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/proto_stamp.py +0 -0
  254. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/protogate.py +0 -0
  255. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/rcsettings.py +0 -0
  256. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/roundtrip.py +0 -0
  257. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/routes.py +0 -0
  258. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/sources.py +0 -0
  259. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/staterequest.py +0 -0
  260. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/stats.py +0 -0
  261. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/structs.py +0 -0
  262. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/txresult.py +0 -0
  263. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/uid.py +0 -0
  264. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/unit.py +0 -0
  265. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/unknowns.py +0 -0
  266. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2ipptp.py +0 -0
  267. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2ipscaling.py +0 -0
  268. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2ipsdp.py +0 -0
  269. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2ipsettings.py +0 -0
  270. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2ipsubject.py +0 -0
  271. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2iptestcard.py +0 -0
  272. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/v2ipvlan.py +0 -0
  273. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/vdetails.py +0 -0
  274. {mx_remote-9.2.0 → mx_remote-9.3.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 9.2.0
3
+ Version: 9.3.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/
@@ -288,6 +288,23 @@ How a source unit encrypts its streams, as it reports it.
288
288
  :members:
289
289
  ```
290
290
 
291
+ ## Application messages
292
+
293
+ Messages applications exchange over the mesh under their own vendor id.
294
+
295
+ ```{eval-rst}
296
+ .. autoclass:: mx_remote.ThirdPartyMessage
297
+ :members:
298
+
299
+ .. autoclass:: mx_remote.ThirdPartyVendor
300
+ :members:
301
+
302
+ .. autoclass:: mx_remote.proto.Constants.ThirdPartyVendorFlag
303
+ :members:
304
+
305
+ .. autodata:: mx_remote.proto.Constants.THIRD_PARTY_PAYLOAD_MAX
306
+ ```
307
+
291
308
  ## OneIP test card
292
309
 
293
310
  A sink's test pattern, tone and lip-sync flash, and the ranges it accepts.
@@ -67,6 +67,7 @@ Available callback methods:
67
67
  | `on_rc_type_changed` | remote control type changed |
68
68
  | `on_amp_zone_settings_changed` | amplifier zone settings changed |
69
69
  | `on_amp_dolby_settings_changed` | amplifier Dolby settings changed |
70
+ | `on_third_party_message` | another application sent a message under its vendor id |
70
71
 
71
72
  You can also register per-device and per-bay callbacks:
72
73
 
@@ -85,6 +86,30 @@ device.unregister_callback(on_device_changed)
85
86
  bay.unregister_callback(on_bay_changed)
86
87
  ```
87
88
 
89
+ ## Application messages
90
+
91
+ Applications built on this library can message each other over the mesh. A
92
+ message carries a vendor id, then up to `THIRD_PARTY_PAYLOAD_MAX` (1444) bytes
93
+ that only the application reads. The vendor id is the vendor's IEEE OUI, or an
94
+ identifier it chose, such as three letters:
95
+
96
+ ```python
97
+ from mx_remote import ThirdPartyVendor
98
+
99
+ ODK = ThirdPartyVendor.private(b'ODK')
100
+
101
+ await mx.send_third_party(ODK, b'hello') # False when over 1444 bytes
102
+
103
+ class MyCallbacks(mx_remote.MxrCallbacks):
104
+ def on_third_party_message(self, dev, message):
105
+ if message.vendor.same_vendor(ODK):
106
+ print(f"{dev} says {message.payload!r}")
107
+ ```
108
+
109
+ Every other client that has heard this one announce itself receives the
110
+ message; this client does not. No unit reads it, and nothing acknowledges it,
111
+ so an application that needs to know a message arrived has its peers answer.
112
+
88
113
  ---
89
114
 
90
115
  [Documentation index](README.md) | [Project README](https://github.com/opdenkamp/mx-remote#readme)
@@ -2601,6 +2601,68 @@ class OneIPPrivacy:
2601
2601
  def __str__(self) -> str:
2602
2602
  return f"video {self.video}, audio {self.audio}, anc {self.anc}"
2603
2603
 
2604
+ @dataclass(frozen=True)
2605
+ class ThirdPartyVendor:
2606
+ """
2607
+ Whose message a third-party message is: a flag byte, then three bytes
2608
+ naming the vendor.
2609
+
2610
+ The three bytes are the vendor's IEEE OUI, in the order it is written, or
2611
+ with ThirdPartyVendorFlag.NOT_OUI set an identifier of the vendor's own
2612
+ choosing, such as three ASCII letters. The flag keeps the two spaces apart,
2613
+ so a chosen identifier never collides with a registered OUI.
2614
+ """
2615
+ flags: ThirdPartyVendorFlag
2616
+ """The flag byte, at offset 0."""
2617
+ id: bytes
2618
+ """The vendor's three identifying bytes, at offsets 1 to 3."""
2619
+
2620
+ def __post_init__(self) -> None:
2621
+ if (len(self.id) != 3):
2622
+ raise ValueError(f"a vendor id is 3 bytes, not {len(self.id)}")
2623
+
2624
+ @classmethod
2625
+ def oui(cls, oui:bytes) -> 'ThirdPartyVendor':
2626
+ """A vendor named by its IEEE OUI."""
2627
+ return cls(flags=ThirdPartyVendorFlag(0), id=bytes(oui))
2628
+
2629
+ @classmethod
2630
+ def private(cls, id:bytes) -> 'ThirdPartyVendor':
2631
+ """A vendor named by an identifier it chose, rather than an OUI."""
2632
+ return cls(flags=ThirdPartyVendorFlag.NOT_OUI, id=bytes(id))
2633
+
2634
+ def same_vendor(self, other:'ThirdPartyVendor') -> bool:
2635
+ """Whether other names the same vendor.
2636
+
2637
+ Only ThirdPartyVendorFlag.NOT_OUI and the three identifying bytes say
2638
+ who the vendor is, so the other flag bits are left out."""
2639
+ return ((ThirdPartyVendorFlag.NOT_OUI in self.flags) == (ThirdPartyVendorFlag.NOT_OUI in other.flags)) \
2640
+ and (self.id == other.id)
2641
+
2642
+ def __str__(self) -> str:
2643
+ if (ThirdPartyVendorFlag.NOT_OUI in self.flags):
2644
+ return f"vendor {self.id!r}"
2645
+ return f"vendor OUI {self.id.hex(':')}"
2646
+
2647
+ @dataclass(frozen=True)
2648
+ class ThirdPartyMessage:
2649
+ """
2650
+ A message an application sent to every device, under its own vendor id.
2651
+
2652
+ The library reads only the vendor id. What follows is the application's,
2653
+ and arrives exactly as it was sent.
2654
+ """
2655
+ vendor: ThirdPartyVendor
2656
+ """Which application's message this is. The library checks none: each
2657
+ application ignores every message whose vendor is not its own."""
2658
+ payload: bytes
2659
+ """The application's bytes, at most THIRD_PARTY_PAYLOAD_MAX."""
2660
+ address: str|None
2661
+ """Where the datagram came from."""
2662
+
2663
+ def __str__(self) -> str:
2664
+ return f"{self.vendor}: {len(self.payload)} bytes"
2665
+
2604
2666
  @dataclass(frozen=True)
2605
2667
  class OneIPTestTone:
2606
2668
  """A OneIP sink's test tone."""
@@ -4370,6 +4432,13 @@ class MxrCallbacks:
4370
4432
  _LOGGER.debug(f"{device} dolby settings changed")
4371
4433
  self.on_device_update(device)
4372
4434
 
4435
+ def on_third_party_message(self, dev:DeviceBase, message:ThirdPartyMessage) -> None:
4436
+ ''' called when the application on 'dev' sent a message under its own
4437
+ vendor id. A message from a sender this client has not yet heard
4438
+ announce itself is dropped, as every frame is, and this client never
4439
+ receives its own. '''
4440
+ pass
4441
+
4373
4442
  # The V2IP spelling of a name here, from before the rename to OneIP.
4374
4443
  from .deprecated import alias_members as _alias_members, module_getattr as _module_getattr
4375
4444
  # Installed through globals() so the generated stubs carry no __getattr__, which
@@ -930,6 +930,23 @@ class OneIPPrivacy:
930
930
  audio: OneIPPrivacyStream
931
931
  anc: OneIPPrivacyStream
932
932
 
933
+ @dataclass(frozen=True)
934
+ class ThirdPartyVendor:
935
+ flags: ThirdPartyVendorFlag
936
+ id: bytes
937
+ def __post_init__(self) -> None: ...
938
+ @classmethod
939
+ def oui(cls, oui: bytes) -> ThirdPartyVendor: ...
940
+ @classmethod
941
+ def private(cls, id: bytes) -> ThirdPartyVendor: ...
942
+ def same_vendor(self, other: ThirdPartyVendor) -> bool: ...
943
+
944
+ @dataclass(frozen=True)
945
+ class ThirdPartyMessage:
946
+ vendor: ThirdPartyVendor
947
+ payload: bytes
948
+ address: str | None
949
+
933
950
  @dataclass(frozen=True)
934
951
  class OneIPTestTone:
935
952
  mode: OneIPToneMode | int = ...
@@ -1620,3 +1637,4 @@ class MxrCallbacks:
1620
1637
  def on_rc_type_changed(self, bay: BayBase, rc_type: RCType | None) -> None: ...
1621
1638
  def on_amp_zone_settings_changed(self, bay: BayBase, settings: AmpZoneSettings) -> None: ...
1622
1639
  def on_amp_dolby_settings_changed(self, device: DeviceBase, settings: AmpDolbySettings) -> None: ...
1640
+ def on_third_party_message(self, dev: DeviceBase, message: ThirdPartyMessage) -> None: ...
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '9.2.0'
17
+ VERSION = '9.3.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -123,6 +123,9 @@ MXR_OPCODE_VERSIONS: dict[int, int] = {
123
123
  0x52: 0x2E, # V2IP_SINK_SDP
124
124
  0x53: 0x2F, # V2IP_OSD_SYNC
125
125
  0x54: 0x30, # V2IP_PRIVACY
126
+ # The lowest stamp, so that every receiver's ceiling passes it. No device
127
+ # decodes the payload, so there is no layout for a stamp to announce.
128
+ 0x55: 0x01, # THIRD_PARTY
126
129
  }
127
130
  """Per-opcode minimum compatible protocol version, mirroring the firmware's
128
131
  own opcode table. Transmitters stamp the
@@ -822,6 +825,21 @@ ONEIP_VLAN_PORT_SFP = 0
822
825
  ONEIP_VLAN_ID_MAX = 4094
823
826
  """The highest VLAN id: 0 means untagged, and 4095 is reserved by IEEE 802.1Q."""
824
827
 
828
+ class ThirdPartyVendorFlag(IntFlag):
829
+ '''The first byte of a third-party vendor id, ahead of its three
830
+ identifying bytes. Bits without a name are kept as they arrived.'''
831
+ NOT_OUI = (1 << 0)
832
+ '''The three bytes are an identifier the vendor chose, rather than its
833
+ IEEE OUI.'''
834
+
835
+ THIRD_PARTY_PAYLOAD_MAX = 1444
836
+ """The largest payload a third-party message carries.
837
+
838
+ A frame this size fills one 1500-byte Ethernet frame exactly: 20 bytes of IPv4
839
+ header, 8 of UDP, the 24-byte frame header and the 4-byte vendor id leave 1444.
840
+ A larger send is refused rather than fragmented, because a lost fragment loses
841
+ the whole datagram and nothing on these paths retries."""
842
+
825
843
  BAY_FEATURE_DOLBY_IN_POS = 24
826
844
 
827
845
  class BayFeaturesMask(IntFlag):
@@ -340,6 +340,11 @@ class OneIPVlanFlag(IntFlag):
340
340
  ONEIP_VLAN_PORTS: int
341
341
  ONEIP_VLAN_PORT_SFP: int
342
342
  ONEIP_VLAN_ID_MAX: int
343
+
344
+ class ThirdPartyVendorFlag(IntFlag):
345
+ NOT_OUI: Incomplete
346
+
347
+ THIRD_PARTY_PAYLOAD_MAX: int
343
348
  BAY_FEATURE_DOLBY_IN_POS: int
344
349
 
345
350
  class BayFeaturesMask(IntFlag):
@@ -249,6 +249,9 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
249
249
  if hdr.opcode == 0x54:
250
250
  from .FrameV2IPPrivacy import FrameV2IPPrivacy
251
251
  return FrameV2IPPrivacy(header=hdr, timestamp=timestamp)
252
+ if hdr.opcode == 0x55:
253
+ from .FrameThirdParty import FrameThirdParty
254
+ return FrameThirdParty(header=hdr, timestamp=timestamp)
252
255
  if hdr.opcode == 0x50:
253
256
  from .FrameUnitCommand import FrameUnitCommand
254
257
  return FrameUnitCommand(header=hdr, timestamp=timestamp)
@@ -0,0 +1,50 @@
1
+ ######################################################
2
+ ## MX Remote Python Interface ##
3
+ ## ##
4
+ ## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
5
+ ## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
6
+ ######################################################
7
+ '''Protocol frame for THIRD_PARTY (opcode 0x55): messages applications
8
+ exchange over the mesh.'''
9
+
10
+ from functools import cached_property
11
+ from ..compat import override
12
+ from .Constants import ThirdPartyVendorFlag
13
+ from .FrameBase import FrameBase
14
+ from ..Interface import DeviceRegistry, ThirdPartyMessage, ThirdPartyVendor
15
+
16
+ # The flag byte at 0 and the three identifying bytes at 1, then the
17
+ # application's bytes to the end of the frame.
18
+ VENDOR_SIZE = 4
19
+
20
+ class FrameThirdParty(FrameBase):
21
+ '''An application's own message, under its vendor id.'''
22
+ @staticmethod
23
+ def construct(mxr:DeviceRegistry, vendor:ThirdPartyVendor, payload:bytes) -> FrameBase|None:
24
+ '''A message to every device: the vendor's flag byte and three
25
+ identifying bytes, then payload as it was given.'''
26
+ data = bytes([int(vendor.flags)]) + vendor.id + bytes(payload)
27
+ return FrameBase.construct_base(mxr=mxr, opcode=0x55, payload=data)
28
+
29
+ @cached_property
30
+ def message(self) -> ThirdPartyMessage|None:
31
+ '''The message, whatever its vendor: telling them apart is the
32
+ application's. None for a payload too short for the vendor id.
33
+ Everything after the id is the application's, so the length is a
34
+ minimum and nothing past it is read.'''
35
+ p = self.payload or b''
36
+ if (len(p) < VENDOR_SIZE):
37
+ return None
38
+ vendor = ThirdPartyVendor(flags=ThirdPartyVendorFlag(p[0]), id=bytes(p[1:VENDOR_SIZE]))
39
+ return ThirdPartyMessage(vendor=vendor, payload=bytes(p[VENDOR_SIZE:]), address=self.address)
40
+
41
+ @override
42
+ def process(self) -> None:
43
+ '''Hand the message to the callbacks, as sent by the device it came from.'''
44
+ if ((message := self.message) is not None) and ((dev := self.remote_device) is not None):
45
+ dev.callbacks.on_third_party_message(dev, message)
46
+
47
+ def __str__(self) -> str:
48
+ if (self.message is None):
49
+ return "third-party message <dropped>"
50
+ return f"third-party message from {self.message}"
@@ -0,0 +1,15 @@
1
+ from ..Interface import DeviceRegistry as DeviceRegistry, ThirdPartyMessage as ThirdPartyMessage, ThirdPartyVendor as ThirdPartyVendor
2
+ from ..compat import override as override
3
+ from .Constants import ThirdPartyVendorFlag as ThirdPartyVendorFlag
4
+ from .FrameBase import FrameBase as FrameBase
5
+ from functools import cached_property
6
+
7
+ VENDOR_SIZE: int
8
+
9
+ class FrameThirdParty(FrameBase):
10
+ @staticmethod
11
+ def construct(mxr: DeviceRegistry, vendor: ThirdPartyVendor, payload: bytes) -> FrameBase | None: ...
12
+ @cached_property
13
+ def message(self) -> ThirdPartyMessage | None: ...
14
+ @override
15
+ def process(self) -> None: ...
@@ -22,12 +22,13 @@ from datetime import datetime, timezone
22
22
  from .ConnectionAsync import ConnectionAsync
23
23
  from ..const import MXR_HELLO_INTERVAL_MIN, MXR_HELLO_INTERVAL_RAND, __version__
24
24
  from .Device import Device
25
- from ..Interface import ConnectionCallbacks, DeviceRegistry, MxrDeviceUid, BayLinks, BayBase, DeviceBase, MxrCallbacks, AudioEndpoint, mxr_broadcast_address, OneIPDeviceSettings, OneIPPowerSaveSchedule, OneIPPtpMessages, OsdSyncContent, OsdSyncRequest
26
- from ..proto.Constants import MXR_PROTOCOL_VERSION, DeviceFeature, OsdSyncWhat
25
+ from ..Interface import ConnectionCallbacks, DeviceRegistry, MxrDeviceUid, BayLinks, BayBase, DeviceBase, MxrCallbacks, AudioEndpoint, mxr_broadcast_address, OneIPDeviceSettings, OneIPPowerSaveSchedule, OneIPPtpMessages, OsdSyncContent, OsdSyncRequest, ThirdPartyVendor
26
+ from ..proto.Constants import MXR_PROTOCOL_VERSION, THIRD_PARTY_PAYLOAD_MAX, DeviceFeature, OsdSyncWhat
27
27
  from ..proto.FrameDiscover import FrameDiscover, constructFrameDiscover
28
28
  from ..proto.Factory import process_mxr_frame
29
29
  from ..proto.FrameHello import FrameHello, constructFrameHello
30
30
  from ..proto.FramePing import FramePing
31
+ from ..proto.FrameThirdParty import FrameThirdParty
31
32
  from ..proto.FrameBase import FrameBase
32
33
  from ..proto.FrameTime import FrameTime
33
34
  from ..proto.FrameTimeZone import FrameTimeZone
@@ -573,6 +574,23 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
573
574
  protocol 0x30 ignores it.'''
574
575
  return self._send_to_all(FrameV2IPPrivacy.construct_request(mxr=self, target=None, target_uid=None))
575
576
 
577
+ async def send_third_party(self, vendor:ThirdPartyVendor, payload:bytes) -> bool:
578
+ '''Send an application's own message to every device, under vendor.
579
+ True when all of it was written.
580
+
581
+ Every other client of this library that has heard this one announce
582
+ itself receives it at MxrCallbacks.on_third_party_message(); this client
583
+ does not, and neither does one sharing its uid. No unit is meant to read
584
+ it. Nothing acknowledges it, so an application that needs to know it
585
+ arrived has to have its peers answer.
586
+
587
+ A payload over THIRD_PARTY_PAYLOAD_MAX bytes is refused.'''
588
+ if (len(payload) > THIRD_PARTY_PAYLOAD_MAX):
589
+ _LOGGER.warning(f"not sending a third-party message: {len(payload)} bytes, "
590
+ f"the most is {THIRD_PARTY_PAYLOAD_MAX}")
591
+ return False
592
+ return self._send_to_all(FrameThirdParty.construct(mxr=self, vendor=vendor, payload=payload))
593
+
576
594
  async def set_mesh_time_zone(self, zone:str, rule:str) -> bool:
577
595
  '''Set the time zone of every device that hears it.
578
596
 
@@ -1,14 +1,15 @@
1
1
  import aiohttp
2
- from ..Interface import AudioEndpoint as AudioEndpoint, BayBase as BayBase, BayLinks as BayLinks, ConnectionCallbacks as ConnectionCallbacks, DeviceBase as DeviceBase, DeviceRegistry as DeviceRegistry, MxrCallbacks as MxrCallbacks, OneIPDeviceSettings as OneIPDeviceSettings, OneIPPowerSaveSchedule as OneIPPowerSaveSchedule, OneIPPtpMessages as OneIPPtpMessages, OsdSyncContent as OsdSyncContent, OsdSyncRequest as OsdSyncRequest, mxr_broadcast_address as mxr_broadcast_address
2
+ from ..Interface import AudioEndpoint as AudioEndpoint, BayBase as BayBase, BayLinks as BayLinks, ConnectionCallbacks as ConnectionCallbacks, DeviceBase as DeviceBase, DeviceRegistry as DeviceRegistry, MxrCallbacks as MxrCallbacks, OneIPDeviceSettings as OneIPDeviceSettings, OneIPPowerSaveSchedule as OneIPPowerSaveSchedule, OneIPPtpMessages as OneIPPtpMessages, OsdSyncContent as OsdSyncContent, OsdSyncRequest as OsdSyncRequest, ThirdPartyVendor as ThirdPartyVendor, mxr_broadcast_address as mxr_broadcast_address
3
3
  from ..Uid import MxrDeviceUid as MxrDeviceUid
4
4
  from ..compat import override as override
5
5
  from ..const import MXR_HELLO_INTERVAL_MIN as MXR_HELLO_INTERVAL_MIN, MXR_HELLO_INTERVAL_RAND as MXR_HELLO_INTERVAL_RAND, MX_BCAST_UDP_PORT as MX_BCAST_UDP_PORT, MX_MCAST_UDP_IP as MX_MCAST_UDP_IP, MX_MCAST_UDP_PORT as MX_MCAST_UDP_PORT, __version__ as __version__
6
- from ..proto.Constants import DeviceFeature as DeviceFeature, MXR_PROTOCOL_VERSION as MXR_PROTOCOL_VERSION, ONEIP_DEVICE_SETTINGS_REPORTED_ONLY as ONEIP_DEVICE_SETTINGS_REPORTED_ONLY, 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_SINK_ROTATION_MAX as ONEIP_SINK_ROTATION_MAX, OneIPDeviceSetting as OneIPDeviceSetting, OneIPPowerSaveLevel as OneIPPowerSaveLevel, OneIPPtpMode as OneIPPtpMode, OsdSyncWhat as OsdSyncWhat
6
+ from ..proto.Constants import DeviceFeature as DeviceFeature, MXR_PROTOCOL_VERSION as MXR_PROTOCOL_VERSION, ONEIP_DEVICE_SETTINGS_REPORTED_ONLY as ONEIP_DEVICE_SETTINGS_REPORTED_ONLY, 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_SINK_ROTATION_MAX as ONEIP_SINK_ROTATION_MAX, OneIPDeviceSetting as OneIPDeviceSetting, OneIPPowerSaveLevel as OneIPPowerSaveLevel, OneIPPtpMode as OneIPPtpMode, OsdSyncWhat as OsdSyncWhat, THIRD_PARTY_PAYLOAD_MAX as THIRD_PARTY_PAYLOAD_MAX
7
7
  from ..proto.Factory import process_mxr_frame as process_mxr_frame
8
8
  from ..proto.FrameBase import FrameBase as FrameBase
9
9
  from ..proto.FrameDiscover import FrameDiscover as FrameDiscover, constructFrameDiscover as constructFrameDiscover
10
10
  from ..proto.FrameHello import FrameHello as FrameHello, constructFrameHello as constructFrameHello
11
11
  from ..proto.FramePing import FramePing as FramePing
12
+ from ..proto.FrameThirdParty import FrameThirdParty as FrameThirdParty
12
13
  from ..proto.FrameTime import FrameTime as FrameTime
13
14
  from ..proto.FrameTimeZone import FrameTimeZone as FrameTimeZone
14
15
  from ..proto.FrameV2IPOsdSync import FrameV2IPOsdSync as FrameV2IPOsdSync
@@ -75,6 +76,7 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
75
76
  async def set_all_oneip_device_settings(self, settings: OneIPDeviceSettings) -> bool: ...
76
77
  async def set_all_oneip_power_save(self, enabled: bool) -> bool: ...
77
78
  async def request_all_oneip_privacy(self) -> bool: ...
79
+ async def send_third_party(self, vendor: ThirdPartyVendor, payload: bytes) -> bool: ...
78
80
  async def set_mesh_time_zone(self, zone: str, rule: str) -> bool: ...
79
81
  async def clear_mesh_time_zone(self) -> bool: ...
80
82
  async def set_mesh_time(self, when: datetime | None = None) -> bool: ...
@@ -63,6 +63,7 @@ BASELINE = {
63
63
  'FrameSystemStatus.FrameSystemStatus',
64
64
  'FrameTXRCAction.FrameTXRCAction',
65
65
  'FrameTXRCKey.FrameTXRCKey',
66
+ 'FrameThirdParty.FrameThirdParty',
66
67
  'FrameTime.FrameTime',
67
68
  'FrameTimeZone.FrameTimeZone',
68
69
  'FrameTopology.FrameTopology',
@@ -59,6 +59,7 @@ SUITES = [
59
59
  'meshbays', # the bays a unit lists for other units, from its mappings
60
60
  'v2ipsdp', # what a OneIP output plays from a session description
61
61
  'privacy', # how a OneIP source unit encrypts its streams
62
+ 'thirdparty', # applications' own messages, sent and received
62
63
  'baycontent', # what the source on an input is showing
63
64
  'osdsync', # overlay sync, watched and sent
64
65
  'osdmessage', # the message on a unit's output, read and written
@@ -12,6 +12,7 @@ print('version', mx_remote.const.VERSION, 'proto', hex(MXR_PROTOCOL_VERSION))
12
12
  assert MXR_PROTOCOL_VERSION == max(MXR_OPCODE_VERSIONS.values()), hex(MXR_PROTOCOL_VERSION)
13
13
  assert MXR_OPCODE_VERSIONS[0x52] == 0x2E and MXR_OPCODE_VERSIONS[0x53] == 0x2F # V2IP_SINK_SDP, V2IP_OSD_SYNC
14
14
  assert MXR_OPCODE_VERSIONS[0x54] == 0x30 # V2IP_PRIVACY
15
+ assert MXR_OPCODE_VERSIONS[0x55] == 0x01 # THIRD_PARTY
15
16
  assert MXR_OPCODE_VERSIONS[0x49] == 0x28 and MXR_OPCODE_VERSIONS[0x3C] == 0x11
16
17
  assert MXR_OPCODE_VERSIONS[0x4F] == 0x2C and MXR_OPCODE_VERSIONS[0x50] == 0x2C # SYS_UNIT_STATUS, SYS_UNIT_COMMAND
17
18
  assert DeviceFeature.VIDEO_WALL == (1 << 24)
@@ -0,0 +1,146 @@
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
+ '''Applications' own messages (0x55), sent and received.
8
+
9
+ The fixtures are composed from the layout this library defines for the opcode,
10
+ so they rule out a slip rather than a shared misreading. Payload bytes are all
11
+ distinct from their neighbours, so a payload read from the wrong offset shows.
12
+ '''
13
+
14
+ import asyncio, os, struct, sys, logging
15
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
+ logging.disable(logging.CRITICAL)
17
+ import mx_remote
18
+ from mx_remote import DeviceFeature, MxrDeviceUid, ThirdPartyVendor
19
+ from mx_remote.proto.Constants import THIRD_PARTY_PAYLOAD_MAX, ThirdPartyVendorFlag
20
+ from mx_remote.proto.Factory import create_mxr_frame
21
+
22
+ ADDR = ('192.0.2.9', 8812)
23
+ OURS = bytes(range(0x20, 0x30))
24
+
25
+ class Listening(mx_remote.MxrCallbacks):
26
+ def __init__(self):
27
+ self.messages = []
28
+ def on_third_party_message(self, dev, message):
29
+ self.messages.append((dev, message))
30
+
31
+ seen = Listening()
32
+ mx = mx_remote.Remote(open_connection=False, callbacks=seen)
33
+ mx._uid = OURS
34
+ sent = []
35
+ mx.transmit = lambda data: (sent.append(bytes(data)), len(data))[1]
36
+
37
+ def uid(n):
38
+ return bytes([n]) * 16
39
+
40
+ def nm(s, sz=16):
41
+ b = s.encode('ascii')[:sz]
42
+ return b + bytes(sz - len(b))
43
+
44
+ def body(n):
45
+ return bytes((0x80 + i * 7) & 0xFF for i in range(n))
46
+
47
+ def rx(sender, payload, protocol=0x01):
48
+ '''A message stamped as a client stamps it, at the opcode's row.'''
49
+ frame = bytearray(create_mxr_frame(sender, 0x55, payload))
50
+ frame[2] = protocol
51
+ mx.process_frame(0.0, bytes(frame), ADDR)
52
+
53
+ def known(n):
54
+ '''Another client, announced.'''
55
+ frame = bytearray(create_mxr_frame(uid(n), 0x00, struct.pack('<H', 0x30) + nm('audio-cast') + nm('P9SN00000000')
56
+ + nm('1.0.0') + struct.pack('<I', int(DeviceFeature.MANAGER))))
57
+ mx.process_frame(0.0, bytes(frame), ADDR)
58
+ seen.messages.clear()
59
+ return mx.get_by_uid(MxrDeviceUid(uid(n)))
60
+
61
+ def call(coro):
62
+ return asyncio.run(coro)
63
+
64
+ # ------------------------------------------------------------------- sending
65
+ # Every device gets it at the lowest stamp, with the flag byte, the three
66
+ # identifying bytes and the payload as given. A flag bit without a name goes
67
+ # out as it was set.
68
+ odk = ThirdPartyVendor(flags=ThirdPartyVendorFlag.NOT_OUI | ThirdPartyVendorFlag(0x40), id=b'ODK')
69
+ sent.clear()
70
+ assert call(mx.send_third_party(odk, body(41))) is True
71
+ frame = sent[-1]
72
+ assert frame[20:22] == bytes([0x55, 0x00]), frame[:24].hex()
73
+ assert frame[2] == 0x01, f'stamped {frame[2]:#04x}'
74
+ assert frame[24:28] == bytes([0x41]) + b'ODK', frame[24:28].hex()
75
+ assert frame[28:] == body(41)
76
+ print('send : stamped 0x01, the vendor id then the payload as given')
77
+
78
+ sent.clear()
79
+ assert call(mx.send_third_party(ThirdPartyVendor.oui(bytes([0, 0x1A, 0x2B])), b'')) is True
80
+ assert sent[-1][24:] == bytes([0, 0, 0x1A, 0x2B]), sent[-1][24:].hex()
81
+
82
+ sent.clear()
83
+ assert call(mx.send_third_party(odk, bytes(THIRD_PARTY_PAYLOAD_MAX))) is True, 'the largest payload was refused'
84
+ assert len(sent[-1]) + 28 == 1500, 'the largest frame does not fill the MTU'
85
+
86
+ sent.clear()
87
+ assert call(mx.send_third_party(odk, bytes(THIRD_PARTY_PAYLOAD_MAX + 1))) is False
88
+ assert not sent, 'an oversized message was sent'
89
+ print('size : an empty payload goes, and up to', THIRD_PARTY_PAYLOAD_MAX, 'bytes, which fill the MTU')
90
+
91
+ # ------------------------------------------------------------------- receiving
92
+ # The flag byte comes first and the three identifying bytes follow, and the
93
+ # payload after them arrives byte for byte.
94
+ dev = known(0x31)
95
+ rx(uid(0x31), bytes([0x81]) + b'ODK' + body(37))
96
+ assert len(seen.messages) == 1, seen.messages
97
+ by, message = seen.messages[0]
98
+ assert by is dev, by
99
+ assert message.vendor.flags == ThirdPartyVendorFlag(0x81), message.vendor.flags
100
+ assert message.vendor.id == b'ODK'
101
+ assert message.payload == body(37)
102
+ assert message.address == ADDR[0]
103
+ assert message.vendor.same_vendor(ThirdPartyVendor.private(b'ODK'))
104
+ print('receive :', message)
105
+
106
+ # A vendor id alone is a message with nothing after it, and anything shorter
107
+ # is not a message.
108
+ known(0x32)
109
+ rx(uid(0x32), bytes([0x00, 0x00, 0x1A, 0x2B]))
110
+ assert len(seen.messages) == 1, seen.messages
111
+ assert seen.messages[0][1].vendor == ThirdPartyVendor.oui(bytes([0x00, 0x1A, 0x2B]))
112
+ assert seen.messages[0][1].payload == b''
113
+ seen.messages.clear()
114
+ for n in range(4):
115
+ rx(uid(0x32), body(n))
116
+ assert not seen.messages, seen.messages
117
+ print('minimum : the vendor id is the whole minimum')
118
+
119
+ # A sender that has not announced itself is not heard.
120
+ rx(uid(0x34), bytes([1]) + b'ODK' + bytes([7]))
121
+ assert not seen.messages, seen.messages
122
+
123
+ # This client never hears itself. Its own uid is never a known device either,
124
+ # so the stranger rule drops the frame too, and only the two together being
125
+ # lost turns this red.
126
+ rx(OURS, bytes([1]) + b'ODK' + bytes([7]))
127
+ assert not seen.messages, seen.messages
128
+ print('senders : a stranger and this client are not heard')
129
+
130
+ # ------------------------------------------------------------------- vendors
131
+ # Whether two ids name the same vendor rests on the not-an-OUI flag and the
132
+ # three bytes, not on the other flag bits.
133
+ plain = ThirdPartyVendor.private(b'ODK')
134
+ assert plain.same_vendor(ThirdPartyVendor(flags=ThirdPartyVendorFlag(0xF1), id=b'ODK'))
135
+ assert not plain.same_vendor(ThirdPartyVendor.oui(b'ODK'))
136
+ for other in (b'ODL', b'PDK', b'OEK'):
137
+ assert not plain.same_vendor(ThirdPartyVendor.private(other)), other
138
+ for wrong in (b'', b'OD', b'ODKX'):
139
+ try:
140
+ ThirdPartyVendor.private(wrong)
141
+ except ValueError:
142
+ continue
143
+ raise AssertionError(f'a {len(wrong)}-byte vendor id was taken')
144
+ print('vendors : the flag and the three bytes name a vendor')
145
+
146
+ 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