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.
- allora-3.0.0/AlLoRa/Connectors/Connector.py +500 -0
- {allora-2.0.0 → allora-3.0.0}/AlLoRa/Connectors/E5_connector.py +5 -6
- {allora-2.0.0 → allora-3.0.0}/AlLoRa/Connectors/LoPy4_connector.py +1 -1
- allora-3.0.0/AlLoRa/Connectors/Loopback_connector.py +90 -0
- allora-3.0.0/AlLoRa/Connectors/SX1262_connector.py +237 -0
- {allora-2.0.0 → allora-3.0.0}/AlLoRa/Connectors/SX127x_connector.py +19 -7
- allora-3.0.0/AlLoRa/Connectors/Split/Adapter.py +209 -0
- allora-3.0.0/AlLoRa/Connectors/Split/Link.py +33 -0
- allora-3.0.0/AlLoRa/Connectors/Split/Loopback_link.py +44 -0
- allora-3.0.0/AlLoRa/Connectors/Split/Serial_adapter.py +21 -0
- allora-3.0.0/AlLoRa/Connectors/Split/Serial_connector.py +171 -0
- allora-3.0.0/AlLoRa/Connectors/Split/Serial_link.py +319 -0
- allora-3.0.0/AlLoRa/Connectors/Split/Tunnel_connector.py +332 -0
- allora-3.0.0/AlLoRa/Connectors/Split/USB_adapter.py +64 -0
- allora-3.0.0/AlLoRa/Connectors/Split/WiFi_adapter.py +73 -0
- allora-3.0.0/AlLoRa/Connectors/Split/WiFi_connector.py +18 -0
- allora-3.0.0/AlLoRa/Connectors/Split/WiFi_link.py +165 -0
- allora-3.0.0/AlLoRa/Connectors/Split/tunnel_codec.py +145 -0
- allora-3.0.0/AlLoRa/Control/Control_Actuator.py +20 -0
- allora-3.0.0/AlLoRa/Control/Control_Root.py +73 -0
- allora-3.0.0/AlLoRa/Control/Control_Root_DataSink.py +184 -0
- allora-3.0.0/AlLoRa/Control/Node_Control_Actuator.py +71 -0
- allora-3.0.0/AlLoRa/Control/control_envelope.py +25 -0
- allora-3.0.0/AlLoRa/Control/control_types.py +31 -0
- allora-3.0.0/AlLoRa/Data/Loop_guard.py +34 -0
- allora-3.0.0/AlLoRa/Data/Sinks/DataSink.py +53 -0
- allora-3.0.0/AlLoRa/Data/Sinks/Disk_DataSink.py +18 -0
- allora-3.0.0/AlLoRa/Data/Sinks/HTTP_DataSink.py +136 -0
- allora-3.0.0/AlLoRa/Data/Sinks/MQTT_DataSink.py +117 -0
- allora-3.0.0/AlLoRa/Data/Sinks/message_template.py +401 -0
- allora-3.0.0/AlLoRa/Data/Sources/DataSource.py +174 -0
- allora-3.0.0/AlLoRa/Data/Sources/Disk_DataSource.py +542 -0
- allora-3.0.0/AlLoRa/Data/Sources/MQTT_DataSource.py +115 -0
- allora-3.0.0/AlLoRa/Data/Sources/Serial_DataSource.py +436 -0
- allora-3.0.0/AlLoRa/Data/mqtt_naming.py +64 -0
- allora-3.0.0/AlLoRa/Digital_Endpoint.py +468 -0
- allora-3.0.0/AlLoRa/File.py +344 -0
- allora-3.0.0/AlLoRa/Management/HTTP_Management_Source.py +300 -0
- allora-3.0.0/AlLoRa/Management/HTTP_Status_Subscriber.py +334 -0
- allora-3.0.0/AlLoRa/Management/Logger.py +217 -0
- allora-3.0.0/AlLoRa/Management/Management_Source.py +69 -0
- allora-3.0.0/AlLoRa/Nodes/Edge.py +122 -0
- allora-3.0.0/AlLoRa/Nodes/Hub.py +1662 -0
- allora-3.0.0/AlLoRa/Nodes/Node.py +2169 -0
- allora-3.0.0/AlLoRa/Protocol/Codec.py +261 -0
- allora-3.0.0/AlLoRa/Protocol/Pacing.py +121 -0
- allora-3.0.0/AlLoRa/Protocol/Packet.py +475 -0
- allora-2.0.0/AlLoRa/Packet.py → allora-3.0.0/AlLoRa/Protocol/Packet_v2.py +26 -14
- allora-3.0.0/AlLoRa/Protocol/Security/AEAD.py +194 -0
- allora-3.0.0/AlLoRa/Protocol/Security/Replay_window.py +36 -0
- allora-3.0.0/AlLoRa/Protocol/Security/Session.py +47 -0
- allora-3.0.0/AlLoRa/Protocol/Security/Session_store.py +36 -0
- allora-3.0.0/AlLoRa/Protocol/Security/__init__.py +3 -0
- allora-3.0.0/AlLoRa/Protocol/Security/ec_p256.py +360 -0
- allora-3.0.0/AlLoRa/Protocol/Security/handshake.py +65 -0
- allora-3.0.0/AlLoRa/Protocol/Security/hmac_sha256.py +36 -0
- allora-3.0.0/AlLoRa/Protocol/Security/identity.py +35 -0
- allora-3.0.0/AlLoRa/Protocol/Security/kdf.py +67 -0
- allora-3.0.0/AlLoRa/Protocol/negotiation.py +23 -0
- allora-3.0.0/AlLoRa/Status.py +35 -0
- allora-3.0.0/AlLoRa/__init__.py +1 -0
- allora-3.0.0/AlLoRa/utils/debug_utils.py +39 -0
- allora-3.0.0/AlLoRa/utils/file_utils.py +78 -0
- allora-3.0.0/AlLoRa/utils/os_utils.py +19 -0
- {allora-2.0.0 → allora-3.0.0}/AlLoRa/utils/time_utils.py +20 -0
- allora-3.0.0/AlLoRa.egg-info/PKG-INFO +944 -0
- allora-3.0.0/AlLoRa.egg-info/SOURCES.txt +190 -0
- allora-3.0.0/AlLoRa.egg-info/requires.txt +1 -0
- allora-3.0.0/AlLoRa.egg-info/top_level.txt +1 -0
- allora-3.0.0/PKG-INFO +944 -0
- allora-3.0.0/README.md +915 -0
- {allora-2.0.0 → allora-3.0.0}/setup.py +12 -3
- allora-3.0.0/tests/test_adapters.py +252 -0
- allora-3.0.0/tests/test_aead.py +276 -0
- allora-3.0.0/tests/test_benchmark_parser.py +44 -0
- allora-3.0.0/tests/test_boundary_config.py +440 -0
- allora-3.0.0/tests/test_chunk_size_ceiling.py +77 -0
- allora-3.0.0/tests/test_chunk_size_one_store.py +270 -0
- allora-3.0.0/tests/test_codec.py +364 -0
- allora-3.0.0/tests/test_config_persistence.py +514 -0
- allora-3.0.0/tests/test_config_schema.py +156 -0
- allora-3.0.0/tests/test_connector_transport.py +88 -0
- allora-3.0.0/tests/test_control_root_minting.py +144 -0
- allora-3.0.0/tests/test_control_root_provisioning.py +425 -0
- allora-3.0.0/tests/test_control_root_sink.py +566 -0
- allora-3.0.0/tests/test_custom_command_example.py +24 -0
- allora-3.0.0/tests/test_custom_control.py +104 -0
- allora-3.0.0/tests/test_damaged_frame_parsing.py +173 -0
- allora-3.0.0/tests/test_data_sink.py +488 -0
- allora-3.0.0/tests/test_datasource_base.py +92 -0
- allora-3.0.0/tests/test_device_section.py +304 -0
- allora-3.0.0/tests/test_disk_datasource.py +455 -0
- allora-3.0.0/tests/test_driver_register_order.py +143 -0
- allora-3.0.0/tests/test_ec_p256.py +418 -0
- allora-3.0.0/tests/test_endpoint_rf.py +203 -0
- allora-3.0.0/tests/test_file_folders.py +46 -0
- allora-3.0.0/tests/test_file_reassembly.py +121 -0
- allora-3.0.0/tests/test_generic_main.py +390 -0
- allora-3.0.0/tests/test_handshake.py +136 -0
- allora-3.0.0/tests/test_handshake_did.py +112 -0
- allora-3.0.0/tests/test_handshake_flow.py +80 -0
- allora-3.0.0/tests/test_hmac_sha256.py +63 -0
- allora-3.0.0/tests/test_http_management_source.py +386 -0
- allora-3.0.0/tests/test_http_status_subscriber.py +687 -0
- allora-3.0.0/tests/test_hub_ask_change_rf.py +452 -0
- allora-3.0.0/tests/test_hub_endpoints.py +454 -0
- allora-3.0.0/tests/test_hub_kwargs.py +68 -0
- allora-3.0.0/tests/test_hub_mqtt_example.py +77 -0
- allora-3.0.0/tests/test_hub_send_control.py +78 -0
- allora-3.0.0/tests/test_identity.py +54 -0
- allora-3.0.0/tests/test_idle_or_unreachable.py +134 -0
- allora-3.0.0/tests/test_in_band_control.py +298 -0
- allora-3.0.0/tests/test_kdf.py +98 -0
- allora-3.0.0/tests/test_logger_handler.py +21 -0
- allora-3.0.0/tests/test_loopback_transfer.py +112 -0
- allora-3.0.0/tests/test_management_plane.py +1237 -0
- allora-3.0.0/tests/test_match_prefix.py +45 -0
- allora-3.0.0/tests/test_message_template.py +381 -0
- allora-3.0.0/tests/test_micropython_compiles.py +54 -0
- allora-3.0.0/tests/test_micropython_portability.py +162 -0
- allora-3.0.0/tests/test_mixed_fleet.py +325 -0
- allora-3.0.0/tests/test_module_header_length.py +78 -0
- allora-3.0.0/tests/test_mqtt_bridge.py +198 -0
- allora-3.0.0/tests/test_mqtt_datasource.py +128 -0
- allora-3.0.0/tests/test_mqtt_naming.py +46 -0
- allora-3.0.0/tests/test_node_control_actuator.py +225 -0
- allora-3.0.0/tests/test_on_demand_serving.py +338 -0
- allora-3.0.0/tests/test_pacing.py +165 -0
- allora-3.0.0/tests/test_pacing_per_link.py +116 -0
- allora-3.0.0/tests/test_packet_v3_codec.py +244 -0
- allora-3.0.0/tests/test_packet_v3_secure.py +161 -0
- allora-3.0.0/tests/test_payload_ceiling.py +170 -0
- allora-3.0.0/tests/test_payload_crc_drop.py +185 -0
- allora-3.0.0/tests/test_provision_adopt_port.py +125 -0
- allora-3.0.0/tests/test_provision_board.py +278 -0
- allora-3.0.0/tests/test_provision_cli.py +824 -0
- allora-3.0.0/tests/test_provision_doctor.py +182 -0
- allora-3.0.0/tests/test_provision_flash_recovery.py +145 -0
- allora-3.0.0/tests/test_provision_fleet.py +447 -0
- allora-3.0.0/tests/test_provision_node_config.py +215 -0
- allora-3.0.0/tests/test_provision_plan.py +808 -0
- allora-3.0.0/tests/test_provision_setup.py +658 -0
- allora-3.0.0/tests/test_readme_refs.py +68 -0
- allora-3.0.0/tests/test_reassembly_length_check.py +150 -0
- allora-3.0.0/tests/test_replay_window.py +45 -0
- allora-3.0.0/tests/test_request_respond.py +72 -0
- allora-3.0.0/tests/test_rf_atomicity.py +121 -0
- allora-3.0.0/tests/test_rf_coordination.py +622 -0
- allora-3.0.0/tests/test_role_reversal.py +852 -0
- allora-3.0.0/tests/test_secure_refuse.py +99 -0
- allora-3.0.0/tests/test_secure_role_reversal.py +931 -0
- allora-3.0.0/tests/test_secure_transfer.py +143 -0
- allora-3.0.0/tests/test_serial_datasource.py +558 -0
- allora-3.0.0/tests/test_serial_link.py +283 -0
- allora-3.0.0/tests/test_serve_engine.py +540 -0
- allora-3.0.0/tests/test_session.py +83 -0
- allora-3.0.0/tests/test_session_store.py +68 -0
- allora-3.0.0/tests/test_sid_collision.py +104 -0
- allora-3.0.0/tests/test_sid_derivation.py +61 -0
- allora-3.0.0/tests/test_source_serves_what_it_announced.py +235 -0
- allora-3.0.0/tests/test_status.py +181 -0
- allora-3.0.0/tests/test_sx1262_target.py +496 -0
- allora-3.0.0/tests/test_sx126x_driver.py +325 -0
- allora-3.0.0/tests/test_sx127x_listens_after_send.py +135 -0
- allora-3.0.0/tests/test_time_utils.py +46 -0
- allora-3.0.0/tests/test_timeout_guards.py +140 -0
- allora-3.0.0/tests/test_toa_model.py +84 -0
- allora-3.0.0/tests/test_transport_recovery.py +433 -0
- allora-3.0.0/tests/test_tunnel_codec.py +134 -0
- allora-3.0.0/tests/test_tunnel_connector_mac.py +69 -0
- allora-3.0.0/tests/test_tunnel_link.py +47 -0
- allora-3.0.0/tests/test_tunnel_transfer.py +108 -0
- allora-3.0.0/tests/test_tunnel_transport.py +233 -0
- allora-3.0.0/tests/test_usb_adapter.py +376 -0
- allora-3.0.0/tests/test_v2_tag_interop.py +119 -0
- allora-3.0.0/tests/test_v3_negotiation.py +78 -0
- allora-3.0.0/tests/test_v3_open_transfer.py +247 -0
- allora-3.0.0/tests/test_wifi_link.py +106 -0
- allora-2.0.0/AlLoRa/Connectors/Connector.py +0 -323
- allora-2.0.0/AlLoRa/Connectors/SX1262_connector.py +0 -119
- allora-2.0.0/AlLoRa/Connectors/Serial_connector.py +0 -262
- allora-2.0.0/AlLoRa/Connectors/Wifi_connector.py +0 -124
- allora-2.0.0/AlLoRa/DataSource.py +0 -118
- allora-2.0.0/AlLoRa/Digital_Endpoint.py +0 -189
- allora-2.0.0/AlLoRa/File.py +0 -147
- allora-2.0.0/AlLoRa/Interfaces/Interface.py +0 -18
- allora-2.0.0/AlLoRa/Interfaces/Serial_interface.py +0 -250
- allora-2.0.0/AlLoRa/Interfaces/WiFi_interface.py +0 -268
- allora-2.0.0/AlLoRa/Nodes/Adapter.py +0 -57
- allora-2.0.0/AlLoRa/Nodes/Gateway.py +0 -103
- allora-2.0.0/AlLoRa/Nodes/Node.py +0 -167
- allora-2.0.0/AlLoRa/Nodes/Requester.py +0 -490
- allora-2.0.0/AlLoRa/Nodes/Source.py +0 -295
- allora-2.0.0/AlLoRa/utils/debug_utils.py +0 -6
- allora-2.0.0/AlLoRa/utils/os_utils.py +0 -6
- allora-2.0.0/AlLoRa.egg-info/PKG-INFO +0 -377
- allora-2.0.0/AlLoRa.egg-info/SOURCES.txt +0 -77
- allora-2.0.0/AlLoRa.egg-info/top_level.txt +0 -2
- allora-2.0.0/PKG-INFO +0 -377
- allora-2.0.0/README.md +0 -351
- allora-2.0.0/examples/Adapters/Serial_adapter/main.py +0 -15
- allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client/boot.py +0 -4
- allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client/main_ESP32.py +0 -7
- allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client/main_LoPy4.py +0 -7
- allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot/boot.py +0 -4
- allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot/main_ESP32.py +0 -9
- allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot/main_LoPy4.py +0 -7
- allora-2.0.0/examples/Gateways/Serial_Gateway/__init__.py +0 -0
- allora-2.0.0/examples/Gateways/Serial_Gateway/main.py +0 -31
- allora-2.0.0/examples/Gateways/Serial_Gateway/rf_changer.py +0 -57
- allora-2.0.0/examples/Gateways/Wifi_Gateway/__init__.py +0 -0
- allora-2.0.0/examples/Gateways/Wifi_Gateway/main.py +0 -15
- allora-2.0.0/examples/Gateways/__init__.py +0 -0
- allora-2.0.0/examples/Requesters/LopyRequester/__init__.py +0 -0
- allora-2.0.0/examples/Requesters/LopyRequester/boot.py +0 -11
- allora-2.0.0/examples/Requesters/LopyRequester/main.py +0 -27
- allora-2.0.0/examples/Requesters/T3S3/__init__.py +0 -0
- allora-2.0.0/examples/Requesters/T3S3/main.py +0 -34
- allora-2.0.0/examples/Requesters/T3S3/main_pro.py +0 -52
- allora-2.0.0/examples/Requesters/__init__.py +0 -0
- allora-2.0.0/examples/Sources/ESP32/__init__.py +0 -0
- allora-2.0.0/examples/Sources/ESP32/boot.py +0 -3
- allora-2.0.0/examples/Sources/ESP32/main_Heltec.py +0 -62
- allora-2.0.0/examples/Sources/ESP32/main_TTGO.py +0 -80
- allora-2.0.0/examples/Sources/LopySource/__init__.py +0 -0
- allora-2.0.0/examples/Sources/LopySource/boot.py +0 -11
- allora-2.0.0/examples/Sources/LopySource/main.py +0 -64
- allora-2.0.0/examples/Sources/T3S3/__init__.py +0 -0
- allora-2.0.0/examples/Sources/T3S3/main.py +0 -57
- allora-2.0.0/examples/Sources/T3S3/main_pro.py +0 -125
- allora-2.0.0/examples/Sources/T3S3-Tester/__init__.py +0 -0
- allora-2.0.0/examples/Sources/T3S3-Tester/main.py +0 -75
- allora-2.0.0/examples/Sources/XIAO-ESP32S3/__init__.py +0 -0
- allora-2.0.0/examples/Sources/XIAO-ESP32S3/main.py +0 -57
- allora-2.0.0/examples/Sources/XIAO-ESP32S3/main_pro.py +0 -121
- allora-2.0.0/examples/Sources/__init__.py +0 -0
- allora-2.0.0/examples/__init__.py +0 -0
- {allora-2.0.0/AlLoRa/Connectors → allora-3.0.0/AlLoRa/Connectors/Split}/__init__.py +0 -0
- {allora-2.0.0/AlLoRa/Interfaces → allora-3.0.0/AlLoRa/Connectors}/__init__.py +0 -0
- {allora-2.0.0/AlLoRa/Nodes → allora-3.0.0/AlLoRa/Control}/__init__.py +0 -0
- {allora-2.0.0/AlLoRa → allora-3.0.0/AlLoRa/Data/Sinks}/__init__.py +0 -0
- {allora-2.0.0/AlLoRa/utils → allora-3.0.0/AlLoRa/Data/Sources}/__init__.py +0 -0
- {allora-2.0.0/examples/Adapters/Serial_adapter → allora-3.0.0/AlLoRa/Data}/__init__.py +0 -0
- {allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Client → allora-3.0.0/AlLoRa/Management}/__init__.py +0 -0
- {allora-2.0.0/examples/Adapters/WiFi_adapters/WiFi-Hotspot → allora-3.0.0/AlLoRa/Nodes}/__init__.py +0 -0
- {allora-2.0.0/examples/Adapters/WiFi_adapters → allora-3.0.0/AlLoRa/Protocol}/__init__.py +0 -0
- {allora-2.0.0/examples/Adapters → allora-3.0.0/AlLoRa/utils}/__init__.py +0 -0
- {allora-2.0.0 → allora-3.0.0}/AlLoRa/utils/json_utils.py +0 -0
- {allora-2.0.0 → allora-3.0.0}/AlLoRa.egg-info/dependency_links.txt +0 -0
- {allora-2.0.0 → allora-3.0.0}/LICENSE +0 -0
- {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.
|
|
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
|
|
237
|
-
|
|
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):
|
|
@@ -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
|