AlLoRa 2.0.0__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. allora-3.0.0/AlLoRa/Connectors/Connector.py +500 -0
  2. {allora-2.0.0 → allora-3.0.0}/AlLoRa/Connectors/E5_connector.py +5 -6
  3. {allora-2.0.0 → allora-3.0.0}/AlLoRa/Connectors/LoPy4_connector.py +1 -1
  4. allora-3.0.0/AlLoRa/Connectors/Loopback_connector.py +90 -0
  5. allora-3.0.0/AlLoRa/Connectors/SX1262_connector.py +237 -0
  6. {allora-2.0.0 → allora-3.0.0}/AlLoRa/Connectors/SX127x_connector.py +19 -7
  7. allora-3.0.0/AlLoRa/Connectors/Split/Adapter.py +209 -0
  8. allora-3.0.0/AlLoRa/Connectors/Split/Link.py +33 -0
  9. allora-3.0.0/AlLoRa/Connectors/Split/Loopback_link.py +44 -0
  10. allora-3.0.0/AlLoRa/Connectors/Split/Serial_adapter.py +21 -0
  11. allora-3.0.0/AlLoRa/Connectors/Split/Serial_connector.py +171 -0
  12. allora-3.0.0/AlLoRa/Connectors/Split/Serial_link.py +319 -0
  13. allora-3.0.0/AlLoRa/Connectors/Split/Tunnel_connector.py +332 -0
  14. allora-3.0.0/AlLoRa/Connectors/Split/USB_adapter.py +64 -0
  15. allora-3.0.0/AlLoRa/Connectors/Split/WiFi_adapter.py +73 -0
  16. allora-3.0.0/AlLoRa/Connectors/Split/WiFi_connector.py +18 -0
  17. allora-3.0.0/AlLoRa/Connectors/Split/WiFi_link.py +165 -0
  18. allora-3.0.0/AlLoRa/Connectors/Split/tunnel_codec.py +145 -0
  19. allora-3.0.0/AlLoRa/Control/Control_Actuator.py +20 -0
  20. allora-3.0.0/AlLoRa/Control/Control_Root.py +73 -0
  21. allora-3.0.0/AlLoRa/Control/Control_Root_DataSink.py +184 -0
  22. allora-3.0.0/AlLoRa/Control/Node_Control_Actuator.py +71 -0
  23. allora-3.0.0/AlLoRa/Control/control_envelope.py +25 -0
  24. allora-3.0.0/AlLoRa/Control/control_types.py +31 -0
  25. allora-3.0.0/AlLoRa/Data/Loop_guard.py +34 -0
  26. allora-3.0.0/AlLoRa/Data/Sinks/DataSink.py +53 -0
  27. allora-3.0.0/AlLoRa/Data/Sinks/Disk_DataSink.py +18 -0
  28. allora-3.0.0/AlLoRa/Data/Sinks/HTTP_DataSink.py +136 -0
  29. allora-3.0.0/AlLoRa/Data/Sinks/MQTT_DataSink.py +117 -0
  30. allora-3.0.0/AlLoRa/Data/Sinks/message_template.py +401 -0
  31. allora-3.0.0/AlLoRa/Data/Sources/DataSource.py +174 -0
  32. allora-3.0.0/AlLoRa/Data/Sources/Disk_DataSource.py +542 -0
  33. allora-3.0.0/AlLoRa/Data/Sources/MQTT_DataSource.py +115 -0
  34. allora-3.0.0/AlLoRa/Data/Sources/Serial_DataSource.py +436 -0
  35. allora-3.0.0/AlLoRa/Data/mqtt_naming.py +64 -0
  36. allora-3.0.0/AlLoRa/Digital_Endpoint.py +468 -0
  37. allora-3.0.0/AlLoRa/File.py +344 -0
  38. allora-3.0.0/AlLoRa/Management/HTTP_Management_Source.py +300 -0
  39. allora-3.0.0/AlLoRa/Management/HTTP_Status_Subscriber.py +334 -0
  40. allora-3.0.0/AlLoRa/Management/Logger.py +217 -0
  41. allora-3.0.0/AlLoRa/Management/Management_Source.py +69 -0
  42. allora-3.0.0/AlLoRa/Nodes/Edge.py +122 -0
  43. allora-3.0.0/AlLoRa/Nodes/Hub.py +1662 -0
  44. allora-3.0.0/AlLoRa/Nodes/Node.py +2169 -0
  45. allora-3.0.0/AlLoRa/Protocol/Codec.py +261 -0
  46. allora-3.0.0/AlLoRa/Protocol/Pacing.py +121 -0
  47. allora-3.0.0/AlLoRa/Protocol/Packet.py +475 -0
  48. allora-2.0.0/AlLoRa/Packet.py → allora-3.0.0/AlLoRa/Protocol/Packet_v2.py +26 -14
  49. allora-3.0.0/AlLoRa/Protocol/Security/AEAD.py +194 -0
  50. allora-3.0.0/AlLoRa/Protocol/Security/Replay_window.py +36 -0
  51. allora-3.0.0/AlLoRa/Protocol/Security/Session.py +47 -0
  52. allora-3.0.0/AlLoRa/Protocol/Security/Session_store.py +36 -0
  53. allora-3.0.0/AlLoRa/Protocol/Security/__init__.py +3 -0
  54. allora-3.0.0/AlLoRa/Protocol/Security/ec_p256.py +360 -0
  55. allora-3.0.0/AlLoRa/Protocol/Security/handshake.py +65 -0
  56. allora-3.0.0/AlLoRa/Protocol/Security/hmac_sha256.py +36 -0
  57. allora-3.0.0/AlLoRa/Protocol/Security/identity.py +35 -0
  58. allora-3.0.0/AlLoRa/Protocol/Security/kdf.py +67 -0
  59. allora-3.0.0/AlLoRa/Protocol/negotiation.py +23 -0
  60. allora-3.0.0/AlLoRa/Status.py +35 -0
  61. allora-3.0.0/AlLoRa/__init__.py +1 -0
  62. allora-3.0.0/AlLoRa/utils/debug_utils.py +39 -0
  63. allora-3.0.0/AlLoRa/utils/file_utils.py +78 -0
  64. allora-3.0.0/AlLoRa/utils/os_utils.py +19 -0
  65. {allora-2.0.0 → allora-3.0.0}/AlLoRa/utils/time_utils.py +20 -0
  66. allora-3.0.0/AlLoRa.egg-info/PKG-INFO +944 -0
  67. allora-3.0.0/AlLoRa.egg-info/SOURCES.txt +190 -0
  68. allora-3.0.0/AlLoRa.egg-info/requires.txt +1 -0
  69. allora-3.0.0/AlLoRa.egg-info/top_level.txt +1 -0
  70. allora-3.0.0/PKG-INFO +944 -0
  71. allora-3.0.0/README.md +915 -0
  72. {allora-2.0.0 → allora-3.0.0}/setup.py +12 -3
  73. allora-3.0.0/tests/test_adapters.py +252 -0
  74. allora-3.0.0/tests/test_aead.py +276 -0
  75. allora-3.0.0/tests/test_benchmark_parser.py +44 -0
  76. allora-3.0.0/tests/test_boundary_config.py +440 -0
  77. allora-3.0.0/tests/test_chunk_size_ceiling.py +77 -0
  78. allora-3.0.0/tests/test_chunk_size_one_store.py +270 -0
  79. allora-3.0.0/tests/test_codec.py +364 -0
  80. allora-3.0.0/tests/test_config_persistence.py +514 -0
  81. allora-3.0.0/tests/test_config_schema.py +156 -0
  82. allora-3.0.0/tests/test_connector_transport.py +88 -0
  83. allora-3.0.0/tests/test_control_root_minting.py +144 -0
  84. allora-3.0.0/tests/test_control_root_provisioning.py +425 -0
  85. allora-3.0.0/tests/test_control_root_sink.py +566 -0
  86. allora-3.0.0/tests/test_custom_command_example.py +24 -0
  87. allora-3.0.0/tests/test_custom_control.py +104 -0
  88. allora-3.0.0/tests/test_damaged_frame_parsing.py +173 -0
  89. allora-3.0.0/tests/test_data_sink.py +488 -0
  90. allora-3.0.0/tests/test_datasource_base.py +92 -0
  91. allora-3.0.0/tests/test_device_section.py +304 -0
  92. allora-3.0.0/tests/test_disk_datasource.py +455 -0
  93. allora-3.0.0/tests/test_driver_register_order.py +143 -0
  94. allora-3.0.0/tests/test_ec_p256.py +418 -0
  95. allora-3.0.0/tests/test_endpoint_rf.py +203 -0
  96. allora-3.0.0/tests/test_file_folders.py +46 -0
  97. allora-3.0.0/tests/test_file_reassembly.py +121 -0
  98. allora-3.0.0/tests/test_generic_main.py +390 -0
  99. allora-3.0.0/tests/test_handshake.py +136 -0
  100. allora-3.0.0/tests/test_handshake_did.py +112 -0
  101. allora-3.0.0/tests/test_handshake_flow.py +80 -0
  102. allora-3.0.0/tests/test_hmac_sha256.py +63 -0
  103. allora-3.0.0/tests/test_http_management_source.py +386 -0
  104. allora-3.0.0/tests/test_http_status_subscriber.py +687 -0
  105. allora-3.0.0/tests/test_hub_ask_change_rf.py +452 -0
  106. allora-3.0.0/tests/test_hub_endpoints.py +454 -0
  107. allora-3.0.0/tests/test_hub_kwargs.py +68 -0
  108. allora-3.0.0/tests/test_hub_mqtt_example.py +77 -0
  109. allora-3.0.0/tests/test_hub_send_control.py +78 -0
  110. allora-3.0.0/tests/test_identity.py +54 -0
  111. allora-3.0.0/tests/test_idle_or_unreachable.py +134 -0
  112. allora-3.0.0/tests/test_in_band_control.py +298 -0
  113. allora-3.0.0/tests/test_kdf.py +98 -0
  114. allora-3.0.0/tests/test_logger_handler.py +21 -0
  115. allora-3.0.0/tests/test_loopback_transfer.py +112 -0
  116. allora-3.0.0/tests/test_management_plane.py +1237 -0
  117. allora-3.0.0/tests/test_match_prefix.py +45 -0
  118. allora-3.0.0/tests/test_message_template.py +381 -0
  119. allora-3.0.0/tests/test_micropython_compiles.py +54 -0
  120. allora-3.0.0/tests/test_micropython_portability.py +162 -0
  121. allora-3.0.0/tests/test_mixed_fleet.py +325 -0
  122. allora-3.0.0/tests/test_module_header_length.py +78 -0
  123. allora-3.0.0/tests/test_mqtt_bridge.py +198 -0
  124. allora-3.0.0/tests/test_mqtt_datasource.py +128 -0
  125. allora-3.0.0/tests/test_mqtt_naming.py +46 -0
  126. allora-3.0.0/tests/test_node_control_actuator.py +225 -0
  127. allora-3.0.0/tests/test_on_demand_serving.py +338 -0
  128. allora-3.0.0/tests/test_pacing.py +165 -0
  129. allora-3.0.0/tests/test_pacing_per_link.py +116 -0
  130. allora-3.0.0/tests/test_packet_v3_codec.py +244 -0
  131. allora-3.0.0/tests/test_packet_v3_secure.py +161 -0
  132. allora-3.0.0/tests/test_payload_ceiling.py +170 -0
  133. allora-3.0.0/tests/test_payload_crc_drop.py +185 -0
  134. allora-3.0.0/tests/test_provision_adopt_port.py +125 -0
  135. allora-3.0.0/tests/test_provision_board.py +278 -0
  136. allora-3.0.0/tests/test_provision_cli.py +824 -0
  137. allora-3.0.0/tests/test_provision_doctor.py +182 -0
  138. allora-3.0.0/tests/test_provision_flash_recovery.py +145 -0
  139. allora-3.0.0/tests/test_provision_fleet.py +447 -0
  140. allora-3.0.0/tests/test_provision_node_config.py +215 -0
  141. allora-3.0.0/tests/test_provision_plan.py +808 -0
  142. allora-3.0.0/tests/test_provision_setup.py +658 -0
  143. allora-3.0.0/tests/test_readme_refs.py +68 -0
  144. allora-3.0.0/tests/test_reassembly_length_check.py +150 -0
  145. allora-3.0.0/tests/test_replay_window.py +45 -0
  146. allora-3.0.0/tests/test_request_respond.py +72 -0
  147. allora-3.0.0/tests/test_rf_atomicity.py +121 -0
  148. allora-3.0.0/tests/test_rf_coordination.py +622 -0
  149. allora-3.0.0/tests/test_role_reversal.py +852 -0
  150. allora-3.0.0/tests/test_secure_refuse.py +99 -0
  151. allora-3.0.0/tests/test_secure_role_reversal.py +931 -0
  152. allora-3.0.0/tests/test_secure_transfer.py +143 -0
  153. allora-3.0.0/tests/test_serial_datasource.py +558 -0
  154. allora-3.0.0/tests/test_serial_link.py +283 -0
  155. allora-3.0.0/tests/test_serve_engine.py +540 -0
  156. allora-3.0.0/tests/test_session.py +83 -0
  157. allora-3.0.0/tests/test_session_store.py +68 -0
  158. allora-3.0.0/tests/test_sid_collision.py +104 -0
  159. allora-3.0.0/tests/test_sid_derivation.py +61 -0
  160. allora-3.0.0/tests/test_source_serves_what_it_announced.py +235 -0
  161. allora-3.0.0/tests/test_status.py +181 -0
  162. allora-3.0.0/tests/test_sx1262_target.py +496 -0
  163. allora-3.0.0/tests/test_sx126x_driver.py +325 -0
  164. allora-3.0.0/tests/test_sx127x_listens_after_send.py +135 -0
  165. allora-3.0.0/tests/test_time_utils.py +46 -0
  166. allora-3.0.0/tests/test_timeout_guards.py +140 -0
  167. allora-3.0.0/tests/test_toa_model.py +84 -0
  168. allora-3.0.0/tests/test_transport_recovery.py +433 -0
  169. allora-3.0.0/tests/test_tunnel_codec.py +134 -0
  170. allora-3.0.0/tests/test_tunnel_connector_mac.py +69 -0
  171. allora-3.0.0/tests/test_tunnel_link.py +47 -0
  172. allora-3.0.0/tests/test_tunnel_transfer.py +108 -0
  173. allora-3.0.0/tests/test_tunnel_transport.py +233 -0
  174. allora-3.0.0/tests/test_usb_adapter.py +376 -0
  175. allora-3.0.0/tests/test_v2_tag_interop.py +119 -0
  176. allora-3.0.0/tests/test_v3_negotiation.py +78 -0
  177. allora-3.0.0/tests/test_v3_open_transfer.py +247 -0
  178. allora-3.0.0/tests/test_wifi_link.py +106 -0
  179. allora-2.0.0/AlLoRa/Connectors/Connector.py +0 -323
  180. allora-2.0.0/AlLoRa/Connectors/SX1262_connector.py +0 -119
  181. allora-2.0.0/AlLoRa/Connectors/Serial_connector.py +0 -262
  182. allora-2.0.0/AlLoRa/Connectors/Wifi_connector.py +0 -124
  183. allora-2.0.0/AlLoRa/DataSource.py +0 -118
  184. allora-2.0.0/AlLoRa/Digital_Endpoint.py +0 -189
  185. allora-2.0.0/AlLoRa/File.py +0 -147
  186. allora-2.0.0/AlLoRa/Interfaces/Interface.py +0 -18
  187. allora-2.0.0/AlLoRa/Interfaces/Serial_interface.py +0 -250
  188. allora-2.0.0/AlLoRa/Interfaces/WiFi_interface.py +0 -268
  189. allora-2.0.0/AlLoRa/Nodes/Adapter.py +0 -57
  190. allora-2.0.0/AlLoRa/Nodes/Gateway.py +0 -103
  191. allora-2.0.0/AlLoRa/Nodes/Node.py +0 -167
  192. allora-2.0.0/AlLoRa/Nodes/Requester.py +0 -490
  193. allora-2.0.0/AlLoRa/Nodes/Source.py +0 -295
  194. allora-2.0.0/AlLoRa/utils/debug_utils.py +0 -6
  195. allora-2.0.0/AlLoRa/utils/os_utils.py +0 -6
  196. allora-2.0.0/AlLoRa.egg-info/PKG-INFO +0 -377
  197. allora-2.0.0/AlLoRa.egg-info/SOURCES.txt +0 -77
  198. allora-2.0.0/AlLoRa.egg-info/top_level.txt +0 -2
  199. allora-2.0.0/PKG-INFO +0 -377
  200. allora-2.0.0/README.md +0 -351
  201. allora-2.0.0/examples/Adapters/Serial_adapter/main.py +0 -15
  202. allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client/boot.py +0 -4
  203. allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client/main_ESP32.py +0 -7
  204. allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client/main_LoPy4.py +0 -7
  205. allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot/boot.py +0 -4
  206. allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot/main_ESP32.py +0 -9
  207. allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot/main_LoPy4.py +0 -7
  208. allora-2.0.0/examples/Gateways/Serial_Gateway/__init__.py +0 -0
  209. allora-2.0.0/examples/Gateways/Serial_Gateway/main.py +0 -31
  210. allora-2.0.0/examples/Gateways/Serial_Gateway/rf_changer.py +0 -57
  211. allora-2.0.0/examples/Gateways/Wifi_Gateway/__init__.py +0 -0
  212. allora-2.0.0/examples/Gateways/Wifi_Gateway/main.py +0 -15
  213. allora-2.0.0/examples/Gateways/__init__.py +0 -0
  214. allora-2.0.0/examples/Requesters/LopyRequester/__init__.py +0 -0
  215. allora-2.0.0/examples/Requesters/LopyRequester/boot.py +0 -11
  216. allora-2.0.0/examples/Requesters/LopyRequester/main.py +0 -27
  217. allora-2.0.0/examples/Requesters/T3S3/__init__.py +0 -0
  218. allora-2.0.0/examples/Requesters/T3S3/main.py +0 -34
  219. allora-2.0.0/examples/Requesters/T3S3/main_pro.py +0 -52
  220. allora-2.0.0/examples/Requesters/__init__.py +0 -0
  221. allora-2.0.0/examples/Sources/ESP32/__init__.py +0 -0
  222. allora-2.0.0/examples/Sources/ESP32/boot.py +0 -3
  223. allora-2.0.0/examples/Sources/ESP32/main_Heltec.py +0 -62
  224. allora-2.0.0/examples/Sources/ESP32/main_TTGO.py +0 -80
  225. allora-2.0.0/examples/Sources/LopySource/__init__.py +0 -0
  226. allora-2.0.0/examples/Sources/LopySource/boot.py +0 -11
  227. allora-2.0.0/examples/Sources/LopySource/main.py +0 -64
  228. allora-2.0.0/examples/Sources/T3S3/__init__.py +0 -0
  229. allora-2.0.0/examples/Sources/T3S3/main.py +0 -57
  230. allora-2.0.0/examples/Sources/T3S3/main_pro.py +0 -125
  231. allora-2.0.0/examples/Sources/T3S3-Tester/__init__.py +0 -0
  232. allora-2.0.0/examples/Sources/T3S3-Tester/main.py +0 -75
  233. allora-2.0.0/examples/Sources/XIAO-ESP32S3/__init__.py +0 -0
  234. allora-2.0.0/examples/Sources/XIAO-ESP32S3/main.py +0 -57
  235. allora-2.0.0/examples/Sources/XIAO-ESP32S3/main_pro.py +0 -121
  236. allora-2.0.0/examples/Sources/__init__.py +0 -0
  237. allora-2.0.0/examples/__init__.py +0 -0
  238. {allora-2.0.0/AlLoRa/Connectors → allora-3.0.0/AlLoRa/Connectors/Split}/__init__.py +0 -0
  239. {allora-2.0.0/AlLoRa/Interfaces → allora-3.0.0/AlLoRa/Connectors}/__init__.py +0 -0
  240. {allora-2.0.0/AlLoRa/Nodes → allora-3.0.0/AlLoRa/Control}/__init__.py +0 -0
  241. {allora-2.0.0/AlLoRa → allora-3.0.0/AlLoRa/Data/Sinks}/__init__.py +0 -0
  242. {allora-2.0.0/AlLoRa/utils → allora-3.0.0/AlLoRa/Data/Sources}/__init__.py +0 -0
  243. {allora-2.0.0/examples/Adapters/Serial_adapter → allora-3.0.0/AlLoRa/Data}/__init__.py +0 -0
  244. {allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client → allora-3.0.0/AlLoRa/Management}/__init__.py +0 -0
  245. {allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot → allora-3.0.0/AlLoRa/Nodes}/__init__.py +0 -0
  246. {allora-2.0.0/examples/Adapters/WiFi_adapters → allora-3.0.0/AlLoRa/Protocol}/__init__.py +0 -0
  247. {allora-2.0.0/examples/Adapters → allora-3.0.0/AlLoRa/utils}/__init__.py +0 -0
  248. {allora-2.0.0 → allora-3.0.0}/AlLoRa/utils/json_utils.py +0 -0
  249. {allora-2.0.0 → allora-3.0.0}/AlLoRa.egg-info/dependency_links.txt +0 -0
  250. {allora-2.0.0 → allora-3.0.0}/LICENSE +0 -0
  251. {allora-2.0.0 → allora-3.0.0}/setup.cfg +0 -0
@@ -0,0 +1,500 @@
1
+ from AlLoRa.Protocol.Packet_v2 import Packet_v2
2
+ from AlLoRa.Protocol.Pacing import Pacing
3
+ from AlLoRa.Protocol.Codec import build_codec
4
+ import gc
5
+ from math import ceil
6
+ from AlLoRa.utils.time_utils import get_time, current_time_ms as time, sleep, sleep_ms
7
+ from AlLoRa.utils.debug_utils import print
8
+ from AlLoRa.utils.os_utils import os
9
+ from os import urandom
10
+
11
+ class Connector:
12
+ MAX_LENGTH_MESSAGE = 255
13
+
14
+ def __init__(self):
15
+ self.MAC = "00000000"
16
+ self.pacing = Pacing() # one home for the adaptive receive window (was adaptive_timeout)
17
+ self.codec = build_codec() # how a Packet is spoken on the wire; rebuilt in config()
18
+ self.debug = False
19
+ self.protocol_version = 2 # config() decides; this is its default until it runs
20
+ self.frame_size = None # set by the node once it knows its clamped chunk size
21
+ # A frame the radio received and threw away as damaged is not the same event as a
22
+ # window in which nothing arrived, and the two deserve different diagnoses. A
23
+ # connector whose radio can tell the difference raises this during recv; listen()
24
+ # lowers it before every window, so a connector that never sets it (all but the
25
+ # SX127x one today) keeps reporting plain timeouts exactly as it always did.
26
+ self.recv_dropped_corrupt = False
27
+
28
+ # `adaptive_timeout` / `observed_min_timeout` now live in `Pacing`; these properties keep
29
+ # every existing call site (the send loop, the Edge's pokes, the tunnels) working.
30
+ @property
31
+ def adaptive_timeout(self):
32
+ return self.pacing.window
33
+
34
+ @adaptive_timeout.setter
35
+ def adaptive_timeout(self, value):
36
+ self.pacing.window = value
37
+
38
+ @property
39
+ def observed_min_timeout(self):
40
+ return self.pacing.observed_min_timeout
41
+
42
+ @observed_min_timeout.setter
43
+ def observed_min_timeout(self, value):
44
+ self.pacing.observed_min_timeout = value
45
+
46
+ def config(self, config_json):
47
+ # JSON Example:
48
+ # {
49
+ # "name": "N",
50
+ # "frequency": 868,
51
+ # "sf": 7,
52
+ # "mesh_mode": false,
53
+ # "debug": false,
54
+ # "min_timeout": 0.5,
55
+ # "max_timeout": 6
56
+ # }
57
+ self.config_parameters = config_json
58
+ if self.config_parameters:
59
+ self.name = self.config_parameters.get('name', "N")
60
+ self.debug = self.config_parameters.get('debug', False)
61
+
62
+ self.frequency = self.config_parameters.get('freq', 868) # 868 MHz
63
+ self.sf = self.config_parameters.get('sf', 7) # SF7
64
+ self.bw = self.config_parameters.get("bandwidth", 125) # 125 kHz
65
+ self.cr = self.config_parameters.get("coding_rate", 1) # 4/5
66
+ self.tx_power = self.config_parameters.get("tx_power", 14) # 14 dBm
67
+
68
+ self.mesh_mode = self.config_parameters.get('mesh_mode', False)
69
+ self.short_mac = self.config_parameters.get('short_mac', False)
70
+
71
+ # v3: which codec parses replies, and how a reply is matched to its request.
72
+ self.protocol_version = self.config_parameters.get('protocol_version', 2)
73
+ self.addressing = self.config_parameters.get('addressing', 'mac')
74
+
75
+ self.min_timeout = self.config_parameters.get('min_timeout', 0.5)
76
+ self.max_timeout = self.config_parameters.get('max_timeout', 6)
77
+ self.timeout_delta = self.config_parameters.get('timeout_delta', 1) # Delta for processing times
78
+
79
+ # Calculate initial adaptive timeouts
80
+ self.update_timeouts()
81
+
82
+ self.adaptive_timeout = self.max_timeout
83
+ self.backup_timeout = self.adaptive_timeout
84
+ self.backup_rf_config()
85
+
86
+ # Build the codec for the negotiated version x posture. Secure posture wiring
87
+ # (session resolver + AEAD backend) threads through here when secure goes live;
88
+ # today every configured node is open, so this picks v2 or v3-open.
89
+ self.codec = build_codec(
90
+ protocol_version=self.protocol_version,
91
+ addressing=self.addressing,
92
+ mesh_mode=self.mesh_mode,
93
+ short_mac=self.short_mac,
94
+ my_mac=self.get_mac(),
95
+ )
96
+ else:
97
+ if self.debug:
98
+ print("Error: No config parameters")
99
+
100
+ def set_secure(self, session_resolver, aead):
101
+ """Switch the codec to secure mode: it seals/opens each frame with the Session the
102
+ resolver returns for the frame's sid (`sid -> Session | None`), using the given AEAD
103
+ backend. Called by a secure-posture Node once it has a session store + a live backend;
104
+ the wire config (version/addressing/mesh) is unchanged, only the posture."""
105
+ self.codec = build_codec(
106
+ protocol_version=self.protocol_version, addressing=self.addressing,
107
+ mesh_mode=self.mesh_mode, short_mac=self.short_mac, my_mac=self.get_mac(),
108
+ security_mode='secure', session_resolver=session_resolver, aead=aead)
109
+
110
+ def get_max_payload_size(self):
111
+ """The largest frame the radio will carry, in bytes.
112
+
113
+ The LoRa explicit-header PHY carries 255 bytes at every spreading factor and every
114
+ bandwidth, and that is what v3 reports. The table this replaced returned 111 at SF11
115
+ and 30 at SF12; those numbers were measured on hardware that never had
116
+ LowDataRateOptimize set, where long frames at high spreading factors genuinely did
117
+ fail, so they described that fault rather than the PHY. With the register written a
118
+ raw sweep put 18 of 18 frames intact at 255 bytes at SF12. The table was also blind
119
+ to bandwidth, which nothing noticed because the configured bandwidth was never
120
+ reaching the radio either.
121
+
122
+ v2 keeps the table it shipped with. It is frozen, and it is the baseline the v3
123
+ throughput comparison is measured against, so moving it would rewrite the instrument
124
+ rather than the protocol.
125
+ """
126
+ if self.protocol_version >= 3:
127
+ return self.MAX_LENGTH_MESSAGE
128
+ if self.sf < 11:
129
+ return 255
130
+ elif self.sf == 11:
131
+ return 111
132
+ elif self.sf == 12:
133
+ return 30 #51
134
+
135
+ def set_frame_size(self, frame_size):
136
+ """Tell the connector how big the frames this node really sends are, so the receive
137
+ window is sized for its traffic instead of for the ceiling.
138
+
139
+ The node owns this number: the clamp that produces it needs the codec's per-frame
140
+ overhead, which the node asks for. Recomputing the timeouts here keeps the window
141
+ honest whenever the chunk size moves, which RF-config coordination does at runtime.
142
+ """
143
+ self.frame_size = frame_size
144
+ self.update_timeouts()
145
+
146
+ def update_timeouts(self):
147
+ # Calculate the min and max timeouts based on the ToA for the current RF settings
148
+ self.max_payload_size = self.get_max_payload_size()
149
+ # The floor is what this node's own traffic costs; the ceiling is what the PHY can
150
+ # still put in front of it. Those used to be one number because the ceiling was
151
+ # small enough that nothing could exceed it, and they must not be collapsed again:
152
+ # the adaptive window can never grow past the ceiling, so a node whose peer sends
153
+ # larger frames than it does would be unable to receive them at all. Sizing only the
154
+ # floor from real traffic is what stops a 30-byte-chunk node at SF12 from sitting on
155
+ # a 7.7-second floor waiting out frames it never sends.
156
+ floor_payload = self.max_payload_size
157
+ if self.frame_size:
158
+ floor_payload = min(floor_payload, self.frame_size)
159
+ min_toa = self.calculate_toa(self.sf, self.bw, self.cr, floor_payload)
160
+ max_toa = self.calculate_toa(self.sf, self.bw, self.cr, self.max_payload_size) * 2
161
+ self.min_timeout = min_toa + self.timeout_delta # Convert ms to seconds
162
+ self.max_timeout = max_toa + self.timeout_delta # Convert ms to seconds and add delta for processing times
163
+ self.pacing.set_bounds(self.min_timeout, self.max_timeout) # bounds change -> window resets to max (as before)
164
+ if self.debug:
165
+ print("Updated timeouts: Min: {} s, Max: {} s".format(self.min_timeout, self.max_timeout))
166
+
167
+ def calculate_toa(self, sf, bw, cr, payload_size):
168
+ if self.debug:
169
+ print("TOA with SF:", sf, "BW:", bw, "CR:", cr, "Payload:", payload_size)
170
+ crc = 1 # CRC enabled
171
+ bw_hz = bw * 1000 # Convert bandwidth to Hz
172
+ t_symbol = (2 ** sf) / bw_hz # Symbol duration
173
+ t_preamble = t_symbol * (8 + 4.25) # Preamble duration
174
+ h = 0 # Implicit header disabled
175
+ # Low data rate optimization. The radio turns this on once a symbol lasts longer
176
+ # than 16 ms, which depends on bandwidth as well as spreading factor: it is SF11
177
+ # and SF12 at BW125, but only SF12 at BW250, and it starts as low as SF10 at
178
+ # BW62.5. Keying off the spreading factor alone made the airtime estimate wrong
179
+ # wherever the bandwidth was not 125 kHz.
180
+ de = 1 if t_symbol > 0.016 else 0
181
+ cr_rate = cr / 4.0 # Coding rate
182
+ # Payload Symbol Calculation
183
+ payload_bits = 8 * payload_size - 4 * sf + 28 + 16 * (1 if crc else 0) - 20 * h
184
+ bits_per_symbol = 4 * (sf - 2 * de)
185
+ n_payload = 8 + max(0, int(ceil(payload_bits / bits_per_symbol) * (cr_rate + 4)))
186
+
187
+ # Payload Duration
188
+ t_payload = t_symbol * n_payload
189
+
190
+ # Total Time on Air
191
+ t_air = t_preamble + t_payload
192
+ if self.debug:
193
+ print("TOA:", t_air)
194
+
195
+ return t_air
196
+
197
+ def backup_config(self):
198
+ # Report the LIVE RF config, not the values first loaded from disk. change_rf_config
199
+ # moves the radio (self.frequency/sf/bw/cr/tx_power) without touching config_parameters,
200
+ # so persisting the raw config_parameters would drop a committed trial: the node would
201
+ # reboot on the old radio config. Overlay the live values under their canonical
202
+ # config keys, preserving every other connector key (timeouts, transport sub-config).
203
+ conf = dict(self.config_parameters) if self.config_parameters else {}
204
+ conf["freq"] = self.frequency
205
+ conf["sf"] = self.sf
206
+ conf["bandwidth"] = self.bw
207
+ conf["coding_rate"] = self.cr
208
+ conf["tx_power"] = self.tx_power
209
+ return conf
210
+
211
+ def get_mac(self):
212
+ return self.MAC
213
+
214
+ def set_mesh_mode(self, mesh_mode=False):
215
+ self.mesh_mode = mesh_mode
216
+
217
+ def transmit(self, wire):
218
+ # Pure byte send: the narrowed transport primitive. Concrete connectors override
219
+ # this (a radio put-on-air, a loopback queue); the base is a stub like recv.
220
+ return None
221
+
222
+ def recv(self, focus_time=12):
223
+ return None
224
+
225
+ def send(self, packet: Packet_v2):
226
+ # Back-compat: frame via the codec, then transmit the bytes. Connectors now override
227
+ # transmit(wire); this keeps every send(packet) caller (Node.send_lora and up) working
228
+ # and routes them through the same framing home.
229
+ return self.transmit(self.codec.frame(packet))
230
+
231
+ def listen(self, window):
232
+ # One timed receive window, measured at the radio: the (wire, td) a split-Connector
233
+ # tunnel bridge would run remotely and report back up (td is the round-trip the
234
+ # policy layer feeds to Pacing).
235
+ self.recv_dropped_corrupt = False # this window's verdict, not the last one's
236
+ t0 = time()
237
+ wire = self.recv(window)
238
+ td = (time() - t0) / 1000
239
+ return wire, td
240
+
241
+ def exchange(self, wire, window, match_key):
242
+ """Transmit a frame and wait, at the radio, for the reply that satisfies `match_key`,
243
+ within a shrinking window. Codec-free and keyless: it matches on the wire prefix
244
+ (`match_key.matches_wire`), so a dumb tunnel bridge can run it and never ferry a
245
+ foreign frame across the link. Returns `(reply_wire, td, status)` with status in
246
+ matched / timeout / exhausted / send_error.
247
+
248
+ This is the transport verb a split Connector's bridge half (an Adapter) serves (D↓/td↑). The
249
+ local engine keeps composing transmit/listen with the codec directly, so its
250
+ corrupt-vs-foreign error taxonomy (which needs the codec) is unchanged.
251
+ """
252
+ if not self.transmit(wire):
253
+ return None, 0, "send_error"
254
+ focus = window
255
+ td = 0
256
+ while focus > 0:
257
+ wire_in, td = self.listen(focus)
258
+ if not wire_in:
259
+ return None, td, "timeout"
260
+ if match_key.matches_wire(wire_in):
261
+ return wire_in, td, "matched"
262
+ # A frame whose addressing bytes aren't ours (foreign, or corruption that hit the
263
+ # addressing prefix): keep waiting within the window rather than ferrying it up.
264
+ focus = window - td
265
+ if focus < self.min_timeout:
266
+ return None, td, "exhausted"
267
+ return None, td, "exhausted"
268
+
269
+ def increase_adaptive_timeout(self):
270
+ self.pacing.on_timeout()
271
+
272
+ def decrease_adaptive_timeout(self, td):
273
+ self.pacing.on_reply(td)
274
+
275
+ def send_and_wait_response(self, packet):
276
+ focus_time = self.adaptive_timeout
277
+ wire = self.codec.frame(packet) # framing goes live on the send path here (was in self.send)
278
+ packet_size_sent = len(wire)
279
+ # How a reply is matched to this request (version/posture-agnostic; the sid or MAC
280
+ # mirror the request). Built once: the request doesn't change across the wait loop.
281
+ match = self.codec.match_spec(packet)
282
+ try:
283
+ send_success = self.transmit(wire)
284
+ if not send_success:
285
+ error_info = {
286
+ "type": "SEND_ERROR",
287
+ "message": "Error sending packet",
288
+ "focus_time": focus_time,
289
+ "adaptive_timeout": self.adaptive_timeout,
290
+ }
291
+ if self.debug:
292
+ print(error_info["message"])
293
+ return error_info, packet_size_sent, 0, 0
294
+ except Exception as e:
295
+ error_info = {
296
+ "type": "EXCEPTION",
297
+ "message": "Exception during send: {}".format(e),
298
+ "focus_time": focus_time,
299
+ "adaptive_timeout": self.adaptive_timeout,
300
+ }
301
+ if self.debug:
302
+ print(error_info["message"])
303
+ self.increase_adaptive_timeout()
304
+ return error_info, packet_size_sent, 0, 0
305
+
306
+ while focus_time > 0:
307
+ try:
308
+ received_data, td = self.listen(focus_time) # one timed window at the radio
309
+ except Exception as e:
310
+ error_info = {
311
+ "type": "EXCEPTION",
312
+ "message": "Exception during recv: {}".format(e),
313
+ "focus_time": focus_time,
314
+ "adaptive_timeout": self.adaptive_timeout,
315
+ }
316
+ if self.debug:
317
+ print(error_info["message"])
318
+ return error_info, packet_size_sent, 0, 0
319
+
320
+ packet_size_received = len(received_data) if received_data else 0
321
+
322
+ if not received_data:
323
+ if self.recv_dropped_corrupt:
324
+ # The reply did arrive, damaged, and the radio dropped it. Report it as
325
+ # the corrupt frame it was: the node counts CorruptedPackets from this
326
+ # label, and that count is how a link's corruption is compared between
327
+ # runs. Reporting silence instead would hide it in the retransmissions.
328
+ # The window is left alone deliberately: the frame arrived inside it, so
329
+ # it was wide enough, and growing it would slow every later round for a
330
+ # reason that is not true.
331
+ error_info = {
332
+ "type": "CORRUPTED_PACKET",
333
+ "message": "Reply dropped at the radio: the payload CRC failed",
334
+ "focus_time": focus_time,
335
+ "time_difference": td,
336
+ "adaptive_timeout": self.adaptive_timeout,
337
+ }
338
+ if self.debug:
339
+ print(error_info["message"])
340
+ return error_info, packet_size_sent, packet_size_received, td
341
+ error_info = {
342
+ "type": "TIMEOUT",
343
+ "message": "No response received",
344
+ "focus_time": focus_time,
345
+ "time_difference": td,
346
+ "adaptive_timeout": self.adaptive_timeout,
347
+ }
348
+ if self.debug:
349
+ print(error_info["message"])
350
+ self.increase_adaptive_timeout()
351
+ return error_info, packet_size_sent, packet_size_received, td
352
+
353
+ if self.debug:
354
+ print("WAIT_RESPONSE({}) at: {}|| source_reply: {}".format(td, self.adaptive_timeout, received_data))
355
+ try:
356
+ response_packet = self.codec.deframe(received_data)
357
+ if response_packet is not None:
358
+ if match.matches(response_packet):
359
+ if len(received_data) > response_packet.HEADER_SIZE + 60: # Hardcoded for only chunks
360
+ self.decrease_adaptive_timeout(td)
361
+ if response_packet.get_debug_hops():
362
+ response_packet.add_hop(self.name, self.get_rssi(), 0)
363
+ return response_packet, packet_size_sent, packet_size_received, td
364
+ # A well-formed frame that isn't our reply (a foreign/stale packet):
365
+ # keep waiting within the window rather than giving up.
366
+ else:
367
+ if len(received_data) > 0:
368
+ error_info = {
369
+ "type": "CORRUPTED_PACKET",
370
+ "message": "{}".format(received_data),
371
+ }
372
+ else:
373
+ error_info = {
374
+ "type": "NO_PACKET",
375
+ "message": "No packet received",
376
+ }
377
+ if self.debug:
378
+ print(error_info["message"])
379
+ return error_info, packet_size_sent, packet_size_received, td
380
+
381
+ except Exception as e:
382
+ error_info = {
383
+ "type": "EXCEPTION",
384
+ "message": "Exception during packet load: {}, data: {}".format(e, received_data),
385
+ }
386
+ if self.debug:
387
+ print(error_info["message"])
388
+ return error_info, packet_size_sent, packet_size_received, td
389
+
390
+ focus_time = self.adaptive_timeout - td
391
+ if focus_time < self.min_timeout:
392
+ focus_time = self.min_timeout
393
+ error_info = {
394
+ "type": "MIN_TIMEOUT_REACHED",
395
+ "message": "Minimum timeout reached, can't wait more",
396
+ "focus_time": focus_time,
397
+ }
398
+ if self.debug:
399
+ print(error_info["message"])
400
+ return error_info, packet_size_sent, packet_size_received, td
401
+
402
+ # This function returns the RSSI of the last received packet
403
+ def get_rssi(self):
404
+ return 0
405
+
406
+ # This function returns the SNR of the last received packet
407
+ def get_snr(self):
408
+ return 0
409
+
410
+ def signal_estimation(self):
411
+ percentage = 0
412
+ rssi = self.get_rssi()
413
+ # A connector that reports its readings honestly has None for "nothing heard yet", and
414
+ # there is no strength to estimate from that. Zero percent is the answer, not a crash
415
+ # and not the full bar that comparing against a missing value would otherwise skip to.
416
+ if rssi is None:
417
+ return percentage
418
+ if (rssi >= -50):
419
+ percentage = 100
420
+ elif (rssi <= -50) and (rssi >= -100):
421
+ percentage = 2 * (rssi + 100)
422
+ elif (rssi < 100):
423
+ percentage = 0
424
+ if self.debug:
425
+ print('SIGNAL STRENGTH', percentage, '%')
426
+ return percentage
427
+
428
+ def get_rf_config(self):
429
+ return [self.frequency, self.sf, self.bw, self.cr, self.tx_power]
430
+
431
+ def change_rf_config(self, frequency=None, sf=None, bw=None, cr=None, tx_power=None, backup=True):
432
+ if backup:
433
+ self.backup_rf_config()
434
+ try:
435
+ if frequency is not None:
436
+ self.set_frequency(frequency)
437
+ if sf is not None:
438
+ self.set_sf(sf)
439
+ if bw is not None:
440
+ self.set_bw(bw)
441
+ if cr is not None:
442
+ self.set_cr(cr)
443
+ if tx_power is not None:
444
+ self.set_transmission_power(tx_power)
445
+ self.update_timeouts() # resets the window when, and only when, the bounds moved
446
+ return True
447
+ except Exception as e:
448
+ if self.debug:
449
+ print("Error changing RF config: ", e)
450
+ self.restore_rf_config()
451
+ return False
452
+
453
+ def backup_rf_config(self):
454
+ self.last_rf_config = [self.frequency,
455
+ self.sf,
456
+ self.bw,
457
+ self.cr,
458
+ self.tx_power]
459
+
460
+ def update_rf_params(self, params):
461
+ """
462
+ Updates the connector's RF parameters.
463
+ Override in derived classes if needed.
464
+ """
465
+ self.frequency = params.get("frequency", self.frequency)
466
+ self.sf = params.get("sf", self.sf)
467
+ self.bw = params.get("bw", self.bw)
468
+ self.cr = params.get("cr", self.cr)
469
+ self.tx_power = params.get("tx_power", self.tx_power)
470
+ if self.debug:
471
+ print("Updated RF parameters:", self.get_rf_config())
472
+
473
+ def restore_rf_config(self):
474
+ frequency = self.last_rf_config[0]
475
+ sf = self.last_rf_config[1]
476
+ bw = self.last_rf_config[2]
477
+ cr = self.last_rf_config[3]
478
+ tx_power = self.last_rf_config[4]
479
+ self.change_rf_config(frequency=frequency,
480
+ sf=sf,
481
+ bw=bw,
482
+ cr=cr,
483
+ tx_power=tx_power,
484
+ backup=False)
485
+
486
+
487
+ def set_frequency(self, frequency):
488
+ pass
489
+
490
+ def set_sf(self, sf):
491
+ pass
492
+
493
+ def set_bw(self, bw):
494
+ pass
495
+
496
+ def set_cr(self, cr):
497
+ pass
498
+
499
+ def set_transmission_power(self, tx_power):
500
+ pass
@@ -5,7 +5,7 @@ import ubinascii
5
5
  import re
6
6
  from math import ceil
7
7
 
8
- from AlLoRa.Packet import Packet
8
+ from AlLoRa.Protocol.Packet_v2 import Packet_v2
9
9
  from AlLoRa.Connectors.Connector import Connector
10
10
  from AlLoRa.utils.debug_utils import print
11
11
 
@@ -233,13 +233,12 @@ class E5_connector(Connector):
233
233
  self.snr = None
234
234
 
235
235
  # Connector Methods
236
- def send(self, packet):
237
- data = packet.get_content()
238
- hex_data = self.bytes_to_hex(data)
236
+ def transmit(self, wire):
237
+ hex_data = self.bytes_to_hex(wire)
239
238
  hex_data = hex_data.strip().strip('"')
240
-
239
+
241
240
  success = self.send_packet(hex_data)
242
-
241
+
243
242
  return success
244
243
 
245
244
  def recv(self, focus_time=12):
@@ -1,6 +1,6 @@
1
1
  from time import sleep
2
2
 
3
- from AlLoRa.Packet import Packet
3
+ from AlLoRa.Protocol.Packet_v2 import Packet_v2
4
4
  from AlLoRa.Connectors.Connector import Connector
5
5
  from AlLoRa.utils.debug_utils import print
6
6
  from network import LoRa
@@ -0,0 +1,90 @@
1
+ import queue
2
+ import random
3
+
4
+ from AlLoRa.Connectors.Connector import Connector
5
+ from AlLoRa.utils.debug_utils import print
6
+
7
+
8
+ class Loopback_connector(Connector):
9
+ """An in-memory `Connector` for testing the protocol on CPython, with no radio.
10
+
11
+ The `Connector` is the v3 transport seam: the protocol engine (an `Edge` or a `Hub`)
12
+ talks only to `send` / `recv` / `send_and_wait_response`, never to a chip. This
13
+ implementation backs that contract with a pair of in-process queues instead of a radio,
14
+ so an `Edge` and a `Hub` can run a full file transfer in a test, the proof that
15
+ the seam is clean.
16
+
17
+ Build a connected pair with `Loopback_connector.create_pair(mac_a, mac_b)`:
18
+ whatever one end `send`s, the other end `recv`s. The link is in-order; pass a
19
+ per-direction `loss` probability + a `seed` to drop frames deterministically and
20
+ exercise the retransmission / coordinated-transition gates (lost-packet injection).
21
+
22
+ CPython-only (uses `queue` / `random` / threads). It never freezes to firmware.
23
+ """
24
+
25
+ def __init__(self, mac, inbox=None, outbox=None, loss=0.0, rng=None):
26
+ super().__init__()
27
+ self.MAC = mac
28
+ # inbox: bytes this connector receives; outbox: bytes it transmits to the peer.
29
+ self.inbox = inbox if inbox is not None else queue.Queue()
30
+ self.outbox = outbox if outbox is not None else queue.Queue()
31
+ self.loss = loss # probability a transmitted frame is lost in the channel
32
+ self._rng = rng # a random.Random driving the loss decisions, or None
33
+ self.dropped = 0 # frames this end transmitted but the channel dropped
34
+
35
+ @staticmethod
36
+ def create_pair(mac_a, mac_b, loss_a_to_b=0.0, loss_b_to_a=0.0, seed=None):
37
+ """Return two connectors wired back-to-back: a's outbox is b's inbox.
38
+
39
+ `loss_a_to_b` / `loss_b_to_a` drop that fraction of frames in that direction.
40
+ Each direction gets its own `random.Random(seed)` so its drop sequence is
41
+ deterministic as long as one thread drives that end's `send` (the usual case:
42
+ one node per connector).
43
+ """
44
+ a_to_b = queue.Queue()
45
+ b_to_a = queue.Queue()
46
+ rng_a = random.Random(seed) if (seed is not None and loss_a_to_b) else None
47
+ rng_b = random.Random(seed) if (seed is not None and loss_b_to_a) else None
48
+ a = Loopback_connector(mac_a, inbox=b_to_a, outbox=a_to_b, loss=loss_a_to_b, rng=rng_a)
49
+ b = Loopback_connector(mac_b, inbox=a_to_b, outbox=b_to_a, loss=loss_b_to_a, rng=rng_b)
50
+ return a, b
51
+
52
+ def get_mac(self):
53
+ return self.MAC
54
+
55
+ def transmit(self, wire):
56
+ # The wire carries the already-framed bytes, exactly as a radio would put on air.
57
+ if self.loss and self._rng is not None and self._rng.random() < self.loss:
58
+ # Lost in the channel: the transmitter still "sent" fine (returns True),
59
+ # the receiver simply never sees it. Its recv will time out and the
60
+ # protocol retransmits.
61
+ self.dropped += 1
62
+ return True
63
+ self.outbox.put(wire)
64
+ return True
65
+
66
+ def recv(self, focus_time=12):
67
+ # Block up to focus_time seconds for a frame, mirroring a radio's receive window.
68
+ try:
69
+ return self.inbox.get(timeout=focus_time)
70
+ except queue.Empty:
71
+ return None
72
+
73
+ # RF setters: the base Connector stubs these for a real radio to override (configure the
74
+ # chip, then update the field). With no chip, the loopback just updates the field, so
75
+ # change_rf_config actually moves this connector's (freq, sf, bw, cr, tx_power) — the
76
+ # RF-config coordination tests need a loopback that models a real reconfiguration.
77
+ def set_frequency(self, frequency):
78
+ self.frequency = frequency
79
+
80
+ def set_sf(self, sf):
81
+ self.sf = sf
82
+
83
+ def set_bw(self, bw):
84
+ self.bw = bw
85
+
86
+ def set_cr(self, cr):
87
+ self.cr = cr
88
+
89
+ def set_transmission_power(self, tx_power):
90
+ self.tx_power = tx_power