open-packet 0.1.0__py3-none-any.whl
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.
- open_packet/__init__.py +0 -0
- open_packet/ax25/__init__.py +0 -0
- open_packet/ax25/address.py +33 -0
- open_packet/ax25/connection.py +467 -0
- open_packet/ax25/frame.py +206 -0
- open_packet/ax25/timer.py +22 -0
- open_packet/engine/__init__.py +0 -0
- open_packet/engine/commands.py +63 -0
- open_packet/engine/engine.py +542 -0
- open_packet/engine/events.py +89 -0
- open_packet/forms/__init__.py +0 -0
- open_packet/forms/loader.py +107 -0
- open_packet/forms/renderer.py +18 -0
- open_packet/forms/updater.py +164 -0
- open_packet/forms/validator.py +43 -0
- open_packet/link/__init__.py +0 -0
- open_packet/link/base.py +24 -0
- open_packet/link/kiss.py +85 -0
- open_packet/link/telnet.py +126 -0
- open_packet/node/__init__.py +0 -0
- open_packet/node/base.py +73 -0
- open_packet/node/bpq.py +274 -0
- open_packet/store/__init__.py +0 -0
- open_packet/store/database.py +603 -0
- open_packet/store/exporter.py +45 -0
- open_packet/store/models.py +106 -0
- open_packet/store/settings.py +73 -0
- open_packet/store/store.py +458 -0
- open_packet/terminal/__init__.py +0 -0
- open_packet/terminal/session.py +91 -0
- open_packet/transport/__init__.py +0 -0
- open_packet/transport/base.py +20 -0
- open_packet/transport/serial.py +45 -0
- open_packet/transport/tcp.py +50 -0
- open_packet/ui/__init__.py +0 -0
- open_packet/ui/base.py +13 -0
- open_packet/ui/tui/__init__.py +0 -0
- open_packet/ui/tui/app.py +1040 -0
- open_packet/ui/tui/screens/__init__.py +3 -0
- open_packet/ui/tui/screens/compose.py +92 -0
- open_packet/ui/tui/screens/compose_bulletin.py +54 -0
- open_packet/ui/tui/screens/connect_terminal.py +169 -0
- open_packet/ui/tui/screens/delete_confirm.py +54 -0
- open_packet/ui/tui/screens/form_fill.py +231 -0
- open_packet/ui/tui/screens/form_picker.py +68 -0
- open_packet/ui/tui/screens/general_settings.py +149 -0
- open_packet/ui/tui/screens/interface_picker.py +102 -0
- open_packet/ui/tui/screens/main.py +118 -0
- open_packet/ui/tui/screens/manage_interfaces.py +147 -0
- open_packet/ui/tui/screens/manage_nodes.py +264 -0
- open_packet/ui/tui/screens/manage_operators.py +162 -0
- open_packet/ui/tui/screens/new_item.py +71 -0
- open_packet/ui/tui/screens/node_multi_picker.py +83 -0
- open_packet/ui/tui/screens/node_picker.py +105 -0
- open_packet/ui/tui/screens/operator_picker.py +102 -0
- open_packet/ui/tui/screens/search.py +150 -0
- open_packet/ui/tui/screens/settings.py +51 -0
- open_packet/ui/tui/screens/setup_interface.py +194 -0
- open_packet/ui/tui/screens/setup_node.py +357 -0
- open_packet/ui/tui/screens/setup_node_group.py +138 -0
- open_packet/ui/tui/screens/setup_operator.py +111 -0
- open_packet/ui/tui/screens/shorter_path_confirm.py +44 -0
- open_packet/ui/tui/widgets/__init__.py +0 -0
- open_packet/ui/tui/widgets/console_panel.py +53 -0
- open_packet/ui/tui/widgets/file_list.py +61 -0
- open_packet/ui/tui/widgets/folder_tree.py +180 -0
- open_packet/ui/tui/widgets/message_body.py +43 -0
- open_packet/ui/tui/widgets/message_list.py +70 -0
- open_packet/ui/tui/widgets/status_bar.py +138 -0
- open_packet/ui/tui/widgets/terminal_view.py +54 -0
- open_packet-0.1.0.dist-info/METADATA +124 -0
- open_packet-0.1.0.dist-info/RECORD +75 -0
- open_packet-0.1.0.dist-info/WHEEL +4 -0
- open_packet-0.1.0.dist-info/entry_points.txt +3 -0
- open_packet-0.1.0.dist-info/licenses/LICENSE +21 -0
open_packet/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class AX25Address:
|
|
7
|
+
callsign: str
|
|
8
|
+
ssid: int
|
|
9
|
+
last: bool
|
|
10
|
+
c_bit: bool = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encode_address(callsign: str, ssid: int, last: bool, c_bit: bool = False) -> bytes:
|
|
14
|
+
padded = callsign.upper().ljust(6)[:6]
|
|
15
|
+
encoded = bytes(ord(c) << 1 for c in padded)
|
|
16
|
+
ssid_byte = (
|
|
17
|
+
(0x80 if c_bit else 0x00)
|
|
18
|
+
| 0b01100000
|
|
19
|
+
| ((ssid & 0x0F) << 1)
|
|
20
|
+
| (1 if last else 0)
|
|
21
|
+
)
|
|
22
|
+
return encoded + bytes([ssid_byte])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def decode_address(data: bytes) -> AX25Address:
|
|
26
|
+
if len(data) < 7:
|
|
27
|
+
raise ValueError(f"Address field must be 7 bytes, got {len(data)}")
|
|
28
|
+
callsign = "".join(chr(b >> 1) for b in data[:6]).rstrip()
|
|
29
|
+
ssid_byte = data[6]
|
|
30
|
+
ssid = (ssid_byte >> 1) & 0x0F
|
|
31
|
+
last = bool(ssid_byte & 0x01)
|
|
32
|
+
c_bit = bool(ssid_byte & 0x80)
|
|
33
|
+
return AX25Address(callsign=callsign, ssid=ssid, last=last, c_bit=c_bit)
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from open_packet.ax25.frame import (
|
|
9
|
+
FrameType,
|
|
10
|
+
encode_sabm, encode_ua, encode_dm, encode_disc,
|
|
11
|
+
encode_i_frame, encode_rr, encode_rnr, encode_rej,
|
|
12
|
+
decode_frame,
|
|
13
|
+
PID_NO_LAYER3,
|
|
14
|
+
)
|
|
15
|
+
from open_packet.ax25.timer import Timer
|
|
16
|
+
from open_packet.link.base import ConnectionBase, ConnectionError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _split_callsign(s: str) -> tuple[str, int]:
|
|
20
|
+
"""Split 'W0RELAY-1' → ('W0RELAY', 1). No dash → ssid 0."""
|
|
21
|
+
if "-" in s:
|
|
22
|
+
call, ssid_str = s.rsplit("-", 1)
|
|
23
|
+
try:
|
|
24
|
+
return call.upper(), int(ssid_str)
|
|
25
|
+
except ValueError:
|
|
26
|
+
pass
|
|
27
|
+
return s.upper(), 0
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _frame_repr(data: bytes) -> str:
|
|
33
|
+
text = "".join(chr(b) if 32 <= b < 127 else "." for b in data)
|
|
34
|
+
return repr(text[:40])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# AX.25 v2.2 defaults (§6.7)
|
|
38
|
+
T1_DEFAULT = 3.0 # Acknowledgment timer (seconds)
|
|
39
|
+
T3_DEFAULT = 30.0 # Inactive-link timer (seconds)
|
|
40
|
+
N2_DEFAULT = 10 # Maximum retries
|
|
41
|
+
K_DEFAULT = 7 # Window size (mod-8)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LinkState(Enum):
|
|
45
|
+
DISCONNECTED = 0
|
|
46
|
+
AWAITING_CONNECTION = 1
|
|
47
|
+
AWAITING_RELEASE = 2
|
|
48
|
+
CONNECTED = 3
|
|
49
|
+
TIMER_RECOVERY = 4
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AX25Connection(ConnectionBase):
|
|
53
|
+
"""
|
|
54
|
+
AX.25 v2.2 data link connection over a KISSLink.
|
|
55
|
+
|
|
56
|
+
connect(dest, ssid) — open transport + SABM/UA exchange
|
|
57
|
+
disconnect() — DISC/UA exchange + close transport
|
|
58
|
+
send_frame(payload) — send payload as I-frame
|
|
59
|
+
receive_frame(timeout) — return payload from next received I-frame
|
|
60
|
+
(also processes supervisory frames in-band)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
kiss: ConnectionBase,
|
|
66
|
+
my_callsign: str,
|
|
67
|
+
my_ssid: int,
|
|
68
|
+
t1: float = T1_DEFAULT,
|
|
69
|
+
t3: float = T3_DEFAULT,
|
|
70
|
+
n2: int = N2_DEFAULT,
|
|
71
|
+
k: int = K_DEFAULT,
|
|
72
|
+
on_frame: Callable[[str, str], None] | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self._kiss = kiss
|
|
75
|
+
self._my_call = my_callsign
|
|
76
|
+
self._my_ssid = my_ssid
|
|
77
|
+
self._on_frame = on_frame
|
|
78
|
+
self._dest_call: Optional[str] = None
|
|
79
|
+
self._dest_ssid: int = 0
|
|
80
|
+
|
|
81
|
+
self._t1_timeout = t1
|
|
82
|
+
self._t3_timeout = t3
|
|
83
|
+
self._n2 = n2
|
|
84
|
+
self._k = k
|
|
85
|
+
|
|
86
|
+
self.state = LinkState.DISCONNECTED
|
|
87
|
+
|
|
88
|
+
# State variables (§4.2.2)
|
|
89
|
+
self.V_S: int = 0
|
|
90
|
+
self.V_R: int = 0
|
|
91
|
+
self.V_A: int = 0
|
|
92
|
+
self.RC: int = 0
|
|
93
|
+
|
|
94
|
+
self._peer_receiver_busy: bool = False
|
|
95
|
+
self._own_receiver_busy: bool = False
|
|
96
|
+
self._reject_exception: bool = False
|
|
97
|
+
self._ack_pending: bool = False
|
|
98
|
+
|
|
99
|
+
self._t1 = Timer()
|
|
100
|
+
self._t3 = Timer()
|
|
101
|
+
|
|
102
|
+
# Unacknowledged I-frames for retransmission: seq_num → payload
|
|
103
|
+
self._unacked: dict[int, bytes] = {}
|
|
104
|
+
|
|
105
|
+
# VIA path for SABM (list of (callsign, ssid) tuples or None)
|
|
106
|
+
self._via = None
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------ #
|
|
109
|
+
# ConnectionBase interface #
|
|
110
|
+
# ------------------------------------------------------------------ #
|
|
111
|
+
|
|
112
|
+
def connect(self, callsign: str, ssid: int, via_path=None) -> None:
|
|
113
|
+
self._dest_call = callsign
|
|
114
|
+
self._dest_ssid = ssid
|
|
115
|
+
self._kiss.connect(callsign, ssid)
|
|
116
|
+
via_tuples = None
|
|
117
|
+
if via_path:
|
|
118
|
+
via_tuples = [_split_callsign(h.callsign) for h in via_path]
|
|
119
|
+
self._establish_data_link(via=via_tuples)
|
|
120
|
+
|
|
121
|
+
def disconnect(self) -> None:
|
|
122
|
+
if self.state not in (LinkState.CONNECTED, LinkState.TIMER_RECOVERY):
|
|
123
|
+
self._kiss.disconnect()
|
|
124
|
+
return
|
|
125
|
+
self._send_disc(poll=True)
|
|
126
|
+
self.state = LinkState.AWAITING_RELEASE
|
|
127
|
+
self.RC = 0
|
|
128
|
+
self._t3.stop()
|
|
129
|
+
self._t1.start(self._t1_timeout)
|
|
130
|
+
self._wait_for_release()
|
|
131
|
+
self._kiss.disconnect()
|
|
132
|
+
|
|
133
|
+
def send_frame(self, data: bytes) -> None:
|
|
134
|
+
if self.state not in (LinkState.CONNECTED, LinkState.TIMER_RECOVERY):
|
|
135
|
+
raise ConnectionError("Cannot send: not connected")
|
|
136
|
+
if ((self.V_S - self.V_A) % 8) >= self._k:
|
|
137
|
+
raise ConnectionError("Send window full — cannot send I-frame")
|
|
138
|
+
self._send_i_frame(data)
|
|
139
|
+
|
|
140
|
+
def receive_frame(self, timeout: float = 5.0) -> bytes | None:
|
|
141
|
+
deadline = time.monotonic() + timeout
|
|
142
|
+
while time.monotonic() < deadline:
|
|
143
|
+
remaining = deadline - time.monotonic()
|
|
144
|
+
raw = self._kiss.receive_frame(timeout=min(remaining, 0.5))
|
|
145
|
+
if raw:
|
|
146
|
+
result = self._process_frame(raw)
|
|
147
|
+
if result is not None:
|
|
148
|
+
return result # I-frame payload addressed to us
|
|
149
|
+
return b"" # frame received but no payload (supervisory/filtered)
|
|
150
|
+
self._check_timers()
|
|
151
|
+
return None # true timeout — no frame received at all
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------ #
|
|
154
|
+
# Internal: connection setup #
|
|
155
|
+
# ------------------------------------------------------------------ #
|
|
156
|
+
|
|
157
|
+
def _establish_data_link(self, via=None) -> None:
|
|
158
|
+
self._via = via # store for retry
|
|
159
|
+
self.state = LinkState.AWAITING_CONNECTION
|
|
160
|
+
self.RC = 0
|
|
161
|
+
self._clear_exception_conditions()
|
|
162
|
+
self._send_sabm(poll=True, via=via)
|
|
163
|
+
self._t1.start(self._t1_timeout)
|
|
164
|
+
|
|
165
|
+
deadline = time.monotonic() + self._t1_timeout * (self._n2 + 1)
|
|
166
|
+
while time.monotonic() < deadline:
|
|
167
|
+
raw = self._kiss.receive_frame(timeout=1.0)
|
|
168
|
+
if not raw:
|
|
169
|
+
if self._t1.expired:
|
|
170
|
+
if self.RC >= self._n2:
|
|
171
|
+
self.state = LinkState.DISCONNECTED
|
|
172
|
+
raise ConnectionError(
|
|
173
|
+
f"No UA after {self._n2} SABM attempts"
|
|
174
|
+
)
|
|
175
|
+
self.RC += 1
|
|
176
|
+
self._send_sabm(poll=True, via=via)
|
|
177
|
+
self._t1.start(self._t1_timeout)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
f = decode_frame(raw)
|
|
181
|
+
|
|
182
|
+
if not self._is_for_us(f):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if f.frame_type == FrameType.UA and f.poll_final:
|
|
186
|
+
self._t1.stop()
|
|
187
|
+
self._t3.stop()
|
|
188
|
+
self.V_S = 0
|
|
189
|
+
self.V_A = 0
|
|
190
|
+
self.V_R = 0
|
|
191
|
+
self.state = LinkState.CONNECTED
|
|
192
|
+
self._t3.start(self._t3_timeout)
|
|
193
|
+
logger.info("AX.25 connected to %s-%d", self._dest_call, self._dest_ssid)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if f.frame_type == FrameType.DM and f.poll_final:
|
|
197
|
+
self.state = LinkState.DISCONNECTED
|
|
198
|
+
raise ConnectionError(
|
|
199
|
+
f"Connection refused by {self._dest_call} (DM received)"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self.state = LinkState.DISCONNECTED
|
|
203
|
+
raise ConnectionError("Connection timed out")
|
|
204
|
+
|
|
205
|
+
def _wait_for_release(self) -> None:
|
|
206
|
+
deadline = time.monotonic() + self._t1_timeout * (self._n2 + 1)
|
|
207
|
+
while time.monotonic() < deadline:
|
|
208
|
+
raw = self._kiss.receive_frame(timeout=1.0)
|
|
209
|
+
if not raw:
|
|
210
|
+
if self._t1.expired:
|
|
211
|
+
if self.RC >= self._n2:
|
|
212
|
+
break
|
|
213
|
+
self.RC += 1
|
|
214
|
+
self._send_disc(poll=True)
|
|
215
|
+
self._t1.start(self._t1_timeout)
|
|
216
|
+
continue
|
|
217
|
+
f = decode_frame(raw)
|
|
218
|
+
if not self._is_for_us(f):
|
|
219
|
+
continue
|
|
220
|
+
if f.frame_type in (FrameType.UA, FrameType.DM):
|
|
221
|
+
self._t1.stop()
|
|
222
|
+
break
|
|
223
|
+
self.state = LinkState.DISCONNECTED
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------ #
|
|
226
|
+
# Internal: frame processing #
|
|
227
|
+
# ------------------------------------------------------------------ #
|
|
228
|
+
|
|
229
|
+
def _is_for_us(self, f) -> bool:
|
|
230
|
+
"""True iff this frame is from our peer addressed to our station."""
|
|
231
|
+
return (
|
|
232
|
+
f.destination.upper() == self._my_call.upper()
|
|
233
|
+
and f.destination_ssid == self._my_ssid
|
|
234
|
+
and self._dest_call is not None
|
|
235
|
+
and f.source.upper() == self._dest_call.upper()
|
|
236
|
+
and f.source_ssid == self._dest_ssid
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _process_frame(self, raw: bytes) -> Optional[bytes]:
|
|
240
|
+
try:
|
|
241
|
+
f = decode_frame(raw)
|
|
242
|
+
except ValueError:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if not self._is_for_us(f):
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
if self._on_frame:
|
|
249
|
+
if f.frame_type == FrameType.I:
|
|
250
|
+
self._on_frame("<", f"I({f.ns},{f.nr}) {_frame_repr(f.info)}")
|
|
251
|
+
else:
|
|
252
|
+
self._on_frame("<", f"{f.frame_type.value} {f.source}→{f.destination}")
|
|
253
|
+
|
|
254
|
+
if f.frame_type == FrameType.I:
|
|
255
|
+
return self._handle_i_frame(f)
|
|
256
|
+
elif f.frame_type == FrameType.RR:
|
|
257
|
+
self._handle_rr(f)
|
|
258
|
+
elif f.frame_type == FrameType.RNR:
|
|
259
|
+
self._handle_rnr(f)
|
|
260
|
+
elif f.frame_type == FrameType.REJ:
|
|
261
|
+
self._handle_rej(f)
|
|
262
|
+
elif f.frame_type == FrameType.SABM:
|
|
263
|
+
self._handle_sabm_reset(f)
|
|
264
|
+
elif f.frame_type == FrameType.DISC:
|
|
265
|
+
self._send_ua(final=bool(f.poll_final))
|
|
266
|
+
self.state = LinkState.DISCONNECTED
|
|
267
|
+
raise ConnectionError("Remote station disconnected")
|
|
268
|
+
elif f.frame_type == FrameType.DM:
|
|
269
|
+
self.state = LinkState.DISCONNECTED
|
|
270
|
+
raise ConnectionError("Remote station sent DM (disconnected mode)")
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
def _handle_i_frame(self, f) -> Optional[bytes]:
|
|
274
|
+
if self._own_receiver_busy:
|
|
275
|
+
if f.poll_final:
|
|
276
|
+
self._send_rnr(final=True)
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
if f.ns != self.V_R:
|
|
280
|
+
if not self._reject_exception:
|
|
281
|
+
self._reject_exception = True
|
|
282
|
+
self._send_rej(poll=False)
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
self.V_R = (self.V_R + 1) % 8
|
|
286
|
+
self._reject_exception = False
|
|
287
|
+
payload = f.info
|
|
288
|
+
|
|
289
|
+
self._send_rr(nr=self.V_R, poll=bool(f.poll_final), command=False)
|
|
290
|
+
self._ack_pending = False
|
|
291
|
+
self._check_i_frame_ack(f.nr)
|
|
292
|
+
|
|
293
|
+
return payload
|
|
294
|
+
|
|
295
|
+
def _handle_rr(self, f) -> None:
|
|
296
|
+
self._peer_receiver_busy = False
|
|
297
|
+
self._check_i_frame_ack(f.nr)
|
|
298
|
+
if f.poll_final:
|
|
299
|
+
if self.state == LinkState.TIMER_RECOVERY:
|
|
300
|
+
self._t1.stop()
|
|
301
|
+
self.state = LinkState.CONNECTED
|
|
302
|
+
self._t3.start(self._t3_timeout)
|
|
303
|
+
self._invoke_retransmission()
|
|
304
|
+
else:
|
|
305
|
+
# Respond to remote's keep-alive poll with F=1 (AX.25 §6.3.8)
|
|
306
|
+
self._send_rr(nr=self.V_R, poll=True, command=False)
|
|
307
|
+
|
|
308
|
+
def _handle_rnr(self, f) -> None:
|
|
309
|
+
self._peer_receiver_busy = True
|
|
310
|
+
self._check_i_frame_ack(f.nr)
|
|
311
|
+
|
|
312
|
+
def _handle_rej(self, f) -> None:
|
|
313
|
+
self._peer_receiver_busy = False
|
|
314
|
+
self._check_i_frame_ack(f.nr)
|
|
315
|
+
self._t1.stop()
|
|
316
|
+
self.V_S = f.nr
|
|
317
|
+
self._t3.start(self._t3_timeout)
|
|
318
|
+
self._invoke_retransmission()
|
|
319
|
+
|
|
320
|
+
def _handle_sabm_reset(self, f) -> None:
|
|
321
|
+
self._send_ua(final=bool(f.poll_final))
|
|
322
|
+
self._clear_exception_conditions()
|
|
323
|
+
self.V_S = 0
|
|
324
|
+
self.V_R = 0
|
|
325
|
+
self.V_A = 0
|
|
326
|
+
self.state = LinkState.CONNECTED
|
|
327
|
+
|
|
328
|
+
def _check_i_frame_ack(self, nr: int) -> None:
|
|
329
|
+
if self._va_leq_nr_leq_vs(nr):
|
|
330
|
+
seq = self.V_A
|
|
331
|
+
while seq != nr:
|
|
332
|
+
self._unacked.pop(seq, None)
|
|
333
|
+
seq = (seq + 1) % 8
|
|
334
|
+
self.V_A = nr
|
|
335
|
+
if self.V_A == self.V_S:
|
|
336
|
+
self._t1.stop()
|
|
337
|
+
self._t3.start(self._t3_timeout)
|
|
338
|
+
else:
|
|
339
|
+
self._t1.start(self._t1_timeout)
|
|
340
|
+
|
|
341
|
+
def _va_leq_nr_leq_vs(self, nr: int) -> bool:
|
|
342
|
+
va, vs = self.V_A, self.V_S
|
|
343
|
+
if va <= vs:
|
|
344
|
+
return va <= nr <= vs
|
|
345
|
+
return nr >= va or nr <= vs
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------ #
|
|
348
|
+
# Internal: timer management #
|
|
349
|
+
# ------------------------------------------------------------------ #
|
|
350
|
+
|
|
351
|
+
def _check_timers(self) -> None:
|
|
352
|
+
if self._t1.expired:
|
|
353
|
+
self._on_t1_expiry()
|
|
354
|
+
elif self._t3.expired:
|
|
355
|
+
self._on_t3_expiry()
|
|
356
|
+
|
|
357
|
+
def _on_t1_expiry(self) -> None:
|
|
358
|
+
if self.RC >= self._n2:
|
|
359
|
+
logger.warning("T1 expired %d times — giving up connection", self.RC)
|
|
360
|
+
self.state = LinkState.DISCONNECTED
|
|
361
|
+
raise ConnectionError("Link failure: T1 expired N2 times")
|
|
362
|
+
self.RC += 1
|
|
363
|
+
self.state = LinkState.TIMER_RECOVERY
|
|
364
|
+
self._send_rr(nr=self.V_R, poll=True)
|
|
365
|
+
self._t1.start(self._t1_timeout)
|
|
366
|
+
|
|
367
|
+
def _on_t3_expiry(self) -> None:
|
|
368
|
+
self.RC = 0
|
|
369
|
+
self._t3.stop()
|
|
370
|
+
self.state = LinkState.TIMER_RECOVERY
|
|
371
|
+
self._send_rr(nr=self.V_R, poll=True)
|
|
372
|
+
self._t1.start(self._t1_timeout)
|
|
373
|
+
|
|
374
|
+
# ------------------------------------------------------------------ #
|
|
375
|
+
# Internal: retransmission #
|
|
376
|
+
# ------------------------------------------------------------------ #
|
|
377
|
+
|
|
378
|
+
def _invoke_retransmission(self) -> None:
|
|
379
|
+
self.V_S = self.V_A
|
|
380
|
+
for payload in list(self._unacked.values()):
|
|
381
|
+
self._send_i_frame(payload)
|
|
382
|
+
|
|
383
|
+
# ------------------------------------------------------------------ #
|
|
384
|
+
# Internal: frame senders #
|
|
385
|
+
# ------------------------------------------------------------------ #
|
|
386
|
+
|
|
387
|
+
def _send_sabm(self, poll: bool = True, via=None) -> None:
|
|
388
|
+
raw = encode_sabm(self._dest_call, self._dest_ssid,
|
|
389
|
+
self._my_call, self._my_ssid, poll=poll, via=via)
|
|
390
|
+
self._kiss.send_frame(raw)
|
|
391
|
+
if self._on_frame:
|
|
392
|
+
self._on_frame(">", f"SABM {self._my_call}→{self._dest_call} (poll)")
|
|
393
|
+
logger.debug("→ SABM (P=%s, via=%s)", poll, via)
|
|
394
|
+
|
|
395
|
+
def _send_ua(self, final: bool = True) -> None:
|
|
396
|
+
raw = encode_ua(self._dest_call, self._dest_ssid,
|
|
397
|
+
self._my_call, self._my_ssid, final=final)
|
|
398
|
+
self._kiss.send_frame(raw)
|
|
399
|
+
|
|
400
|
+
def _send_dm(self, final: bool = True) -> None:
|
|
401
|
+
raw = encode_dm(self._dest_call, self._dest_ssid,
|
|
402
|
+
self._my_call, self._my_ssid, final=final)
|
|
403
|
+
self._kiss.send_frame(raw)
|
|
404
|
+
|
|
405
|
+
def _send_disc(self, poll: bool = True) -> None:
|
|
406
|
+
raw = encode_disc(self._dest_call, self._dest_ssid,
|
|
407
|
+
self._my_call, self._my_ssid, poll=poll)
|
|
408
|
+
self._kiss.send_frame(raw)
|
|
409
|
+
if self._on_frame:
|
|
410
|
+
self._on_frame(">", f"DISC {self._my_call}→{self._dest_call}")
|
|
411
|
+
|
|
412
|
+
def _send_i_frame(self, payload: bytes) -> None:
|
|
413
|
+
ns = self.V_S
|
|
414
|
+
raw = encode_i_frame(
|
|
415
|
+
self._dest_call, self._dest_ssid,
|
|
416
|
+
self._my_call, self._my_ssid,
|
|
417
|
+
ns=ns, nr=self.V_R, payload=payload,
|
|
418
|
+
)
|
|
419
|
+
self._unacked[ns] = payload
|
|
420
|
+
self.V_S = (self.V_S + 1) % 8
|
|
421
|
+
self._kiss.send_frame(raw)
|
|
422
|
+
if self._on_frame:
|
|
423
|
+
self._on_frame(">", f"I({ns},{self.V_A}) {_frame_repr(payload)}")
|
|
424
|
+
self._ack_pending = False
|
|
425
|
+
if not self._t1.running:
|
|
426
|
+
self._t1.start(self._t1_timeout)
|
|
427
|
+
if self._t3.running:
|
|
428
|
+
self._t3.stop()
|
|
429
|
+
logger.debug("→ I(%d,%d) %d bytes", ns, self.V_R, len(payload))
|
|
430
|
+
|
|
431
|
+
def _send_rr(self, nr: int, poll: bool = False, command: bool = True) -> None:
|
|
432
|
+
raw = encode_rr(self._dest_call, self._dest_ssid,
|
|
433
|
+
self._my_call, self._my_ssid,
|
|
434
|
+
nr=nr, poll=poll, command=command)
|
|
435
|
+
self._kiss.send_frame(raw)
|
|
436
|
+
if self._on_frame:
|
|
437
|
+
self._on_frame(">", f"RR NR={nr}")
|
|
438
|
+
|
|
439
|
+
def _send_rnr(self, final: bool = False) -> None:
|
|
440
|
+
raw = encode_rnr(self._dest_call, self._dest_ssid,
|
|
441
|
+
self._my_call, self._my_ssid,
|
|
442
|
+
nr=self.V_R, poll=final, command=not final)
|
|
443
|
+
self._kiss.send_frame(raw)
|
|
444
|
+
if self._on_frame:
|
|
445
|
+
self._on_frame(">", f"RNR NR={self.V_R}")
|
|
446
|
+
|
|
447
|
+
def _send_rej(self, poll: bool = False) -> None:
|
|
448
|
+
raw = encode_rej(self._dest_call, self._dest_ssid,
|
|
449
|
+
self._my_call, self._my_ssid,
|
|
450
|
+
nr=self.V_R, poll=poll)
|
|
451
|
+
self._kiss.send_frame(raw)
|
|
452
|
+
if self._on_frame:
|
|
453
|
+
self._on_frame(">", f"REJ NR={self.V_R}")
|
|
454
|
+
|
|
455
|
+
# ------------------------------------------------------------------ #
|
|
456
|
+
# Internal: helpers #
|
|
457
|
+
# ------------------------------------------------------------------ #
|
|
458
|
+
|
|
459
|
+
def _clear_exception_conditions(self) -> None:
|
|
460
|
+
self._peer_receiver_busy = False
|
|
461
|
+
self._own_receiver_busy = False
|
|
462
|
+
self._reject_exception = False
|
|
463
|
+
self._ack_pending = False
|
|
464
|
+
self._unacked.clear()
|
|
465
|
+
self._t1.stop()
|
|
466
|
+
self._t3.stop()
|
|
467
|
+
self.RC = 0
|