mx-remote 5.7.0__tar.gz → 5.9.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 (226) hide show
  1. {mx_remote-5.7.0 → mx_remote-5.9.0}/AGENTS.md +33 -0
  2. {mx_remote-5.7.0 → mx_remote-5.9.0}/PKG-INFO +1 -1
  3. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/Interface.py +78 -20
  4. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/Interface.pyi +12 -1
  5. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/const.py +1 -1
  6. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Constants.py +42 -5
  7. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Constants.pyi +12 -0
  8. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayConfig.py +1 -0
  9. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameLinks.py +5 -1
  10. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCIr.py +56 -20
  11. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCIr.pyi +2 -0
  12. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSignalStatusNew.py +26 -3
  13. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +79 -11
  14. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +4 -2
  15. mx_remote-5.9.0/mx_remote/proto/FrameV2IPSources.py +112 -0
  16. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPSources.pyi +4 -0
  17. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPStats.py +9 -14
  18. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPStats.pyi +1 -1
  19. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/V2IPStats.py +0 -9
  20. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/V2IPStats.pyi +0 -1
  21. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Device.py +138 -10
  22. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Device.pyi +12 -2
  23. mx_remote-5.9.0/tests/complete.py +105 -0
  24. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/merge.py +57 -13
  25. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/run.py +1 -0
  26. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/sources.py +47 -0
  27. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/stats.py +22 -24
  28. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/unknowns.py +5 -6
  29. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/v2ipsubject.py +71 -1
  30. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/wirefix.py +40 -11
  31. mx_remote-5.7.0/mx_remote/proto/FrameV2IPSources.py +0 -54
  32. {mx_remote-5.7.0 → mx_remote-5.9.0}/.gitignore +0 -0
  33. {mx_remote-5.7.0 → mx_remote-5.9.0}/CLAUDE.md +0 -0
  34. {mx_remote-5.7.0 → mx_remote-5.9.0}/LICENSE +0 -0
  35. {mx_remote-5.7.0 → mx_remote-5.9.0}/README.md +0 -0
  36. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/README.md +0 -0
  37. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/_static/brand.css +0 -0
  38. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/_static/favicon.svg +0 -0
  39. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/_static/logo-dark.svg +0 -0
  40. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/_static/logo.svg +0 -0
  41. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/api.md +0 -0
  42. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/audio.md +0 -0
  43. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/callbacks.md +0 -0
  44. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/conf.py +0 -0
  45. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/configuration.md +0 -0
  46. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/devices-and-bays.md +0 -0
  47. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/diagnostics.md +0 -0
  48. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/index.md +0 -0
  49. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/multiviewer.md +0 -0
  50. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/oneip.md +0 -0
  51. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/requirements.txt +0 -0
  52. {mx_remote-5.7.0 → mx_remote-5.9.0}/docs/routing.md +0 -0
  53. {mx_remote-5.7.0 → mx_remote-5.9.0}/hatch_build.py +0 -0
  54. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/Uid.py +0 -0
  55. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/Uid.pyi +0 -0
  56. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/__init__.py +0 -0
  57. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/__init__.pyi +0 -0
  58. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/compat.py +0 -0
  59. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/compat.pyi +0 -0
  60. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/const.pyi +0 -0
  61. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/main.py +0 -0
  62. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/main.pyi +0 -0
  63. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/BayConfig.py +0 -0
  64. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/BayConfig.pyi +0 -0
  65. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Data.py +0 -0
  66. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Data.pyi +0 -0
  67. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Factory.py +0 -0
  68. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Factory.pyi +0 -0
  69. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  70. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  71. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  72. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  73. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  74. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  75. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  76. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  77. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBase.py +0 -0
  78. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBase.pyi +0 -0
  79. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  80. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  81. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  82. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayHide.py +0 -0
  83. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  84. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  85. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  86. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  87. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  88. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameDebug.py +0 -0
  89. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  90. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameDiscover.py +0 -0
  91. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  92. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameEDID.py +0 -0
  93. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  94. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  95. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  96. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  97. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  98. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  99. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  100. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  101. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  102. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameHeader.py +0 -0
  103. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  104. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameHello.py +0 -0
  105. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameHello.pyi +0 -0
  106. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  107. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  108. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  109. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  110. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  111. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  112. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  113. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  114. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  115. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FramePowerChange.py +0 -0
  116. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  117. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCAction.py +0 -0
  118. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  119. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCKey.py +0 -0
  120. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  121. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  122. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  123. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameReboot.py +0 -0
  124. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  125. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  126. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  127. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  128. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  129. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetName.py +0 -0
  130. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  131. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  132. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  133. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  134. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  135. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  136. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  137. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  138. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  139. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  140. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  141. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  142. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  143. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  144. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  145. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  146. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTopology.py +0 -0
  147. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  148. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTxIR.py +0 -0
  149. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  150. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  151. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  152. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  153. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  154. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  155. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  156. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  157. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  158. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  159. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  160. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  161. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  162. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  163. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  164. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  165. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  166. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  167. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  168. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  169. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  170. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  171. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  172. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  173. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  174. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolume.py +0 -0
  175. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  176. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  177. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  178. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  179. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  180. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  181. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  182. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/LinkConfig.py +0 -0
  183. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  184. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Multiviewer.py +0 -0
  185. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  186. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Svd.py +0 -0
  187. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/Svd.pyi +0 -0
  188. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/V2IPConfig.py +0 -0
  189. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  190. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/__init__.py +0 -0
  191. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/__init__.pyi +0 -0
  192. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/proto/svd.csv +0 -0
  193. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/py.typed +0 -0
  194. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Bay.py +0 -0
  195. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Bay.pyi +0 -0
  196. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  197. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  198. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Link.py +0 -0
  199. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Link.pyi +0 -0
  200. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Remote.py +0 -0
  201. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/Remote.pyi +0 -0
  202. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/State.py +0 -0
  203. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/State.pyi +0 -0
  204. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/V2IP.py +0 -0
  205. {mx_remote-5.7.0 → mx_remote-5.9.0}/mx_remote/remote/V2IP.pyi +0 -0
  206. {mx_remote-5.7.0 → mx_remote-5.9.0}/pyproject.toml +0 -0
  207. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/audio_ir.py +0 -0
  208. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/capture.py +0 -0
  209. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/e2e.py +0 -0
  210. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/entry.py +0 -0
  211. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/handlers.py +0 -0
  212. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/handlers_cover.py +0 -0
  213. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/hello.py +0 -0
  214. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/idempotent.py +0 -0
  215. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/lengths.py +0 -0
  216. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/newops.py +0 -0
  217. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/offsets.py +0 -0
  218. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/proto_stamp.py +0 -0
  219. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/protogate.py +0 -0
  220. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/rcsettings.py +0 -0
  221. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/roundtrip.py +0 -0
  222. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/smoke.py +0 -0
  223. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/structs.py +0 -0
  224. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/txresult.py +0 -0
  225. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/v2ipscaling.py +0 -0
  226. {mx_remote-5.7.0 → mx_remote-5.9.0}/tests/vdetails.py +0 -0
@@ -85,6 +85,21 @@ Version is single-sourced in `mx_remote/const.py` (`VERSION = '...'`); `pyprojec
85
85
  reads it via regex, so bump it there only. Release commits follow the convention
86
86
  `bump to X.Y.Z`.
87
87
 
88
+ ### Releases
89
+ `.github/workflows/release.yml` builds and uploads, so a release needs no local
90
+ build and no PyPI credentials:
91
+
92
+ ```sh
93
+ # after the `bump to X.Y.Z` commit is pushed
94
+ git tag -a vX.Y.Z -m 'mx-remote X.Y.Z' && git push origin vX.Y.Z
95
+ gh release create vX.Y.Z --title 'mx-remote X.Y.Z' --verify-tag --notes '...'
96
+ ```
97
+
98
+ Publishing the release runs the suites against the tag, builds the wheel and
99
+ sdist, uploads them to PyPI over Trusted Publishing, and attaches them to the
100
+ release page. Notes stay hand-written: a one-line summary, `##` sections, and a
101
+ closing `**Full Changelog**:` compare link.
102
+
88
103
  ### Build hook
89
104
  `hatch_build.py` generates `.pyi` type stubs via `mypy stubgen` at build time and
90
105
  force-includes them in the wheel (they are gitignored). `mx_remote` ships as typed
@@ -198,6 +213,24 @@ selector. A payload can widen without its floor moving, and then the stamp says
198
213
  nothing about which layout arrived — dispatch on payload length instead.
199
214
  `tests/wirefix.py` covers the cases where the two come apart.
200
215
 
216
+ A row can also go down, which turns a version test into a silent drop. Rows are
217
+ not monotonic: one is lowered to bring a family back under a product's receive
218
+ cap, with no layout changing. A test added while a row was high then rejects
219
+ every frame on that opcode from every sender at once, and what it guards simply
220
+ stops being reported. So prefer the length, which no stamp change can
221
+ invalidate.
222
+
223
+ Two decoders read the stamp, and each says in place what it rests on. The mesh
224
+ operation gate: that struct is fixed size and what changed is what its bytes
225
+ mean, so no length distinguishes the forms. The network status layout selector:
226
+ the forms are the same length, so the stamp chooses between them rather than
227
+ admitting one, and getting it wrong yields a complete, plausible, wrong port
228
+ status rather than a lost frame.
229
+
230
+ A fixture stamped at a version no device sends is a frame that exists nowhere
231
+ else, and every assertion about it says nothing about the network. Stamp what a
232
+ device stamps, which for a report is the opcode's own row.
233
+
201
234
  ## Writing down a deliberate divergence
202
235
 
203
236
  A note recording *what the firmware does* reads as *what this library should
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 5.7.0
3
+ Version: 5.9.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/
@@ -1384,10 +1384,12 @@ class DeviceV2IPScalingSettings(ABC):
1384
1384
  def flags(self) -> int:
1385
1385
  '''mxr_scaling_config.flags.
1386
1386
 
1387
- MXR_SCALING_FLAG_MODE_VALID and MXR_SCALING_FLAG_OPTIONS_VALID mark
1388
- which half a sender meant to send, and are separately valid; a sender
1389
- setting neither is offering no scaling at all. MXR_SCALING_FLAG_AUTO_SCALING
1390
- is the only option bit with a defined meaning.
1387
+ MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS_VALID and
1388
+ MXR_SCALING_FLAG_OPTIONS2_VALID mark which part a sender meant to send,
1389
+ and are separately valid; a sender setting none of them is offering no
1390
+ scaling at all. MXR_SCALING_FLAG_AUTO_SCALING sits behind the first
1391
+ marker, MXR_SCALING_FLAG_MATCH_SOURCE and MXR_SCALING_FLAG_SKIP_420
1392
+ behind the second.
1391
1393
  '''
1392
1394
 
1393
1395
  @property
@@ -1412,6 +1414,37 @@ class DeviceV2IPScalingSettings(ABC):
1412
1414
  return None
1413
1415
  return ((self.flags & MXR_SCALING_FLAG_AUTO_SCALING) != 0)
1414
1416
 
1417
+ @property
1418
+ def match_source(self) -> bool|None:
1419
+ '''Whether the output follows its source's format, None when the device
1420
+ has never said.
1421
+
1422
+ Firmware with this option announces it on every configuration it sends
1423
+ about itself, so a device that has reported it once is known to have it.
1424
+ The cached block accumulates its validity bit, so a later write carrying
1425
+ only the first options group does not take that back.
1426
+
1427
+ Reported only from a sender announcing DeviceBase.config_initialised,
1428
+ which is why this needs no caveat of its own: no firmware has these
1429
+ options without that announcement, and one that lacks it would be
1430
+ reporting uninitialised stack. That is the difference from
1431
+ configured_mode, which is reported from any sender because a mode can be
1432
+ genuine on one of those.
1433
+ '''
1434
+ if ((self.flags & MXR_SCALING_FLAG_OPTIONS2_VALID) == 0):
1435
+ return None
1436
+ return ((self.flags & MXR_SCALING_FLAG_MATCH_SOURCE) != 0)
1437
+
1438
+ @property
1439
+ def skip_420(self) -> bool|None:
1440
+ '''Whether the output declines 4:2:0 rather than scaling it, None when
1441
+ the device has never said. Reported on the same terms as match_source,
1442
+ which shares its validity bit.
1443
+ '''
1444
+ if ((self.flags & MXR_SCALING_FLAG_OPTIONS2_VALID) == 0):
1445
+ return None
1446
+ return ((self.flags & MXR_SCALING_FLAG_SKIP_420) != 0)
1447
+
1415
1448
  def v2ip_stream_valid(stream:'V2IPStreamSource|None') -> bool:
1416
1449
  """
1417
1450
  True when a stream source carries an address a frame actually meant to send.
@@ -1677,33 +1710,39 @@ class DeviceV2IPDetails:
1677
1710
  """
1678
1711
  Merge a scaling config field by field, the way firmware applies it.
1679
1712
 
1680
- The mode/refresh pair and the options nibble are separately valid, so a
1681
- write carrying only one of them must leave the other as it was.
1713
+ The mode/refresh pair and the two options groups are separately valid,
1714
+ so a write carrying one of them must leave the others as they were. Each
1715
+ group is replaced whole behind its own marker, which is what keeps the
1716
+ marker itself - and so the knowledge that the device has those options -
1717
+ from being taken back by a later write that carries another group.
1682
1718
  """
1683
1719
  incoming = self._scaling
1684
1720
  if (incoming is None) or (previous is None):
1685
1721
  return incoming if (incoming is not None) else previous
1686
1722
  mode_valid = ((incoming.flags & MXR_SCALING_FLAG_MODE_VALID) != 0)
1687
1723
  options_valid = ((incoming.flags & MXR_SCALING_FLAG_OPTIONS_VALID) != 0)
1688
- if not mode_valid and not options_valid:
1724
+ options2_valid = ((incoming.flags & MXR_SCALING_FLAG_OPTIONS2_VALID) != 0)
1725
+ if not mode_valid and not options_valid and not options2_valid:
1689
1726
  # carries no scaling at all
1690
1727
  return previous
1691
- if mode_valid and options_valid:
1692
- return incoming
1693
1728
  mode = incoming.mode if mode_valid else previous.mode
1694
1729
  refresh = incoming.refresh if mode_valid else previous.refresh
1695
1730
  flags = previous.flags
1696
1731
  if mode_valid:
1697
1732
  flags |= MXR_SCALING_FLAG_MODE_VALID
1698
- else:
1699
- # Options only. Carry AUTO_SCALING alone rather than the whole upper
1700
- # nibble: bits 4..6 have no defined meaning, and on a receiver-capable
1701
- # unit running firmware before the scaling-config initialisation fix
1702
- # they are stack noise - the sender declares mxr_scaling_config
1703
- # uninitialised and only ever |= flags onto it. Masking to the bit we
1704
- # mean cannot lose information even on fixed firmware, and stops the
1705
- # noise at this boundary instead of caching it as a peer's config.
1706
- flags = (flags & 0x0F) | MXR_SCALING_FLAG_OPTIONS_VALID | (incoming.flags & MXR_SCALING_FLAG_AUTO_SCALING)
1733
+ if options_valid:
1734
+ # Replace AUTO_SCALING alone rather than the whole upper nibble: on a
1735
+ # receiver-capable unit running firmware that leaves mxr_scaling_config
1736
+ # uninitialised, every other bit up there is stack noise the sender
1737
+ # only ever |= its flags onto, and the settings behind the second
1738
+ # marker are a group of their own that this write does not speak for.
1739
+ flags = (flags & ~MXR_SCALING_FLAG_AUTO_SCALING) | MXR_SCALING_FLAG_OPTIONS_VALID \
1740
+ | (incoming.flags & MXR_SCALING_FLAG_AUTO_SCALING)
1741
+ if options2_valid:
1742
+ flags = (flags & ~MXR_SCALING_OPTIONS2_SETTINGS) | MXR_SCALING_FLAG_OPTIONS2_VALID \
1743
+ | (incoming.flags & MXR_SCALING_OPTIONS2_SETTINGS)
1744
+ if (mode == incoming.mode) and (refresh == incoming.refresh) and (flags == incoming.flags):
1745
+ return incoming
1707
1746
  return V2IPScalingSettings(mode=mode, refresh=refresh, flags=flags)
1708
1747
 
1709
1748
  class DeviceV2IPSink:
@@ -2031,6 +2070,17 @@ class DeviceBase(ABC):
2031
2070
  def temperatures(self) -> dict[str,int]:
2032
2071
  ''' temperature sensor reports '''
2033
2072
 
2073
+ @property
2074
+ @abstractmethod
2075
+ def v2ip_features(self) -> 'V2IPFpgaFeature|None':
2076
+ '''What this device's video processor supports, None until it has said.
2077
+
2078
+ A device reports the mask only on a configuration it sends about itself,
2079
+ and reports nothing at all until its processor has answered - which
2080
+ looks the same on the wire as a processor without any of the optional
2081
+ commands, so neither is read as a capability set.
2082
+ '''
2083
+
2034
2084
  @property
2035
2085
  @abstractmethod
2036
2086
  def v2ip_sources(self) -> V2IPStreamSourcesList|None:
@@ -2221,8 +2271,16 @@ class DeviceBase(ABC):
2221
2271
  '''system health status'''
2222
2272
 
2223
2273
  @abstractmethod
2224
- def on_link_config_received(self) -> None:
2225
- '''internal callback'''
2274
+ def merge_v2ip_sources(self, first:int, total:int, page:'V2IPStreamSourcesList') -> None:
2275
+ '''merge one frame of this device's V2IP source list into the cached list'''
2276
+
2277
+ @abstractmethod
2278
+ def note_link_record(self, port:int) -> None:
2279
+ '''internal callback: the bay on this port reported its link record'''
2280
+
2281
+ @abstractmethod
2282
+ def note_bay_config(self) -> None:
2283
+ '''internal callback: the primary bay configuration was received'''
2226
2284
 
2227
2285
  @abstractmethod
2228
2286
  async def get_api(self, uri:str) -> dict[str, Any]|None:
@@ -586,6 +586,10 @@ class DeviceV2IPScalingSettings(ABC):
586
586
  def configured_mode(self) -> tuple[MxrSignalType, int] | None: ...
587
587
  @property
588
588
  def auto_scaling(self) -> bool | None: ...
589
+ @property
590
+ def match_source(self) -> bool | None: ...
591
+ @property
592
+ def skip_420(self) -> bool | None: ...
589
593
 
590
594
  def v2ip_stream_valid(stream: V2IPStreamSource | None) -> bool: ...
591
595
  def v2ip_av_source_valid(video: V2IPStreamSource | None, anc: V2IPStreamSource | None) -> bool: ...
@@ -819,6 +823,9 @@ class DeviceBase(ABC):
819
823
  def temperatures(self) -> dict[str, int]: ...
820
824
  @property
821
825
  @abstractmethod
826
+ def v2ip_features(self) -> V2IPFpgaFeature | None: ...
827
+ @property
828
+ @abstractmethod
822
829
  def v2ip_sources(self) -> V2IPStreamSourcesList | None: ...
823
830
  @property
824
831
  @abstractmethod
@@ -907,7 +914,11 @@ class DeviceBase(ABC):
907
914
  @abstractmethod
908
915
  def status_message(self) -> str: ...
909
916
  @abstractmethod
910
- def on_link_config_received(self) -> None: ...
917
+ def merge_v2ip_sources(self, first: int, total: int, page: V2IPStreamSourcesList) -> None: ...
918
+ @abstractmethod
919
+ def note_link_record(self, port: int) -> None: ...
920
+ @abstractmethod
921
+ def note_bay_config(self) -> None: ...
911
922
  @abstractmethod
912
923
  async def get_api(self, uri: str) -> dict[str, Any] | None: ...
913
924
  @abstractmethod
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '5.7.0'
17
+ VERSION = '5.9.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -140,11 +140,22 @@ def v2ip_rate_valid(rate:int|None) -> bool:
140
140
  '''True when a tx_rate carries an actual rate (firmware v2ip_source_rate_valid()).'''
141
141
  return (rate is not None) and (V2IP_SOURCE_RATE_MIN <= rate <= V2IP_SOURCE_RATE_MAX)
142
142
 
143
- MXR_SCALING_FLAG_MODE_VALID = (1 << 0)
144
- MXR_SCALING_FLAG_OPTIONS_VALID = (1 << 1)
145
- MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
146
- """mxr_scaling_config.flags. The mode/refresh pair and the options nibble are
147
- separately valid: a sender carrying neither flag is offering no scaling at all."""
143
+ MXR_SCALING_FLAG_MODE_VALID = (1 << 0)
144
+ MXR_SCALING_FLAG_OPTIONS_VALID = (1 << 1)
145
+ MXR_SCALING_FLAG_OPTIONS2_VALID = (1 << 4)
146
+ MXR_SCALING_FLAG_MATCH_SOURCE = (1 << 5)
147
+ MXR_SCALING_FLAG_SKIP_420 = (1 << 6)
148
+ MXR_SCALING_FLAG_AUTO_SCALING = (1 << 7)
149
+ """mxr_scaling_config.flags. The mode/refresh pair and each options group are
150
+ separately valid: a sender carrying none of the three markers is offering no
151
+ scaling at all.
152
+
153
+ Firmware that has the second group sets its marker on every configuration it
154
+ sends about itself, so that bit doubles as the report that the device has those
155
+ options at all. Bits 2 and 3 have no meaning."""
156
+
157
+ MXR_SCALING_OPTIONS2_SETTINGS = (MXR_SCALING_FLAG_MATCH_SOURCE | MXR_SCALING_FLAG_SKIP_420)
158
+ """The settings behind MXR_SCALING_FLAG_OPTIONS2_VALID, which carries both."""
148
159
 
149
160
  V2IP_SCALING_REFRESH_MIN = 24
150
161
  V2IP_SCALING_REFRESH_MAX = 120
@@ -221,6 +232,32 @@ class DeviceFeature(IntFlag):
221
232
  CONFIG_INITIALISED = (1 << 25)
222
233
  BOOT_BIT = (1 << 31)
223
234
 
235
+ class V2IPFpgaFeature(IntFlag):
236
+ '''What a V2IP device's video processor supports, as the device reports it in
237
+ its configuration.
238
+
239
+ Read-only, and a device's own: it fills the field in only on the frame
240
+ describing itself, and leaves it zero on one it sends to configure another
241
+ device. There is no write path.
242
+
243
+ Bits are assigned by the video processor and only ever appended, so a bit
244
+ this library has no name for is a later capability rather than an error. A
245
+ device reports no features at all until its processor answers, and an older
246
+ processor answers with none of the optional commands, so an empty mask is
247
+ never a capability set.'''
248
+ SOURCE_DSCP = (1 << 0)
249
+ '''Applies a DSCP marking to the streams it sources.'''
250
+ SINK_AUDIO_FORMAT = (1 << 1)
251
+ '''Reports the audio format arriving at its sink.'''
252
+ SINK_TILING_WINDOW = (1 << 2)
253
+ '''Places a tiling window on its sink.'''
254
+ SINK_OVERLAY_STATE = (1 << 3)
255
+ '''Reports the state of its sink's overlay.'''
256
+ SINK_STATE = (1 << 4)
257
+ '''Reports its sink's state.'''
258
+ SINK_STREAM_INFO = (1 << 5)
259
+ '''Reports information about the stream its sink receives.'''
260
+
224
261
  BAY_FEATURE_DOLBY_IN_POS = 24
225
262
 
226
263
  class BayFeaturesMask(IntFlag):
@@ -17,7 +17,11 @@ def v2ip_rate_valid(rate: int | None) -> bool: ...
17
17
 
18
18
  MXR_SCALING_FLAG_MODE_VALID: Incomplete
19
19
  MXR_SCALING_FLAG_OPTIONS_VALID: Incomplete
20
+ MXR_SCALING_FLAG_OPTIONS2_VALID: Incomplete
21
+ MXR_SCALING_FLAG_MATCH_SOURCE: Incomplete
22
+ MXR_SCALING_FLAG_SKIP_420: Incomplete
20
23
  MXR_SCALING_FLAG_AUTO_SCALING: Incomplete
24
+ MXR_SCALING_OPTIONS2_SETTINGS = MXR_SCALING_FLAG_MATCH_SOURCE | MXR_SCALING_FLAG_SKIP_420
21
25
  V2IP_SCALING_REFRESH_MIN: int
22
26
  V2IP_SCALING_REFRESH_MAX: int
23
27
  MXR_V2IP_DSCP_SET: int
@@ -56,6 +60,14 @@ class DeviceFeature(IntFlag):
56
60
  CONFIG_INITIALISED: Incomplete
57
61
  BOOT_BIT: Incomplete
58
62
 
63
+ class V2IPFpgaFeature(IntFlag):
64
+ SOURCE_DSCP: Incomplete
65
+ SINK_AUDIO_FORMAT: Incomplete
66
+ SINK_TILING_WINDOW: Incomplete
67
+ SINK_OVERLAY_STATE: Incomplete
68
+ SINK_STATE: Incomplete
69
+ SINK_STREAM_INFO: Incomplete
70
+
59
71
  BAY_FEATURE_DOLBY_IN_POS: int
60
72
 
61
73
  class BayFeaturesMask(IntFlag):
@@ -49,6 +49,7 @@ class FrameBayConfig(FrameBase):
49
49
  for bayconfig in self.bays:
50
50
  _LOGGER.debug(f"process {bayconfig}")
51
51
  dev.on_mxr_update(bayconfig)
52
+ dev.note_bay_config()
52
53
 
53
54
  def __str__(self) -> str:
54
55
  return f"{self.remote_device} bay config page: {len(self.bays)} bays"
@@ -44,7 +44,11 @@ class FrameLinks(FrameBase):
44
44
  return
45
45
  for link in self.links:
46
46
  link.process()
47
- dev.on_link_config_received()
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)
48
52
 
49
53
  def __str__(self) -> str:
50
54
  return f"{self.remote_device} links config page: {len(self.links)} links"
@@ -27,15 +27,48 @@ _LOGGER = logging.getLogger(__name__)
27
27
  _META_OFFSET = 12
28
28
  _TIMINGS_OFFSET = 24
29
29
 
30
- # A receiver requires one timing beyond the struct, not just the struct, and it
31
- # requires this protocol version. Both are the firmware's own gates. The length
32
- # one reads like an off-by-one and is not: a capture with no timing is a burst
33
- # nothing blasted, with nothing in it to replay, and it decodes perfectly.
30
+ # A receiver requires one timing beyond the struct, not just the struct, which
31
+ # is the firmware's own gate. It reads like an off-by-one and is not: a capture
32
+ # with no timing is a burst nothing blasted, with nothing in it to replay, and
33
+ # it decodes perfectly.
34
34
  _MIN_SIZE = _TIMINGS_OFFSET + 2
35
- _ACCEPT_PROTOCOL = 0x19
35
+
36
+ # Two timing widths exist on the wire, and the header counts timings rather than
37
+ # bytes, so what follows the header is what says which arrived: the same count
38
+ # takes twice the room in the wider form, and a narrow list is exactly as long
39
+ # as its count, so it can never be mistaken for a wide one.
40
+ _TIMING_WIDE = 2
41
+ _TIMING_NARROW = 1
36
42
 
37
43
  class FrameRCIr(FrameBase):
38
44
  '''IR key press event received by a bay.'''
45
+ @cached_property
46
+ def timing_width(self) -> int|None:
47
+ '''Bytes per timing as this sender wrote them, None when the frame is
48
+ not a capture this library will read.
49
+
50
+ Tested widest first, which is what separates the two forms: a narrow
51
+ list is exactly as long as its count, so a wide one is never read as a
52
+ narrow one carrying twice as many.
53
+
54
+ The count is a declaration, not a measurement: nothing on the wire ties
55
+ it to the number of timings that arrived. A frame short of its own count
56
+ at either width is refused rather than handed to a caller that would
57
+ index the timings by it.
58
+ '''
59
+ pl = self.payload
60
+ if (pl is None) or (len(pl) < _MIN_SIZE):
61
+ return None
62
+ nb = self.payload_u16(idx=_META_OFFSET + 4)
63
+ if (nb is None):
64
+ return None
65
+ tail = len(pl) - _TIMINGS_OFFSET
66
+ if (tail >= (nb * _TIMING_WIDE)):
67
+ return _TIMING_WIDE
68
+ if (tail >= (nb * _TIMING_NARROW)):
69
+ return _TIMING_NARROW
70
+ return None
71
+
39
72
  @cached_property
40
73
  def acceptable(self) -> bool:
41
74
  '''Whether a receiver would act on this frame at all.
@@ -43,16 +76,7 @@ class FrameRCIr(FrameBase):
43
76
  Every field below reads None or empty when this is False, so a frame no
44
77
  device blasted cannot be mistaken for a capture with nothing in it.
45
78
  '''
46
- pl = self.payload
47
- if (pl is None) or (len(pl) < _MIN_SIZE):
48
- return False
49
- # The count is a declaration, not a measurement: nothing on the wire
50
- # ties it to the number of timings that arrived. A frame claiming more
51
- # than it carries is refused rather than handed to a caller that would
52
- # index the timings by it.
53
- nb = self.payload_u16(idx=_META_OFFSET + 4)
54
- return (nb is not None) and (nb <= ((len(pl) - _TIMINGS_OFFSET) // 2)) \
55
- and (self.protocol >= _ACCEPT_PROTOCOL)
79
+ return (self.timing_width is not None)
56
80
 
57
81
  @cached_property
58
82
  def bay(self) -> BayBase|None:
@@ -112,14 +136,19 @@ class FrameRCIr(FrameBase):
112
136
 
113
137
  @cached_property
114
138
  def timings(self) -> bytes:
115
- '''Raw on/off timings appended after the struct.
139
+ '''On/off timings appended after the struct, two bytes each.
140
+
141
+ Always that width, whichever the sender used: the narrower form older
142
+ devices send is widened here, so the values are the device's and only
143
+ the encoding is this library's. nb_timings counts the timings, so this
144
+ is twice that long.
116
145
 
117
146
  A receiver drops the first timing and blasts from the second, so this
118
147
  list is one longer than what is emitted. The first is discarded
119
148
  outright, not treated as a leading gap: the interval before the burst
120
149
  comes from the sender's timestamp instead.
121
150
 
122
- The targeted transmit request (0x48) carries its timings the same way,
151
+ The targeted transmit request (0x48) carries its timings at this width,
123
152
  so a capture replays by passing this across byte for byte, with the
124
153
  declared count unchanged and the discarded first slot included. Its
125
154
  header is a different struct and must be rebuilt, and its timestamp has
@@ -130,11 +159,18 @@ class FrameRCIr(FrameBase):
130
159
  receiver applies its own tick length rather than the resolution
131
160
  reported beside them.
132
161
  '''
133
- if not self.acceptable:
162
+ width = self.timing_width
163
+ nb = self.nb_timings
164
+ if (width is None) or (nb is None):
134
165
  return bytes()
135
- if ((pl := self.payload_idx(_TIMINGS_OFFSET)) is None):
166
+ # Bounded by the timings the header counts, so a field appended behind
167
+ # the list does not arrive as extra timings.
168
+ pl = self.payload_idx(start=_TIMINGS_OFFSET, end=(_TIMINGS_OFFSET + (nb * width)))
169
+ if (pl is None):
136
170
  return bytes()
137
- return pl
171
+ if (width == _TIMING_WIDE):
172
+ return pl
173
+ return bytes([b for timing in pl for b in (timing, 0)])
138
174
 
139
175
  def __str__(self) -> str:
140
176
  return f"IR key press bay {self.bay} at {self.ir_timestamp} freq {self.frequency} timings {self.nb_timings}"
@@ -3,6 +3,8 @@ from .FrameBase import FrameBase as FrameBase
3
3
  from functools import cached_property
4
4
 
5
5
  class FrameRCIr(FrameBase):
6
+ @cached_property
7
+ def timing_width(self) -> int | None: ...
6
8
  @cached_property
7
9
  def acceptable(self) -> bool: ...
8
10
  @cached_property
@@ -163,7 +163,25 @@ class FrameSignalStatusNew(FrameBase):
163
163
  attributed to a bay and is dropped.
164
164
 
165
165
  An empty payload requests a report from every device; a 16-byte payload
166
- requests one from the unit it addresses. '''
166
+ requests one from the unit it addresses.
167
+
168
+ **On a decoder's output bay the signal description is a snapshot of the
169
+ routed source, taken when that stream last started.** Nothing refreshes it
170
+ while the route holds, so a source that changes format in place leaves
171
+ video, audio, infoframe and vsync all reading the format from before the
172
+ change - worse than an unfilled field, because the value stays plausible.
173
+ Re-routing the bay resamples them, unless the newly selected source's record
174
+ has not arrived yet, which leaves the previous values standing rather than
175
+ clearing them.
176
+
177
+ bay_status and scaling are re-stamped on every scaling change as well, so
178
+ they can be newer than the description beside them. clock_rate sits with
179
+ them but is computed from the snapshot and carries its age.
180
+
181
+ **For a decoder's current input format, read the input bay it is routed
182
+ to.** That record is refreshed by the source's own reports, and it is the
183
+ reason a decoder's own output bay can disagree with the picture on the
184
+ cable. '''
167
185
  @cached_property
168
186
  def signal_header_version(self) -> int:
169
187
  if ((pl := self.payload_u16(0)) is not None):
@@ -236,7 +254,8 @@ class FrameSignalStatusNew(FrameBase):
236
254
 
237
255
  @cached_property
238
256
  def scaling(self) -> MxrSignalType|None:
239
- '''Signal type the bay is scaling to, or None on a short report.'''
257
+ '''Format the bay is scaling to, unset while it is not scaling, or None
258
+ on a short report.'''
240
259
  details = self.bay_details
241
260
  if len(details) < 8:
242
261
  return None
@@ -244,7 +263,11 @@ class FrameSignalStatusNew(FrameBase):
244
263
 
245
264
  @cached_property
246
265
  def clock_rate(self) -> int|None:
247
- '''Video clock rate in Hz, or None on a short report.'''
266
+ '''Video clock rate in Hz, or None on a short report.
267
+
268
+ Computed rather than measured: the pixel clock of scaling while the bay
269
+ scales, and otherwise the signal description's own, halved for 4:2:0 and
270
+ scaled by colour depth.'''
248
271
  details = self.bay_details
249
272
  if len(details) < 12:
250
273
  return None