remoteRF-server 1.4.5__tar.gz → 1.4.7__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 (189) hide show
  1. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/PKG-INFO +1 -1
  2. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/pyproject.toml +1 -1
  3. {remoterf_server-1.4.5/src/remoteRF_server/drivers/adalm_pluto → remoterf_server-1.4.7/src/remoteRF_server/common/idl}/pluto_schema.py +19 -4
  4. remoterf_server-1.4.7/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +274 -0
  5. {remoterf_server-1.4.5/src/remoteRF_server/common/idl → remoterf_server-1.4.7/src/remoteRF_server/drivers/adalm_pluto}/pluto_schema.py +19 -4
  6. remoterf_server-1.4.7/src/remoteRF_server/drivers/bluetooth_diag.py +51 -0
  7. remoterf_server-1.4.7/src/remoteRF_server/drivers/gps_diag.py +77 -0
  8. remoterf_server-1.4.7/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +116 -0
  9. remoterf_server-1.4.7/src/remoteRF_server/drivers/usrp/usrp_diag.py +149 -0
  10. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/commands.py +3 -2
  11. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/device_manager.py +30 -0
  12. remoterf_server-1.4.7/src/remoteRF_server/server/diag.py +369 -0
  13. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/dynamic_v2.py +11 -0
  14. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/cluster.py +19 -6
  15. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/grpc_server.py +12 -0
  16. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/reservation.py +86 -0
  17. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/startup_check.py +31 -15
  18. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/serverrf_cli.py +505 -6
  19. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  20. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server.egg-info/SOURCES.txt +9 -0
  21. remoterf_server-1.4.7/tests/test_availability.py +118 -0
  22. remoterf_server-1.4.7/tests/test_devices_add_wizard.py +426 -0
  23. remoterf_server-1.4.7/tests/test_diag.py +578 -0
  24. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_reservation_policy_b.py +12 -0
  25. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/LICENSE +0 -0
  26. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/README.md +0 -0
  27. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/setup.cfg +0 -0
  28. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/__init__.py +0 -0
  29. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/__init__.py +0 -0
  30. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  31. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  32. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  33. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  34. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  35. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  36. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  37. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
  38. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
  39. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  40. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  41. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  42. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  43. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  44. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/idl/__init__.py +0 -0
  45. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  46. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  47. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/idl/schema.py +0 -0
  48. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/idl/v2.py +0 -0
  49. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/__init__.py +0 -0
  50. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  51. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/api_token.py +0 -0
  52. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  53. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/db_location.py +0 -0
  54. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/list_string.py +0 -0
  55. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/panel.py +0 -0
  56. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  57. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/__init__.py +0 -0
  58. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  59. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  60. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  61. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
  62. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  63. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  64. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  65. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  66. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  67. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  68. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  69. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  70. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  71. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  72. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  73. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  74. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  75. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/host/__init__.py +0 -0
  76. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/host/host_auth_token.py +0 -0
  77. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/host/host_directory_store.py +0 -0
  78. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  79. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/__init__.py +0 -0
  80. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/acc_perms.py +0 -0
  81. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/cert_provider.py +0 -0
  82. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/console.py +0 -0
  83. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/control.py +0 -0
  84. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  85. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  86. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/direct_path.py +0 -0
  87. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  88. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/__init__.py +0 -0
  89. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/admin.py +0 -0
  90. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/cli.py +0 -0
  91. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/contracts.py +0 -0
  92. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/destination.py +0 -0
  93. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/devices.py +0 -0
  94. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  95. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/global_home.py +0 -0
  96. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/home.py +0 -0
  97. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  98. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/identity.py +0 -0
  99. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  100. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/migrations.py +0 -0
  101. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/peers.py +0 -0
  102. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/permissions.py +0 -0
  103. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/principals.py +0 -0
  104. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/runtime.py +0 -0
  105. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/federation/trust.py +0 -0
  106. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/global_onboarding.py +0 -0
  107. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  108. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/rpc_manager.py +0 -0
  109. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/user_group_cli.py +0 -0
  110. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/user_group_handler.py +0 -0
  111. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/server/v2_errors.py +0 -0
  112. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/tools/__init__.py +0 -0
  113. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/tools/gen_certs.py +0 -0
  114. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/tools/gist_status.py +0 -0
  115. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  116. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  117. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  118. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  119. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server.egg-info/requires.txt +0 -0
  120. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  121. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_account_hardening.py +0 -0
  122. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_account_names.py +0 -0
  123. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_account_removal.py +0 -0
  124. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_activity_log.py +0 -0
  125. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_bind_host.py +0 -0
  126. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_cert_provider.py +0 -0
  127. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_client_sessions.py +0 -0
  128. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_cluster.py +0 -0
  129. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_commands.py +0 -0
  130. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_console.py +0 -0
  131. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_contrib_drivers.py +0 -0
  132. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_default_rtl_sdr_schema.py +0 -0
  133. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_default_usrp_schema.py +0 -0
  134. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_deployment_capabilities.py +0 -0
  135. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_detached_device_rpc.py +0 -0
  136. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_device_handles.py +0 -0
  137. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_device_init.py +0 -0
  138. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_devices_list.py +0 -0
  139. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_direct_fixed_port.py +0 -0
  140. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_direct_path_server.py +0 -0
  141. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_dynamic_v2.py +0 -0
  142. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_federated_direct_path.py +0 -0
  143. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_federated_dynamic_v2.py +0 -0
  144. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_federation_edges.py +0 -0
  145. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_first_run.py +0 -0
  146. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_d_contracts.py +0 -0
  147. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_e_federation.py +0 -0
  148. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_e_proto.py +0 -0
  149. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_f_enrollment.py +0 -0
  150. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_g1_logging.py +0 -0
  151. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_g_permissions.py +0 -0
  152. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_h_devices.py +0 -0
  153. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gate_i_direct_path.py +0 -0
  154. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_gen_certs.py +0 -0
  155. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_global_accounts_migration.py +0 -0
  156. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_global_home.py +0 -0
  157. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_global_onboarding.py +0 -0
  158. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_global_v1_disposal.py +0 -0
  159. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_hackrf_schema.py +0 -0
  160. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_home_device_plane.py +0 -0
  161. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_host_directory_store.py +0 -0
  162. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_host_dynamic_v2.py +0 -0
  163. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_idl_schema.py +0 -0
  164. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_panel.py +0 -0
  165. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_peer_transport.py +0 -0
  166. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_pluto_compat.py +0 -0
  167. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_pluto_poe.py +0 -0
  168. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_process_arg.py +0 -0
  169. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_reservation_handoff.py +0 -0
  170. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_reservation_timestamps.py +0 -0
  171. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_rpc_manager_batch.py +0 -0
  172. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_rpc_manager_calln.py +0 -0
  173. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_rpc_manager_rx_n.py +0 -0
  174. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_rtl_sdr_schema.py +0 -0
  175. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_soak_federation.py +0 -0
  176. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_startup_check.py +0 -0
  177. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_status_push.py +0 -0
  178. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_ti_mmwave_hardware.py +0 -0
  179. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_ti_mmwave_schema.py +0 -0
  180. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_user_group_cli.py +0 -0
  181. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  182. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_client_server_hardware.py +0 -0
  183. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_client_server_localhost.py +0 -0
  184. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  185. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_preload.py +0 -0
  186. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_profiles.py +0 -0
  187. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  188. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_usrp_uhd410_hardware.py +0 -0
  189. {remoterf_server-1.4.5 → remoterf_server-1.4.7}/tests/test_wipe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.4.5
3
+ Version: 1.4.7
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.4.5"
7
+ version = "1.4.7"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -46,8 +46,24 @@ def _normalize_channels(value):
46
46
 
47
47
  # == PLUTO Connection Logic ==
48
48
 
49
- def connect_pluto(*, serial: str):
49
+ def connect_pluto(*, serial: str, ip: str = ""):
50
50
  serial = (serial or "").strip()
51
+ ip = (ip or "").strip()
52
+ if ip:
53
+ # An always-powered Pluto on the network (its static ipaddr_eth).
54
+ try:
55
+ dev = adi.Pluto(f"ip:{ip}")
56
+ found = str(dev._ctx.attrs.get("hw_serial", "")).strip()
57
+ if serial and found.lower() != serial.lower():
58
+ print(f"ip:{ip} is Pluto {found}, expected {serial}")
59
+ return None
60
+ from remoteRF_server.common.utils.ansi_codes import Sty, printf
61
+
62
+ printf(f"Connected to Pluto via ip:{ip}", Sty.GRAY)
63
+ return dev
64
+ except Exception as e:
65
+ print(f"Failed to connect to Pluto ip:{ip}: {e}")
66
+ return None
51
67
  if not serial:
52
68
  print("A Pluto serial must be provided")
53
69
  return None
@@ -88,7 +104,7 @@ class PlutoSchema(DeviceSchema):
88
104
 
89
105
  device_type = "adalm_pluto"
90
106
  client_class = "Pluto"
91
- driver_version = "0.0.3"
107
+ driver_version = "0.0.4"
92
108
  _fir_rate = None # the rate whose pyadi FIR this schema last loaded
93
109
 
94
110
  @staticmethod
@@ -96,8 +112,7 @@ class PlutoSchema(DeviceSchema):
96
112
  # init.poe: true -> the Pluto is powered from a PoE switch port and starts off.
97
113
  if is_poe(kwargs):
98
114
  return poe_pluto_from_init(kwargs)
99
- serial = kwargs.get("serial")
100
- return connect_pluto(serial=serial)
115
+ return connect_pluto(serial=kwargs.get("serial"), ip=kwargs.get("ip"))
101
116
 
102
117
  # region ad9364
103
118
 
@@ -0,0 +1,274 @@
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
+ """`diag` tests for ADALM-Pluto (USB, PoE, MIMO). Nothing radiates: the tone
17
+ tests use the AD936x internal digital loopback at the lowest TX gain, and a
18
+ Multi-Tap's coax loopback (init `multi_tap: true`) stays at TX <= -10 dB."""
19
+
20
+ from __future__ import annotations
21
+
22
+ import time
23
+
24
+ import numpy as np
25
+
26
+ from remoteRF_server.drivers.adalm_pluto.poe import PoePluto
27
+ from remoteRF_server.server.diag import Skip
28
+
29
+ TX_MIN_DB = -89.75
30
+ FS = 3_000_000
31
+ TONE_N = 6000 # FS / TONE_N = 500 Hz: every offset below is a whole number of cycles
32
+ RX_N = 8192
33
+ OFFSETS = (100_000, 250_000, -300_000)
34
+ MIN_SNR_DB = 20.0
35
+ COAX_TX_DB = (-40, -30, -20, -10) # never above -10 dB
36
+ COAX_RX_DB = (10, 30, 50)
37
+ SNAPSHOT = ("sample_rate", "rx_lo", "tx_lo", "rx_rf_bandwidth", "tx_rf_bandwidth",
38
+ "gain_control_mode_chan0", "rx_hardwaregain_chan0", "tx_hardwaregain_chan0", "rx_buffer_size",
39
+ "tx_cyclic_buffer", "loopback")
40
+ MIMO_SNAPSHOT = ("tx_hardwaregain_chan1", "tx_enabled_channels", "rx_enabled_channels")
41
+
42
+
43
+ def _poe(ctx):
44
+ native = ctx["schema"].device
45
+ return native if isinstance(native, PoePluto) else None
46
+
47
+
48
+ def _rx(schema):
49
+ data = schema.device.rx()
50
+ return np.asarray(data[0] if isinstance(data, (list, tuple)) else data)
51
+
52
+
53
+ def _close(got, want, tol, what):
54
+ if abs(float(got) - float(want)) > tol:
55
+ raise RuntimeError(f"{what}: set {want:.10g}, read back {float(got):.10g}")
56
+
57
+
58
+ def power(ctx):
59
+ schema = ctx["schema"]
60
+ poe = _poe(ctx)
61
+ ctx["was_on"] = poe is None or poe.power_state == "on"
62
+ started = time.monotonic()
63
+ if poe is not None and not ctx["was_on"]:
64
+ poe.power_up()
65
+ rate = int(schema.get_sample_rate())
66
+ mimo = ctx.get("type") == "adalm_pluto_mimo"
67
+ ctx["snapshot"] = {name: getattr(schema, f"get_{name}")() for name in SNAPSHOT + (MIMO_SNAPSHOT if mimo else ())}
68
+ # Quiet the transmitter before anything retunes it: a boot-time DDS tone or a
69
+ # previous holder's cyclic buffer would otherwise follow the LO sweep on air.
70
+ schema.call_disable_dds()
71
+ schema.call_tx_destroy_buffer()
72
+ schema.set_tx_hardwaregain_chan0(TX_MIN_DB)
73
+ if mimo: # every tone test sends and reads channel 0 only
74
+ schema.set_tx_hardwaregain_chan1(TX_MIN_DB)
75
+ schema.set_tx_enabled_channels([0])
76
+ schema.set_rx_enabled_channels([0])
77
+ if ctx["was_on"]:
78
+ return f"answering at {rate} S/s", {"sample_rate": rate}
79
+ took = round(time.monotonic() - started, 1)
80
+ return f"booted in {took} s", {"boot_s": took}
81
+
82
+
83
+ def context(ctx):
84
+ attrs = dict(ctx["schema"].device._ctx.attrs)
85
+ values = {k: str(attrs[k]) for k in ("hw_model", "hw_serial", "fw_version", "ad9361-phy,model") if k in attrs}
86
+ if "hw_serial" not in values:
87
+ raise RuntimeError("IIO context has no hw_serial")
88
+ return f"{values.get('hw_model', 'Pluto')} fw {values.get('fw_version', '?')} sn {values['hw_serial']}", values
89
+
90
+
91
+ def temperature(ctx):
92
+ dev = ctx["schema"].device
93
+ try:
94
+ celsius = float(dev._get_iio_attr("temp0", "input", False)) / 1000
95
+ except Exception:
96
+ raise Skip("no temperature sensor exposed")
97
+ if not 0 < celsius < 95:
98
+ raise RuntimeError(f"{celsius:.1f} °C is out of range")
99
+ return f"{celsius:.1f} °C", {"celsius": celsius}
100
+
101
+
102
+ def sample_rates(ctx):
103
+ schema = ctx["schema"]
104
+ for rate in (1_000_000, 2_500_000, 10_000_000, 20_000_000, FS):
105
+ schema.set_sample_rate(rate)
106
+ _close(schema.get_sample_rate(), rate, rate * 0.01, "sample rate")
107
+ return "1, 2.5, 10, 20, 3 MS/s read back"
108
+
109
+
110
+ def lo_tuning(ctx):
111
+ schema = ctx["schema"]
112
+ freqs = (400e6, 915e6, 2.4e9, 3.5e9) # inside the stock AD9363 range
113
+ for f in freqs:
114
+ schema.set_rx_lo(int(f))
115
+ schema.set_tx_lo(int(f))
116
+ _close(schema.get_rx_lo(), f, 1e3, "RX LO")
117
+ _close(schema.get_tx_lo(), f, 1e3, "TX LO")
118
+ return "RX/TX LO 400 MHz-3.5 GHz read back"
119
+
120
+
121
+ def bandwidth(ctx):
122
+ schema = ctx["schema"]
123
+ for bw in (1_000_000, 5_000_000, 18_000_000):
124
+ schema.set_rx_rf_bandwidth(bw)
125
+ schema.set_tx_rf_bandwidth(bw)
126
+ _close(schema.get_rx_rf_bandwidth(), bw, bw * 0.1, "RX bandwidth")
127
+ _close(schema.get_tx_rf_bandwidth(), bw, bw * 0.1, "TX bandwidth")
128
+ return "RX/TX 1, 5, 18 MHz read back"
129
+
130
+
131
+ def gains(ctx):
132
+ schema = ctx["schema"]
133
+ schema.call_disable_dds() # nothing queued for TX while its gain moves
134
+ for mode in ("slow_attack", "fast_attack", "manual"):
135
+ schema.set_gain_control_mode_chan0(mode)
136
+ if schema.get_gain_control_mode_chan0() != mode:
137
+ raise RuntimeError(f"gain mode {mode} did not stick")
138
+ for gain in (10, 30, 50):
139
+ schema.set_rx_hardwaregain_chan0(gain)
140
+ _close(schema.get_rx_hardwaregain_chan0(), gain, 1, "RX gain")
141
+ for gain in (-60, -75, TX_MIN_DB):
142
+ schema.set_tx_hardwaregain_chan0(gain)
143
+ _close(schema.get_tx_hardwaregain_chan0(), gain, 0.5, "TX gain")
144
+ return "3 gain modes, RX 10/30/50 dB, TX -60/-75/-89.75 dB"
145
+
146
+
147
+ def rx_buffers(ctx):
148
+ schema = ctx["schema"]
149
+ schema.set_sample_rate(FS)
150
+ schema.set_gain_control_mode_chan0("manual")
151
+ schema.set_rx_hardwaregain_chan0(30)
152
+ schema.set_rx_buffer_size(RX_N)
153
+ power = []
154
+ for _ in range(3):
155
+ data = _rx(schema)
156
+ if data.size != RX_N:
157
+ raise RuntimeError(f"read {data.size} samples, expected {RX_N}")
158
+ if not np.all(np.isfinite(data)) or not np.any(data):
159
+ raise RuntimeError("buffer is all zero or not finite")
160
+ power.append(float(np.mean(np.abs(data) ** 2)))
161
+ dbfs = round(10 * np.log10(max(np.mean(power), 1e-12) / 2**22), 1)
162
+ return f"3 x {RX_N} samples, {dbfs} dBFS", {"rx_dbfs": dbfs}
163
+
164
+
165
+ def _tone(schema, offset, tx_db, rx_db):
166
+ """Peak frequency and SNR (dB) of a tone at `offset` Hz sent TX -> RX."""
167
+ schema.set_tx_hardwaregain_chan0(tx_db)
168
+ schema.set_gain_control_mode_chan0("manual")
169
+ schema.set_rx_hardwaregain_chan0(rx_db)
170
+ n = np.arange(TONE_N)
171
+ schema.call_tx_destroy_buffer()
172
+ schema.set_tx_cyclic_buffer(True)
173
+ schema.call_tx(0.5 * 2**14 * np.exp(2j * np.pi * offset * n / FS))
174
+ try:
175
+ schema.call_rx_destroy_buffer()
176
+ for _ in range(2): # the first buffers can predate the tone
177
+ _rx(schema)
178
+ data = _rx(schema)
179
+ finally:
180
+ schema.call_tx_destroy_buffer()
181
+ spectrum = np.abs(np.fft.fftshift(np.fft.fft(data * np.hanning(data.size)))) ** 2
182
+ freqs = np.fft.fftshift(np.fft.fftfreq(data.size, 1 / FS))
183
+ peak = int(np.argmax(spectrum))
184
+ snr = 10 * np.log10(max(float(spectrum[peak]), 1e-30) / max(float(np.median(spectrum)), 1e-30))
185
+ return float(freqs[peak]), round(float(snr), 1)
186
+
187
+
188
+ def _prepare_tone(schema, lo):
189
+ schema.set_sample_rate(FS)
190
+ schema.set_rx_lo(int(lo))
191
+ schema.set_tx_lo(int(lo))
192
+ schema.set_rx_buffer_size(RX_N)
193
+
194
+
195
+ def loopback_tone(ctx):
196
+ schema = ctx["schema"]
197
+ _prepare_tone(schema, 915e6)
198
+ values = {}
199
+ schema.set_loopback(1) # AD936x digital TX -> RX, inside the chip
200
+ try:
201
+ for offset in OFFSETS:
202
+ peak, snr = _tone(schema, offset, TX_MIN_DB, 10)
203
+ values[f"{offset // 1000:+d}kHz"] = {"peak_hz": peak, "snr_db": snr}
204
+ if abs(peak - offset) > 5_000:
205
+ raise RuntimeError(f"tone at {offset / 1e3:+g} kHz came back at {peak / 1e3:+.1f} kHz")
206
+ if snr < MIN_SNR_DB:
207
+ raise RuntimeError(f"tone at {offset / 1e3:+g} kHz: SNR {snr} dB < {MIN_SNR_DB}")
208
+ finally:
209
+ schema.set_loopback(0)
210
+ worst = min(v["snr_db"] for v in values.values())
211
+ return f"{len(OFFSETS)} offsets found, SNR >= {worst} dB", values
212
+
213
+
214
+ def coax_loopback(ctx):
215
+ if not ctx["init"].get("multi_tap"):
216
+ raise Skip("not a Multi-Tap")
217
+ schema = ctx["schema"]
218
+ _prepare_tone(schema, 915e6)
219
+ schema.set_loopback(0)
220
+ for tx_db in COAX_TX_DB:
221
+ for rx_db in COAX_RX_DB:
222
+ peak, snr = _tone(schema, OFFSETS[0], tx_db, rx_db)
223
+ if abs(peak - OFFSETS[0]) <= 5_000 and snr >= MIN_SNR_DB:
224
+ return f"tone at TX {tx_db} dB / RX {rx_db} dB, SNR {snr} dB", {
225
+ "tx_db": tx_db, "rx_db": rx_db, "snr_db": snr}
226
+ raise RuntimeError(f"no tone over the coax up to TX {COAX_TX_DB[-1]} dB / RX {COAX_RX_DB[-1]} dB")
227
+
228
+
229
+ def _boot_s(schema):
230
+ """A PoE Pluto in standby boots in ~30 s."""
231
+ native = schema.device
232
+ return 30 if isinstance(native, PoePluto) and native.power_state != "on" else 1
233
+
234
+
235
+ # (name, typical s, timeout s, fn); the first is the gate.
236
+ TESTS = [
237
+ ("power", _boot_s, 100, power),
238
+ ("iio context", 1, 10, context),
239
+ ("temperature", 1, 10, temperature),
240
+ ("sample rate", 5, 40, sample_rates),
241
+ ("lo tuning", 3, 30, lo_tuning),
242
+ ("bandwidth", 2, 20, bandwidth),
243
+ ("gain", 2, 20, gains),
244
+ ("rx buffers", 2, 20, rx_buffers),
245
+ ("loopback tone", 4, 40, loopback_tone),
246
+ ("coax loopback", 2, 60, coax_loopback),
247
+ ]
248
+
249
+
250
+ def teardown(ctx):
251
+ """Stop TX; a PoE Pluto that was in standby goes back to it unless now reserved,
252
+ anything else gets its settings back."""
253
+ schema = ctx["schema"]
254
+ poe = _poe(ctx)
255
+ if poe is not None and not ctx.get("was_on", True) and not ctx["reserved"]():
256
+ schema.call_power_down()
257
+ return
258
+ schema.call_tx_destroy_buffer()
259
+ schema.call_rx_destroy_buffer()
260
+ schema.set_loopback(0)
261
+ snapshot = dict(ctx.get("snapshot", {}))
262
+ if snapshot.get("gain_control_mode_chan0") != "manual":
263
+ snapshot.pop("rx_hardwaregain_chan0", None) # an AGC mode owns it
264
+ failed = []
265
+ for name, value in snapshot.items():
266
+ try:
267
+ getattr(schema, f"set_{name}")(value)
268
+ except Exception:
269
+ failed.append(name)
270
+ if failed:
271
+ raise RuntimeError(f"could not restore {', '.join(failed)}")
272
+
273
+
274
+ TEARDOWN_S = (2, 30)
@@ -46,8 +46,24 @@ def _normalize_channels(value):
46
46
 
47
47
  # == PLUTO Connection Logic ==
48
48
 
49
- def connect_pluto(*, serial: str):
49
+ def connect_pluto(*, serial: str, ip: str = ""):
50
50
  serial = (serial or "").strip()
51
+ ip = (ip or "").strip()
52
+ if ip:
53
+ # An always-powered Pluto on the network (its static ipaddr_eth).
54
+ try:
55
+ dev = adi.Pluto(f"ip:{ip}")
56
+ found = str(dev._ctx.attrs.get("hw_serial", "")).strip()
57
+ if serial and found.lower() != serial.lower():
58
+ print(f"ip:{ip} is Pluto {found}, expected {serial}")
59
+ return None
60
+ from remoteRF_server.common.utils.ansi_codes import Sty, printf
61
+
62
+ printf(f"Connected to Pluto via ip:{ip}", Sty.GRAY)
63
+ return dev
64
+ except Exception as e:
65
+ print(f"Failed to connect to Pluto ip:{ip}: {e}")
66
+ return None
51
67
  if not serial:
52
68
  print("A Pluto serial must be provided")
53
69
  return None
@@ -88,7 +104,7 @@ class PlutoSchema(DeviceSchema):
88
104
 
89
105
  device_type = "adalm_pluto"
90
106
  client_class = "Pluto"
91
- driver_version = "0.0.3"
107
+ driver_version = "0.0.4"
92
108
  _fir_rate = None # the rate whose pyadi FIR this schema last loaded
93
109
 
94
110
  @staticmethod
@@ -96,8 +112,7 @@ class PlutoSchema(DeviceSchema):
96
112
  # init.poe: true -> the Pluto is powered from a PoE switch port and starts off.
97
113
  if is_poe(kwargs):
98
114
  return poe_pluto_from_init(kwargs)
99
- serial = kwargs.get("serial")
100
- return connect_pluto(serial=serial)
115
+ return connect_pluto(serial=kwargs.get("serial"), ip=kwargs.get("ip"))
101
116
 
102
117
  # region ad9364
103
118
 
@@ -0,0 +1,51 @@
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
+ """`diag` tests for a Bluetooth adapter (contrib/drivers/bluetooth_schema.py)."""
17
+
18
+ from __future__ import annotations
19
+
20
+ SCAN_S = 5
21
+
22
+
23
+ def powered(ctx):
24
+ schema = ctx["schema"]
25
+ if not schema.get_powered():
26
+ raise RuntimeError("adapter is not powered")
27
+ return f"{schema.get_address()} powered", {"address": str(schema.get_address())}
28
+
29
+
30
+ def scan(ctx):
31
+ summary = ctx["schema"].call_scan_summary(timeout=SCAN_S)
32
+ if not isinstance(summary, dict):
33
+ raise RuntimeError("scan returned nothing")
34
+ count = summary.get("total_devices", "?")
35
+ return f"{SCAN_S} s scan, {count} devices nearby", {"devices": count}
36
+
37
+
38
+ TESTS = [
39
+ ("adapter", 1, 10, powered),
40
+ ("scan", SCAN_S, SCAN_S + 15, scan),
41
+ ]
42
+
43
+
44
+ def teardown(ctx):
45
+ """The next holder must not read diag's scan of nearby devices."""
46
+ native = ctx["schema"].device
47
+ native.device_cache = {}
48
+ native.last_scan_time = 0.0
49
+
50
+
51
+ TEARDOWN_S = (0, 5)
@@ -0,0 +1,77 @@
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
+ """`diag` tests for an NMEA GPS receiver (contrib/drivers/gps_schema.py)."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import time
21
+ from functools import reduce
22
+
23
+ from remoteRF_server.server.diag import Skip
24
+
25
+ LISTEN_S = 6
26
+
27
+
28
+ def _checksum_ok(line: str) -> bool:
29
+ body, _, given = line.strip().lstrip("$").partition("*")
30
+ try:
31
+ return reduce(lambda a, c: a ^ ord(c), body, 0) == int(given[:2], 16)
32
+ except ValueError:
33
+ return False
34
+
35
+
36
+ def liveness(ctx):
37
+ schema = ctx["schema"]
38
+ first = list(schema.call_read_raw(50))
39
+ end = time.monotonic() + LISTEN_S
40
+ while time.monotonic() < end:
41
+ time.sleep(0.5)
42
+ now = list(schema.call_read_raw(50))
43
+ if now and now != first:
44
+ ctx["lines"] = now
45
+ return f"new sentences within {LISTEN_S} s", {"sentences": len(now)}
46
+ raise RuntimeError(f"no new NMEA sentence in {LISTEN_S} s")
47
+
48
+
49
+ def sentence_types(ctx):
50
+ kinds = sorted({line.strip().lstrip("$").split(",")[0][2:] for line in ctx["lines"] if line.strip().startswith("$")})
51
+ if not {"GGA", "RMC"} & set(kinds):
52
+ raise RuntimeError(f"no GGA or RMC among {', '.join(kinds) or 'nothing'}")
53
+ return ", ".join(kinds), {"types": kinds}
54
+
55
+
56
+ def parse(ctx):
57
+ lines = [l for l in ctx["lines"] if "*" in l]
58
+ good = sum(_checksum_ok(l) for l in lines)
59
+ if not lines or good < 0.8 * len(lines):
60
+ raise RuntimeError(f"{good} of {len(lines)} checksums valid")
61
+ return f"{good}/{len(lines)} checksums valid", {"valid": good, "total": len(lines)}
62
+
63
+
64
+ def fix(ctx):
65
+ schema = ctx["schema"]
66
+ if not schema.get_has_fix():
67
+ raise Skip("no fix yet (optional)")
68
+ values = {"satellites": int(schema.get_satellites()), "quality": int(schema.get_fix_quality())}
69
+ return f"fix, {values['satellites']} satellites", values
70
+
71
+
72
+ TESTS = [
73
+ ("nmea liveness", LISTEN_S, LISTEN_S + 5, liveness),
74
+ ("sentence types", 0, 5, sentence_types),
75
+ ("checksums", 0, 5, parse),
76
+ ("fix", 0, 5, fix),
77
+ ]
@@ -0,0 +1,116 @@
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
+ """`diag` tests for RTL-SDR (receive only)."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import numpy as np
21
+
22
+ SNAPSHOT = ("sample_rate", "center_freq", "gain")
23
+
24
+
25
+ def _close(got, want, tol, what):
26
+ if abs(float(got) - float(want)) > tol:
27
+ raise RuntimeError(f"{what}: set {want:.10g}, read back {float(got):.10g}")
28
+
29
+
30
+ def tuner(ctx):
31
+ schema = ctx["schema"]
32
+ ctx["snapshot"] = {name: getattr(schema, f"get_{name}")() for name in SNAPSHOT}
33
+ values = {"tuner": str(schema.get_tuner_type()), "serial": str(schema.get_serial_number() or "")}
34
+ return f"tuner {values['tuner']}, sn {values['serial'] or '-'}", values
35
+
36
+
37
+ def sample_rates(ctx):
38
+ schema = ctx["schema"]
39
+ for rate in (1_024_000, 2_048_000, 2_400_000):
40
+ schema.set_sample_rate(rate)
41
+ _close(schema.get_sample_rate(), rate, rate * 0.01, "sample rate")
42
+ return "1.024, 2.048, 2.4 MS/s read back"
43
+
44
+
45
+ def center_freqs(ctx):
46
+ schema = ctx["schema"]
47
+ for f in (100e6, 433.92e6, 915e6, 1.5e9):
48
+ schema.set_center_freq(int(f))
49
+ _close(schema.get_center_freq(), f, 10e3, "center frequency")
50
+ return "100 MHz-1.5 GHz read back"
51
+
52
+
53
+ def gains(ctx):
54
+ schema = ctx["schema"]
55
+ valid = sorted(float(g) for g in schema.get_valid_gains_db())
56
+ if not valid:
57
+ raise RuntimeError("tuner reports no gains")
58
+ unreported = 0
59
+ for gain in (valid[0], valid[len(valid) // 2], valid[-1]):
60
+ schema.set_gain(gain)
61
+ got = schema.get_gain()
62
+ if gain and not float(got): # RTL-SDR Blog V4 (R828D) reads 0 for any manual gain
63
+ unreported += 1
64
+ continue
65
+ _close(got, gain, 1, "gain")
66
+ note = ", tuner does not report gain" if unreported else ""
67
+ return f"{len(valid)} gains, {valid[0]:g}-{valid[-1]:g} dB set{note}", {"gains": len(valid)}
68
+
69
+
70
+ def byte_reads(ctx):
71
+ schema = ctx["schema"]
72
+ schema.set_sample_rate(2_048_000)
73
+ schema.set_center_freq(100_000_000)
74
+ try:
75
+ schema.call_reset_buffer()
76
+ except Exception: # noqa: BLE001 - optional in pyrtlsdr; the server's reads never need it
77
+ pass
78
+ for n in (512, 512 * 32, 512 * 256):
79
+ data = np.frombuffer(bytes(schema.call_read_bytes(n)), dtype=np.uint8)
80
+ if data.size != n or data.size % 512:
81
+ raise RuntimeError(f"asked {n} bytes, got {data.size}")
82
+ mean = float(data.mean())
83
+ if data.std() == 0:
84
+ raise RuntimeError(f"every byte is {int(data[0])}")
85
+ if not 64 < mean < 192:
86
+ raise RuntimeError(f"bytes average {mean:.0f}, expected near 127.5")
87
+ return f"512 / 16 KiB / 128 KiB reads, mean {mean:.1f}", {"byte_mean": round(mean, 1)}
88
+
89
+
90
+ def samples(ctx):
91
+ data = np.asarray(ctx["schema"].call_read_samples(16384))
92
+ if data.size != 16384 or not np.all(np.isfinite(data)):
93
+ raise RuntimeError("short or non-finite IQ read")
94
+ dc = float(abs(data.mean()))
95
+ if dc > 0.5 or float(data.std()) == 0:
96
+ raise RuntimeError(f"IQ looks stuck (dc {dc:.2f})")
97
+ return f"16384 IQ samples, dc {dc:.3f}", {"dc": round(dc, 4)}
98
+
99
+
100
+ # (name, typical s, timeout s, fn); the first is the gate.
101
+ TESTS = [
102
+ ("tuner", 1, 10, tuner),
103
+ ("sample rate", 1, 15, sample_rates),
104
+ ("center freq", 1, 15, center_freqs),
105
+ ("gain", 1, 15, gains),
106
+ ("byte reads", 1, 15, byte_reads),
107
+ ("iq samples", 1, 15, samples),
108
+ ]
109
+
110
+
111
+ def teardown(ctx):
112
+ for name, value in ctx.get("snapshot", {}).items():
113
+ getattr(ctx["schema"], f"set_{name}")(value)
114
+
115
+
116
+ TEARDOWN_S = (1, 10)