mx-remote 5.9.0__tar.gz → 5.10.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 (229) hide show
  1. {mx_remote-5.9.0 → mx_remote-5.10.0}/PKG-INFO +1 -1
  2. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/api.md +17 -0
  3. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/oneip.md +37 -4
  4. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Interface.py +145 -6
  5. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Interface.pyi +28 -1
  6. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/const.py +9 -1
  7. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/const.pyi +1 -0
  8. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Constants.py +53 -2
  9. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Constants.pyi +17 -0
  10. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameLinks.py +1 -5
  11. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +89 -5
  12. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +6 -2
  13. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Device.py +156 -29
  14. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Device.pyi +10 -3
  15. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Remote.py +13 -5
  16. mx_remote-5.10.0/tests/capture.py +118 -0
  17. mx_remote-5.10.0/tests/complete.py +218 -0
  18. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/handlers_cover.py +18 -0
  19. mx_remote-5.10.0/tests/offline.py +107 -0
  20. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/run.py +2 -0
  21. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/smoke.py +1 -1
  22. mx_remote-5.10.0/tests/v2ipsettings.py +228 -0
  23. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/v2ipsubject.py +30 -0
  24. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/wirefix.py +4 -3
  25. mx_remote-5.9.0/tests/capture.py +0 -50
  26. mx_remote-5.9.0/tests/complete.py +0 -105
  27. {mx_remote-5.9.0 → mx_remote-5.10.0}/.gitignore +0 -0
  28. {mx_remote-5.9.0 → mx_remote-5.10.0}/AGENTS.md +0 -0
  29. {mx_remote-5.9.0 → mx_remote-5.10.0}/CLAUDE.md +0 -0
  30. {mx_remote-5.9.0 → mx_remote-5.10.0}/LICENSE +0 -0
  31. {mx_remote-5.9.0 → mx_remote-5.10.0}/README.md +0 -0
  32. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/README.md +0 -0
  33. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/brand.css +0 -0
  34. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/favicon.svg +0 -0
  35. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/logo-dark.svg +0 -0
  36. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/_static/logo.svg +0 -0
  37. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/audio.md +0 -0
  38. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/callbacks.md +0 -0
  39. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/conf.py +0 -0
  40. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/configuration.md +0 -0
  41. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/devices-and-bays.md +0 -0
  42. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/diagnostics.md +0 -0
  43. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/index.md +0 -0
  44. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/multiviewer.md +0 -0
  45. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/requirements.txt +0 -0
  46. {mx_remote-5.9.0 → mx_remote-5.10.0}/docs/routing.md +0 -0
  47. {mx_remote-5.9.0 → mx_remote-5.10.0}/hatch_build.py +0 -0
  48. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Uid.py +0 -0
  49. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/Uid.pyi +0 -0
  50. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/__init__.py +0 -0
  51. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/__init__.pyi +0 -0
  52. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/compat.py +0 -0
  53. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/compat.pyi +0 -0
  54. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/main.py +0 -0
  55. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/main.pyi +0 -0
  56. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/BayConfig.py +0 -0
  57. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/BayConfig.pyi +0 -0
  58. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Data.py +0 -0
  59. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Data.pyi +0 -0
  60. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Factory.py +0 -0
  61. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Factory.pyi +0 -0
  62. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  63. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  64. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  65. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  66. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  67. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  68. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  69. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  70. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBase.py +0 -0
  71. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBase.pyi +0 -0
  72. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  73. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  74. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  75. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  76. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayHide.py +0 -0
  77. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  78. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  79. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  80. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  81. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  82. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDebug.py +0 -0
  83. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  84. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDiscover.py +0 -0
  85. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  86. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDID.py +0 -0
  87. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  88. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  89. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  90. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  91. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  92. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  93. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  94. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  95. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  96. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHeader.py +0 -0
  97. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  98. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHello.py +0 -0
  99. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameHello.pyi +0 -0
  100. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  101. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  102. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  103. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  104. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  105. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  106. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  107. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  108. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  109. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FramePowerChange.py +0 -0
  110. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  111. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCAction.py +0 -0
  112. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  113. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCIr.py +0 -0
  114. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  115. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCKey.py +0 -0
  116. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  117. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  118. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  119. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameReboot.py +0 -0
  120. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  121. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  122. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  123. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  124. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  125. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetName.py +0 -0
  126. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  127. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  128. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  129. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  130. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  131. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  132. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  133. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  134. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  135. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  136. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  137. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  138. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  139. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  140. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  141. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  142. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  143. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTopology.py +0 -0
  144. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  145. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTxIR.py +0 -0
  146. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  147. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  148. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  149. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  150. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  151. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  152. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  153. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  154. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  155. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  156. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  157. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  158. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  159. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  160. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  161. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  162. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  163. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  164. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  165. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  166. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  167. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  168. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  169. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  170. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  171. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  172. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  173. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  174. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  175. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolume.py +0 -0
  176. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  177. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  178. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  179. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  180. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  181. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  182. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  183. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/LinkConfig.py +0 -0
  184. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  185. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Multiviewer.py +0 -0
  186. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  187. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Svd.py +0 -0
  188. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/Svd.pyi +0 -0
  189. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPConfig.py +0 -0
  190. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  191. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPStats.py +0 -0
  192. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  193. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/__init__.py +0 -0
  194. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/__init__.pyi +0 -0
  195. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/proto/svd.csv +0 -0
  196. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/py.typed +0 -0
  197. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Bay.py +0 -0
  198. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Bay.pyi +0 -0
  199. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  200. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  201. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Link.py +0 -0
  202. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Link.pyi +0 -0
  203. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/Remote.pyi +0 -0
  204. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/State.py +0 -0
  205. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/State.pyi +0 -0
  206. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/V2IP.py +0 -0
  207. {mx_remote-5.9.0 → mx_remote-5.10.0}/mx_remote/remote/V2IP.pyi +0 -0
  208. {mx_remote-5.9.0 → mx_remote-5.10.0}/pyproject.toml +0 -0
  209. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/audio_ir.py +0 -0
  210. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/e2e.py +0 -0
  211. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/entry.py +0 -0
  212. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/handlers.py +0 -0
  213. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/hello.py +0 -0
  214. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/idempotent.py +0 -0
  215. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/lengths.py +0 -0
  216. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/merge.py +0 -0
  217. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/newops.py +0 -0
  218. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/offsets.py +0 -0
  219. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/proto_stamp.py +0 -0
  220. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/protogate.py +0 -0
  221. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/rcsettings.py +0 -0
  222. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/roundtrip.py +0 -0
  223. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/sources.py +0 -0
  224. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/stats.py +0 -0
  225. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/structs.py +0 -0
  226. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/txresult.py +0 -0
  227. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/unknowns.py +0 -0
  228. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/v2ipscaling.py +0 -0
  229. {mx_remote-5.9.0 → mx_remote-5.10.0}/tests/vdetails.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 5.9.0
3
+ Version: 5.10.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/
@@ -57,3 +57,20 @@ The key and action codes a bay reports, and the target a key was aimed at.
57
57
 
58
58
  .. autodata:: mx_remote.proto.Constants.MXR_PROTOCOL_VERSION
59
59
  ```
60
+
61
+ ## V2IP device settings
62
+
63
+ The settings a V2IP device reports and a controller writes, each behind its own
64
+ bit, and the range of the infrared profiles.
65
+
66
+ ```{eval-rst}
67
+ .. autoclass:: mx_remote.proto.Constants.V2IPDeviceSetting
68
+ :members:
69
+ :undoc-members:
70
+
71
+ .. autodata:: mx_remote.proto.Constants.V2IP_DEVICE_SETTING_SWITCHES
72
+
73
+ .. autodata:: mx_remote.proto.Constants.V2IP_IR_PROFILE_NOT_SET
74
+
75
+ .. autodata:: mx_remote.proto.Constants.V2IP_IR_PROFILE_MAX
76
+ ```
@@ -141,16 +141,15 @@ expect `device.v2ip_sink` to read empty for a moment afterwards - see
141
141
 
142
142
  `device.v2ip_sink` addresses that read as unset mean "no route, or the sink
143
143
  could not work one out", never "definitely not subscribed". This is the one part
144
- of a device configuration with no validity marker of its own, so a sender with
145
- nothing to say sends zeros and every receiver stores them. A sender leaves it
144
+ of a device configuration with no validity marker of its own, so a sink with
145
+ nothing to say sends zeros and the library stores them. A sender leaves it
146
146
  empty whenever its own stream configuration does not resolve, which covers more
147
147
  than having no route: a selected source whose record has not arrived yet, the
148
148
  state after a restart at either end, missing audio bay configuration, or a
149
149
  stream failing its validity check.
150
150
 
151
151
  Expect that reading rather than guarding against it. Any scaling change rebuilds
152
- and rebroadcasts the block, and a write aimed at a remote bay sends it zeroed
153
- however it was requested, so an empty reading turns up most often during exactly
152
+ and rebroadcasts the block, so an empty reading turns up most often during exactly
154
153
  the no-signal troubleshooting that prompted the change. A device's periodic
155
154
  report puts a real route back within a minute of it having one, so wait one out
156
155
  rather than treat the first empty reading as an answer.
@@ -159,6 +158,40 @@ The library caches an empty reading rather than dropping it. A sink that has
159
158
  genuinely lost its route sends the same zeros, and so does every report after
160
159
  it, so refusing them would hold a route that nothing later could clear.
161
160
 
161
+ Only the device's own report sets the block. A controller writing another
162
+ device's configuration sends it zeroed, and the library ignores that copy.
163
+
164
+ ## Device settings
165
+
166
+ A V2IP device reports its settings in its configuration: decoder auto-disable,
167
+ HDMI off without signal, IR modulation, the status and network LEDs, quiet fan,
168
+ CEC combo keys, and the infrared profiles. Each sits behind its own bit, so a
169
+ device reports only the settings it has, and a write changes one without
170
+ restating the rest.
171
+
172
+ ```python
173
+ from mx_remote import V2IPDeviceSetting
174
+
175
+ settings = device.v2ip_settings # None until the device has reported
176
+ print(settings.get(V2IPDeviceSetting.FAN_QUIET)) # None: the device lacks it
177
+ print(settings.ir_profile, settings.ir_profile_sink)
178
+
179
+ await device.set_v2ip_setting(V2IPDeviceSetting.STATUS_LED, False)
180
+ await device.set_v2ip_ir_profile(2)
181
+ await device.set_v2ip_sink_ir_profile(mx_remote.V2IP_IR_PROFILE_NOT_SET) # follow the global port
182
+ ```
183
+
184
+ A write returns False, without sending anything, for whatever the device would
185
+ ignore in silence: a setting it has not reported, a profile out of range, or a
186
+ device that has reported no settings at all. So a True is never a change that
187
+ does not happen, though nothing acknowledges it either: the device answers by
188
+ reporting its settings, and until then `v2ip_settings` reads back what was
189
+ written.
190
+
191
+ A write from another controller is cached only as far as the device takes it.
192
+ The list of stored infrared profiles is the device's own and is never taken
193
+ from a write.
194
+
162
195
  ## Mesh and firmware
163
196
 
164
197
  ```python
@@ -1757,8 +1757,8 @@ class DeviceV2IPSink:
1757
1757
 
1758
1758
  **An address that reads as unset means "no route, or the sink could not work
1759
1759
  one out", never "definitely not subscribed".** This block is the one part of
1760
- a device configuration with no validity marker of its own, so a sender with
1761
- nothing to say sends zeros and every receiver stores them. A sender leaves it
1760
+ a device configuration with no validity marker of its own, so a sink with
1761
+ nothing to say sends zeros and they are stored. A sender leaves it
1762
1762
  empty whenever its own stream configuration does not resolve, which covers
1763
1763
  more than having no route: a selected source whose record has not arrived
1764
1764
  yet, the state after a restart at either end, missing audio bay
@@ -1766,8 +1766,7 @@ class DeviceV2IPSink:
1766
1766
  second gate of its own, so it can be absent while the addresses are not.
1767
1767
 
1768
1768
  Expect that reading rather than guarding against it. Any scaling change makes
1769
- the device rebuild and rebroadcast this block, and a write aimed at a remote
1770
- bay sends it zeroed however it was requested, so an empty reading turns up
1769
+ the device rebuild and rebroadcast this block, so an empty reading turns up
1771
1770
  most often during exactly the no-signal troubleshooting that prompted the
1772
1771
  change. A device's periodic report puts a real route back within a minute of
1773
1772
  it having one, so a reader that needs certainty should wait one out rather
@@ -1776,6 +1775,9 @@ class DeviceV2IPSink:
1776
1775
  An empty reading is cached rather than dropped on purpose. A sink that has
1777
1776
  genuinely lost its route sends the same zeros, and so does every report after
1778
1777
  it, so refusing them would hold a route that nothing later could clear.
1778
+
1779
+ Only the device's own report sets this. A controller writing another
1780
+ device's configuration sends the block zeroed, and that frame is ignored.
1779
1781
  """
1780
1782
  def __init__(self, addresses:V2IPStreamSources|None, audio_fmt:V2IPAudioFormat|None) -> None:
1781
1783
  self._addresses = addresses
@@ -1792,6 +1794,122 @@ class DeviceV2IPSink:
1792
1794
  def __str__(self) -> str:
1793
1795
  return f"addresses=[{self._addresses}] audio_fmt={self._audio_fmt}"
1794
1796
 
1797
+ class V2IPDeviceSettings:
1798
+ """
1799
+ The device settings of a V2IP unit, as it reports them and as its controller
1800
+ changes them.
1801
+
1802
+ Each setting is carried only behind its bit in ``valid``, so a frame changes
1803
+ one setting without restating the others, and a device reports only the
1804
+ settings it has. Read a setting through ``get()`` and the profile
1805
+ properties, which answer None for one the device has not reported.
1806
+ """
1807
+ def __init__(self, valid:V2IPDeviceSetting=V2IPDeviceSetting(0),
1808
+ flags:V2IPDeviceSetting=V2IPDeviceSetting(0), ir_profiles:int=0,
1809
+ ir_profile:int=0, ir_profile_sink:int=0) -> None:
1810
+ self._valid = V2IPDeviceSetting(valid)
1811
+ self._flags = V2IPDeviceSetting(flags)
1812
+ self._ir_profiles = ir_profiles
1813
+ self._ir_profile = ir_profile
1814
+ self._ir_profile_sink = ir_profile_sink
1815
+
1816
+ @property
1817
+ def valid(self) -> V2IPDeviceSetting:
1818
+ """The settings reported so far."""
1819
+ return self._valid
1820
+
1821
+ @property
1822
+ def flags(self) -> V2IPDeviceSetting:
1823
+ """The values of the on/off settings among ``valid``."""
1824
+ return self._flags
1825
+
1826
+ def get(self, setting:V2IPDeviceSetting) -> bool|None:
1827
+ """Whether an on/off setting is on, None while the device has not reported it."""
1828
+ if ((self._valid & setting) != setting):
1829
+ return None
1830
+ return ((self._flags & setting) == setting)
1831
+
1832
+ @property
1833
+ def ir_profile(self) -> int|None:
1834
+ """The global infrared port's profile, None while it is not reported."""
1835
+ if (V2IPDeviceSetting.IR_PROFILE not in self._valid):
1836
+ return None
1837
+ return self._ir_profile
1838
+
1839
+ @property
1840
+ def ir_profile_sink(self) -> int|None:
1841
+ """The output infrared port's profile, None while it is not reported.
1842
+ V2IP_IR_PROFILE_NOT_SET means the port follows the global one."""
1843
+ if (V2IPDeviceSetting.IR_PROFILE_SINK not in self._valid):
1844
+ return None
1845
+ return self._ir_profile_sink
1846
+
1847
+ @property
1848
+ def stored_ir_profiles(self) -> int|None:
1849
+ """The infrared profiles stored on the device, bit n for profile n, None
1850
+ while it is not reported."""
1851
+ if (V2IPDeviceSetting.IR_PROFILES not in self._valid):
1852
+ return None
1853
+ return self._ir_profiles
1854
+
1855
+ def merge(self, previous:'V2IPDeviceSettings|None') -> 'V2IPDeviceSettings':
1856
+ """
1857
+ Fold this received block onto the cached one.
1858
+
1859
+ Each bit in this block's ``valid`` replaces its own setting and leaves
1860
+ the others alone, so a write about one setting does not clear what was
1861
+ known about the rest.
1862
+ """
1863
+ if (previous is None):
1864
+ previous = V2IPDeviceSettings()
1865
+ valid = self._valid
1866
+ return V2IPDeviceSettings(
1867
+ valid=(previous._valid | valid),
1868
+ flags=((previous._flags & ~valid) | (self._flags & valid)),
1869
+ ir_profiles=(self._ir_profiles if (V2IPDeviceSetting.IR_PROFILES in valid)
1870
+ else previous._ir_profiles),
1871
+ ir_profile=(self._ir_profile if (V2IPDeviceSetting.IR_PROFILE in valid)
1872
+ else previous._ir_profile),
1873
+ ir_profile_sink=(self._ir_profile_sink if (V2IPDeviceSetting.IR_PROFILE_SINK in valid)
1874
+ else previous._ir_profile_sink))
1875
+
1876
+ def as_applied_to(self, reported:V2IPDeviceSetting) -> 'V2IPDeviceSettings':
1877
+ """
1878
+ This block limited to what a device takes from a write about it.
1879
+
1880
+ A device applies a setting only if it has it, a profile only within its
1881
+ range, and never the list of stored profiles, which only it knows.
1882
+ """
1883
+ valid = (self._valid & reported) & ~V2IPDeviceSetting.IR_PROFILES
1884
+ if not (0 <= self._ir_profile < V2IP_IR_PROFILE_MAX):
1885
+ valid &= ~V2IPDeviceSetting.IR_PROFILE
1886
+ if not (V2IP_IR_PROFILE_NOT_SET <= self._ir_profile_sink < V2IP_IR_PROFILE_MAX):
1887
+ valid &= ~V2IPDeviceSetting.IR_PROFILE_SINK
1888
+ return V2IPDeviceSettings(valid=valid, flags=self._flags, ir_profiles=self._ir_profiles,
1889
+ ir_profile=self._ir_profile, ir_profile_sink=self._ir_profile_sink)
1890
+
1891
+ def __eq__(self, other:Any) -> bool:
1892
+ if not isinstance(other, V2IPDeviceSettings):
1893
+ return NotImplemented
1894
+ return ((self._valid, self._flags, self._ir_profiles, self._ir_profile, self._ir_profile_sink)
1895
+ == (other._valid, other._flags, other._ir_profiles, other._ir_profile, other._ir_profile_sink))
1896
+
1897
+ def __ne__(self, other:Any) -> bool:
1898
+ result = self.__eq__(other)
1899
+ return result if (result is NotImplemented) else (not result)
1900
+
1901
+ def __str__(self) -> str:
1902
+ parts = [f"{setting.name}={'on' if self.get(setting) else 'off'}"
1903
+ for setting in V2IPDeviceSetting
1904
+ if (setting in V2IP_DEVICE_SETTING_SWITCHES) and (self.get(setting) is not None)]
1905
+ if (self.ir_profile is not None):
1906
+ parts.append(f"ir_profile={self.ir_profile}")
1907
+ if (self.ir_profile_sink is not None):
1908
+ parts.append(f"ir_profile_sink={self.ir_profile_sink}")
1909
+ if (self.stored_ir_profiles is not None):
1910
+ parts.append(f"ir_profiles={self.stored_ir_profiles:#x}")
1911
+ return " ".join(parts)
1912
+
1795
1913
  def __repr__(self) -> str:
1796
1914
  return str(self)
1797
1915
 
@@ -2101,6 +2219,15 @@ class DeviceBase(ABC):
2101
2219
  def v2ip_sink(self) -> DeviceV2IPSink|None:
2102
2220
  '''V2IP sink-side multicast subscriptions and resolved audio format (None until first announced)'''
2103
2221
 
2222
+ @property
2223
+ @abstractmethod
2224
+ def v2ip_settings(self) -> V2IPDeviceSettings|None:
2225
+ '''The V2IP device settings, None until the device has reported any.
2226
+
2227
+ A device reports only the settings it has, so one missing from valid
2228
+ once the rest have arrived is one the device does not have.
2229
+ '''
2230
+
2104
2231
  @property
2105
2232
  @abstractmethod
2106
2233
  def v2ip_source_local(self) -> V2IPStreamSources|None:
@@ -2275,8 +2402,8 @@ class DeviceBase(ABC):
2275
2402
  '''merge one frame of this device's V2IP source list into the cached list'''
2276
2403
 
2277
2404
  @abstractmethod
2278
- def note_link_record(self, port:int) -> None:
2279
- '''internal callback: the bay on this port reported its link record'''
2405
+ def note_link_config(self) -> None:
2406
+ '''internal callback: a page of the link configuration was received'''
2280
2407
 
2281
2408
  @abstractmethod
2282
2409
  def note_bay_config(self) -> None:
@@ -2322,6 +2449,18 @@ class DeviceBase(ABC):
2322
2449
  async def clear_v2ip_output_mode(self) -> bool:
2323
2450
  '''clear the output format this sink is configured to scale to'''
2324
2451
 
2452
+ @abstractmethod
2453
+ async def set_v2ip_setting(self, setting:V2IPDeviceSetting, enabled:bool) -> bool:
2454
+ '''switch on/off V2IP device settings, all to the same value'''
2455
+
2456
+ @abstractmethod
2457
+ async def set_v2ip_ir_profile(self, profile:int) -> bool:
2458
+ '''set the infrared profile of the device's global infrared port'''
2459
+
2460
+ @abstractmethod
2461
+ async def set_v2ip_sink_ir_profile(self, profile:int) -> bool:
2462
+ '''set the infrared profile of the device's output infrared port'''
2463
+
2325
2464
  @abstractmethod
2326
2465
  async def get_log(self) -> str|None:
2327
2466
  '''read the log from the device and return it as string'''
@@ -656,6 +656,24 @@ class DeviceV2IPSink:
656
656
  @property
657
657
  def audio_fmt(self) -> V2IPAudioFormat | None: ...
658
658
 
659
+ class V2IPDeviceSettings:
660
+ def __init__(self, valid: V2IPDeviceSetting = ..., flags: V2IPDeviceSetting = ..., ir_profiles: int = 0, ir_profile: int = 0, ir_profile_sink: int = 0) -> None: ...
661
+ @property
662
+ def valid(self) -> V2IPDeviceSetting: ...
663
+ @property
664
+ def flags(self) -> V2IPDeviceSetting: ...
665
+ def get(self, setting: V2IPDeviceSetting) -> bool | None: ...
666
+ @property
667
+ def ir_profile(self) -> int | None: ...
668
+ @property
669
+ def ir_profile_sink(self) -> int | None: ...
670
+ @property
671
+ def stored_ir_profiles(self) -> int | None: ...
672
+ def merge(self, previous: V2IPDeviceSettings | None) -> V2IPDeviceSettings: ...
673
+ def as_applied_to(self, reported: V2IPDeviceSetting) -> V2IPDeviceSettings: ...
674
+ def __eq__(self, other: Any) -> bool: ...
675
+ def __ne__(self, other: Any) -> bool: ...
676
+
659
677
  class UtpLinkErrorStatus(ABC):
660
678
  @property
661
679
  @abstractmethod
@@ -838,6 +856,9 @@ class DeviceBase(ABC):
838
856
  def v2ip_sink(self) -> DeviceV2IPSink | None: ...
839
857
  @property
840
858
  @abstractmethod
859
+ def v2ip_settings(self) -> V2IPDeviceSettings | None: ...
860
+ @property
861
+ @abstractmethod
841
862
  def v2ip_source_local(self) -> V2IPStreamSources | None: ...
842
863
  @property
843
864
  @abstractmethod
@@ -916,7 +937,7 @@ class DeviceBase(ABC):
916
937
  @abstractmethod
917
938
  def merge_v2ip_sources(self, first: int, total: int, page: V2IPStreamSourcesList) -> None: ...
918
939
  @abstractmethod
919
- def note_link_record(self, port: int) -> None: ...
940
+ def note_link_config(self) -> None: ...
920
941
  @abstractmethod
921
942
  def note_bay_config(self) -> None: ...
922
943
  @abstractmethod
@@ -940,6 +961,12 @@ class DeviceBase(ABC):
940
961
  @abstractmethod
941
962
  async def clear_v2ip_output_mode(self) -> bool: ...
942
963
  @abstractmethod
964
+ async def set_v2ip_setting(self, setting: V2IPDeviceSetting, enabled: bool) -> bool: ...
965
+ @abstractmethod
966
+ async def set_v2ip_ir_profile(self, profile: int) -> bool: ...
967
+ @abstractmethod
968
+ async def set_v2ip_sink_ir_profile(self, profile: int) -> bool: ...
969
+ @abstractmethod
943
970
  async def get_log(self) -> str | None: ...
944
971
  @abstractmethod
945
972
  def on_mxr_update(self, data: object) -> None: ...
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '5.9.0'
17
+ VERSION = '5.10.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -37,6 +37,14 @@ re-drawn after every hello that is sent, so devices announcing on the same mesh
37
37
  not converge into synchronised bursts.
38
38
  """
39
39
 
40
+ MXR_CONFIG_TIMEOUT = 15
41
+ """How long a device is given to finish describing itself, in seconds.
42
+
43
+ Past this a device is reported as fully described without its link
44
+ configuration, and keeps being asked for the rest. It never stands in for the
45
+ bay configuration - see ``Device.has_bays``.
46
+ """
47
+
40
48
  V2IP_UDP_PORT_VIDEO = 50020
41
49
  """Default destination UDP port for a V2IP video stream (firmware ``V2IP_UDP_PORT_VIDEO``)."""
42
50
 
@@ -7,6 +7,7 @@ MX_MCAST_UDP_IP: str
7
7
  MX_MCAST_UDP_PORT: int
8
8
  MXR_HELLO_INTERVAL_MIN: float
9
9
  MXR_HELLO_INTERVAL_RAND: float
10
+ MXR_CONFIG_TIMEOUT: int
10
11
  V2IP_UDP_PORT_VIDEO: int
11
12
  V2IP_UDP_PORT_ANC: int
12
13
  V2IP_UDP_PORT_AUDIO: int
@@ -10,7 +10,7 @@
10
10
  from enum import IntEnum, IntFlag
11
11
  from typing import TypeVar
12
12
 
13
- MXR_PROTOCOL_VERSION = 0x29
13
+ MXR_PROTOCOL_VERSION = 0x2A
14
14
  """Highest mx_remote protocol version this library understands.
15
15
 
16
16
  Mirrors the firmware's own protocol version. Bump
@@ -21,7 +21,14 @@ frame.
21
21
  This is also a receive ceiling: Remote.process_frame drops any frame stamped
22
22
  above it, unparsed. So it has to reach the stamp of every layout revision
23
23
  decoded below, or that revision goes silent on the mesh rather than noisy -
24
- 0x29 is what a MatrixOS 10.12.46 unit stamps on V2IP_STATS."""
24
+ 0x29 is what a MatrixOS 10.12.46 unit stamps on V2IP_STATS.
25
+
26
+ 0x2A is also what MatrixOS reads a hello by: a device on 0x2A or later is given
27
+ 60s to hello again rather than 15s, and a unit only slows its own hello to one
28
+ every 20-30s once every device it knows is on 0x2A."""
29
+
30
+ MXR_PROTOCOL_VERSION_SLOW_HELLO = 0x2A
31
+ """First protocol version whose devices may hello only every 20-30s."""
25
32
 
26
33
  MXR_OPCODE_VERSIONS: dict[int, int] = {
27
34
  0x00: 0x01, # SYS_HELLO
@@ -258,6 +265,50 @@ class V2IPFpgaFeature(IntFlag):
258
265
  SINK_STREAM_INFO = (1 << 5)
259
266
  '''Reports information about the stream its sink receives.'''
260
267
 
268
+ class V2IPDeviceSetting(IntFlag):
269
+ '''The device settings a V2IP configuration can carry, each on its own bit.
270
+
271
+ The same bits serve as the settings a frame carries and as the values of the
272
+ on/off ones among them. The last three carry a value elsewhere in the block
273
+ and have no on/off value of their own.
274
+
275
+ A device reports every setting it has, so one it never reports is one it
276
+ does not have.'''
277
+ SINK_CHECK_POWER = (1 << 0)
278
+ '''Disables the decoder while the display is off.'''
279
+ SINK_OFF_NO_SIGNAL = (1 << 1)
280
+ '''Disables the HDMI output while there is no signal.'''
281
+ IR_TX_MODULATED = (1 << 2)
282
+ '''Sends infrared modulated.'''
283
+ STATUS_LED = (1 << 3)
284
+ '''Lights the status LED.'''
285
+ NETWORK_LED = (1 << 4)
286
+ '''Lights the network port LEDs.'''
287
+ FAN_QUIET = (1 << 5)
288
+ '''Runs the fan in quiet mode.'''
289
+ CEC_COMBO_KEYS = (1 << 6)
290
+ '''Accepts CEC combo keys.'''
291
+ CEC_COMBO_INPUT = (1 << 7)
292
+ '''Accepts CEC combo keys for the device's own input.'''
293
+ IR_PROFILE = (1 << 8)
294
+ '''The infrared profile of the device's global infrared port.'''
295
+ IR_PROFILE_SINK = (1 << 9)
296
+ '''The infrared profile of the output's infrared port.'''
297
+ IR_PROFILES = (1 << 10)
298
+ '''The infrared profiles stored on the device. Reported by the device
299
+ itself and never written.'''
300
+
301
+ V2IP_DEVICE_SETTING_SWITCHES = V2IPDeviceSetting(0xFF)
302
+ """The settings that are on or off, as opposed to carrying a value."""
303
+
304
+ V2IP_IR_PROFILE_NOT_SET = -1
305
+ """The infrared profile an output's port reports when it follows the device's
306
+ global one."""
307
+
308
+ V2IP_IR_PROFILE_MAX = 18
309
+ """One past the highest infrared profile: the default profile is 0, the Atlona
310
+ profile 1, and user-defined profiles 1 to 16 are 2 to 17."""
311
+
261
312
  BAY_FEATURE_DOLBY_IN_POS = 24
262
313
 
263
314
  class BayFeaturesMask(IntFlag):
@@ -2,6 +2,7 @@ from _typeshed import Incomplete
2
2
  from enum import IntEnum, IntFlag
3
3
 
4
4
  MXR_PROTOCOL_VERSION: int
5
+ MXR_PROTOCOL_VERSION_SLOW_HELLO: int
5
6
  MXR_OPCODE_VERSIONS: dict[int, int]
6
7
  MXR_CFG_SIGNAL_STATUS_LEN: int
7
8
  MXR_DEVICE_NAME_LEN: int
@@ -68,6 +69,22 @@ class V2IPFpgaFeature(IntFlag):
68
69
  SINK_STATE: Incomplete
69
70
  SINK_STREAM_INFO: Incomplete
70
71
 
72
+ class V2IPDeviceSetting(IntFlag):
73
+ SINK_CHECK_POWER: Incomplete
74
+ SINK_OFF_NO_SIGNAL: Incomplete
75
+ IR_TX_MODULATED: Incomplete
76
+ STATUS_LED: Incomplete
77
+ NETWORK_LED: Incomplete
78
+ FAN_QUIET: Incomplete
79
+ CEC_COMBO_KEYS: Incomplete
80
+ CEC_COMBO_INPUT: Incomplete
81
+ IR_PROFILE: Incomplete
82
+ IR_PROFILE_SINK: Incomplete
83
+ IR_PROFILES: Incomplete
84
+
85
+ V2IP_DEVICE_SETTING_SWITCHES: Incomplete
86
+ V2IP_IR_PROFILE_NOT_SET: int
87
+ V2IP_IR_PROFILE_MAX: int
71
88
  BAY_FEATURE_DOLBY_IN_POS: int
72
89
 
73
90
  class BayFeaturesMask(IntFlag):
@@ -44,11 +44,7 @@ class FrameLinks(FrameBase):
44
44
  return
45
45
  for link in self.links:
46
46
  link.process()
47
- # Only a record that landed on a known bay counts. One naming a bay
48
- # this client has not seen is dropped rather than held, which is
49
- # what a device does with the same frame, and the device re-sends.
50
- if (link.remote_bay is not None):
51
- dev.note_link_record(link.remote_port)
47
+ dev.note_link_config()
52
48
 
53
49
  def __str__(self) -> str:
54
50
  return f"{self.remote_device} links config page: {len(self.links)} links"
@@ -12,8 +12,10 @@ from .FrameBase import FrameBase
12
12
  from .FrameHeader import FrameHeader
13
13
  from ..Uid import MxrDeviceUid
14
14
  from ..Interface import (DeviceBase, DeviceRegistry, DeviceV2IPDetails, DeviceV2IPScalingSettings,
15
- DeviceV2IPSink, V2IPAudioFormat, V2IPDscpConfig, V2IPStreamSource)
16
- from .Constants import (V2IPFpgaFeature, MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID,
15
+ DeviceV2IPSink, V2IPAudioFormat, V2IPDeviceSettings, V2IPDscpConfig,
16
+ V2IPStreamSource)
17
+ from .Constants import (V2IPDeviceSetting, V2IPFpgaFeature, MXR_SCALING_FLAG_AUTO_SCALING,
18
+ MXR_SCALING_FLAG_MODE_VALID,
17
19
  MXR_SCALING_FLAG_OPTIONS_VALID, MXR_SCALING_FLAG_OPTIONS2_VALID,
18
20
  MXR_SCALING_OPTIONS2_SETTINGS, MxrSignalType, v2ip_dscp_value,
19
21
  v2ip_rate_valid)
@@ -30,6 +32,8 @@ from .V2IPConfig import V2IPStreamSourceImpl, parse_v2ip_av_source
30
32
  # 88..112 v2ip_av_source sink, zero when no route is active
31
33
  # 112..120 v2ip_audio_format sink_audio_fmt
32
34
  # 120..128 u64 video processor feature mask
35
+ # 128..144 device settings: u32 valid, u32 flags, u32 stored ir profiles,
36
+ # s8 ir_profile, s8 ir_profile_sink, 2 reserved
33
37
  #
34
38
  # Each of those blocks was appended behind what came before it, leaving every
35
39
  # offset ahead of it where it was, so the length is what says whether one is
@@ -52,7 +56,8 @@ from .V2IPConfig import V2IPStreamSourceImpl, parse_v2ip_av_source
52
56
  # DeviceV2IPDetails.merge() carries the unmarked fields forward. Replacing the
53
57
  # cache wholesale reports a peer's addresses as 0.0.0.0 the moment a controller
54
58
  # writes anything else. The sink trailer is the exception: present or absent by
55
- # length, with no marker.
59
+ # length, with no marker, and taken only from a device describing itself.
60
+ # The settings block marks each setting with its own bit in valid.
56
61
  #
57
62
  # Trust the scaling block only from a peer whose hello carries
58
63
  # MXR_FEATURE_CONFIG_INITIALISED. Without it the sender may have built those
@@ -80,6 +85,9 @@ _SINK_OFFSET = _BASE_SIZE
80
85
  _SINK_AUDIO_OFFSET = _BASE_SIZE + 24
81
86
  _CODEC_OFFSET = _WITH_OPTIONS_SIZE
82
87
  _CODEC_SIZE = 8
88
+ _SETTINGS_OFFSET = _CODEC_OFFSET + _CODEC_SIZE
89
+ _SETTINGS_SIZE = 16
90
+ _WITH_SETTINGS_SIZE = _SETTINGS_OFFSET + _SETTINGS_SIZE
83
91
 
84
92
  class V2IPDeviceOptions:
85
93
  '''Parsed V2IP device options (TX rate and per-stream DSCP marking).'''
@@ -236,6 +244,46 @@ class FrameV2IPDeviceConfiguration(FrameBase):
236
244
  payload += bytes(_BASE_SIZE - len(payload))
237
245
  return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
238
246
 
247
+ @staticmethod
248
+ def construct_settings(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid,
249
+ settings:V2IPDeviceSettings) -> FrameBase|None:
250
+ '''Build the 0x3C write that changes a device's settings and nothing else.
251
+
252
+ 144 bytes: the configuration as construct_scaling() lays it out but with
253
+ no scaling validity bit, then the options trailer - the sink block at
254
+ 88..120, the processor's feature word at 120..128 and the settings at
255
+ 128..144. The settings are the last block, so a frame that carries them
256
+ carries the two before them too.
257
+
258
+ **Both of those go out zeroed.** A device leaves the feature word zero on
259
+ a frame about another device, and a receiver takes the sink block only
260
+ from a device describing itself - except on a receiver that predates the
261
+ settings block, which copies it into its record of the target until the
262
+ target next reports. That is the frame a controlling device sends for
263
+ the same change.
264
+
265
+ Only the settings named in settings.valid are carried; the device keeps
266
+ the rest.
267
+ '''
268
+ payload = bytearray(target_uid.byte_value)
269
+ if (len(payload) != 16):
270
+ raise ValueError(f"invalid uid length: {len(payload)}")
271
+ # source: three stream slots, left zeroed so the encoder keeps its own.
272
+ payload += bytes(40 - len(payload))
273
+ payload.append(_RATE_UNSET)
274
+ # No dscp, no audio return, a scaling block without a validity bit and a
275
+ # tiling window whose zero uid says none is carried; then the zeroed
276
+ # sink block and feature word.
277
+ payload += bytes(_SETTINGS_OFFSET - len(payload))
278
+ payload += int(settings.valid).to_bytes(4, 'little')
279
+ payload += int(settings.flags).to_bytes(4, 'little')
280
+ # A value not named in valid is not read, so it goes out as zero.
281
+ payload += (settings.stored_ir_profiles or 0).to_bytes(4, 'little')
282
+ payload += (settings.ir_profile or 0).to_bytes(1, 'little', signed=True)
283
+ payload += (settings.ir_profile_sink or 0).to_bytes(1, 'little', signed=True)
284
+ payload += bytes(_WITH_SETTINGS_SIZE - len(payload))
285
+ return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
286
+
239
287
  @property
240
288
  def target_uid(self) -> MxrDeviceUid|None:
241
289
  return self.payload_uuid(idx=0)
@@ -312,14 +360,47 @@ class FrameV2IPDeviceConfiguration(FrameBase):
312
360
  @cached_property
313
361
  def sink(self) -> DeviceV2IPSink|None:
314
362
  '''Sink-side state a longer frame appends to the configuration; None when
315
- the frame stops in front of it.'''
363
+ the frame stops in front of it.
364
+
365
+ Read only from a frame a device sent about itself. A controller writing
366
+ another device's configuration sends the block zeroed, because it has
367
+ nothing to say about that sink's subscription, and caching those zeros
368
+ would clear the route on every unrelated write.
369
+ '''
316
370
  if (self.payload is None) or (len(self.payload) < _WITH_OPTIONS_SIZE):
317
371
  return None
372
+ if not self.target_self:
373
+ return None
318
374
  return DeviceV2IPSink(
319
375
  addresses=parse_v2ip_av_source(self.payload, _SINK_OFFSET),
320
376
  audio_fmt=V2IPAudioFormat.from_bytes(self.payload[_SINK_AUDIO_OFFSET:_SINK_AUDIO_OFFSET + 8]),
321
377
  )
322
378
 
379
+ @cached_property
380
+ def settings(self) -> V2IPDeviceSettings|None:
381
+ '''The device settings block, as the subject will hold it; None when the
382
+ frame stops in front of it or nothing would act on it.
383
+
384
+ A device reports every setting it has. A controller sends only the ones
385
+ it changes, and the device applies only those it has, so a write is
386
+ limited here to what the device will take from it and no further.
387
+ '''
388
+ if (self.payload is None) or (len(self.payload) < _WITH_SETTINGS_SIZE):
389
+ return None
390
+ if ((dev := self.subject_device) is None):
391
+ return None
392
+ block = self.payload[_SETTINGS_OFFSET:_WITH_SETTINGS_SIZE]
393
+ frame = V2IPDeviceSettings(
394
+ valid=V2IPDeviceSetting(int.from_bytes(block[0:4], 'little')),
395
+ flags=V2IPDeviceSetting(int.from_bytes(block[4:8], 'little')),
396
+ ir_profiles=int.from_bytes(block[8:12], 'little'),
397
+ ir_profile=int.from_bytes(block[12:13], 'little', signed=True),
398
+ ir_profile_sink=int.from_bytes(block[13:14], 'little', signed=True))
399
+ if self.target_self:
400
+ return frame
401
+ reported = dev.v2ip_settings
402
+ return frame.as_applied_to(reported.valid if (reported is not None) else V2IPDeviceSetting(0))
403
+
323
404
  def process(self) -> None:
324
405
  '''Update the cache of the device this configuration names, not of its sender.'''
325
406
  if ((dev := self.subject_device) is None):
@@ -329,8 +410,11 @@ class FrameV2IPDeviceConfiguration(FrameBase):
329
410
  dev.on_mxr_update(sink)
330
411
  if ((features := self.video_processor_features) is not None):
331
412
  dev.on_mxr_update(features)
413
+ if ((settings := self.settings) is not None):
414
+ dev.on_mxr_update(settings)
332
415
 
333
416
  def __str__(self) -> str:
334
417
  sink_str = f" sink=[{self.sink}]" if (self.sink is not None) else ""
335
418
  fpga_str = f" fpga={self.video_processor_features}" if (self.video_processor_features is not None) else ""
336
- return f"V2IP device configuration self={self.target_self} {self.video} {self.audio} {self.anc} {self.arc} options={self.options}{sink_str}{fpga_str}"
419
+ settings_str = f" settings=[{self.settings}]" if (self.settings is not None) else ""
420
+ return f"V2IP device configuration self={self.target_self} {self.video} {self.audio} {self.anc} {self.arc} options={self.options}{sink_str}{fpga_str}{settings_str}"