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.
Files changed (75) hide show
  1. open_packet/__init__.py +0 -0
  2. open_packet/ax25/__init__.py +0 -0
  3. open_packet/ax25/address.py +33 -0
  4. open_packet/ax25/connection.py +467 -0
  5. open_packet/ax25/frame.py +206 -0
  6. open_packet/ax25/timer.py +22 -0
  7. open_packet/engine/__init__.py +0 -0
  8. open_packet/engine/commands.py +63 -0
  9. open_packet/engine/engine.py +542 -0
  10. open_packet/engine/events.py +89 -0
  11. open_packet/forms/__init__.py +0 -0
  12. open_packet/forms/loader.py +107 -0
  13. open_packet/forms/renderer.py +18 -0
  14. open_packet/forms/updater.py +164 -0
  15. open_packet/forms/validator.py +43 -0
  16. open_packet/link/__init__.py +0 -0
  17. open_packet/link/base.py +24 -0
  18. open_packet/link/kiss.py +85 -0
  19. open_packet/link/telnet.py +126 -0
  20. open_packet/node/__init__.py +0 -0
  21. open_packet/node/base.py +73 -0
  22. open_packet/node/bpq.py +274 -0
  23. open_packet/store/__init__.py +0 -0
  24. open_packet/store/database.py +603 -0
  25. open_packet/store/exporter.py +45 -0
  26. open_packet/store/models.py +106 -0
  27. open_packet/store/settings.py +73 -0
  28. open_packet/store/store.py +458 -0
  29. open_packet/terminal/__init__.py +0 -0
  30. open_packet/terminal/session.py +91 -0
  31. open_packet/transport/__init__.py +0 -0
  32. open_packet/transport/base.py +20 -0
  33. open_packet/transport/serial.py +45 -0
  34. open_packet/transport/tcp.py +50 -0
  35. open_packet/ui/__init__.py +0 -0
  36. open_packet/ui/base.py +13 -0
  37. open_packet/ui/tui/__init__.py +0 -0
  38. open_packet/ui/tui/app.py +1040 -0
  39. open_packet/ui/tui/screens/__init__.py +3 -0
  40. open_packet/ui/tui/screens/compose.py +92 -0
  41. open_packet/ui/tui/screens/compose_bulletin.py +54 -0
  42. open_packet/ui/tui/screens/connect_terminal.py +169 -0
  43. open_packet/ui/tui/screens/delete_confirm.py +54 -0
  44. open_packet/ui/tui/screens/form_fill.py +231 -0
  45. open_packet/ui/tui/screens/form_picker.py +68 -0
  46. open_packet/ui/tui/screens/general_settings.py +149 -0
  47. open_packet/ui/tui/screens/interface_picker.py +102 -0
  48. open_packet/ui/tui/screens/main.py +118 -0
  49. open_packet/ui/tui/screens/manage_interfaces.py +147 -0
  50. open_packet/ui/tui/screens/manage_nodes.py +264 -0
  51. open_packet/ui/tui/screens/manage_operators.py +162 -0
  52. open_packet/ui/tui/screens/new_item.py +71 -0
  53. open_packet/ui/tui/screens/node_multi_picker.py +83 -0
  54. open_packet/ui/tui/screens/node_picker.py +105 -0
  55. open_packet/ui/tui/screens/operator_picker.py +102 -0
  56. open_packet/ui/tui/screens/search.py +150 -0
  57. open_packet/ui/tui/screens/settings.py +51 -0
  58. open_packet/ui/tui/screens/setup_interface.py +194 -0
  59. open_packet/ui/tui/screens/setup_node.py +357 -0
  60. open_packet/ui/tui/screens/setup_node_group.py +138 -0
  61. open_packet/ui/tui/screens/setup_operator.py +111 -0
  62. open_packet/ui/tui/screens/shorter_path_confirm.py +44 -0
  63. open_packet/ui/tui/widgets/__init__.py +0 -0
  64. open_packet/ui/tui/widgets/console_panel.py +53 -0
  65. open_packet/ui/tui/widgets/file_list.py +61 -0
  66. open_packet/ui/tui/widgets/folder_tree.py +180 -0
  67. open_packet/ui/tui/widgets/message_body.py +43 -0
  68. open_packet/ui/tui/widgets/message_list.py +70 -0
  69. open_packet/ui/tui/widgets/status_bar.py +138 -0
  70. open_packet/ui/tui/widgets/terminal_view.py +54 -0
  71. open_packet-0.1.0.dist-info/METADATA +124 -0
  72. open_packet-0.1.0.dist-info/RECORD +75 -0
  73. open_packet-0.1.0.dist-info/WHEEL +4 -0
  74. open_packet-0.1.0.dist-info/entry_points.txt +3 -0
  75. open_packet-0.1.0.dist-info/licenses/LICENSE +21 -0
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