mx-remote 8.1.0__tar.gz → 9.0.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 (269) hide show
  1. {mx_remote-8.1.0 → mx_remote-9.0.0}/PKG-INFO +1 -1
  2. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/api.md +19 -0
  3. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/oneip.md +39 -5
  4. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/Interface.py +87 -13
  5. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/Interface.pyi +21 -1
  6. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/const.py +1 -1
  7. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Constants.py +34 -3
  8. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Constants.pyi +15 -0
  9. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +25 -8
  10. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPOsdSync.py +45 -12
  11. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +10 -2
  12. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Device.py +81 -8
  13. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Device.pyi +9 -2
  14. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/OsdSync.py +1 -11
  15. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Remote.py +8 -7
  16. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Remote.pyi +1 -1
  17. mx_remote-9.0.0/tests/osdmessage.py +114 -0
  18. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/osdsync.py +12 -13
  19. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/run.py +1 -0
  20. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2ipsettings.py +49 -0
  21. {mx_remote-8.1.0 → mx_remote-9.0.0}/.gitignore +0 -0
  22. {mx_remote-8.1.0 → mx_remote-9.0.0}/AGENTS.md +0 -0
  23. {mx_remote-8.1.0 → mx_remote-9.0.0}/CLAUDE.md +0 -0
  24. {mx_remote-8.1.0 → mx_remote-9.0.0}/LICENSE +0 -0
  25. {mx_remote-8.1.0 → mx_remote-9.0.0}/README.md +0 -0
  26. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/README.md +0 -0
  27. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/_static/brand.css +0 -0
  28. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/_static/favicon.svg +0 -0
  29. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/_static/logo-dark.svg +0 -0
  30. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/_static/logo.svg +0 -0
  31. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/audio.md +0 -0
  32. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/callbacks.md +0 -0
  33. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/conf.py +0 -0
  34. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/configuration.md +0 -0
  35. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/devices-and-bays.md +0 -0
  36. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/diagnostics.md +0 -0
  37. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/index.md +0 -0
  38. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/multiviewer.md +0 -0
  39. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/requirements.txt +0 -0
  40. {mx_remote-8.1.0 → mx_remote-9.0.0}/docs/routing.md +0 -0
  41. {mx_remote-8.1.0 → mx_remote-9.0.0}/hatch_build.py +0 -0
  42. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/Uid.py +0 -0
  43. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/Uid.pyi +0 -0
  44. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/__init__.py +0 -0
  45. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/__init__.pyi +0 -0
  46. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/compat.py +0 -0
  47. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/compat.pyi +0 -0
  48. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/const.pyi +0 -0
  49. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/deprecated.py +0 -0
  50. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/deprecated.pyi +0 -0
  51. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/main.py +0 -0
  52. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/main.pyi +0 -0
  53. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/BayConfig.py +0 -0
  54. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/BayConfig.pyi +0 -0
  55. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Data.py +0 -0
  56. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Data.pyi +0 -0
  57. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Factory.py +0 -0
  58. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Factory.pyi +0 -0
  59. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  60. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  61. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  62. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  63. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  64. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  65. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  66. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  67. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBase.py +0 -0
  68. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBase.pyi +0 -0
  69. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  70. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  71. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  72. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  73. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayContent.py +0 -0
  74. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  75. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayHide.py +0 -0
  76. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  77. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  78. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  79. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  80. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  81. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameDebug.py +0 -0
  82. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  83. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameDiscover.py +0 -0
  84. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  85. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameEDID.py +0 -0
  86. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  87. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  88. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  89. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  90. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  91. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  92. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  93. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  94. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  95. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameHeader.py +0 -0
  96. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  97. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameHello.py +0 -0
  98. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameHello.pyi +0 -0
  99. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameLinks.py +0 -0
  100. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  101. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  102. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  103. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  104. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  105. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  106. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  107. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  108. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  109. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FramePing.py +0 -0
  110. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FramePing.pyi +0 -0
  111. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FramePowerChange.py +0 -0
  112. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  113. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCAction.py +0 -0
  114. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  115. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCIr.py +0 -0
  116. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  117. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCKey.py +0 -0
  118. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  119. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  120. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  121. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameReboot.py +0 -0
  122. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  123. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  124. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  125. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  126. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  127. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetName.py +0 -0
  128. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  129. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  130. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  131. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  132. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  133. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  134. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  135. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  136. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  137. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  138. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  139. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  140. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  141. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  142. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  143. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  144. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  145. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTime.py +0 -0
  146. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTime.pyi +0 -0
  147. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  148. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  149. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTopology.py +0 -0
  150. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  151. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTxIR.py +0 -0
  152. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  153. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  154. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  155. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  156. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  157. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  158. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  159. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  160. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  161. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  162. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  163. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  164. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  165. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  166. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  167. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  168. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  169. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  170. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  171. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  172. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  173. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  174. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  175. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  176. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  177. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  178. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  179. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  180. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSinkSdp.py +0 -0
  181. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +0 -0
  182. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  183. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  184. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  185. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  186. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  187. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  188. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  189. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  190. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  191. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  192. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  193. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  194. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolume.py +0 -0
  195. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  196. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  197. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  198. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  199. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  200. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  201. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  202. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/LinkConfig.py +0 -0
  203. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  204. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Multiviewer.py +0 -0
  205. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  206. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Svd.py +0 -0
  207. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/Svd.pyi +0 -0
  208. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/V2IPConfig.py +0 -0
  209. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  210. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/V2IPStats.py +0 -0
  211. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  212. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/__init__.py +0 -0
  213. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/__init__.pyi +0 -0
  214. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/proto/svd.csv +0 -0
  215. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/py.typed +0 -0
  216. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Bay.py +0 -0
  217. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Bay.pyi +0 -0
  218. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  219. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  220. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Link.py +0 -0
  221. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/Link.pyi +0 -0
  222. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/OsdSync.pyi +0 -0
  223. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/State.py +0 -0
  224. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/State.pyi +0 -0
  225. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/V2IP.py +0 -0
  226. {mx_remote-8.1.0 → mx_remote-9.0.0}/mx_remote/remote/V2IP.pyi +0 -0
  227. {mx_remote-8.1.0 → mx_remote-9.0.0}/pyproject.toml +0 -0
  228. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/audio_ir.py +0 -0
  229. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/audiolock.py +0 -0
  230. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/baycontent.py +0 -0
  231. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/capture.py +0 -0
  232. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/complete.py +0 -0
  233. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/deprecated.py +0 -0
  234. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/e2e.py +0 -0
  235. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/entry.py +0 -0
  236. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/handlers.py +0 -0
  237. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/handlers_cover.py +0 -0
  238. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/hello.py +0 -0
  239. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/idempotent.py +0 -0
  240. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/lengths.py +0 -0
  241. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/merge.py +0 -0
  242. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/mesh.py +0 -0
  243. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/meshbays.py +0 -0
  244. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/newops.py +0 -0
  245. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/offline.py +0 -0
  246. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/offsets.py +0 -0
  247. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/ping.py +0 -0
  248. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/privacy.py +0 -0
  249. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/proto_stamp.py +0 -0
  250. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/protogate.py +0 -0
  251. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/rcsettings.py +0 -0
  252. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/roundtrip.py +0 -0
  253. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/routes.py +0 -0
  254. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/smoke.py +0 -0
  255. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/sources.py +0 -0
  256. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/staterequest.py +0 -0
  257. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/stats.py +0 -0
  258. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/structs.py +0 -0
  259. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/txresult.py +0 -0
  260. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/unit.py +0 -0
  261. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/unknowns.py +0 -0
  262. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2ipptp.py +0 -0
  263. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2ipscaling.py +0 -0
  264. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2ipsdp.py +0 -0
  265. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2ipsubject.py +0 -0
  266. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2iptestcard.py +0 -0
  267. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/v2ipvlan.py +0 -0
  268. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/vdetails.py +0 -0
  269. {mx_remote-8.1.0 → mx_remote-9.0.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 8.1.0
3
+ Version: 9.0.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/
@@ -85,6 +85,23 @@ place, backgrounds and logo images.
85
85
  .. autodata:: mx_remote.proto.Constants.OSD_SYNC_PERCENT_MAX
86
86
  ```
87
87
 
88
+ ## Overlay messages
89
+
90
+ The message on a unit's output, which the overlay sync opcode carries too.
91
+
92
+ ```{eval-rst}
93
+ .. autoclass:: mx_remote.OsdMessage
94
+ :members:
95
+
96
+ .. autoclass:: mx_remote.proto.Constants.OsdMessageFlag
97
+ :members:
98
+
99
+ .. autoclass:: mx_remote.proto.Constants.OsdMessagePriority
100
+ :members:
101
+
102
+ .. autodata:: mx_remote.proto.Constants.OSD_MESSAGE_TEXT_MAX
103
+ ```
104
+
88
105
  ## Device identity
89
106
 
90
107
  ```{eval-rst}
@@ -180,6 +197,8 @@ state only the device reports.
180
197
 
181
198
  .. autodata:: mx_remote.proto.Constants.ONEIP_STREAM_TTL_DEFAULT
182
199
 
200
+ .. autodata:: mx_remote.proto.Constants.ONEIP_SINK_ROTATION_MAX
201
+
183
202
  .. autodata:: mx_remote.proto.Constants.ONEIP_PTP_DOMAIN_MAX
184
203
  ```
185
204
 
@@ -288,10 +288,23 @@ same block, behind `SAP_EXPIRE` and `SAP_INTERVAL`, and are set the same way;
288
288
  `SAP_LISTEN` and `SAP_ANNOUNCE` are switches like the other on/off settings.
289
289
  A device with an encoder reports its streams' TTL there too, behind
290
290
  `STREAM_TTL`; a TTL of 0 is refused, as a device changes nothing for one.
291
+ A device with an HDMI output reports the angle its output is turned by,
292
+ clockwise, behind `SINK_ROTATION`, with the step its video processor turns by.
293
+ It takes 0 or a multiple of that step up to `ONEIP_SINK_ROTATION_MAX`, so any
294
+ other angle is refused before it is sent, and the step is never written.
295
+ `set_all_oneip_device_settings` can only check the range; each device judges
296
+ the step.
297
+
298
+ ```python
299
+ m = settings.ptp_messages
300
+ print(m.sink_rotation, m.sink_rotation_step) # e.g. 0 and 90
301
+ await device.set_oneip_ptp_messages(OneIPDeviceSetting.SINK_ROTATION,
302
+ OneIPPtpMessages(sink_rotation=180))
303
+ ```
291
304
 
292
305
  Only the values behind the bits named are sent. A DSCP above `ONEIP_DSCP_MAX` is
293
306
  refused, and not cached from another controller's write. A write carrying one of
294
- these runs on to 224 bytes, or 232 with the TTL; every other write keeps the 176
307
+ these runs on to 224 bytes, or 232 with the TTL or the rotation; every other write keeps the 176
295
308
  every device knows.
296
309
 
297
310
  What the device's clock is actually doing is its PTP state, which only the
@@ -412,13 +425,34 @@ await mx.clear_osd_sync() # stop answering for it
412
425
  Each unit asks for the settings it lacks and takes only those, so this client
413
426
  holds the content and answers until it is replaced or cleared, announcing
414
427
  itself meanwhile as the mesh's overlay sync manager: a unit waking from power
415
- save then takes this content rather than the controller's. The controller
416
- still answers a unit that asks for settings without naming whose sync they are
417
- from, as an older overlay module does, with its own sync, which replaces this
418
- client's on that unit. Logo images are
428
+ save then takes this content rather than the controller's. Logo images are
419
429
  served over HTTP from this client, on `logo_port` or a free port. Content a unit
420
430
  would refuse is refused before anything is sent; see `send_osd_sync`.
421
431
 
432
+ ### Overlay messages
433
+
434
+ A unit shows a message on its output, over the picture, until it restarts. The
435
+ text is markup of at most `OSD_MESSAGE_TEXT_MAX` bytes with no control
436
+ character, shown with a logo: 0 the built-in one, -1 none, 1 to 5 a slot. Its
437
+ priority places it against the unit's own messages, such as Locate Device.
438
+
439
+ ```python
440
+ from mx_remote.proto.Constants import OsdMessagePriority
441
+
442
+ await sink.show_osd_message('Back at <b>3</b>', logo=-1, priority=OsdMessagePriority.TOP)
443
+ await sink.request_osd_message() # the unit answers with what it shows
444
+ await sink.clear_osd_message()
445
+
446
+ print(sink.osd_message) # None until the unit has answered
447
+ ```
448
+
449
+ A unit takes a message only from management, which this client announces
450
+ itself as, and answers every write and request with the message it then has;
451
+ `on_osd_message_changed` reports a change. The answer carries
452
+ `OsdMessageFlag.REFUSED` when the unit did not take the write, as when its
453
+ video processor has no message layer. A text, logo or priority a unit would
454
+ refuse is refused before it is sent, as is a device that is not a OneIP output.
455
+
422
456
  ### Stream encryption
423
457
 
424
458
  A source unit from protocol 0x30 encrypts its video, audio and ancillary
@@ -6,7 +6,7 @@
6
6
  ######################################################
7
7
 
8
8
  from abc import ABC, abstractmethod
9
- from dataclasses import dataclass
9
+ from dataclasses import dataclass, replace
10
10
  from datetime import datetime, timezone
11
11
  from enum import IntEnum
12
12
  import time
@@ -2215,8 +2215,8 @@ def _wire_enum(cls:Any, value:int) -> Any:
2215
2215
  @dataclass(frozen=True)
2216
2216
  class OneIPPtpMessages:
2217
2217
  """
2218
- How a OneIP device paces its PTP messages, its SAP timings and its streams'
2219
- TTL, which a frame carries in the same block. Each value is behind its own bit of
2218
+ How a OneIP device paces its PTP messages, its SAP timings, its streams'
2219
+ TTL and its output's rotation, which a frame carries in the same block. Each value is behind its own bit of
2220
2220
  OneIPDeviceSettings.valid, one of ONEIP_DEVICE_SETTINGS_BLOCK.
2221
2221
 
2222
2222
  Intervals are base-2 logarithms of seconds, so -3 is 125 ms and 1 is 2 s.
@@ -2241,6 +2241,19 @@ class OneIPPtpMessages:
2241
2241
  """The TTL of the device's streams, 1 to 255, behind STREAM_TTL. A device
2242
2242
  whose video processor cannot change it keeps the setting and sends
2243
2243
  ONEIP_STREAM_TTL_DEFAULT."""
2244
+ sink_rotation: int = 0
2245
+ """The degrees the device's HDMI output is turned by, clockwise, at most
2246
+ ONEIP_SINK_ROTATION_MAX, behind SINK_ROTATION."""
2247
+ sink_rotation_step: int = 0
2248
+ """The step the output turns by: the device takes only an angle that is a
2249
+ multiple of it, and 0 alone when it is 0. Only the device reports it,
2250
+ behind the same bit."""
2251
+
2252
+ def takes_sink_rotation(self, degrees:int) -> bool:
2253
+ """Whether a device reporting this step takes degrees as its output's
2254
+ rotation."""
2255
+ return (degrees == 0) or ((0 < degrees <= ONEIP_SINK_ROTATION_MAX) and (self.sink_rotation_step != 0)
2256
+ and (degrees % self.sink_rotation_step == 0))
2244
2257
 
2245
2258
  def merge(self, other:'OneIPPtpMessages', which:OneIPDeviceSetting) -> 'OneIPPtpMessages':
2246
2259
  """The values behind the bits of which copied from other, the rest kept."""
@@ -2253,7 +2266,9 @@ class OneIPPtpMessages:
2253
2266
  dscp=(other if (S.PTP_DSCP in which) else self).dscp,
2254
2267
  sap_expire_min=(other if (S.SAP_EXPIRE in which) else self).sap_expire_min,
2255
2268
  sap_interval_s=(other if (S.SAP_INTERVAL in which) else self).sap_interval_s,
2256
- stream_ttl=(other if (S.STREAM_TTL in which) else self).stream_ttl)
2269
+ stream_ttl=(other if (S.STREAM_TTL in which) else self).stream_ttl,
2270
+ sink_rotation=(other if (S.SINK_ROTATION in which) else self).sink_rotation,
2271
+ sink_rotation_step=(other if (S.SINK_ROTATION in which) else self).sink_rotation_step)
2257
2272
 
2258
2273
  def describe(self, valid:OneIPDeviceSetting) -> list[str]:
2259
2274
  """The values behind the bits of valid, one "name=value" each."""
@@ -2266,7 +2281,8 @@ class OneIPPtpMessages:
2266
2281
  (S.PTP_DSCP, 'ptp_dscp', self.dscp),
2267
2282
  (S.SAP_EXPIRE, 'sap_expire_min', self.sap_expire_min),
2268
2283
  (S.SAP_INTERVAL, 'sap_interval_s', self.sap_interval_s),
2269
- (S.STREAM_TTL, 'stream_ttl', self.stream_ttl))
2284
+ (S.STREAM_TTL, 'stream_ttl', self.stream_ttl),
2285
+ (S.SINK_ROTATION, 'sink_rotation', self.sink_rotation))
2270
2286
  if (bit in valid)]
2271
2287
 
2272
2288
  @dataclass(frozen=True)
@@ -2317,8 +2333,7 @@ class OsdSyncRequest:
2317
2333
  missing: bytes
2318
2334
  """The settings it lacks, bit i standing for setting i, 16 bytes."""
2319
2335
  source: MxrDeviceUid
2320
- """Who sent the sync it asks about. Empty when it asks for a new sync, and
2321
- from a unit whose module does not name the source."""
2336
+ """Who sent the sync it asks about. Empty when it asks for a new sync."""
2322
2337
 
2323
2338
  def is_missing(self, index:int) -> bool:
2324
2339
  """Whether the unit lacks setting index."""
@@ -2394,6 +2409,28 @@ class OsdSyncReport:
2394
2409
  reason: str
2395
2410
  """Why it failed or refused any."""
2396
2411
 
2412
+ @dataclass(frozen=True)
2413
+ class OsdMessage:
2414
+ """
2415
+ The message on a unit's output, as the unit last reported it. The unit
2416
+ holds it until it restarts.
2417
+ """
2418
+ flags: OsdMessageFlag
2419
+ """Whether a message is set and shown, whether the unit can show one, and
2420
+ whether it refused the write it answers. Without SET the other fields are
2421
+ empty."""
2422
+ logo: int
2423
+ """The logo shown with it: 0 the built-in logo, -1 none, 1 to 5 a slot."""
2424
+ priority: OsdMessagePriority|int
2425
+ """Where it stands against the unit's own messages."""
2426
+ text: str
2427
+ """Its markup."""
2428
+
2429
+ def __str__(self) -> str:
2430
+ if (OsdMessageFlag.SET not in self.flags):
2431
+ return f"none{' (refused)' if (OsdMessageFlag.REFUSED in self.flags) else ''}"
2432
+ return f"'{self.text}' logo {self.logo} {self.flags!r}"
2433
+
2397
2434
  @dataclass(frozen=True)
2398
2435
  class OsdSyncEntry:
2399
2436
  """One setting of an overlay sync this client sends."""
@@ -2767,16 +2804,25 @@ class OneIPDeviceSettings:
2767
2804
  else previous._power_save_level),
2768
2805
  ptp_messages=previous._ptp_messages.merge(self._ptp_messages, valid))
2769
2806
 
2770
- def as_applied_to(self, reported:OneIPDeviceSetting) -> 'OneIPDeviceSettings':
2807
+ def as_applied_to(self, reported:'OneIPDeviceSettings|OneIPDeviceSetting|None') -> 'OneIPDeviceSettings':
2771
2808
  """
2772
- This block limited to what a device takes from a write about it.
2809
+ This block limited to what a device that reported reported takes from a
2810
+ write about it.
2773
2811
 
2774
2812
  A device applies a setting only if it has it, a profile, PTP mode, PTP
2775
2813
  domain, power save level or PTP DSCP only within its range, a stream
2776
- TTL only when it is not 0, and never what only it can report about
2777
- itself.
2814
+ TTL only when it is not 0, a rotation only by its own step, and never
2815
+ what only it can report about itself. The step stays the device's.
2816
+
2817
+ reported may be only the bits the device reported, which says nothing
2818
+ of its step, so a rotation other than 0 is then left out.
2778
2819
  """
2779
- valid = (self._valid & reported) & ~ONEIP_DEVICE_SETTINGS_REPORTED_ONLY
2820
+ if (reported is None):
2821
+ reported = OneIPDeviceSettings()
2822
+ elif not isinstance(reported, OneIPDeviceSettings):
2823
+ reported = OneIPDeviceSettings(valid=OneIPDeviceSetting(reported))
2824
+ own = reported._ptp_messages
2825
+ valid = (self._valid & reported._valid) & ~ONEIP_DEVICE_SETTINGS_REPORTED_ONLY
2780
2826
  if not (0 <= self._ir_profile < ONEIP_IR_PROFILE_MAX):
2781
2827
  valid &= ~OneIPDeviceSetting.IR_PROFILE
2782
2828
  if not (ONEIP_IR_PROFILE_NOT_SET <= self._ir_profile_sink < ONEIP_IR_PROFILE_MAX):
@@ -2791,13 +2837,16 @@ class OneIPDeviceSettings:
2791
2837
  valid &= ~OneIPDeviceSetting.PTP_DSCP
2792
2838
  if (self._ptp_messages.stream_ttl == 0):
2793
2839
  valid &= ~OneIPDeviceSetting.STREAM_TTL
2840
+ if not own.takes_sink_rotation(self._ptp_messages.sink_rotation):
2841
+ valid &= ~OneIPDeviceSetting.SINK_ROTATION
2794
2842
  return OneIPDeviceSettings(valid=valid, flags=self._flags, ir_profiles=self._ir_profiles,
2795
2843
  ir_profile=self._ir_profile, ir_profile_sink=self._ir_profile_sink,
2796
2844
  auto_power_save=self._auto_power_save, power_save=self._power_save,
2797
2845
  ptp_mode=self._ptp_mode, ptp_domain=self._ptp_domain,
2798
2846
  ptp_priority1=self._ptp_priority1,
2799
2847
  power_save_level=self._power_save_level,
2800
- ptp_messages=self._ptp_messages)
2848
+ ptp_messages=replace(self._ptp_messages,
2849
+ sink_rotation_step=own.sink_rotation_step))
2801
2850
 
2802
2851
  def __eq__(self, other:Any) -> bool:
2803
2852
  if not isinstance(other, OneIPDeviceSettings):
@@ -3505,6 +3554,26 @@ class DeviceBase(ABC):
3505
3554
  def osd_sync_report(self) -> OsdSyncReport|None:
3506
3555
  '''This unit's last report on taking an overlay sync, None until it has.'''
3507
3556
 
3557
+ @property
3558
+ @abstractmethod
3559
+ def osd_message(self) -> OsdMessage|None:
3560
+ '''The message on this unit's output, as it last reported it to this
3561
+ client, None until it has. A unit reports it only when asked, or when
3562
+ this client writes it.'''
3563
+
3564
+ @abstractmethod
3565
+ async def request_osd_message(self) -> bool:
3566
+ '''ask this unit for the message on its output'''
3567
+
3568
+ @abstractmethod
3569
+ async def show_osd_message(self, text:str, logo:int=-1,
3570
+ priority:OsdMessagePriority=OsdMessagePriority.NORMAL) -> bool:
3571
+ '''show a message on this unit's output, in place of any it has'''
3572
+
3573
+ @abstractmethod
3574
+ async def clear_osd_message(self) -> bool:
3575
+ '''take the message down from this unit's output'''
3576
+
3508
3577
  @property
3509
3578
  @abstractmethod
3510
3579
  def oneip_sink_sdp(self) -> OneIPSinkSdp|None:
@@ -4120,6 +4189,11 @@ class MxrCallbacks:
4120
4189
  _LOGGER.debug(f"{dev} overlay sync {report.seq}: {report.state!r} {report.reason}")
4121
4190
  self.on_device_update(dev)
4122
4191
 
4192
+ def on_osd_message_changed(self, dev:DeviceBase, message:OsdMessage) -> None:
4193
+ ''' called when unit 'dev' reported something different about the message on its output '''
4194
+ _LOGGER.debug(f"{dev} overlay message: {message}")
4195
+ self.on_device_update(dev)
4196
+
4123
4197
  def on_osd_sync_requested(self, dev:DeviceBase, request:OsdSyncRequest) -> None:
4124
4198
  ''' called when 'dev' asked for an overlay sync, or for settings of one '''
4125
4199
  pass
@@ -787,6 +787,9 @@ class OneIPPtpMessages:
787
787
  sap_expire_min: int = ...
788
788
  sap_interval_s: int = ...
789
789
  stream_ttl: int = ...
790
+ sink_rotation: int = ...
791
+ sink_rotation_step: int = ...
792
+ def takes_sink_rotation(self, degrees: int) -> bool: ...
790
793
  def merge(self, other: OneIPPtpMessages, which: OneIPDeviceSetting) -> OneIPPtpMessages: ...
791
794
  def describe(self, valid: OneIPDeviceSetting) -> list[str]: ...
792
795
 
@@ -853,6 +856,13 @@ class OsdSyncReport:
853
856
  refused: int
854
857
  reason: str
855
858
 
859
+ @dataclass(frozen=True)
860
+ class OsdMessage:
861
+ flags: OsdMessageFlag
862
+ logo: int
863
+ priority: OsdMessagePriority | int
864
+ text: str
865
+
856
866
  @dataclass(frozen=True)
857
867
  class OsdSyncEntry:
858
868
  value: OsdSyncValue
@@ -968,7 +978,7 @@ class OneIPDeviceSettings:
968
978
  @property
969
979
  def ptp_messages(self) -> OneIPPtpMessages | None: ...
970
980
  def merge(self, previous: OneIPDeviceSettings | None) -> OneIPDeviceSettings: ...
971
- def as_applied_to(self, reported: OneIPDeviceSetting) -> OneIPDeviceSettings: ...
981
+ def as_applied_to(self, reported: OneIPDeviceSettings | OneIPDeviceSetting | None) -> OneIPDeviceSettings: ...
972
982
  def __eq__(self, other: Any) -> bool: ...
973
983
  def __ne__(self, other: Any) -> bool: ...
974
984
 
@@ -1324,6 +1334,15 @@ class DeviceBase(ABC):
1324
1334
  def osd_sync_report(self) -> OsdSyncReport | None: ...
1325
1335
  @property
1326
1336
  @abstractmethod
1337
+ def osd_message(self) -> OsdMessage | None: ...
1338
+ @abstractmethod
1339
+ async def request_osd_message(self) -> bool: ...
1340
+ @abstractmethod
1341
+ async def show_osd_message(self, text: str, logo: int = -1, priority: OsdMessagePriority = ...) -> bool: ...
1342
+ @abstractmethod
1343
+ async def clear_osd_message(self) -> bool: ...
1344
+ @property
1345
+ @abstractmethod
1327
1346
  def oneip_sink_sdp(self) -> OneIPSinkSdp | None: ...
1328
1347
  @property
1329
1348
  @abstractmethod
@@ -1561,6 +1580,7 @@ class MxrCallbacks:
1561
1580
  def on_status_hdbt_connected_changed(self, bay: BayBase, val: bool) -> None: ...
1562
1581
  def on_osd_sync(self, dev: DeviceBase, sync: OsdSync) -> None: ...
1563
1582
  def on_osd_sync_report(self, dev: DeviceBase, report: OsdSyncReport) -> None: ...
1583
+ def on_osd_message_changed(self, dev: DeviceBase, message: OsdMessage) -> None: ...
1564
1584
  def on_osd_sync_requested(self, dev: DeviceBase, request: OsdSyncRequest) -> None: ...
1565
1585
  def on_osd_sync_setting(self, dev: DeviceBase, setting: OsdSyncSetting) -> None: ...
1566
1586
  def on_bay_content_changed(self, bay: BayBase, content: BayContent) -> None: ...
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '8.1.0'
17
+ VERSION = '9.0.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -379,6 +379,9 @@ class OneIPDeviceSetting(IntFlag):
379
379
  CLOUD = (1 << 30)
380
380
  '''Connects to the cloud service. A device that does not report it
381
381
  connects regardless.'''
382
+ SINK_ROTATION = (1 << 31)
383
+ '''The angle the device's HDMI output is turned by. Reported only by a
384
+ device with an HDMI output.'''
382
385
 
383
386
  ONEIP_DEVICE_SETTING_SWITCHES = OneIPDeviceSetting(0xFF) | OneIPDeviceSetting.AES67 \
384
387
  | OneIPDeviceSetting.SAP_LISTEN | OneIPDeviceSetting.SAP_ANNOUNCE | OneIPDeviceSetting.POWER_SAVE_DISABLED \
@@ -398,14 +401,17 @@ ONEIP_DEVICE_SETTINGS_PTP_MESSAGES = OneIPDeviceSetting.PTP_ANNOUNCE | OneIPDevi
398
401
  """The PTP message settings."""
399
402
 
400
403
  ONEIP_DEVICE_SETTINGS_BLOCK = ONEIP_DEVICE_SETTINGS_PTP_MESSAGES | OneIPDeviceSetting.SAP_EXPIRE \
401
- | OneIPDeviceSetting.SAP_INTERVAL | OneIPDeviceSetting.STREAM_TTL
404
+ | OneIPDeviceSetting.SAP_INTERVAL | OneIPDeviceSetting.STREAM_TTL | OneIPDeviceSetting.SINK_ROTATION
402
405
  """The settings whose values a frame carries in a block of their own behind the
403
- rest, OneIPPtpMessages: the PTP message settings, the two SAP values and the
404
- stream TTL."""
406
+ rest, OneIPPtpMessages: the PTP message settings, the two SAP values, the
407
+ stream TTL and the output's rotation."""
405
408
 
406
409
  ONEIP_STREAM_TTL_DEFAULT = 64
407
410
  """The TTL a OneIP device's streams carry until it is set."""
408
411
 
412
+ ONEIP_SINK_ROTATION_MAX = 359
413
+ """The largest angle a OneIP device's HDMI output is turned by, in degrees."""
414
+
409
415
  class OneIPPtpMode(IntEnum):
410
416
  '''How a OneIP device takes part in PTP.'''
411
417
  OFF = 0
@@ -434,6 +440,31 @@ class OsdSyncWhat(IntFlag):
434
440
  LOGO_SLOTS = (1 << 3)
435
441
  '''The logo images in the four slots.'''
436
442
 
443
+ class OsdMessageFlag(IntFlag):
444
+ '''What a unit says of the message on its output.'''
445
+ SET = (1 << 0)
446
+ '''A message is set. In a write: show it, rather than take it down.'''
447
+ SHOWN = (1 << 1)
448
+ '''The output shows it, rather than another message ahead of it.'''
449
+ SUPPORTED = (1 << 2)
450
+ '''The unit's video processor composes the message layer.'''
451
+ REFUSED = (1 << 3)
452
+ '''The unit did not take the write it answers, and reports the message it
453
+ kept.'''
454
+
455
+ class OsdMessagePriority(IntEnum):
456
+ '''Where a message stands against the unit's own messages: its upgrade,
457
+ Locate Device and encrypted source messages.'''
458
+ LOW = 0
459
+ '''After them.'''
460
+ NORMAL = 1
461
+ '''Between them.'''
462
+ TOP = 2
463
+ '''Before them.'''
464
+
465
+ OSD_MESSAGE_TEXT_MAX = 255
466
+ """The longest message text a unit takes, in bytes."""
467
+
437
468
  class OsdSyncState(IntEnum):
438
469
  '''How far a unit has got with taking an overlay sync.'''
439
470
  TAKING = 0
@@ -109,6 +109,7 @@ class OneIPDeviceSetting(IntFlag):
109
109
  POWER_SAVE_DISABLED: Incomplete
110
110
  STREAM_TTL: Incomplete
111
111
  CLOUD: Incomplete
112
+ SINK_ROTATION: Incomplete
112
113
 
113
114
  ONEIP_DEVICE_SETTING_SWITCHES: Incomplete
114
115
  ONEIP_DEVICE_SETTINGS_REPORTED_ONLY: Incomplete
@@ -116,6 +117,7 @@ ONEIP_DEVICE_SETTINGS_PTP: Incomplete
116
117
  ONEIP_DEVICE_SETTINGS_PTP_MESSAGES: Incomplete
117
118
  ONEIP_DEVICE_SETTINGS_BLOCK: Incomplete
118
119
  ONEIP_STREAM_TTL_DEFAULT: int
120
+ ONEIP_SINK_ROTATION_MAX: int
119
121
 
120
122
  class OneIPPtpMode(IntEnum):
121
123
  OFF: int
@@ -132,6 +134,19 @@ class OsdSyncWhat(IntFlag):
132
134
  BACKGROUNDS: Incomplete
133
135
  LOGO_SLOTS: Incomplete
134
136
 
137
+ class OsdMessageFlag(IntFlag):
138
+ SET: Incomplete
139
+ SHOWN: Incomplete
140
+ SUPPORTED: Incomplete
141
+ REFUSED: Incomplete
142
+
143
+ class OsdMessagePriority(IntEnum):
144
+ LOW: int
145
+ NORMAL: int
146
+ TOP: int
147
+
148
+ OSD_MESSAGE_TEXT_MAX: int
149
+
135
150
  class OsdSyncState(IntEnum):
136
151
  TAKING: int
137
152
  DONE: int
@@ -133,7 +133,9 @@ SETTINGS_BLOCK_OFFSET = _PTP_STATE_OFFSET + _PTP_STATE_SIZE
133
133
  state.'''
134
134
  SETTINGS_BLOCK_SIZE = 8
135
135
  _STREAM_TTL_AT = 8
136
- _SETTINGS_BLOCK_TTL_SIZE = 16
136
+ _SINK_ROTATION_AT = 10
137
+ _SINK_ROTATION_STEP_AT = 12
138
+ _SETTINGS_BLOCK_LONG_SIZE = 16
137
139
 
138
140
  def carries_settings_block(settings:OneIPDeviceSettings) -> bool:
139
141
  '''Whether settings carry a value only the block of their own can hold.'''
@@ -141,14 +143,24 @@ def carries_settings_block(settings:OneIPDeviceSettings) -> bool:
141
143
 
142
144
  def own_settings_block(settings:OneIPDeviceSettings) -> bytes:
143
145
  '''The block the settings in ONEIP_DEVICE_SETTINGS_BLOCK travel in: 8
144
- bytes, or 16 with the stream TTL at 8 when the settings carry it.'''
146
+ bytes, or 16 when the settings carry the stream TTL, at 8, or the output's
147
+ rotation, a u16 at 10. Only a device that has either reads past 8, so a
148
+ frame without them keeps the length every device knows. The rotation's
149
+ step at 12 is the device's to report, and is left 0.'''
145
150
  m = settings.ptp_messages or OneIPPtpMessages()
146
151
  block = struct.pack('<bbbBBBH', m.log_announce_interval, m.log_sync_interval,
147
152
  m.log_delay_req_interval, m.announce_timeout, m.dscp, m.sap_expire_min,
148
153
  m.sap_interval_s)
149
- if (OneIPDeviceSetting.STREAM_TTL in settings.valid):
150
- block += bytes([m.stream_ttl]).ljust(_SETTINGS_BLOCK_TTL_SIZE - len(block), b'\0')
151
- return block
154
+ ttl = (OneIPDeviceSetting.STREAM_TTL in settings.valid)
155
+ rotation = (OneIPDeviceSetting.SINK_ROTATION in settings.valid)
156
+ if not (ttl or rotation):
157
+ return block
158
+ tail = bytearray(_SETTINGS_BLOCK_LONG_SIZE - len(block))
159
+ if ttl:
160
+ tail[_STREAM_TTL_AT - len(block)] = m.stream_ttl
161
+ if rotation:
162
+ struct.pack_into('<H', tail, _SINK_ROTATION_AT - len(block), m.sink_rotation)
163
+ return block + bytes(tail)
152
164
 
153
165
  def parse_own_settings_block(block:bytes, valid:OneIPDeviceSetting) -> tuple[OneIPPtpMessages, OneIPDeviceSetting]:
154
166
  '''The block the settings in ONEIP_DEVICE_SETTINGS_BLOCK travel in, and
@@ -158,10 +170,16 @@ def parse_own_settings_block(block:bytes, valid:OneIPDeviceSetting) -> tuple[One
158
170
  if (len(block) <= _STREAM_TTL_AT):
159
171
  valid &= ~OneIPDeviceSetting.STREAM_TTL
160
172
  ttl = block[_STREAM_TTL_AT] if (len(block) > _STREAM_TTL_AT) else 0
173
+ rotation, step = 0, 0
174
+ if (len(block) >= _SINK_ROTATION_STEP_AT + 2):
175
+ rotation, step = struct.unpack_from('<HH', block, _SINK_ROTATION_AT)
176
+ else:
177
+ valid &= ~OneIPDeviceSetting.SINK_ROTATION
161
178
  announce, sync, delay_req, timeout, dscp, expire, interval = struct.unpack_from('<bbbBBBH', block)
162
179
  return (OneIPPtpMessages(log_announce_interval=announce, log_sync_interval=sync,
163
180
  log_delay_req_interval=delay_req, announce_timeout=timeout, dscp=dscp,
164
- sap_expire_min=expire, sap_interval_s=interval, stream_ttl=ttl), valid)
181
+ sap_expire_min=expire, sap_interval_s=interval, stream_ttl=ttl,
182
+ sink_rotation=rotation, sink_rotation_step=step), valid)
165
183
 
166
184
  def settings_block(settings:OneIPDeviceSettings) -> bytes:
167
185
  '''The 48-byte device settings block.
@@ -520,8 +538,7 @@ class FrameV2IPDeviceConfiguration(FrameBase):
520
538
  frame = parse_settings_block(self.payload[_SETTINGS_OFFSET:], self.payload[SETTINGS_BLOCK_OFFSET:])
521
539
  if self.target_self:
522
540
  return frame
523
- reported = dev.oneip_settings
524
- return frame.as_applied_to(reported.valid if (reported is not None) else OneIPDeviceSetting(0))
541
+ return frame.as_applied_to(dev.oneip_settings)
525
542
 
526
543
  @cached_property
527
544
  def vlan(self) -> OneIPVlan|None:
@@ -17,12 +17,14 @@ sync carries the same seq, which the source numbers on its own.
17
17
  from functools import cached_property
18
18
  import ipaddress
19
19
  import struct
20
+ from typing import Any
20
21
  import zlib
21
22
  from ..compat import override
22
23
  from .Constants import (OSD_SYNC_RECORDS_MAX, OSD_SYNC_SLOTS, DeviceFeature, OsdCaptionAlign,
23
- OsdCaptionFormat, OsdSyncSize, OsdSyncState, OsdSyncWhat)
24
+ OsdCaptionFormat, OsdMessageFlag, OsdMessagePriority, OsdSyncSize, OsdSyncState,
25
+ OsdSyncWhat)
24
26
  from .FrameBase import FrameBase
25
- from ..Interface import (DeviceRegistry, OsdCaption, OsdLogo, OsdSync, OsdSyncEntry, OsdSyncReport,
27
+ from ..Interface import (DeviceRegistry, OsdCaption, OsdLogo, OsdMessage, OsdSync, OsdSyncEntry, OsdSyncReport,
26
28
  OsdSyncRequest, OsdSyncSetting, OsdSyncSlot, OsdSyncValue, _wire_enum)
27
29
  from ..Uid import MxrDeviceUid
28
30
 
@@ -32,13 +34,16 @@ from ..Uid import MxrDeviceUid
32
34
  # sync what at 20, u16 HTTP port at 22, the source's IPv4 address at 24,
33
35
  # u16 records at 28, then four 12-byte logo slots from 32 (u32
34
36
  # bytes, u32 CRC-32, u8 percent): 80 bytes
35
- # request what at 20, the 16-byte missing-settings bitmap at 22, then from
36
- # 54 the sync's source, which units on an older overlay module do
37
- # not send: 38 or 54 bytes
37
+ # request what at 20, the 16-byte missing-settings bitmap at 22, then the
38
+ # sync's source at 38: 54 bytes
38
39
  # setting u16 index at 20, u16 records at 22, kind at 24, size at 25, u16
39
40
  # layers at 26, u16 set at 28, then the body at 30
40
41
  # report state at 20, u16 written at 22, u16 refused at 24, a 64-byte
41
42
  # reason at 26: 90 bytes
43
+ # message flags at 20, logo at 21, priority at 22, a reserved byte, u16 text
44
+ # length at 24, then the text, unterminated, with a seq of 0: a
45
+ # request for the message on the unit's output, a write of one, or
46
+ # the unit's answer to either
42
47
  OSD_SYNC_VERSION = 2
43
48
  '''The payload version every frame opens with; a unit drops any other.'''
44
49
  _HEADER_SIZE = 20
@@ -47,14 +52,18 @@ OSD_KIND_SYNC = 0
47
52
  OSD_KIND_REQUEST = 1
48
53
  OSD_KIND_SETTING = 2
49
54
  OSD_KIND_REPORT = 3
55
+ OSD_KIND_MESSAGE_REQUEST = 4
56
+ OSD_KIND_MESSAGE_SET = 5
57
+ OSD_KIND_MESSAGE_STATE = 6
50
58
 
51
59
  _SYNC_SIZE = 80
52
60
  _SLOTS_AT = 32
53
61
  _SLOT_SIZE = 12
54
- _REQUEST_SIZE = 38
62
+ _REQUEST_SIZE = 54
55
63
  _REQUEST_SOURCE_AT = 38
56
64
  _SETTING_BODY_AT = 30
57
65
  _REPORT_SIZE = 90
66
+ _MESSAGE_SIZE = 26
58
67
 
59
68
  OSD_SETTING_CAPTION = 0
60
69
  OSD_SETTING_LOGO = 1
@@ -114,6 +123,15 @@ class FrameV2IPOsdSync(FrameBase):
114
123
  p += struct.pack('<I', value)
115
124
  return FrameBase.construct_base(mxr=mxr, opcode=0x53, payload=p, target=to)
116
125
 
126
+ @staticmethod
127
+ def construct_message(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid, kind:int,
128
+ flags:OsdMessageFlag=OsdMessageFlag(0), logo:int=0,
129
+ priority:OsdMessagePriority=OsdMessagePriority.LOW, text:bytes=b'') -> FrameBase|None:
130
+ '''A message frame of kind for the output of target_uid.'''
131
+ p = (_header(kind, 0, target_uid) + struct.pack('<BbBxH', int(flags), logo, int(priority), len(text))
132
+ + text)
133
+ return FrameBase.construct_base(target=target, mxr=mxr, opcode=0x53, payload=p)
134
+
117
135
  @cached_property
118
136
  def kind(self) -> int|None:
119
137
  '''The kind, None for a frame a unit drops: shorter than its header, or
@@ -168,15 +186,14 @@ class FrameV2IPOsdSync(FrameBase):
168
186
  def request(self) -> OsdSyncRequest|None:
169
187
  '''The request, None for any other kind or one no unit takes. A unit
170
188
  asks about itself; management may also ask for a new sync for another
171
- unit. A request carries the sync's source from 54 bytes; a 38-byte one,
172
- as units on an older module send, reads its source as empty.'''
189
+ unit.'''
173
190
  p = self.payload or b''
174
191
  if (self.kind != OSD_KIND_REQUEST) or (len(p) < _REQUEST_SIZE):
175
192
  return None
176
193
  target, seq = self.target, self.seq or 0
177
194
  if (target != self.remote_id) and not ((seq == 0) and self.from_management):
178
195
  return None
179
- source = MxrDeviceUid(p[_REQUEST_SOURCE_AT:_REQUEST_SOURCE_AT + 16].ljust(16, b'\0'))
196
+ source = MxrDeviceUid(p[_REQUEST_SOURCE_AT:_REQUEST_SOURCE_AT + 16])
180
197
  return OsdSyncRequest(target=target, seq=seq, what=OsdSyncWhat(p[20]), missing=bytes(p[22:38]),
181
198
  source=source)
182
199
 
@@ -223,29 +240,45 @@ class FrameV2IPOsdSync(FrameBase):
223
240
  return OsdSyncReport(seq=self.seq or 0, state=_wire_enum(OsdSyncState, p[20]), written=written,
224
241
  refused=refused, reason=_cstr(p[26:_REPORT_SIZE]))
225
242
 
243
+ @cached_property
244
+ def message(self) -> OsdMessage|None:
245
+ '''The message a unit reports on its own output, None for any other
246
+ kind, one about another unit, or one short of the text it counts.'''
247
+ p = self.payload or b''
248
+ if (self.kind != OSD_KIND_MESSAGE_STATE) or (len(p) < _MESSAGE_SIZE) or (self.target != self.remote_id):
249
+ return None
250
+ flags, logo, priority, length = struct.unpack_from('<BbBxH', p, 20)
251
+ text = p[_MESSAGE_SIZE:]
252
+ if (len(text) < length):
253
+ return None
254
+ return OsdMessage(flags=OsdMessageFlag(flags), logo=logo, priority=_wire_enum(OsdMessagePriority, priority),
255
+ text=text[:length].decode('utf-8', errors='replace'))
256
+
226
257
  @override
227
258
  def process(self) -> None:
228
259
  '''Record a sync and a report against their sender, and hand a request
229
260
  and a setting to the callbacks. A request this client may answer goes
230
261
  to the registry: one asking for a new sync, or one naming this client
231
- as the source, or, from an older overlay module, naming none.'''
262
+ as the source.'''
232
263
  if ((dev := self.remote_device) is None):
233
264
  return
234
265
  if ((sync := self.sync) is not None):
235
266
  dev.on_mxr_update(sync)
236
267
  elif ((report := self.report) is not None):
237
268
  dev.on_mxr_update(report)
269
+ elif ((message := self.message) is not None):
270
+ dev.on_mxr_update(message)
238
271
  elif ((setting := self.setting) is not None):
239
272
  dev.callbacks.on_osd_sync_setting(dev, setting)
240
273
  elif ((request := self.request) is not None):
241
274
  dev.callbacks.on_osd_sync_requested(dev, request)
242
- if (request.seq == 0) or request.source.empty or (request.source == self.mxr.uid):
275
+ if (request.seq == 0) or (request.source == self.mxr.uid):
243
276
  self.mxr.on_mxr_update(request)
244
277
 
245
278
  def __str__(self) -> str:
246
279
  if (self.kind is None):
247
280
  return "overlay sync <dropped>"
248
- for item in (self.sync, self.request, self.setting, self.report):
281
+ for item in (self.sync, self.request, self.setting, self.report, self.message):
249
282
  if (item is not None):
250
283
  return f"overlay sync {item}"
251
284
  return f"overlay sync kind {self.kind} seq {self.seq} not taken"