pluto-siege 2.0.1__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.
@@ -0,0 +1,240 @@
1
+ # PlutoSiege - RF capture and replay tool for PlutoSDR.
2
+ # Copyright (C) 2026 SketchTurnerDev
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ """Transmit screen: pick a recording and replay it."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import curses
22
+ import datetime
23
+ import glob
24
+ import os
25
+
26
+ from pluto_siege.config import CONFIG, AppConfig
27
+ from pluto_siege.constants import (
28
+ BYTES_PER_SAMPLE,
29
+ IO_TIMEOUT_UNAVAILABLE,
30
+ RECORDS_DIR,
31
+ )
32
+ from pluto_siege.device import SDRDevice
33
+ from pluto_siege.engine import TransmitEngine
34
+ from pluto_siege.sigmf import load_sigmf_geolocation, load_sigmf_meta
35
+ from pluto_siege.sound import SoundCue, play_cue
36
+ from pluto_siege.ui.widgets.framework import (
37
+ C_DIM,
38
+ C_ERR,
39
+ C_OK,
40
+ C_WARN,
41
+ KEY_ENTER,
42
+ KEY_ESC,
43
+ _tail,
44
+ confirm_dialog,
45
+ draw_chrome,
46
+ flush_input,
47
+ menu_select,
48
+ message_box,
49
+ screen_is_usable,
50
+ scroll_view,
51
+ wait_for_usable_screen,
52
+ )
53
+
54
+ DELETE_KEYS = (curses.KEY_DC, curses.KEY_BACKSPACE, 8, 127, ord("d"), ord("D"))
55
+
56
+
57
+ def _label_for_recording(f: str, bounds: tuple[int, int] | None = None) -> str:
58
+ """Human-readable label from a recording filename with freq, sample rate, sample count, and GPS tag."""
59
+ name = os.path.basename(f).removeprefix("rec_").removesuffix(".sigmf-data")
60
+ parts = name.split("_")
61
+ try:
62
+ use_sr, use_freq, err = load_sigmf_meta(f, bounds=bounds)
63
+ if err is not None or use_sr is None or use_freq is None:
64
+ return name
65
+ n_samples = os.path.getsize(f) // BYTES_PER_SAMPLE
66
+ if len(parts) >= 3:
67
+ dt = datetime.datetime.strptime(
68
+ "_".join(parts[:2]), "%Y%m%d_%H%M%S"
69
+ ).replace(tzinfo=datetime.timezone.utc)
70
+ dt_str = f"{dt:%Y-%m-%d %H:%M:%S}"
71
+ else:
72
+ dt_str = name
73
+ geo = load_sigmf_geolocation(f)
74
+ if geo is not None:
75
+ lon, lat, _alt = geo
76
+ lat_card = "N" if lat >= 0 else "S"
77
+ lon_card = "E" if lon >= 0 else "W"
78
+ geo_tag = f" [{abs(lat):.3f}°{lat_card}, {abs(lon):.3f}°{lon_card}]"
79
+ else:
80
+ geo_tag = ""
81
+ return (
82
+ f"{dt_str} {use_freq / 1e6:.3f} MHz "
83
+ f"{use_sr / 1e6:.2f} MSPS {n_samples:,} Samples{geo_tag}"
84
+ )
85
+ except (OSError, ValueError, KeyError):
86
+ return name
87
+
88
+
89
+ def pick_recording(
90
+ win: curses.window, bounds: tuple[int, int] | None = None
91
+ ) -> str | None:
92
+ try:
93
+ os.makedirs(RECORDS_DIR, exist_ok=True)
94
+ except OSError as e:
95
+ message_box(win, "Transmit", [(f"Cannot open recordings folder: {e}", C_ERR)])
96
+ return None
97
+
98
+ sel_idx = 0
99
+ while True:
100
+ files = sorted(
101
+ glob.glob(os.path.join(RECORDS_DIR, "*.sigmf-data")), reverse=True
102
+ )
103
+ if not files:
104
+ message_box(
105
+ win,
106
+ "Select Recording to Transmit (0 found)",
107
+ [("No recordings found. Capture a key first.", C_WARN)],
108
+ )
109
+ return None
110
+
111
+ labels = [_label_for_recording(f, bounds=bounds) for f in files]
112
+ sel, key = menu_select(
113
+ win,
114
+ f"Select Recording to Transmit ({len(files)} found)",
115
+ labels,
116
+ start_idx=sel_idx,
117
+ hint="Up/Down = move ▎ Enter = transmit ▎ D/Del = delete ▎ Esc/Q = back",
118
+ action_keys=DELETE_KEYS,
119
+ )
120
+
121
+ if sel is None or key in (KEY_ESC, ord("q"), ord("Q")):
122
+ return None
123
+
124
+ sel_idx = sel
125
+ target_file = files[sel]
126
+
127
+ if key in DELETE_KEYS:
128
+ filename = os.path.basename(target_file)
129
+ if confirm_dialog(
130
+ win,
131
+ "Delete Recording",
132
+ f"Delete recording {filename}?",
133
+ ):
134
+ meta_file = target_file.removesuffix(".sigmf-data") + ".sigmf-meta"
135
+ try:
136
+ os.remove(target_file)
137
+ except OSError as err:
138
+ message_box(
139
+ win, "Delete Recording", [(f"Failed to delete: {err}", C_ERR)]
140
+ )
141
+ continue
142
+ try:
143
+ if os.path.exists(meta_file):
144
+ os.remove(meta_file)
145
+ except OSError as err:
146
+ message_box(
147
+ win,
148
+ "Delete Recording",
149
+ [(f"Failed to delete metadata: {err}", C_ERR)],
150
+ )
151
+ continue
152
+
153
+ return target_file
154
+
155
+
156
+ def do_transmit(
157
+ win: curses.window,
158
+ sdr: SDRDevice,
159
+ path: str,
160
+ config: AppConfig | None = None,
161
+ ) -> int:
162
+ """Replay one recording, then show the result. Returns the key that closed it."""
163
+ cfg = CONFIG if config is None else config
164
+ if not screen_is_usable(win) and not wait_for_usable_screen(win):
165
+ return KEY_ESC
166
+ log: list[tuple[str, int]] = []
167
+ tx_ok = True
168
+
169
+ def render(extra_hint: str = "Please wait...") -> None:
170
+ start = draw_chrome(win, "Transmit Mode", extra_hint)
171
+ if start != -1:
172
+ _tail(log, win, start, 2)
173
+ win.refresh()
174
+
175
+ try:
176
+ log.append(("Configuring transmitter...", C_DIM))
177
+ render()
178
+
179
+ data, use_sr, use_freq = TransmitEngine.prepare_transmission(
180
+ path, bounds=cfg.freq_bounds
181
+ )
182
+
183
+ sound_enabled = cfg.enable_sound
184
+ log.append(
185
+ (
186
+ f"{use_freq / 1e6:.3f} MHz {use_sr / 1e6:.2f} MSPS {data.size:,} Samples",
187
+ C_DIM,
188
+ )
189
+ )
190
+ geo = load_sigmf_geolocation(path)
191
+ if geo is not None:
192
+ lon, lat, alt = geo
193
+ lat_card = "N" if lat >= 0 else "S"
194
+ lon_card = "E" if lon >= 0 else "W"
195
+ log.append(
196
+ (
197
+ f"Geotag: {abs(lat):.5f}°{lat_card}, {abs(lon):.5f}°{lon_card} ({alt:.1f} m)",
198
+ C_DIM,
199
+ )
200
+ )
201
+ log.append((f"Transmitting recording: {os.path.basename(path)}...", C_DIM))
202
+ play_cue(SoundCue.TX_START, enabled=sound_enabled)
203
+ render()
204
+
205
+ timeout_ok = TransmitEngine.transmit(sdr, data, use_sr, use_freq, config=cfg)
206
+ if not timeout_ok:
207
+ log.append((IO_TIMEOUT_UNAVAILABLE, C_WARN))
208
+
209
+ play_cue(SoundCue.TX_COMPLETE, enabled=sound_enabled)
210
+ log.append(("Signal replayed successfully!", C_OK))
211
+ except (OSError, RuntimeError, ValueError) as e:
212
+ tx_ok = False
213
+ play_cue(SoundCue.FAIL, enabled=cfg.enable_sound)
214
+ log.append((f"TX failed: {e}", C_ERR))
215
+
216
+ flush_input(win)
217
+ title = "Transmit Complete" if tx_ok else "Transmit Failed"
218
+ hint = "Enter/R = replay ▎ Esc/Q = back" if tx_ok else "Enter/R = retry ▎ Esc/Q = back"
219
+ return scroll_view(
220
+ win,
221
+ title,
222
+ log,
223
+ hint=hint,
224
+ exit_keys=(KEY_ESC, ord("q"), ord("Q"), ord("r"), ord("R")) + KEY_ENTER,
225
+ )
226
+
227
+
228
+ def screen_transmit(
229
+ win: curses.window, sdr: SDRDevice, config: AppConfig | None = None
230
+ ) -> None:
231
+ cfg = CONFIG if config is None else config
232
+ bounds = cfg.freq_bounds
233
+ while True:
234
+ path = pick_recording(win, bounds=bounds)
235
+ if path is None:
236
+ return
237
+ while True:
238
+ key = do_transmit(win, sdr, path, config=cfg)
239
+ if key not in KEY_ENTER and key not in (ord("r"), ord("R")):
240
+ break
@@ -0,0 +1,45 @@
1
+ # PlutoSiege - RF capture and replay tool for PlutoSDR.
2
+ # Copyright (C) 2026 SketchTurnerDev
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ """UI widget and rendering framework subpackage."""
18
+
19
+ from pluto_siege.ui.widgets.framework import (
20
+ confirm_dialog,
21
+ draw_chrome,
22
+ draw_gps_status,
23
+ draw_signal_meter,
24
+ edit_number,
25
+ edit_text,
26
+ menu_select,
27
+ message_box,
28
+ parse_number,
29
+ scroll_view,
30
+ set_cursor_visible,
31
+ )
32
+
33
+ __all__ = [
34
+ "confirm_dialog",
35
+ "draw_chrome",
36
+ "draw_gps_status",
37
+ "draw_signal_meter",
38
+ "edit_number",
39
+ "edit_text",
40
+ "menu_select",
41
+ "message_box",
42
+ "parse_number",
43
+ "scroll_view",
44
+ "set_cursor_visible",
45
+ ]