meshcore 2.3.12__tar.gz → 2.3.13__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.
- {meshcore-2.3.12 → meshcore-2.3.13}/PKG-INFO +1 -1
- {meshcore-2.3.12 → meshcore-2.3.13}/pyproject.toml +1 -1
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/messaging.py +74 -45
- meshcore-2.3.13/tests/unit/test_send_msg_with_retry.py +194 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/.github/funding.yml +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/.github/python-test.yml +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/.gitignore +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/LICENSE +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/README.md +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/.pubsub_example.py.swp +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_chat.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_pin_pairing_example.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_private_key_export.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_sign_example.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_stats.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_t1000_chan_msg.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_t1000_custom_vars.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_t1000_infos.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_t1000_msg.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_t1000_msg_retries.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/ble_t1000_set_cv.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/chan_recv_with_path.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/connection_events_example.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/mepo_mc_gps.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/pubsub_example.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/rf_packet_monitor.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_battery_monitor.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_channel_manager.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_chat.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_contacts.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_infos.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_meshcore_ollama.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_msg.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_pingbot.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_repeater_status.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_repeater_telemetry.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_rss_bot.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/serial_trace.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/tcp_chat.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/tcp_login_status.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/tcp_mchome_contacts.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/tcp_mchome_infos.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/tcp_mchome_msg.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/examples/tcp_mchome_readmsgs.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/pytest.ini +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/__init__.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/ble_cx.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/__init__.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/base.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/binary.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/contact.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/control_data.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/commands/device.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/connection_manager.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/events.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/lpp_json_encoder.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/meshcore.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/meshcore_parser.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/packets.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/parsing.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/reader.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/serial_cx.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/src/meshcore/tcp_cx.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/README.md +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/test_ble_connection.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/test_ble_pin_pairing.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/test_meshcore_ble_pin.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_asyncio_lifecycle.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_ble_reconnect_cleanup.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_commands.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_connection_manager.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_error_handling.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_events.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_lpp_parsing.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_parse_status.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_path_discovery_response.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_private_key_export.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_protocol_surface_gaps.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_reader.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_rxlog_keyerror_regression.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_serial_connection.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_standalone_fixes.py +0 -0
- {meshcore-2.3.12 → meshcore-2.3.13}/tests/unit/test_transport_symmetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: meshcore
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.13
|
|
4
4
|
Summary: Base classes for communicating with meshcore companion radios
|
|
5
5
|
Project-URL: Homepage, https://github.com/fdlamotte/meshcore_py
|
|
6
6
|
Project-URL: Issues, https://github.com/fdlamotte/meshcore_py/issues
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
3
|
import random
|
|
3
4
|
from typing import Optional, Union
|
|
@@ -112,7 +113,7 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
112
113
|
async def send_msg_with_retry (
|
|
113
114
|
self, dst: DestinationType, msg: str, timestamp: Optional[int] = None,
|
|
114
115
|
max_attempts=3, max_flood_attempts=2, flood_after=2, timeout=0, min_timeout=0
|
|
115
|
-
) -> Event:
|
|
116
|
+
) -> Optional[Event]:
|
|
116
117
|
|
|
117
118
|
# try to get a 32 bytes key (for flood reset), fallback to 6
|
|
118
119
|
try:
|
|
@@ -127,50 +128,78 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
127
128
|
attempts = 0
|
|
128
129
|
flood_attempts = 0
|
|
129
130
|
flood = False # by default consider we sent direct (will be overriden after send)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if flood:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
131
|
+
if timestamp is None:
|
|
132
|
+
# same timestamp on every attempt, the attempt number is what
|
|
133
|
+
# makes each packet unique (and lets receivers spot retries)
|
|
134
|
+
import time
|
|
135
|
+
timestamp = int(time.time())
|
|
136
|
+
|
|
137
|
+
# expected ack code -> MSG_SENT of the attempt that expects it
|
|
138
|
+
# the device keeps codes of earlier attempts, so a late ack for
|
|
139
|
+
# any of them still means the message got through
|
|
140
|
+
sent = {}
|
|
141
|
+
early_acks = set()
|
|
142
|
+
acked = asyncio.get_running_loop().create_future()
|
|
143
|
+
|
|
144
|
+
def on_ack(event):
|
|
145
|
+
code = event.attributes.get("code")
|
|
146
|
+
if code in sent:
|
|
147
|
+
if not acked.done():
|
|
148
|
+
acked.set_result(code)
|
|
149
|
+
else:
|
|
150
|
+
# ack dispatched before send_msg returned its code
|
|
151
|
+
early_acks.add(code)
|
|
152
|
+
|
|
153
|
+
# subscribe before sending, the ack can be queued right behind MSG_SENT
|
|
154
|
+
ack_sub = self.dispatcher.subscribe(EventType.ACK, on_ack)
|
|
155
|
+
try:
|
|
156
|
+
while attempts < max_attempts and not acked.done() \
|
|
157
|
+
and (not flood or flood_attempts < max_flood_attempts):
|
|
158
|
+
if attempts == flood_after and not flood : # change path to flood
|
|
159
|
+
if len(dst_bytes) < 32: # can only reset with full key
|
|
160
|
+
logger.info("Don't have full key, retreiving contacts so we can reset path")
|
|
161
|
+
await self.get_contacts()
|
|
162
|
+
contact = self._get_contact_by_prefix(dst_bytes.hex())
|
|
163
|
+
if not contact is None:
|
|
164
|
+
dst_bytes = _validate_destination(contact, prefix_length=32)
|
|
165
|
+
logger.info("Resetting path")
|
|
166
|
+
rp_res = await self.reset_path(dst_bytes)
|
|
167
|
+
if rp_res.type == EventType.ERROR:
|
|
168
|
+
logger.error(f"Couldn't reset path {rp_res} continuing ...")
|
|
169
|
+
else:
|
|
170
|
+
flood = True
|
|
171
|
+
if not contact is None:
|
|
172
|
+
contact["out_path"] = ""
|
|
173
|
+
contact["out_path_len"] = -1
|
|
174
|
+
|
|
175
|
+
if attempts > 0:
|
|
176
|
+
logger.info(f"Retry sending msg: {attempts + 1}")
|
|
177
|
+
|
|
178
|
+
result = await self.send_msg(dst, msg, timestamp, attempt=attempts)
|
|
179
|
+
if result.is_error():
|
|
180
|
+
logger.error(f"Failed to send message: {result.payload}")
|
|
181
|
+
attempts += 1
|
|
182
|
+
if flood:
|
|
183
|
+
flood_attempts += 1
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
flood = result.payload["type"] == 1 # we can sync flood flag from result ...
|
|
187
|
+
exp_ack = result.payload["expected_ack"].hex()
|
|
188
|
+
sent[exp_ack] = result
|
|
189
|
+
if exp_ack in early_acks and not acked.done():
|
|
190
|
+
acked.set_result(exp_ack)
|
|
191
|
+
|
|
192
|
+
atimeout = result.payload["suggested_timeout"] / 1000 * 1.2 if timeout==0 else timeout
|
|
193
|
+
atimeout = atimeout if atimeout > min_timeout else min_timeout
|
|
194
|
+
await asyncio.wait([acked], timeout=atimeout)
|
|
195
|
+
|
|
196
|
+
attempts = attempts + 1
|
|
197
|
+
if flood :
|
|
198
|
+
flood_attempts = flood_attempts + 1
|
|
199
|
+
finally:
|
|
200
|
+
ack_sub.unsubscribe()
|
|
201
|
+
|
|
202
|
+
return sent[acked.result()] if acked.done() else None
|
|
174
203
|
|
|
175
204
|
async def send_chan_msg(self, chan: int, msg: str, timestamp: Optional[int|bytes] = None) -> Event:
|
|
176
205
|
logger.debug(f"Sending channel message to channel {chan}: {msg}")
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""send_msg_with_retry against a fake companion radio and a real dispatcher.
|
|
2
|
+
|
|
3
|
+
The fake radio mirrors the firmware: each attempt gets its own expected ack
|
|
4
|
+
(sha256 over timestamp, attempt & 3 and text) and a suggested timeout, and
|
|
5
|
+
the ack for an attempt is pushed after a per-attempt delay.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import time
|
|
9
|
+
from hashlib import sha256
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import pytest_asyncio
|
|
13
|
+
|
|
14
|
+
from meshcore.commands import CommandHandler
|
|
15
|
+
from meshcore.events import Event, EventDispatcher, EventType
|
|
16
|
+
|
|
17
|
+
pytestmark = pytest.mark.asyncio
|
|
18
|
+
|
|
19
|
+
PUBKEY = bytes(range(32))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FakeRadio:
|
|
23
|
+
def __init__(self, dispatcher, *, direct_timeout_ms=100, flood_timeout_ms=100,
|
|
24
|
+
ack_delays=None, ack_with_msg_sent=False):
|
|
25
|
+
self.dispatcher = dispatcher
|
|
26
|
+
self.direct_timeout_ms = direct_timeout_ms
|
|
27
|
+
self.flood_timeout_ms = flood_timeout_ms
|
|
28
|
+
# attempt -> seconds until its ack, missing = never acked
|
|
29
|
+
self.ack_delays = ack_delays or {}
|
|
30
|
+
self.ack_with_msg_sent = ack_with_msg_sent
|
|
31
|
+
self.flood = False
|
|
32
|
+
self.frames = []
|
|
33
|
+
self.tasks = []
|
|
34
|
+
|
|
35
|
+
async def send(self, data):
|
|
36
|
+
data = bytes(data)
|
|
37
|
+
if data[0] == 0x0D: # reset_path
|
|
38
|
+
self.flood = True
|
|
39
|
+
await self.dispatcher.dispatch(Event(EventType.OK, {}))
|
|
40
|
+
return
|
|
41
|
+
assert data[:2] == b"\x02\x00"
|
|
42
|
+
self.frames.append(data)
|
|
43
|
+
attempt, ts, text = data[2], data[3:7], data[13:]
|
|
44
|
+
ack = sha256(ts + bytes([attempt & 3]) + text + PUBKEY).digest()[:4]
|
|
45
|
+
est = self.flood_timeout_ms if self.flood else self.direct_timeout_ms
|
|
46
|
+
await self.dispatcher.dispatch(Event(
|
|
47
|
+
EventType.MSG_SENT,
|
|
48
|
+
{"type": int(self.flood), "expected_ack": ack, "suggested_timeout": est},
|
|
49
|
+
))
|
|
50
|
+
ack_event = Event(EventType.ACK, {"code": ack.hex()}, {"code": ack.hex()})
|
|
51
|
+
if self.ack_with_msg_sent:
|
|
52
|
+
# both frames already queued when the caller resumes
|
|
53
|
+
await self.dispatcher.dispatch(ack_event)
|
|
54
|
+
elif attempt in self.ack_delays:
|
|
55
|
+
async def push():
|
|
56
|
+
await asyncio.sleep(self.ack_delays[attempt])
|
|
57
|
+
await self.dispatcher.dispatch(ack_event)
|
|
58
|
+
self.tasks.append(asyncio.create_task(push()))
|
|
59
|
+
|
|
60
|
+
def attempts(self):
|
|
61
|
+
return [f[2] for f in self.frames]
|
|
62
|
+
|
|
63
|
+
def timestamps(self):
|
|
64
|
+
return {f[3:7] for f in self.frames}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest_asyncio.fixture
|
|
68
|
+
async def dispatcher():
|
|
69
|
+
d = EventDispatcher()
|
|
70
|
+
await d.start()
|
|
71
|
+
yield d
|
|
72
|
+
await d.stop()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def make_handler(dispatcher, radio, out_path_len=0):
|
|
76
|
+
handler = CommandHandler()
|
|
77
|
+
handler.set_dispatcher(dispatcher)
|
|
78
|
+
handler._sender_func = radio.send
|
|
79
|
+
contact = {"public_key": PUBKEY.hex(), "out_path_len": out_path_len, "out_path": ""}
|
|
80
|
+
handler._get_contact_by_prefix = lambda prefix: contact
|
|
81
|
+
radio.flood = out_path_len == -1
|
|
82
|
+
return handler, contact
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ack_subscriptions(dispatcher):
|
|
86
|
+
return [s for s in dispatcher.subscriptions if s.event_type == EventType.ACK]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def test_ack_queued_behind_msg_sent_is_not_lost(dispatcher):
|
|
90
|
+
radio = FakeRadio(dispatcher, ack_with_msg_sent=True)
|
|
91
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
92
|
+
|
|
93
|
+
result = await handler.send_msg_with_retry(contact, "hello")
|
|
94
|
+
|
|
95
|
+
assert result is not None and result.type == EventType.MSG_SENT
|
|
96
|
+
assert radio.attempts() == [0]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def test_late_ack_for_earlier_attempt_counts(dispatcher):
|
|
100
|
+
# window is 0.12s, attempt 0 is acked at 0.2s while attempt 1 is pending
|
|
101
|
+
radio = FakeRadio(dispatcher, ack_delays={0: 0.2})
|
|
102
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
103
|
+
|
|
104
|
+
result = await handler.send_msg_with_retry(contact, "hello")
|
|
105
|
+
|
|
106
|
+
assert result is not None
|
|
107
|
+
assert radio.attempts() == [0, 1]
|
|
108
|
+
first_ack = sha256(radio.frames[0][3:7] + b"\x00" + b"hello" + PUBKEY).digest()[:4]
|
|
109
|
+
assert result.payload["expected_ack"] == first_ack
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def test_flood_attempt_uses_its_own_timeout(dispatcher):
|
|
113
|
+
# direct path is dead; the flood after the path reset is acked at 0.3s,
|
|
114
|
+
# past the direct window (0.12s) but inside the flood one (0.6s)
|
|
115
|
+
radio = FakeRadio(dispatcher, direct_timeout_ms=100, flood_timeout_ms=500,
|
|
116
|
+
ack_delays={2: 0.3})
|
|
117
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
118
|
+
|
|
119
|
+
result = await handler.send_msg_with_retry(contact, "hello")
|
|
120
|
+
|
|
121
|
+
assert result is not None
|
|
122
|
+
assert radio.attempts() == [0, 1, 2]
|
|
123
|
+
assert radio.flood and contact["out_path_len"] == -1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def test_explicit_timeout_applies_to_every_attempt(dispatcher):
|
|
127
|
+
radio = FakeRadio(dispatcher, direct_timeout_ms=100, flood_timeout_ms=5000)
|
|
128
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
129
|
+
|
|
130
|
+
start = time.monotonic()
|
|
131
|
+
result = await handler.send_msg_with_retry(contact, "hello", timeout=0.05)
|
|
132
|
+
|
|
133
|
+
assert result is None
|
|
134
|
+
assert radio.attempts() == [0, 1, 2]
|
|
135
|
+
assert time.monotonic() - start < 1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def test_attempts_share_one_timestamp(dispatcher, monkeypatch):
|
|
139
|
+
clock = iter(range(1_700_000_000, 1_700_000_100))
|
|
140
|
+
monkeypatch.setattr(time, "time", lambda: next(clock))
|
|
141
|
+
radio = FakeRadio(dispatcher)
|
|
142
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
143
|
+
|
|
144
|
+
result = await handler.send_msg_with_retry(contact, "hello")
|
|
145
|
+
|
|
146
|
+
assert result is None
|
|
147
|
+
assert radio.attempts() == [0, 1, 2]
|
|
148
|
+
assert len(radio.timestamps()) == 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def test_caller_timestamp_is_kept(dispatcher):
|
|
152
|
+
radio = FakeRadio(dispatcher, ack_delays={0: 0.01})
|
|
153
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
154
|
+
|
|
155
|
+
await handler.send_msg_with_retry(contact, "hello", timestamp=1234)
|
|
156
|
+
|
|
157
|
+
assert radio.timestamps() == {(1234).to_bytes(4, "little")}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def test_flood_contact_stops_at_max_flood_attempts(dispatcher):
|
|
161
|
+
radio = FakeRadio(dispatcher)
|
|
162
|
+
handler, contact = make_handler(dispatcher, radio, out_path_len=-1)
|
|
163
|
+
|
|
164
|
+
result = await handler.send_msg_with_retry(contact, "hello")
|
|
165
|
+
|
|
166
|
+
assert result is None
|
|
167
|
+
assert radio.attempts() == [0, 1]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def test_unrelated_ack_is_ignored(dispatcher):
|
|
171
|
+
radio = FakeRadio(dispatcher)
|
|
172
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
173
|
+
|
|
174
|
+
async def stray_ack():
|
|
175
|
+
await asyncio.sleep(0.05)
|
|
176
|
+
await dispatcher.dispatch(Event(EventType.ACK, {"code": "deadbeef"}, {"code": "deadbeef"}))
|
|
177
|
+
|
|
178
|
+
stray = asyncio.create_task(stray_ack())
|
|
179
|
+
result = await handler.send_msg_with_retry(contact, "hello", max_attempts=1)
|
|
180
|
+
await stray
|
|
181
|
+
|
|
182
|
+
assert result is None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def test_ack_subscription_is_removed(dispatcher):
|
|
186
|
+
radio = FakeRadio(dispatcher, ack_delays={0: 0.01})
|
|
187
|
+
handler, contact = make_handler(dispatcher, radio)
|
|
188
|
+
|
|
189
|
+
assert await handler.send_msg_with_retry(contact, "hello") is not None
|
|
190
|
+
assert ack_subscriptions(dispatcher) == []
|
|
191
|
+
|
|
192
|
+
radio.ack_delays = {}
|
|
193
|
+
assert await handler.send_msg_with_retry(contact, "hello") is None
|
|
194
|
+
assert ack_subscriptions(dispatcher) == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|