remoteRF-server 1.1.0__tar.gz → 1.1.2__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 (166) hide show
  1. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/PKG-INFO +2 -1
  2. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/README.md +1 -0
  3. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/pyproject.toml +1 -1
  4. {remoterf_server-1.1.0/src/remoteRF_server/drivers/adalm_pluto → remoterf_server-1.1.2/src/remoteRF_server/common/idl}/pluto_schema.py +3 -0
  5. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/api_token.py +19 -1
  6. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +1 -2
  7. {remoterf_server-1.1.0/src/remoteRF_server/common/idl → remoterf_server-1.1.2/src/remoteRF_server/drivers/adalm_pluto}/pluto_schema.py +3 -0
  8. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/poe.py +62 -16
  9. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/commands.py +2 -5
  10. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/device_manager.py +12 -2
  11. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/direct_path.py +69 -13
  12. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/global_onboarding.py +45 -22
  13. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/grpc_server.py +1 -1
  14. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/reservation.py +98 -16
  15. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/rpc_manager.py +14 -4
  16. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/serverrf_cli.py +36 -0
  17. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/PKG-INFO +2 -1
  18. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/SOURCES.txt +4 -1
  19. remoterf_server-1.1.2/tests/test_account_hardening.py +134 -0
  20. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_cluster.py +1 -1
  21. remoterf_server-1.1.2/tests/test_direct_path_server.py +174 -0
  22. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_global_accounts_migration.py +3 -1
  23. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_global_onboarding.py +98 -30
  24. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_pluto_compat.py +1 -1
  25. remoterf_server-1.1.2/tests/test_pluto_poe.py +209 -0
  26. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_rpc_manager_calln.py +11 -3
  27. remoterf_server-1.1.2/tests/test_wipe.py +20 -0
  28. remoterf_server-1.1.0/tests/test_pluto_poe.py +0 -100
  29. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/LICENSE +0 -0
  30. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/setup.cfg +0 -0
  31. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/__init__.py +0 -0
  32. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/__init__.py +0 -0
  33. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  34. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  35. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  36. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  37. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  38. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  39. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  40. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
  41. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
  42. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  43. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  44. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  45. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  46. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  47. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/__init__.py +0 -0
  48. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  49. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  50. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/schema.py +0 -0
  51. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/v2.py +0 -0
  52. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/__init__.py +0 -0
  53. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  54. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  55. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/db_location.py +0 -0
  56. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/list_string.py +0 -0
  57. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/panel.py +0 -0
  58. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  59. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/__init__.py +0 -0
  60. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  61. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  62. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  63. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  64. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  65. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  66. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  67. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  68. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  69. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  70. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  71. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  72. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  73. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  74. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  75. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/__init__.py +0 -0
  76. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_auth_token.py +0 -0
  77. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_directory_store.py +0 -0
  78. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  79. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/__init__.py +0 -0
  80. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/acc_perms.py +0 -0
  81. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/cert_provider.py +0 -0
  82. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/console.py +0 -0
  83. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/control.py +0 -0
  84. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  85. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  86. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/dynamic_v2.py +0 -0
  87. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  88. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/__init__.py +0 -0
  89. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/admin.py +0 -0
  90. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/cli.py +0 -0
  91. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/cluster.py +0 -0
  92. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/contracts.py +0 -0
  93. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/destination.py +0 -0
  94. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/devices.py +0 -0
  95. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  96. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/home.py +0 -0
  97. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  98. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/identity.py +0 -0
  99. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  100. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/migrations.py +0 -0
  101. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/peers.py +0 -0
  102. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/permissions.py +0 -0
  103. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/principals.py +0 -0
  104. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/runtime.py +0 -0
  105. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/trust.py +0 -0
  106. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  107. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/user_group_cli.py +0 -0
  108. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/user_group_handler.py +0 -0
  109. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/v2_errors.py +0 -0
  110. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/__init__.py +0 -0
  111. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gen_certs.py +0 -0
  112. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gist_status.py +0 -0
  113. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  114. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  115. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  116. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  117. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/requires.txt +0 -0
  118. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  119. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_bind_host.py +0 -0
  120. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_cert_provider.py +0 -0
  121. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_client_sessions.py +0 -0
  122. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_commands.py +0 -0
  123. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_console.py +0 -0
  124. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_default_rtl_sdr_schema.py +0 -0
  125. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_default_usrp_schema.py +0 -0
  126. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_deployment_capabilities.py +0 -0
  127. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_direct_fixed_port.py +0 -0
  128. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_dynamic_v2.py +0 -0
  129. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_federated_direct_path.py +0 -0
  130. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_federated_dynamic_v2.py +0 -0
  131. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_federation_edges.py +0 -0
  132. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_first_run.py +0 -0
  133. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_d_contracts.py +0 -0
  134. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_e_federation.py +0 -0
  135. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_e_proto.py +0 -0
  136. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_f_enrollment.py +0 -0
  137. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_g1_logging.py +0 -0
  138. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_g_permissions.py +0 -0
  139. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_h_devices.py +0 -0
  140. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_i_direct_path.py +0 -0
  141. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gen_certs.py +0 -0
  142. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_global_v1_disposal.py +0 -0
  143. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_hackrf_schema.py +0 -0
  144. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_home_device_plane.py +0 -0
  145. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_host_directory_store.py +0 -0
  146. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_host_dynamic_v2.py +0 -0
  147. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_idl_schema.py +0 -0
  148. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_panel.py +0 -0
  149. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_peer_transport.py +0 -0
  150. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_process_arg.py +0 -0
  151. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_reservation_handoff.py +0 -0
  152. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_reservation_policy_b.py +0 -0
  153. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_reservation_timestamps.py +0 -0
  154. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_rtl_sdr_schema.py +0 -0
  155. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_soak_federation.py +0 -0
  156. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_status_push.py +0 -0
  157. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_ti_mmwave_hardware.py +0 -0
  158. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_ti_mmwave_schema.py +0 -0
  159. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_user_group_cli.py +0 -0
  160. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  161. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_client_server_hardware.py +0 -0
  162. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_client_server_localhost.py +0 -0
  163. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  164. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_profiles.py +0 -0
  165. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  166. {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_uhd410_hardware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -932,6 +932,7 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
932
932
  ### Database
933
933
 
934
934
  * `db purge` — Remove all database entries
935
+ * `wipe` — Factory reset: delete the whole config dir (devices, drivers, databases, certs) and the Global identity keys; the next `serverrf -s` runs first-time setup *(server must be stopped; prompts for confirmation, `-y` skips)*
935
936
 
936
937
 
937
938
  ## Adding Hosts
@@ -901,6 +901,7 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
901
901
  ### Database
902
902
 
903
903
  * `db purge` — Remove all database entries
904
+ * `wipe` — Factory reset: delete the whole config dir (devices, drivers, databases, certs) and the Global identity keys; the next `serverrf -s` runs first-time setup *(server must be stopped; prompts for confirmation, `-y` skips)*
904
905
 
905
906
 
906
907
  ## Adding Hosts
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -202,6 +202,9 @@ class PlutoSchema(DeviceSchema):
202
202
 
203
203
  @idl_expose(kind="set")
204
204
  def set_rx_buffer_size(self, value):
205
+ # pyadi reads rx_buffer_size only when it (re)creates the RX buffer.
206
+ if value != self.device.rx_buffer_size:
207
+ self.device.rx_destroy_buffer()
205
208
  self.device.rx_buffer_size = value
206
209
 
207
210
  @idl_expose(kind="call")
@@ -15,6 +15,7 @@
15
15
 
16
16
  import os
17
17
  import hashlib
18
+ import hmac
18
19
  import base64
19
20
  import secrets
20
21
  from dotenv import load_dotenv, find_dotenv
@@ -33,4 +34,21 @@ def validate_token(salt, hash, token) -> bool:
33
34
  def hash_token(token: str) -> tuple[str, str]:
34
35
  salt = os.urandom(16).hex()
35
36
  hashed = hashlib.sha256(bytes.fromhex(salt) + token.encode()).hexdigest()
36
- return salt, hashed
37
+ return salt, hashed
38
+
39
+ # Passwords get a real KDF; tokens stay on hash_token (random, high entropy).
40
+ PASSWORD_HASH_PREFIX = "pbkdf2$"
41
+ _PBKDF2_ROUNDS = 210_000
42
+
43
+ def hash_password(password: str) -> tuple[str, str]:
44
+ salt = os.urandom(16).hex()
45
+ digest = hashlib.pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt), _PBKDF2_ROUNDS)
46
+ return salt, f"{PASSWORD_HASH_PREFIX}{_PBKDF2_ROUNDS}${digest.hex()}"
47
+
48
+ def verify_password(salt, hash, password) -> tuple[bool, bool]:
49
+ """Returns (matches, needs_rehash). Accepts legacy hash_token() hashes."""
50
+ if not hash.startswith(PASSWORD_HASH_PREFIX):
51
+ return validate_token(salt, hash, password), True
52
+ _, rounds, digest = hash.split('$', 2)
53
+ computed = hashlib.pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt), int(rounds))
54
+ return hmac.compare_digest(computed.hex(), digest), False
@@ -90,8 +90,7 @@ class PlutoServer:
90
90
  if result is not None:
91
91
  return {function_name: map_arg(result)}
92
92
  else:
93
- # If there's no return value, return some placeholder
94
- return {function_name: map_arg('None')}
93
+ return {function_name: map_arg(None)}
95
94
 
96
95
  @staticmethod
97
96
  def get(*, function_name, token):
@@ -202,6 +202,9 @@ class PlutoSchema(DeviceSchema):
202
202
 
203
203
  @idl_expose(kind="set")
204
204
  def set_rx_buffer_size(self, value):
205
+ # pyadi reads rx_buffer_size only when it (re)creates the RX buffer.
206
+ if value != self.device.rx_buffer_size:
207
+ self.device.rx_destroy_buffer()
205
208
  self.device.rx_buffer_size = value
206
209
 
207
210
  @idl_expose(kind="call")
@@ -26,9 +26,11 @@ import threading
26
26
  import time
27
27
 
28
28
  from remoteRF_server.common.utils import get_remoterf_root
29
+ from remoteRF_server.common.utils.ansi_codes import Sty, printf
29
30
 
30
31
  IIOD_PORT = 30431
31
- BOOT_TIMEOUT_S = 120
32
+ BOOT_TIMEOUT_S = 45
33
+ LINK_CHECK_S = 30 # no IIOD by then: ask the switch whether the port even has a link
32
34
  OFF_S = 3 # long enough for the Pluto to lose power before it is switched back on
33
35
 
34
36
 
@@ -90,6 +92,19 @@ def set_poe(*, switch, user, password, port, on):
90
92
  ros.close()
91
93
 
92
94
 
95
+ def port_running(*, switch, user, password, port):
96
+ """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()
102
+ for sentence in replies:
103
+ if sentence[0] == "!re":
104
+ return "=running=true" in sentence
105
+ raise RuntimeError(f"switch has no interface {port}")
106
+
107
+
93
108
  def _iiod_up(ip):
94
109
  try:
95
110
  socket.create_connection((ip, IIOD_PORT), timeout=1).close()
@@ -110,6 +125,7 @@ class PoePluto:
110
125
  _serial=str(serial or "").strip(),
111
126
  _switch={"switch": switch, "user": user, "password": password},
112
127
  _pluto=None,
128
+ fault=None, # message of the last failed boot; None once a boot succeeds
113
129
  _lock=threading.RLock(),
114
130
  )
115
131
 
@@ -119,28 +135,58 @@ class PoePluto:
119
135
  def _poe(self, on):
120
136
  set_poe(port=self._port, on=on, **self._switch)
121
137
 
138
+ @property
139
+ def power_state(self):
140
+ if self.fault:
141
+ return "fault"
142
+ return "on" if self._pluto is not None else "standby"
143
+
122
144
  def power_up(self):
123
145
  with self._lock:
124
146
  if self._pluto is not None:
125
147
  return
126
- self._poe(True)
127
- deadline = time.monotonic() + BOOT_TIMEOUT_S
128
- while not _iiod_up(self._ip):
129
- if time.monotonic() > deadline:
130
- self._poe(False)
131
- raise RuntimeError(f"PoE Pluto {self.uri} did not boot within {BOOT_TIMEOUT_S} s on {self._port}")
132
- time.sleep(1)
133
- import adi
134
-
135
- pluto = adi.Pluto(self.uri)
136
- found = str(pluto._ctx.attrs.get("hw_serial", "")).strip()
137
- if self._serial and found.lower() != self._serial.lower():
138
- del pluto
148
+ try:
149
+ self._boot()
150
+ except Exception as e:
151
+ vars(self)["fault"] = str(e)
152
+ printf(f"{time.strftime('%H:%M:%S')} {e}", Sty.GRAY)
153
+ raise
154
+ vars(self)["fault"] = None
155
+
156
+ def _boot(self):
157
+ self._poe(True)
158
+ started = time.monotonic()
159
+ link_checked = False
160
+ while not _iiod_up(self._ip):
161
+ waited = time.monotonic() - started
162
+ if waited > BOOT_TIMEOUT_S:
139
163
  self._poe(False)
140
- raise RuntimeError(f"{self.uri} on {self._port} is Pluto {found}, expected {self._serial}")
141
- vars(self)["_pluto"] = pluto
164
+ raise RuntimeError(f"PoE Pluto {self.uri} did not boot within {BOOT_TIMEOUT_S} s on {self._port}")
165
+ if waited > LINK_CHECK_S and not link_checked:
166
+ link_checked = True
167
+ try:
168
+ running = port_running(port=self._port, **self._switch)
169
+ except Exception:
170
+ running = True # switch unreachable: keep waiting for IIOD
171
+ if not running:
172
+ self._poe(False)
173
+ raise RuntimeError(
174
+ f"Pluto {self.uri}: PoE on {self._port} but no Ethernet link after {LINK_CHECK_S} s (cable/port/board fault)"
175
+ )
176
+ time.sleep(1)
177
+ import adi
178
+
179
+ pluto = adi.Pluto(self.uri)
180
+ found = str(pluto._ctx.attrs.get("hw_serial", "")).strip()
181
+ if self._serial and found.lower() != self._serial.lower():
182
+ del pluto
183
+ self._poe(False)
184
+ raise RuntimeError(f"{self.uri} on {self._port} is Pluto {found}, expected {self._serial}")
185
+ vars(self)["_pluto"] = pluto
142
186
 
143
187
  def power_down(self):
188
+ """Cut power. The Pluto boots with default settings: sample rate, LOs,
189
+ gains and rx_buffer_size are all reset by the next power_up()."""
144
190
  with self._lock:
145
191
  # Drop the handle while the Pluto can still answer its buffer teardown.
146
192
  vars(self)["_pluto"] = None
@@ -106,7 +106,7 @@ def _table() -> list[Command]:
106
106
  # ---- devices ----
107
107
  Command(("devices list",), "Configured devices", _shell("cmd_devices_list"), "Devices"),
108
108
  Command(("devices status",), "Live device online/last_seen/route", _daemon("cmd_devices_status"), "Devices", where=D),
109
- Command(("devices add",), "Add a device (asks type, name, serial)", _shell("cmd_devices_add"), "Devices", after="reload_devices"),
109
+ Command(("devices add",), "Add a device (asks type, name, serial)", _shell("cmd_devices_add"), "Devices", after="reload_devices", hidden=True),
110
110
  Command(("devices remove",), "Remove a device by id", _shell("cmd_devices_remove"), "Devices", after="reload_devices"),
111
111
  Command(("devices rename",), "Rename a device: devices rename <id> <name>", _shell("cmd_devices_rename"), "Devices", after="reload_devices"),
112
112
  Command(("devices wipe",), "Remove ALL devices [-y]", _shell("cmd_devices_wipe"), "Devices", after="reload_devices"),
@@ -168,6 +168,7 @@ def _table() -> list[Command]:
168
168
  Command(("db export",), "Archive DBs, devices, keys, certs to ~/Downloads [dir]", _shell("cmd_db_export"), "Database"),
169
169
  Command(("stats purge",), "Delete the activity log (reservation stats) [-y]", _daemon("cmd_stats_purge"), "Database", where=D),
170
170
  Command(("db purge",), "Remove all database entries", _daemon("cmd_db_purge"), "Database", where=D),
171
+ Command(("wipe",), "Factory reset: delete all config and data [-y]", _shell("cmd_wipe"), "Database"),
171
172
  ]
172
173
 
173
174
 
@@ -263,10 +264,6 @@ def cmd_help(argv: list[str]) -> int:
263
264
  return 2
264
265
  rows = [c for c in _visible(_IN_DAEMON) if c.section == section]
265
266
  lines = [_SECTION_BLURB.get(section, ""), ""] + _columns([(prefix + c.names[0], c.help) for c in rows])
266
- if section == "Devices":
267
- lines += ["", "Types for `devices add`:",
268
- " pluto, pluto-mimo, rtl-sdr, hackrf, ti-mmwave,",
269
- " usrp-2901, usrp-b205-mini, usrp-n210, usrp-profile"]
270
267
  print_panel(section, lines, literal=True)
271
268
  return 0
272
269
  lines: list[str] = []
@@ -77,6 +77,10 @@ def _is_connected(st: _DeviceState) -> bool:
77
77
  # host
78
78
  return bool(st.online)
79
79
 
80
+ def _poe_fault(st: _DeviceState):
81
+ """Last boot failure of a local PoE Pluto (None otherwise; see drivers/adalm_pluto/poe.py)."""
82
+ return getattr(getattr(st.dev, "device", None), "fault", None)
83
+
80
84
  # Host device pulling/grabbing
81
85
 
82
86
  def _get_host_registry():
@@ -187,6 +191,12 @@ def _local_devices_str_snapshot() -> Dict[int, str]:
187
191
  name = (st.name or f"device-{gid}").strip()
188
192
 
189
193
  status = "online" if _is_connected(st) else "offline"
194
+ fault = _poe_fault(st)
195
+ power = getattr(getattr(st.dev, "device", None), "power_state", None)
196
+ if fault:
197
+ status = f"offline: {fault}"
198
+ elif power:
199
+ status = f"{status}, {power}"
190
200
  if st.salt or st.hsh:
191
201
  status = f"{status}, reserved"
192
202
 
@@ -728,7 +738,7 @@ def describe_devices(ids) -> list[dict]:
728
738
  if st is None:
729
739
  continue
730
740
  out.append({"id": int(gid), "name": (st.name or f"device-{gid}").strip(),
731
- "type": st.dtype or "", "online": _is_connected(st)})
741
+ "type": st.dtype or "", "online": _is_connected(st) and not _poe_fault(st)})
732
742
  return out
733
743
 
734
744
 
@@ -1187,7 +1197,7 @@ def device_inventory_snapshot() -> Dict[int, Dict[str, object]]:
1187
1197
  gid: {
1188
1198
  "name": st.name or f"device {gid}",
1189
1199
  "device_type": st.dtype or None,
1190
- "online": _is_connected(st),
1200
+ "online": _is_connected(st) and not _poe_fault(st),
1191
1201
  "origin": st.origin,
1192
1202
  }
1193
1203
  for gid, st in _devices.items()
@@ -45,6 +45,7 @@ import os
45
45
  import secrets
46
46
  import socket
47
47
  import threading
48
+ import time
48
49
  from pathlib import Path
49
50
  from typing import Optional
50
51
 
@@ -125,6 +126,20 @@ ALPN = "remoterf-direct/1"
125
126
  MAX_FRAME = 100 * 1024 * 1024 # the gRPC message ceiling
126
127
  IDLE_SECONDS = 600
127
128
  CONNECT_SECONDS = 30 # ICE checks, then the QUIC handshake; a silent client is dropped
129
+ # How long the punch waits for the client's first check when the answer also
130
+ # offered the fixed port. A 2.1.0 client dials that port for 2 s before its
131
+ # first check; a check of ours reaching its NAT before then leaves a stale
132
+ # entry there that shadows the port its own checks are then mapped to (Linux
133
+ # NAT picks another), and the punch can never complete. A check that arrives
134
+ # sooner ends the wait; a client that never reaches us gets ours after it.
135
+ # 3.0 = the 2 s dial plus ~1 s for the relay round trip that carries the
136
+ # answer; a shorter hold reproduces the shadow on a slow relay.
137
+ HOLD_SECONDS = 3.0
138
+ # A client that punches at once ("race" in its offer) sends its first check
139
+ # as soon as the answer reaches it, but that check is dropped at a border
140
+ # that has not seen ours yet, so it cannot end the hold: ours must simply
141
+ # leave after its first check has, and before its retransmit at 0.5 s.
142
+ RACE_HOLD_SECONDS = 0.4
128
143
  MAX_PATHS_PER_SESSION = 8 # processes of one login; the oldest path makes room
129
144
  DEFAULT_STUN = "stun.l.google.com:19302"
130
145
  STUN_SECONDS = 1.5 # a STUN server answers in milliseconds or not at all
@@ -145,6 +160,23 @@ _listener = None
145
160
  _listener_port = 0
146
161
 
147
162
 
163
+ def say(message: str) -> None:
164
+ """A console line about a path, stamped: paths fail minutes apart."""
165
+ printf(f"{time.strftime('%H:%M:%S')} {message}", Sty.GRAY)
166
+
167
+
168
+ def describe_failure(exc: BaseException, stage: str) -> str:
169
+ """The exception class and a message that says what did not happen."""
170
+ if isinstance(exc, asyncio.TimeoutError):
171
+ return f"TimeoutError: {stage} did not complete within {CONNECT_SECONDS} s"
172
+ return f"{type(exc).__name__}: {exc}"
173
+
174
+
175
+ def describe_remotes(ice: Connection) -> str:
176
+ """The candidates the client offered, host:port/type each."""
177
+ return " ".join(f"{c.host}:{c.port}/{c.type}" for c in ice.remote_candidates) or "none"
178
+
179
+
148
180
  def frame(payload: bytes) -> bytes:
149
181
  return len(payload).to_bytes(4, "big") + payload
150
182
 
@@ -187,8 +219,15 @@ class Ice(Connection):
187
219
  server, and aioice would hold gathering for its whole 5 s timeout on each
188
220
  one, on every offer. One interface and a short STUN wait instead; a
189
221
  machine without a default route gathers the way aioice does.
222
+
223
+ ``heard`` is set by the peer's first check, which may arrive before
224
+ connect() (aioice holds it as an early check until then).
190
225
  """
191
226
 
227
+ def __init__(self, *args, **kwargs):
228
+ super().__init__(*args, **kwargs)
229
+ self.heard = asyncio.Event()
230
+
192
231
  async def gather_candidates(self) -> None:
193
232
  address = default_route_address()
194
233
  if address is None or self._local_candidates_start:
@@ -199,6 +238,10 @@ class Ice(Connection):
199
238
  )
200
239
  self._local_candidates_end = True
201
240
 
241
+ def request_received(self, *args) -> None:
242
+ super().request_received(*args)
243
+ self.heard.set()
244
+
202
245
 
203
246
  def peer_address(ice: Connection) -> tuple[str, int]:
204
247
  """The remote end of the nominated pair (aioice keeps it private)."""
@@ -299,11 +342,12 @@ class _Protocol(QuicConnectionProtocol):
299
342
  class _Path:
300
343
  """One client session's punched pair and the QUIC connection over it."""
301
344
 
302
- def __init__(self, key: str, user: str, ice: Connection, bound: Optional[dict] = None):
345
+ def __init__(self, key: str, user: str, ice: Connection, bound: Optional[dict] = None, hold: float = 0.0):
303
346
  self.key = key
304
347
  self.user = user
305
348
  self.ice = ice
306
349
  self.bound = bound
350
+ self.hold = hold # seconds to wait for the client's first check (a fixed port was offered too)
307
351
  self.peer: Optional[tuple[str, int]] = None
308
352
  self.calls = 0
309
353
  self.task: Optional[asyncio.Task] = None
@@ -312,24 +356,31 @@ class _Path:
312
356
 
313
357
  async def run(self) -> None:
314
358
  protocol = None
359
+ stage = "ICE"
315
360
  try:
361
+ if self.hold:
362
+ with contextlib.suppress(asyncio.TimeoutError):
363
+ await asyncio.wait_for(self.ice.heard.wait(), self.hold)
316
364
  await asyncio.wait_for(self.ice.connect(), CONNECT_SECONDS)
317
365
  self.peer = peer_address(self.ice)
366
+ stage = "the QUIC Initial"
318
367
  protocol = await asyncio.wait_for(self._accept(), CONNECT_SECONDS)
319
368
  pump = asyncio.ensure_future(self._pump(protocol))
320
369
  closed = asyncio.ensure_future(protocol.wait_closed())
321
370
  try:
371
+ stage = "the QUIC handshake"
322
372
  try:
323
373
  await asyncio.wait_for(protocol.wait_connected(), CONNECT_SECONDS)
324
374
  except ConnectionError:
325
375
  raise ConnectionError(f"QUIC handshake failed: {protocol.terminated}") from None
326
376
  where = "offsite" if is_offsite(self.peer[0]) else "local"
327
- printf(f"Direct path open: {self.user} from {self.peer[0]}:{self.peer[1]} ({where})", Sty.GRAY)
377
+ say(f"Direct path open: {self.user} from {self.peer[0]}:{self.peer[1]} ({where})")
328
378
  await asyncio.wait({pump, closed}, return_when=asyncio.FIRST_COMPLETED)
329
379
  finally:
330
380
  await cancel_tasks((pump, closed))
331
381
  except Exception as exc: # noqa: BLE001 - a failed punch is expected on some networks
332
- printf(f"Direct path failed: {self.user}: {type(exc).__name__}: {exc}", Sty.GRAY)
382
+ held = f"; held {self.hold:g} s" if self.hold else ""
383
+ say(f"Direct path failed: {self.user}: {describe_failure(exc, stage)}; offered {describe_remotes(self.ice)}{held}")
333
384
  finally:
334
385
  if protocol is not None:
335
386
  protocol.close()
@@ -339,7 +390,7 @@ class _Path:
339
390
  await self.close()
340
391
  if self.peer is not None:
341
392
  why = f" ({protocol.terminated})" if protocol is not None and protocol.terminated else ""
342
- printf(f"Direct path closed: {self.user} from {self.peer[0]}:{self.peer[1]} after {self.calls} calls{why}", Sty.GRAY)
393
+ say(f"Direct path closed: {self.user} from {self.peer[0]}:{self.peer[1]} after {self.calls} calls{why}")
343
394
 
344
395
  async def _accept(self) -> QuicConnectionProtocol:
345
396
  """The client's Initial packet decides the connection, as aioquic's server does."""
@@ -423,10 +474,10 @@ async def _serve_call(path, reader: asyncio.StreamReader, writer: asyncio.Stream
423
474
  if bad_frame is not None:
424
475
  # A client that cannot frame a request is not ours; its bytes are
425
476
  # still arriving and would only pile up in this stream's buffer.
426
- printf(f"Direct path dropped: {path.user} sent a {bad_frame}", Sty.GRAY)
477
+ say(f"Direct path dropped: {path.user} sent a {bad_frame}")
427
478
  await path.drop()
428
479
  if refused is not None:
429
- printf(f"Direct path refused: {path.user}: {refused}", Sty.GRAY)
480
+ say(f"Direct path refused: {path.user}: {refused}")
430
481
  await path.drop()
431
482
 
432
483
 
@@ -475,7 +526,7 @@ class _Conn(_Protocol):
475
526
  if self.key is not None and _paths.get(self.key) is self:
476
527
  del _paths[self.key]
477
528
  why = f" ({self.terminated})" if self.terminated else ""
478
- printf(f"Direct path closed: {self.user} from {self.peer[0]}:{self.peer[1]} after {self.calls} calls{why}", Sty.GRAY)
529
+ say(f"Direct path closed: {self.user} from {self.peer[0]}:{self.peer[1]} after {self.calls} calls{why}")
479
530
 
480
531
  def _stream(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
481
532
  task = asyncio.ensure_future(self._hello(reader, writer) if self.key is None else _serve_call(self, reader, writer))
@@ -487,7 +538,7 @@ class _Conn(_Protocol):
487
538
  await self._claim(reader, writer)
488
539
  except Exception as exc: # noqa: BLE001 - a hello that cannot be honoured closes the connection
489
540
  logger.warning("direct path fixed-port hello failed: %r", exc)
490
- printf(f"Direct path fixed-port hello failed: {type(exc).__name__}: {exc}", Sty.GRAY)
541
+ say(f"Direct path fixed-port hello failed: {type(exc).__name__}: {exc}")
491
542
  self.close()
492
543
 
493
544
  async def _claim(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
@@ -509,7 +560,7 @@ class _Conn(_Protocol):
509
560
  writer.write(frame(json.dumps({"ok": True}).encode()))
510
561
  writer.write_eof()
511
562
  where = "offsite" if is_offsite(self.peer[0]) else "local"
512
- printf(f"Direct path open: {self.user} from {self.peer[0]}:{self.peer[1]} ({where}, fixed port)", Sty.GRAY)
563
+ say(f"Direct path open: {self.user} from {self.peer[0]}:{self.peer[1]} ({where}, fixed port)")
513
564
 
514
565
  async def drop(self) -> None:
515
566
  if self.key is not None and _paths.get(self.key) is self:
@@ -569,13 +620,17 @@ async def _offer(key: str, user: str, remote: dict, bound: Optional[dict] = None
569
620
  except Exception:
570
621
  await close_ice(ice)
571
622
  raise
572
- path = _paths[key] = _Path(key, user, ice, bound)
623
+ fixed = _fixed_port_offer(key, user, bound)
624
+ # A client that races the fixed port against the punch says so ("race")
625
+ # and needs only a short hold; a 2.1.0 offer has no such key.
626
+ hold = 0.0 if not fixed else RACE_HOLD_SECONDS if remote.get("race") else HOLD_SECONDS
627
+ path = _paths[key] = _Path(key, user, ice, bound, hold=hold)
573
628
  path.task = asyncio.ensure_future(path.run())
574
629
  return {
575
630
  "ufrag": ice.local_username,
576
631
  "pwd": ice.local_password,
577
632
  "candidates": [candidate.to_sdp() for candidate in ice.local_candidates],
578
- **_fixed_port_offer(key, user, bound),
633
+ **fixed,
579
634
  }
580
635
 
581
636
 
@@ -612,8 +667,9 @@ def offer(session_key: str, user: str, ice: str) -> dict:
612
667
  answer = asyncio.run_coroutine_threadsafe(
613
668
  _offer(key, user, remote), _get_loop()
614
669
  ).result(20)
615
- except Exception as exc: # noqa: BLE001
616
- return {"ace": map_arg(f"direct path unavailable: {exc}")}
670
+ except Exception as exc: # noqa: BLE001 - the console gets the detail, the client a fixed line
671
+ say(f"Direct path offer failed: {user}: {type(exc).__name__}: {exc}")
672
+ return {"ace": map_arg("direct path unavailable")}
617
673
  return {"ice": map_arg(json.dumps(answer)), "server_name": map_arg(hostname() or "")}
618
674
 
619
675