remoteRF-server 1.1.3__tar.gz → 1.4.3__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 (180) hide show
  1. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/PKG-INFO +3 -4
  2. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/README.md +1 -2
  3. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/pyproject.toml +2 -2
  4. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/federation.proto +3 -0
  5. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/federation_pb2.py +6 -6
  6. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/grpc_host.proto +1 -0
  7. remoterf_server-1.4.3/src/remoteRF_server/common/grpc/grpc_host_pb2.py +78 -0
  8. {remoterf_server-1.1.3/src/remoteRF_server/drivers/adalm_pluto → remoterf_server-1.4.3/src/remoteRF_server/common/idl}/pluto_schema.py +23 -0
  9. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/__init__.py +9 -1
  10. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/process_arg.py +8 -6
  11. {remoterf_server-1.1.3/src/remoteRF_server/common/idl → remoterf_server-1.4.3/src/remoteRF_server/drivers/adalm_pluto}/pluto_schema.py +23 -0
  12. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/adalm_pluto/poe.py +23 -11
  13. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +12 -3
  14. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp/usrp_schema.py +55 -3
  15. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/host/host_directory_store.py +25 -0
  16. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/host/host_tunnel_server.py +55 -2
  17. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/acc_perms.py +5 -1
  18. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/commands.py +10 -9
  19. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/device_manager.py +196 -79
  20. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/direct_path.py +23 -51
  21. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/dynamic_v2.py +9 -2
  22. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/cli.py +26 -51
  23. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/cluster.py +149 -109
  24. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/destination.py +142 -66
  25. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/devices.py +87 -38
  26. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/enrollment.py +47 -16
  27. remoterf_server-1.4.3/src/remoteRF_server/server/federation/global_home.py +81 -0
  28. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/home.py +184 -48
  29. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/home_devices.py +27 -18
  30. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/migrations.py +3 -0
  31. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/global_onboarding.py +14 -1
  32. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/grpc_server.py +42 -29
  33. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/reservation.py +139 -35
  34. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/rpc_manager.py +104 -0
  35. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/user_group_cli.py +109 -15
  36. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/user_group_handler.py +88 -23
  37. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/serverrf_cli.py +97 -18
  38. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server.egg-info/PKG-INFO +3 -4
  39. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server.egg-info/SOURCES.txt +13 -0
  40. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server.egg-info/requires.txt +1 -1
  41. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server.egg-info/top_level.txt +0 -1
  42. remoterf_server-1.4.3/tests/test_account_names.py +59 -0
  43. remoterf_server-1.4.3/tests/test_account_removal.py +52 -0
  44. remoterf_server-1.4.3/tests/test_activity_log.py +26 -0
  45. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_cluster.py +70 -227
  46. remoterf_server-1.4.3/tests/test_contrib_drivers.py +238 -0
  47. remoterf_server-1.4.3/tests/test_detached_device_rpc.py +447 -0
  48. remoterf_server-1.4.3/tests/test_device_handles.py +460 -0
  49. remoterf_server-1.4.3/tests/test_device_init.py +123 -0
  50. remoterf_server-1.4.3/tests/test_devices_list.py +22 -0
  51. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_direct_fixed_port.py +25 -3
  52. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_direct_path_server.py +78 -49
  53. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_dynamic_v2.py +80 -0
  54. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_federated_direct_path.py +72 -3
  55. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_e_federation.py +32 -0
  56. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_e_proto.py +4 -1
  57. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_f_enrollment.py +40 -5
  58. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_h_devices.py +96 -0
  59. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_i_direct_path.py +7 -3
  60. remoterf_server-1.4.3/tests/test_global_home.py +144 -0
  61. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_pluto_compat.py +62 -0
  62. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_pluto_poe.py +48 -0
  63. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_process_arg.py +11 -0
  64. remoterf_server-1.4.3/tests/test_reservation_handoff.py +127 -0
  65. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_reservation_policy_b.py +10 -0
  66. remoterf_server-1.4.3/tests/test_rpc_manager_batch.py +156 -0
  67. remoterf_server-1.4.3/tests/test_rpc_manager_rx_n.py +93 -0
  68. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_rtl_sdr_schema.py +47 -2
  69. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_user_group_cli.py +56 -1
  70. remoterf_server-1.4.3/tests/test_usrp_preload.py +84 -0
  71. remoterf_server-1.1.3/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -78
  72. remoterf_server-1.1.3/tests/test_reservation_handoff.py +0 -70
  73. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/LICENSE +0 -0
  74. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/setup.cfg +0 -0
  75. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/__init__.py +0 -0
  76. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/__init__.py +0 -0
  77. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  78. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  79. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  80. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  81. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  82. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
  83. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  84. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  85. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  86. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  87. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/idl/__init__.py +0 -0
  88. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  89. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  90. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/idl/schema.py +0 -0
  91. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/idl/v2.py +0 -0
  92. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  93. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/api_token.py +0 -0
  94. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  95. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/db_location.py +0 -0
  96. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/list_string.py +0 -0
  97. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/common/utils/panel.py +0 -0
  98. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/__init__.py +0 -0
  99. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  100. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  101. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  102. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  103. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  104. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  105. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  106. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  107. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  108. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  109. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  110. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  111. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  112. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  113. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/host/__init__.py +0 -0
  114. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/host/host_auth_token.py +0 -0
  115. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/__init__.py +0 -0
  116. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/cert_provider.py +0 -0
  117. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/console.py +0 -0
  118. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/control.py +0 -0
  119. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  120. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  121. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  122. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/__init__.py +0 -0
  123. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/admin.py +0 -0
  124. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/contracts.py +0 -0
  125. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/identity.py +0 -0
  126. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  127. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/peers.py +0 -0
  128. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/permissions.py +0 -0
  129. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/principals.py +0 -0
  130. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/runtime.py +0 -0
  131. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/federation/trust.py +0 -0
  132. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  133. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/server/v2_errors.py +0 -0
  134. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/tools/__init__.py +0 -0
  135. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/tools/gen_certs.py +0 -0
  136. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/tools/gist_status.py +0 -0
  137. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  138. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  139. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  140. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  141. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_account_hardening.py +0 -0
  142. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_bind_host.py +0 -0
  143. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_cert_provider.py +0 -0
  144. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_client_sessions.py +0 -0
  145. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_commands.py +0 -0
  146. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_console.py +0 -0
  147. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_default_rtl_sdr_schema.py +0 -0
  148. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_default_usrp_schema.py +0 -0
  149. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_deployment_capabilities.py +0 -0
  150. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_federated_dynamic_v2.py +0 -0
  151. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_federation_edges.py +0 -0
  152. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_first_run.py +0 -0
  153. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_d_contracts.py +0 -0
  154. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_g1_logging.py +0 -0
  155. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gate_g_permissions.py +0 -0
  156. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_gen_certs.py +0 -0
  157. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_global_accounts_migration.py +0 -0
  158. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_global_onboarding.py +0 -0
  159. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_global_v1_disposal.py +0 -0
  160. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_hackrf_schema.py +0 -0
  161. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_home_device_plane.py +0 -0
  162. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_host_directory_store.py +0 -0
  163. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_host_dynamic_v2.py +0 -0
  164. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_idl_schema.py +0 -0
  165. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_panel.py +0 -0
  166. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_peer_transport.py +0 -0
  167. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_reservation_timestamps.py +0 -0
  168. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_rpc_manager_calln.py +0 -0
  169. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_soak_federation.py +0 -0
  170. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_status_push.py +0 -0
  171. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_ti_mmwave_hardware.py +0 -0
  172. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_ti_mmwave_schema.py +0 -0
  173. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  174. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_client_server_hardware.py +0 -0
  175. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_client_server_localhost.py +0 -0
  176. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  177. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_profiles.py +0 -0
  178. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  179. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_usrp_uhd410_hardware.py +0 -0
  180. {remoterf_server-1.1.3 → remoterf_server-1.4.3}/tests/test_wipe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.1.3
3
+ Version: 1.4.3
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -9,7 +9,7 @@ Classifier: Operating System :: OS Independent
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
- Requires-Dist: remoterf-federation-core==0.16.0
12
+ Requires-Dist: remoterf-federation-core==0.21.0
13
13
  Requires-Dist: grpcio<2.0.0,>=1.81.1
14
14
  Requires-Dist: protobuf<7.0.0,>=6.33.5
15
15
  Requires-Dist: numpy
@@ -890,8 +890,7 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
890
890
 
891
891
  ### Devices
892
892
 
893
- * `devices list` — List all devices
894
- * `devices status` — Show live device routing/online/last_seen from the Host Tunnel
893
+ * `devices list` — List all devices, with the host each is on (`local` for this server's own) and when it was last seen
895
894
 
896
895
  ---
897
896
 
@@ -859,8 +859,7 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
859
859
 
860
860
  ### Devices
861
861
 
862
- * `devices list` — List all devices
863
- * `devices status` — Show live device routing/online/last_seen from the Host Tunnel
862
+ * `devices list` — List all devices, with the host each is on (`local` for this server's own) and when it was last seen
864
863
 
865
864
  ---
866
865
 
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.1.3"
7
+ version = "1.4.3"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "remoterf-federation-core==0.16.0",
12
+ "remoterf-federation-core==0.21.0",
13
13
  "grpcio>=1.81.1,<2.0.0",
14
14
  "protobuf>=6.33.5,<7.0.0",
15
15
  "numpy",
@@ -11,10 +11,13 @@ message FederationRequest {
11
11
  string protocol_version = 1;
12
12
  string grant = 2;
13
13
  string request_json = 3;
14
+ bytes frame = 4; // raw GenericRPCRequest when request_json is detached; else empty
15
+ bool accept_detached = 5; // requester can take a detached result + frame
14
16
  }
15
17
 
16
18
  message FederationResponse {
17
19
  string protocol_version = 1;
18
20
  string signed_result = 2;
19
21
  ErrorEnvelope error = 3;
22
+ bytes frame = 4; // raw GenericRPCResponse when signed_result is detached; else empty
20
23
  }
@@ -25,7 +25,7 @@ _sym_db = _symbol_database.Default()
25
25
  from . import grpc_pb2 as grpc__pb2
26
26
 
27
27
 
28
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x66\x65\x64\x65ration.proto\x12\tremote_rf\x1a\ngrpc.proto\"R\n\x11\x46\x65\x64\x65rationRequest\x12\x18\n\x10protocol_version\x18\x01 \x01(\t\x12\r\n\x05grant\x18\x02 \x01(\t\x12\x14\n\x0crequest_json\x18\x03 \x01(\t\"n\n\x12\x46\x65\x64\x65rationResponse\x12\x18\n\x10protocol_version\x18\x01 \x01(\t\x12\x15\n\rsigned_result\x18\x02 \x01(\t\x12\'\n\x05\x65rror\x18\x03 \x01(\x0b\x32\x18.remote_rf.ErrorEnvelope2V\n\x0c\x46\x65\x64\x65rationV2\x12\x46\n\x07\x45xecute\x12\x1c.remote_rf.FederationRequest\x1a\x1d.remote_rf.FederationResponseb\x06proto3')
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x66\x65\x64\x65ration.proto\x12\tremote_rf\x1a\ngrpc.proto\"z\n\x11\x46\x65\x64\x65rationRequest\x12\x18\n\x10protocol_version\x18\x01 \x01(\t\x12\r\n\x05grant\x18\x02 \x01(\t\x12\x14\n\x0crequest_json\x18\x03 \x01(\t\x12\r\n\x05\x66rame\x18\x04 \x01(\x0c\x12\x17\n\x0f\x61\x63\x63\x65pt_detached\x18\x05 \x01(\x08\"}\n\x12\x46\x65\x64\x65rationResponse\x12\x18\n\x10protocol_version\x18\x01 \x01(\t\x12\x15\n\rsigned_result\x18\x02 \x01(\t\x12\'\n\x05\x65rror\x18\x03 \x01(\x0b\x32\x18.remote_rf.ErrorEnvelope\x12\r\n\x05\x66rame\x18\x04 \x01(\x0c\x32V\n\x0c\x46\x65\x64\x65rationV2\x12\x46\n\x07\x45xecute\x12\x1c.remote_rf.FederationRequest\x1a\x1d.remote_rf.FederationResponseb\x06proto3')
29
29
 
30
30
  _globals = globals()
31
31
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -33,9 +33,9 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'federation_pb2', _globals)
33
33
  if not _descriptor._USE_C_DESCRIPTORS:
34
34
  DESCRIPTOR._loaded_options = None
35
35
  _globals['_FEDERATIONREQUEST']._serialized_start=43
36
- _globals['_FEDERATIONREQUEST']._serialized_end=125
37
- _globals['_FEDERATIONRESPONSE']._serialized_start=127
38
- _globals['_FEDERATIONRESPONSE']._serialized_end=237
39
- _globals['_FEDERATIONV2']._serialized_start=239
40
- _globals['_FEDERATIONV2']._serialized_end=325
36
+ _globals['_FEDERATIONREQUEST']._serialized_end=165
37
+ _globals['_FEDERATIONRESPONSE']._serialized_start=167
38
+ _globals['_FEDERATIONRESPONSE']._serialized_end=292
39
+ _globals['_FEDERATIONV2']._serialized_start=294
40
+ _globals['_FEDERATIONV2']._serialized_end=380
41
41
  # @@protoc_insertion_point(module_scope)
@@ -31,6 +31,7 @@ message DeviceInfo {
31
31
  string device_id = 3;
32
32
  string serial = 4;
33
33
  string kind = 5;
34
+ string handle = 6;
34
35
  }
35
36
 
36
37
  message DeviceAnnounce {
@@ -0,0 +1,78 @@
1
+ # Copyright (C) 2026 RemoteRF
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ # -*- coding: utf-8 -*-
17
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
18
+ # NO CHECKED-IN PROTOBUF GENCODE
19
+ # source: grpc_host.proto
20
+ # Protobuf Python Version: 6.31.1
21
+ """Generated protocol buffer code."""
22
+ from google.protobuf import descriptor as _descriptor
23
+ from google.protobuf import descriptor_pool as _descriptor_pool
24
+ from google.protobuf import runtime_version as _runtime_version
25
+ from google.protobuf import symbol_database as _symbol_database
26
+ from google.protobuf.internal import builder as _builder
27
+ _runtime_version.ValidateProtobufRuntimeVersion(
28
+ _runtime_version.Domain.PUBLIC,
29
+ 6,
30
+ 31,
31
+ 1,
32
+ '',
33
+ 'grpc_host.proto'
34
+ )
35
+ # @@protoc_insertion_point(imports)
36
+
37
+ _sym_db = _symbol_database.Default()
38
+
39
+
40
+ from . import grpc_pb2 as grpc__pb2
41
+
42
+
43
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fgrpc_host.proto\x12\x08remoterf\x1a\ngrpc.proto\"A\n\tHostHello\x12\x0f\n\x07host_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x12\n\nhost_token\x18\x03 \x01(\t\"n\n\nDeviceInfo\x12\x10\n\x08local_id\x18\x01 \x01(\r\x12\r\n\x05label\x18\x02 \x01(\t\x12\x11\n\tdevice_id\x18\x03 \x01(\t\x12\x0e\n\x06serial\x18\x04 \x01(\t\x12\x0c\n\x04kind\x18\x05 \x01(\t\x12\x0e\n\x06handle\x18\x06 \x01(\t\"_\n\x0e\x44\x65viceAnnounce\x12%\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\x14.remoterf.DeviceInfo\x12\x0f\n\x07unix_ms\x18\x02 \x01(\x04\x12\x15\n\rfull_snapshot\x18\x03 \x01(\x08\"\xab\x01\n\nRpcRequest\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x18\n\x10global_device_id\x18\x02 \x01(\r\x12\x17\n\x0flocal_device_id\x18\x03 \x01(\r\x12\x11\n\tdevice_id\x18\x04 \x01(\t\x12-\n\x07request\x18\x05 \x01(\x0b\x32\x1c.remote_rf.GenericRPCRequest\x12\x18\n\x10\x64\x65\x61\x64line_unix_ms\x18\x06 \x01(\x04\"i\n\x0bRpcResponse\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12/\n\x08response\x18\x03 \x01(\x0b\x32\x1d.remote_rf.GenericRPCResponse\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x1c\n\tHeartbeat\x12\x0f\n\x07unix_ms\x18\x01 \x01(\x04\"\x18\n\x06\x43\x61ncel\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"e\n\x0bMetaRequest\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x18\n\x10include_platform\x18\x02 \x01(\x08\x12\x13\n\x0binclude_env\x18\x03 \x01(\x08\x12\x17\n\x0finclude_devices\x18\x04 \x01(\x08\"\xb7\x01\n\x08HostMeta\x12\x0f\n\x07host_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0f\n\x07unix_ms\x18\x03 \x01(\x04\x12&\n\x02kv\x18\x04 \x03(\x0b\x32\x1a.remoterf.HostMeta.KvEntry\x12%\n\x07\x64\x65vices\x18\x05 \x03(\x0b\x32\x14.remoterf.DeviceInfo\x1a)\n\x07KvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"[\n\x0cMetaResponse\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12 \n\x04meta\x18\x04 \x01(\x0b\x32\x12.remoterf.HostMeta\"\xf7\x02\n\tHostFrame\x12$\n\x05hello\x18\x01 \x01(\x0b\x32\x13.remoterf.HostHelloH\x00\x12\x33\n\x0f\x64\x65vice_announce\x18\x02 \x01(\x0b\x32\x18.remoterf.DeviceAnnounceH\x00\x12+\n\x0brpc_request\x18\x03 \x01(\x0b\x32\x14.remoterf.RpcRequestH\x00\x12-\n\x0crpc_response\x18\x04 \x01(\x0b\x32\x15.remoterf.RpcResponseH\x00\x12(\n\theartbeat\x18\x05 \x01(\x0b\x32\x13.remoterf.HeartbeatH\x00\x12\"\n\x06\x63\x61ncel\x18\x06 \x01(\x0b\x32\x10.remoterf.CancelH\x00\x12-\n\x0cmeta_request\x18\x07 \x01(\x0b\x32\x15.remoterf.MetaRequestH\x00\x12/\n\rmeta_response\x18\x08 \x01(\x0b\x32\x16.remoterf.MetaResponseH\x00\x42\x05\n\x03msg2E\n\nHostTunnel\x12\x37\n\x07\x43onnect\x12\x13.remoterf.HostFrame\x1a\x13.remoterf.HostFrame(\x01\x30\x01\x62\x06proto3')
44
+
45
+ _globals = globals()
46
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
47
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_host_pb2', _globals)
48
+ if not _descriptor._USE_C_DESCRIPTORS:
49
+ DESCRIPTOR._loaded_options = None
50
+ _globals['_HOSTMETA_KVENTRY']._loaded_options = None
51
+ _globals['_HOSTMETA_KVENTRY']._serialized_options = b'8\001'
52
+ _globals['_HOSTHELLO']._serialized_start=41
53
+ _globals['_HOSTHELLO']._serialized_end=106
54
+ _globals['_DEVICEINFO']._serialized_start=108
55
+ _globals['_DEVICEINFO']._serialized_end=218
56
+ _globals['_DEVICEANNOUNCE']._serialized_start=220
57
+ _globals['_DEVICEANNOUNCE']._serialized_end=315
58
+ _globals['_RPCREQUEST']._serialized_start=318
59
+ _globals['_RPCREQUEST']._serialized_end=489
60
+ _globals['_RPCRESPONSE']._serialized_start=491
61
+ _globals['_RPCRESPONSE']._serialized_end=596
62
+ _globals['_HEARTBEAT']._serialized_start=598
63
+ _globals['_HEARTBEAT']._serialized_end=626
64
+ _globals['_CANCEL']._serialized_start=628
65
+ _globals['_CANCEL']._serialized_end=652
66
+ _globals['_METAREQUEST']._serialized_start=654
67
+ _globals['_METAREQUEST']._serialized_end=755
68
+ _globals['_HOSTMETA']._serialized_start=758
69
+ _globals['_HOSTMETA']._serialized_end=941
70
+ _globals['_HOSTMETA_KVENTRY']._serialized_start=900
71
+ _globals['_HOSTMETA_KVENTRY']._serialized_end=941
72
+ _globals['_METARESPONSE']._serialized_start=943
73
+ _globals['_METARESPONSE']._serialized_end=1034
74
+ _globals['_HOSTFRAME']._serialized_start=1037
75
+ _globals['_HOSTFRAME']._serialized_end=1412
76
+ _globals['_HOSTTUNNEL']._serialized_start=1414
77
+ _globals['_HOSTTUNNEL']._serialized_end=1483
78
+ # @@protoc_insertion_point(module_scope)
@@ -89,6 +89,7 @@ class PlutoSchema(DeviceSchema):
89
89
  device_type = "adalm_pluto"
90
90
  client_class = "Pluto"
91
91
  driver_version = "0.0.3"
92
+ _fir_rate = None # the rate whose pyadi FIR this schema last loaded
92
93
 
93
94
  @staticmethod
94
95
  def make_device(**kwargs):
@@ -106,6 +107,7 @@ class PlutoSchema(DeviceSchema):
106
107
 
107
108
  @idl_expose(kind="set")
108
109
  def set_filter(self, value):
110
+ self._fir_rate = None
109
111
  self.device.filter = value
110
112
 
111
113
  @idl_expose(kind="get")
@@ -164,7 +166,20 @@ class PlutoSchema(DeviceSchema):
164
166
 
165
167
  @idl_expose(kind="set")
166
168
  def set_sample_rate(self, value):
169
+ # pyadi reloads its FIR and re-runs the AD9361 rate chain (~0.8 s) even
170
+ # for an unchanged rate: skip only what our own last set left in place.
171
+ if value == self._fir_rate:
172
+ try:
173
+ if (
174
+ self.device.sample_rate == value
175
+ and self.device._get_iio_attr("out", "voltage_filter_fir_en", False) == 1
176
+ ):
177
+ return
178
+ except Exception:
179
+ pass
180
+ self._fir_rate = None
167
181
  self.device.sample_rate = value
182
+ self._fir_rate = value
168
183
 
169
184
  @idl_expose(kind="get")
170
185
  def get_rx_lo(self):
@@ -222,12 +237,18 @@ class PlutoSchema(DeviceSchema):
222
237
  def reset_for_next_holder(self):
223
238
  """A reservation ended: a PoE Pluto powers off. Otherwise stop its cyclic
224
239
  transmission, and drop its RX buffer so the next holder's rx_buffer_size takes effect."""
240
+ self._fir_rate = None
225
241
  if isinstance(self.device, PoePluto):
226
242
  self.device.power_down()
227
243
  elif self.device is not None:
228
244
  self.device.tx_destroy_buffer()
229
245
  self.device.rx_destroy_buffer()
230
246
 
247
+ def prepare_for_holder(self):
248
+ """A reservation started: a PoE Pluto boots now, not at the holder's first command."""
249
+ if isinstance(self.device, PoePluto):
250
+ self.device.power_up()
251
+
231
252
  @idl_expose(kind="call")
232
253
  def call_disable_dds(self):
233
254
  """Disable the internal DDS tone generator."""
@@ -244,12 +265,14 @@ class PlutoSchema(DeviceSchema):
244
265
  @idl_expose(kind="call")
245
266
  def call_power_down(self):
246
267
  """PoE Plutos: cut power; the next command powers it back up. No-op on other Plutos."""
268
+ self._fir_rate = None
247
269
  if isinstance(self.device, PoePluto):
248
270
  self.device.power_down()
249
271
 
250
272
  @idl_expose(kind="call")
251
273
  def call_power_cycle(self):
252
274
  """PoE Plutos: power off, then on, and wait for boot. No-op on other Plutos."""
275
+ self._fir_rate = None
253
276
  if isinstance(self.device, PoePluto):
254
277
  self.device.power_cycle()
255
278
 
@@ -13,9 +13,17 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
15
 
16
+ import re
17
+
16
18
  from .api_token import validate_token, generate_token, hash_token
17
19
  from .process_arg import unmap_arg, map_arg
18
20
  from .ansi_codes import printf, stylize, Sty
19
21
  from .list_string import list_to_str, str_to_list
20
22
  from .db_connection import db_connection
21
- from .db_location import get_db_dir, get_certs_dir, get_drivers_dir, get_remoterf_root
23
+ from .db_location import get_db_dir, get_certs_dir, get_drivers_dir, get_remoterf_root
24
+
25
+
26
+ def device_name_key(name: str) -> str:
27
+ """A device name as typed and matched: whitespace and '_' are interchangeable,
28
+ runs collapse and the ends are ignored ('pluto A' == 'pluto_A'). Case is kept."""
29
+ return re.sub(r"[\s_]+", "_", name).strip("_")
@@ -98,7 +98,7 @@ def _json_safe(value: Any):
98
98
  return _json_safe(value.as_payload())
99
99
  if isinstance(value, np.ndarray):
100
100
  if value.dtype == object:
101
- raise ValueError(f"Cannot JSON-map object-dtype array: {value!r}")
101
+ return _json_safe(value.tolist())
102
102
  array = np.asarray(value)
103
103
  if np.iscomplexobj(array):
104
104
  data = [[float(num.real), float(num.imag)] for num in array.ravel()]
@@ -206,13 +206,15 @@ def map_arg(value: Any) -> grpc_pb2.Argument:
206
206
  try:
207
207
  array = np.stack(value)
208
208
  except Exception:
209
- arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
210
- return arg
209
+ pass
210
+ if array.dtype == object:
211
+ arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
212
+ return arg
211
213
  return map_arg(array)
212
214
  elif isinstance(value, np.ndarray):
213
- if value.dtype == object:
214
- raise ValueError(f"Cannot map object-dtype array: {value!r}")
215
- if value.dtype.kind not in _SUPPORTED_NDARRAY_KINDS:
215
+ # Object arrays (a list of dicts, say) and non-numeric dtypes go as
216
+ # JSON, which the client decodes back to lists.
217
+ if value.dtype == object or value.dtype.kind not in _SUPPORTED_NDARRAY_KINDS:
216
218
  arg.json_value = json.dumps(
217
219
  _json_safe(value.tolist()), separators=(",", ":")
218
220
  )
@@ -89,6 +89,7 @@ class PlutoSchema(DeviceSchema):
89
89
  device_type = "adalm_pluto"
90
90
  client_class = "Pluto"
91
91
  driver_version = "0.0.3"
92
+ _fir_rate = None # the rate whose pyadi FIR this schema last loaded
92
93
 
93
94
  @staticmethod
94
95
  def make_device(**kwargs):
@@ -106,6 +107,7 @@ class PlutoSchema(DeviceSchema):
106
107
 
107
108
  @idl_expose(kind="set")
108
109
  def set_filter(self, value):
110
+ self._fir_rate = None
109
111
  self.device.filter = value
110
112
 
111
113
  @idl_expose(kind="get")
@@ -164,7 +166,20 @@ class PlutoSchema(DeviceSchema):
164
166
 
165
167
  @idl_expose(kind="set")
166
168
  def set_sample_rate(self, value):
169
+ # pyadi reloads its FIR and re-runs the AD9361 rate chain (~0.8 s) even
170
+ # for an unchanged rate: skip only what our own last set left in place.
171
+ if value == self._fir_rate:
172
+ try:
173
+ if (
174
+ self.device.sample_rate == value
175
+ and self.device._get_iio_attr("out", "voltage_filter_fir_en", False) == 1
176
+ ):
177
+ return
178
+ except Exception:
179
+ pass
180
+ self._fir_rate = None
167
181
  self.device.sample_rate = value
182
+ self._fir_rate = value
168
183
 
169
184
  @idl_expose(kind="get")
170
185
  def get_rx_lo(self):
@@ -222,12 +237,18 @@ class PlutoSchema(DeviceSchema):
222
237
  def reset_for_next_holder(self):
223
238
  """A reservation ended: a PoE Pluto powers off. Otherwise stop its cyclic
224
239
  transmission, and drop its RX buffer so the next holder's rx_buffer_size takes effect."""
240
+ self._fir_rate = None
225
241
  if isinstance(self.device, PoePluto):
226
242
  self.device.power_down()
227
243
  elif self.device is not None:
228
244
  self.device.tx_destroy_buffer()
229
245
  self.device.rx_destroy_buffer()
230
246
 
247
+ def prepare_for_holder(self):
248
+ """A reservation started: a PoE Pluto boots now, not at the holder's first command."""
249
+ if isinstance(self.device, PoePluto):
250
+ self.device.power_up()
251
+
231
252
  @idl_expose(kind="call")
232
253
  def call_disable_dds(self):
233
254
  """Disable the internal DDS tone generator."""
@@ -244,12 +265,14 @@ class PlutoSchema(DeviceSchema):
244
265
  @idl_expose(kind="call")
245
266
  def call_power_down(self):
246
267
  """PoE Plutos: cut power; the next command powers it back up. No-op on other Plutos."""
268
+ self._fir_rate = None
247
269
  if isinstance(self.device, PoePluto):
248
270
  self.device.power_down()
249
271
 
250
272
  @idl_expose(kind="call")
251
273
  def call_power_cycle(self):
252
274
  """PoE Plutos: power off, then on, and wait for boot. No-op on other Plutos."""
275
+ self._fir_rate = None
253
276
  if isinstance(self.device, PoePluto):
254
277
  self.device.power_cycle()
255
278
 
@@ -32,6 +32,9 @@ IIOD_PORT = 30431
32
32
  BOOT_TIMEOUT_S = 45
33
33
  LINK_CHECK_S = 30 # no IIOD by then: ask the switch whether the port even has a link
34
34
  OFF_S = 3 # long enough for the Pluto to lose power before it is switched back on
35
+ # Reservations starting together (the top of an hour) power every Pluto at
36
+ # once: keep the switch's API logins to a few at a time.
37
+ _SWITCH_SESSIONS = threading.BoundedSemaphore(4)
35
38
 
36
39
 
37
40
  class RouterOS:
@@ -85,20 +88,22 @@ class RouterOS:
85
88
 
86
89
 
87
90
  def set_poe(*, switch, user, password, port, on):
88
- ros = RouterOS(switch, user, password)
89
- try:
90
- ros.cmd("/interface/ethernet/poe/set", f"=.id={port}", f"=poe-out={'auto-on' if on else 'off'}")
91
- finally:
92
- ros.close()
91
+ with _SWITCH_SESSIONS:
92
+ ros = RouterOS(switch, user, password)
93
+ try:
94
+ ros.cmd("/interface/ethernet/poe/set", f"=.id={port}", f"=poe-out={'auto-on' if on else 'off'}")
95
+ finally:
96
+ ros.close()
93
97
 
94
98
 
95
99
  def port_running(*, switch, user, password, port):
96
100
  """Whether the switch port has an Ethernet link (RouterOS 'running')."""
97
- ros = RouterOS(switch, user, password)
98
- try:
99
- replies = ros.cmd("/interface/ethernet/print", f"?name={port}", "=.proplist=name,running")
100
- finally:
101
- ros.close()
101
+ with _SWITCH_SESSIONS:
102
+ ros = RouterOS(switch, user, password)
103
+ try:
104
+ replies = ros.cmd("/interface/ethernet/print", f"?name={port}", "=.proplist=name,running")
105
+ finally:
106
+ ros.close()
102
107
  for sentence in replies:
103
108
  if sentence[0] == "!re":
104
109
  return "=running=true" in sentence
@@ -133,7 +138,14 @@ class PoePluto:
133
138
  return f"<PoE Pluto {self.uri} on {self._port}, {'on' if self._pluto is not None else 'off'}>"
134
139
 
135
140
  def _poe(self, on):
136
- set_poe(port=self._port, on=on, **self._switch)
141
+ # The switch API occasionally times out; a lost power-off leaves the Pluto on.
142
+ for attempt in range(3):
143
+ try:
144
+ return set_poe(port=self._port, on=on, **self._switch)
145
+ except OSError:
146
+ if attempt == 2:
147
+ raise
148
+ time.sleep(2)
137
149
 
138
150
  @property
139
151
  def power_state(self):
@@ -25,6 +25,13 @@ from remoteRF_server.common.idl import DeviceSchema, idl_expose, idl_register
25
25
 
26
26
  _MAX_REMOTE_PAYLOAD_BYTES = 4 * 1024 * 1024
27
27
  _MAX_COMPLEX64_SAMPLES = _MAX_REMOTE_PAYLOAD_BYTES // np.dtype(np.complex64).itemsize
28
+ # librtlsdr's synchronous read fails on a partial USB packet, and pyrtlsdr then
29
+ # closes the device but keeps its freed handle: read whole packets and slice.
30
+ _USB_PACKET_BYTES = 512
31
+
32
+
33
+ def _whole_packets(num_bytes: int) -> int:
34
+ return -(-num_bytes // _USB_PACKET_BYTES) * _USB_PACKET_BYTES
28
35
 
29
36
  def _require_rtlsdr():
30
37
  try:
@@ -151,7 +158,8 @@ class RtlSdrSchema(DeviceSchema):
151
158
  @property
152
159
  def _native(self):
153
160
  native = getattr(self.device, "native", None)
154
- if native is None:
161
+ # pyrtlsdr closes itself on a failed read; its handle is freed then.
162
+ if native is None or getattr(native, "device_opened", True) is False:
155
163
  raise RuntimeError("RTL-SDR device is closed")
156
164
  return native
157
165
 
@@ -319,7 +327,8 @@ class RtlSdrSchema(DeviceSchema):
319
327
  label="num_samples",
320
328
  maximum=_MAX_COMPLEX64_SAMPLES,
321
329
  )
322
- samples = np.asarray(self._native.read_samples(count), dtype=np.complex64)
330
+ read = _whole_packets(2 * count) // 2
331
+ samples = np.asarray(self._native.read_samples(read), dtype=np.complex64)[:count]
323
332
  if samples.size != count:
324
333
  raise RuntimeError(
325
334
  f"short RTL-SDR read: requested {count}, received {samples.size}"
@@ -334,7 +343,7 @@ class RtlSdrSchema(DeviceSchema):
334
343
  label="num_bytes",
335
344
  maximum=_MAX_REMOTE_PAYLOAD_BYTES,
336
345
  )
337
- payload = bytes(self._native.read_bytes(count))
346
+ payload = bytes(self._native.read_bytes(_whole_packets(count)))[:count]
338
347
  if len(payload) != count:
339
348
  raise RuntimeError(
340
349
  f"short RTL-SDR byte read: requested {count}, received {len(payload)}"
@@ -27,6 +27,9 @@ c = idl_client
27
27
 
28
28
  import itertools
29
29
  import os
30
+ import subprocess
31
+ import sys
32
+ import threading
30
33
 
31
34
 
32
35
  def _configure_uhd_console_logging():
@@ -114,8 +117,7 @@ def _remote_transport_args(value):
114
117
  return f"{args},{setting}" if args else setting
115
118
 
116
119
 
117
- def _connect_usrp(args=None, serial=None, addr=None, **kwargs):
118
- uhd_module = _require_uhd()
120
+ def _usrp_args(args=None, serial=None, addr=None, **kwargs):
119
121
  args = args or kwargs.pop("device_args", None) or kwargs.pop("init_args", None)
120
122
  type_hint = kwargs.pop("type", None) or kwargs.pop("device_type", None)
121
123
  profile_name = kwargs.pop("profile", None)
@@ -165,8 +167,53 @@ def _connect_usrp(args=None, serial=None, addr=None, **kwargs):
165
167
  if profile.uhd_device_type and "type" not in arg_keys:
166
168
  args = f"type={profile.uhd_device_type},{arg_text}"
167
169
 
168
- args = _remote_transport_args(args)
170
+ return _remote_transport_args(args)
171
+
172
+
173
+ # A cold B2xx open (firmware + FPGA image) took ~68 s inside MultiUSRP(),
174
+ # which holds the GIL, so the whole lab stopped answering. A child process
175
+ # pays that instead while this one waits without the GIL; the open after it
176
+ # is warm (~2.5 s).
177
+ # ponytail: "warm" is per process; a USB replug while the Server runs makes
178
+ # the next open cold in-process again. Clear the mark on hotplug if that bites.
179
+ _PRELOAD_TIMEOUT = 180.0
180
+ _preloaded = set()
181
+ _preload_locks = {}
182
+ _preload_guard = threading.Lock()
183
+
184
+
185
+ def _preload_image(args):
186
+ if str(os.getenv("REMOTERF_USRP_PRELOAD", "1")).strip().lower() in {"0", "false", "no", "off"}:
187
+ return
188
+ with _preload_guard:
189
+ lock = _preload_locks.setdefault(args, threading.Lock())
190
+ # One device, one driver at a time: an open waits here for a pre-load.
191
+ with lock:
192
+ if args in _preloaded:
193
+ return
194
+ try:
195
+ done = subprocess.run(
196
+ [sys.executable, "-c", "import sys, uhd; uhd.usrp.MultiUSRP(sys.argv[1])", args],
197
+ stdin=subprocess.DEVNULL,
198
+ stdout=subprocess.DEVNULL,
199
+ stderr=subprocess.DEVNULL,
200
+ timeout=_PRELOAD_TIMEOUT,
201
+ )
202
+ except (OSError, subprocess.TimeoutExpired) as exc:
203
+ print(f"Warning: USRP image pre-load for {args!r} failed: {exc}")
204
+ return
205
+ if done.returncode == 0:
206
+ _preloaded.add(args)
207
+ else:
208
+ print(f"Warning: USRP image pre-load for {args!r} exited {done.returncode}")
209
+
210
+
211
+ def _connect_usrp(args=None, serial=None, addr=None, **kwargs):
212
+ uhd_module = _require_uhd()
213
+ args = _usrp_args(args, serial, addr, **kwargs)
214
+ _preload_image(args)
169
215
  dev = uhd_module.usrp.MultiUSRP(args)
216
+ _preloaded.add(args)
170
217
  return dev
171
218
 
172
219
 
@@ -618,6 +665,11 @@ class UsrpSchema(DeviceSchema):
618
665
  def open_native_device(self):
619
666
  return _connect_usrp(**self._inventory)
620
667
 
668
+ def prepare_for_holder(self):
669
+ """A reservation started: load a cold image now, not in the holder's open."""
670
+ if uhd is not None:
671
+ _preload_image(_usrp_args(**self._inventory))
672
+
621
673
  def create_session_schema(self):
622
674
  """Create an isolated, live adapter for one authenticated reservation."""
623
675
  clone = type(self)()
@@ -125,6 +125,31 @@ class EnvStore:
125
125
 
126
126
  self._write_lines(lines)
127
127
 
128
+ def remove_from_csv_list(self, key: str, value: str) -> None:
129
+ key = sanitize_env_key(key)
130
+ value = (value or "").strip()
131
+
132
+ with self._lock:
133
+ lines = self._read_lines()
134
+ for i, ln in enumerate(lines):
135
+ if ln.startswith(key + "="):
136
+ existing = csv_split(ln[len(key) + 1:])
137
+ if value not in existing:
138
+ return
139
+ existing.remove(value)
140
+ lines[i] = f"{key}=" + ",".join(existing)
141
+ self._write_lines(lines)
142
+ return
143
+
144
+ def remove_keys(self, keys: List[str]) -> None:
145
+ prefixes = tuple(sanitize_env_key(k) + "=" for k in keys)
146
+
147
+ with self._lock:
148
+ lines = self._read_lines()
149
+ kept = [ln for ln in lines if not ln.startswith(prefixes)]
150
+ if len(kept) != len(lines):
151
+ self._write_lines(kept)
152
+
128
153
  def upsert_kv(self, key: str, value: str) -> None:
129
154
  key = sanitize_env_key(key)
130
155
  value = (value or "").strip()