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.
- pluto_siege/__init__.py +43 -0
- pluto_siege/__main__.py +23 -0
- pluto_siege/config.py +249 -0
- pluto_siege/constants.py +132 -0
- pluto_siege/device.py +311 -0
- pluto_siege/dsp.py +157 -0
- pluto_siege/engine.py +460 -0
- pluto_siege/gps.py +419 -0
- pluto_siege/settings.py +54 -0
- pluto_siege/sigmf.py +329 -0
- pluto_siege/sound.py +162 -0
- pluto_siege/ui/__init__.py +21 -0
- pluto_siege/ui/screens/__init__.py +32 -0
- pluto_siege/ui/screens/capture.py +226 -0
- pluto_siege/ui/screens/loopback.py +90 -0
- pluto_siege/ui/screens/main_menu.py +363 -0
- pluto_siege/ui/screens/settings.py +504 -0
- pluto_siege/ui/screens/transmit.py +240 -0
- pluto_siege/ui/widgets/__init__.py +45 -0
- pluto_siege/ui/widgets/framework.py +863 -0
- pluto_siege-2.0.1.dist-info/METADATA +187 -0
- pluto_siege-2.0.1.dist-info/RECORD +26 -0
- pluto_siege-2.0.1.dist-info/WHEEL +5 -0
- pluto_siege-2.0.1.dist-info/entry_points.txt +2 -0
- pluto_siege-2.0.1.dist-info/licenses/LICENSE +674 -0
- pluto_siege-2.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|