piwave 2.0.9__py3-none-any.whl → 2.1.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.
- piwave/__init__.py +3 -1
- piwave/__main__.py +105 -0
- piwave/backends/__init__.py +126 -0
- piwave/backends/base.py +245 -0
- piwave/backends/fm_transmitter.py +44 -0
- piwave/backends/pi_fm_rds.py +49 -0
- piwave/logger.py +99 -0
- piwave/piwave.py +283 -177
- piwave-2.1.1.dist-info/METADATA +750 -0
- piwave-2.1.1.dist-info/RECORD +13 -0
- piwave-2.0.9.dist-info/METADATA +0 -419
- piwave-2.0.9.dist-info/RECORD +0 -7
- {piwave-2.0.9.dist-info → piwave-2.1.1.dist-info}/WHEEL +0 -0
- {piwave-2.0.9.dist-info → piwave-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.0.9.dist-info → piwave-2.1.1.dist-info}/top_level.txt +0 -0
piwave/__init__.py
CHANGED
piwave/__main__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/__main__.py : handles cli inputs
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
from .backends import search_backends, list_backends, backends, discover_backends
|
|
8
|
+
from .logger import Log
|
|
9
|
+
from .piwave import PiWave, PiWaveError
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
if len(sys.argv) < 2:
|
|
13
|
+
Log.header("PiWave CLI")
|
|
14
|
+
Log.info("Usage: python3 -m piwave <command> [args]\n")
|
|
15
|
+
Log.info("Available commands:")
|
|
16
|
+
Log.info(" search - Search for available backends")
|
|
17
|
+
Log.info(" list - List cached backends")
|
|
18
|
+
Log.info(" add <name> <path> - Manually add backend executable path")
|
|
19
|
+
Log.info(" info - Show package info")
|
|
20
|
+
Log.info(" broadcast <file> - Broadcast a file over FM")
|
|
21
|
+
sys.exit(0)
|
|
22
|
+
|
|
23
|
+
cmd = sys.argv[1].lower()
|
|
24
|
+
|
|
25
|
+
if cmd == "search":
|
|
26
|
+
search_backends()
|
|
27
|
+
|
|
28
|
+
elif cmd == "list":
|
|
29
|
+
discover_backends()
|
|
30
|
+
list_backends()
|
|
31
|
+
|
|
32
|
+
elif cmd == "add":
|
|
33
|
+
if len(sys.argv) < 4:
|
|
34
|
+
Log.error("Usage: python3 -m piwave add <backend_name> <path>")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
backend_name = sys.argv[2]
|
|
38
|
+
backend_path = sys.argv[3]
|
|
39
|
+
|
|
40
|
+
discover_backends()
|
|
41
|
+
if backend_name not in backends:
|
|
42
|
+
Log.error(f"Unknown backend '{backend_name}'. Run `python3 -m piwave search` first.")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
backend_class = backends[backend_name]
|
|
46
|
+
try:
|
|
47
|
+
backend = backend_class()
|
|
48
|
+
backend.cache_file.write_text(backend_path)
|
|
49
|
+
Log.success(f"Backend '{backend_name}' path set to {backend_path}")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
Log.error(f"Failed to add backend '{backend_name}': {e}")
|
|
52
|
+
|
|
53
|
+
elif cmd == "info":
|
|
54
|
+
Log.header("PiWave Module Info")
|
|
55
|
+
Log.info("Python-based FM transmitter manager for Raspberry Pi")
|
|
56
|
+
Log.info("Commands: search, list, add, info, broadcast")
|
|
57
|
+
|
|
58
|
+
elif cmd == "broadcast":
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
prog="python3 -m piwave broadcast",
|
|
61
|
+
description="Broadcast a file using PiWave"
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument("file", help="Path to the audio file to broadcast")
|
|
64
|
+
parser.add_argument("--frequency", type=float, default=90.0, help="FM frequency (MHz)")
|
|
65
|
+
parser.add_argument("--ps", type=str, default="PiWave", help="Program Service name (max 8 chars)")
|
|
66
|
+
parser.add_argument("--rt", type=str, default="PiWave: The best python module for managing your pi radio", help="Radio Text (max 64 chars)")
|
|
67
|
+
parser.add_argument("--pi", type=str, default="FFFF", help="Program Identification code (4 hex digits)")
|
|
68
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
69
|
+
parser.add_argument("--silent", action="store_true", help="Suppress logs")
|
|
70
|
+
parser.add_argument("--loop", action="store_true", help="Loop playback")
|
|
71
|
+
parser.add_argument("--backend", type=str, default="auto", help="Choose a specific backend")
|
|
72
|
+
|
|
73
|
+
args = parser.parse_args(sys.argv[2:])
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
pw = PiWave(
|
|
77
|
+
frequency=args.frequency,
|
|
78
|
+
ps=args.ps,
|
|
79
|
+
rt=args.rt,
|
|
80
|
+
pi=args.pi,
|
|
81
|
+
debug=args.debug,
|
|
82
|
+
silent=args.silent,
|
|
83
|
+
loop=args.loop,
|
|
84
|
+
backend=args.backend
|
|
85
|
+
)
|
|
86
|
+
pw.play(args.file)
|
|
87
|
+
|
|
88
|
+
Log.info("Press Ctrl+C to stop broadcasting...")
|
|
89
|
+
try:
|
|
90
|
+
while True:
|
|
91
|
+
pass
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
pw.stop()
|
|
94
|
+
Log.info("Broadcast stopped")
|
|
95
|
+
except PiWaveError as e:
|
|
96
|
+
Log.error(f"PiWaveError: {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
Log.error(f"Unknown command: {cmd}")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/backends/__init__.py : some utility funcs for backends
|
|
4
|
+
|
|
5
|
+
from .pi_fm_rds import PiFmRdsBackend
|
|
6
|
+
from .fm_transmitter import FmTransmitterBackend
|
|
7
|
+
from ..logger import Log
|
|
8
|
+
|
|
9
|
+
# auto discover backends
|
|
10
|
+
backends = {}
|
|
11
|
+
backend_classes = {
|
|
12
|
+
"pi_fm_rds": PiFmRdsBackend,
|
|
13
|
+
"fm_transmitter": FmTransmitterBackend
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_best_backend(mode: str, frequency: float):
|
|
18
|
+
if mode == "file_broadcast":
|
|
19
|
+
if 88.0 <= frequency <= 108.0:
|
|
20
|
+
if "pi_fm_rds" in backends:
|
|
21
|
+
return "pi_fm_rds"
|
|
22
|
+
elif "fm_transmitter" in backends:
|
|
23
|
+
return "fm_transmitter"
|
|
24
|
+
else:
|
|
25
|
+
if "fm_transmitter" in backends:
|
|
26
|
+
return "fm_transmitter"
|
|
27
|
+
elif "pi_fm_rds" in backends:
|
|
28
|
+
return "pi_fm_rds"
|
|
29
|
+
|
|
30
|
+
elif mode == "live_broadcast":
|
|
31
|
+
if "fm_transmitter" in backends:
|
|
32
|
+
backend = backends["fm_transmitter"]()
|
|
33
|
+
min_freq, max_freq = backend.frequency_range
|
|
34
|
+
if min_freq <= frequency <= max_freq:
|
|
35
|
+
return "fm_transmitter"
|
|
36
|
+
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def discover_backends():
|
|
42
|
+
"""
|
|
43
|
+
Populate `backends` dictionary with available backends.
|
|
44
|
+
Reads cache files first, probes hardware only if no cache exists.
|
|
45
|
+
Stores executable path in cache if available, "None" otherwise.
|
|
46
|
+
"""
|
|
47
|
+
backends.clear()
|
|
48
|
+
|
|
49
|
+
for name, backend_class in backend_classes.items():
|
|
50
|
+
backend = backend_class()
|
|
51
|
+
|
|
52
|
+
if backend.cache_file.exists():
|
|
53
|
+
content = backend.cache_file.read_text().strip()
|
|
54
|
+
if content == "None":
|
|
55
|
+
available = False
|
|
56
|
+
else:
|
|
57
|
+
available = True
|
|
58
|
+
backend._executable_path = content
|
|
59
|
+
else:
|
|
60
|
+
try:
|
|
61
|
+
available = backend.is_available()
|
|
62
|
+
except Exception:
|
|
63
|
+
available = False
|
|
64
|
+
|
|
65
|
+
if available:
|
|
66
|
+
backend.cache_file.write_text(backend.required_executable)
|
|
67
|
+
else:
|
|
68
|
+
backend.cache_file.write_text("None")
|
|
69
|
+
|
|
70
|
+
if available:
|
|
71
|
+
backends[name] = backend_class
|
|
72
|
+
|
|
73
|
+
return backends
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def search_backends():
|
|
77
|
+
"""
|
|
78
|
+
Probe all backends directly, ignoring any cache.
|
|
79
|
+
Updates the cache files with the actual availability.
|
|
80
|
+
Overwrites existing cache files.
|
|
81
|
+
"""
|
|
82
|
+
backends.clear()
|
|
83
|
+
|
|
84
|
+
for name, backend_class in backend_classes.items():
|
|
85
|
+
backend = backend_class()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
available = backend.is_available()
|
|
89
|
+
except Exception:
|
|
90
|
+
available = False
|
|
91
|
+
|
|
92
|
+
if available:
|
|
93
|
+
backend.cache_file.write_text(backend.required_executable)
|
|
94
|
+
backends[name] = backend_class
|
|
95
|
+
else:
|
|
96
|
+
backend.cache_file.write_text("None")
|
|
97
|
+
|
|
98
|
+
if backends:
|
|
99
|
+
Log.success(f"Found backends: {', '.join(backends.keys())}")
|
|
100
|
+
else:
|
|
101
|
+
Log.warning("No backends found. Please install pi_fm_rds or fm_transmitter")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def list_backends():
|
|
106
|
+
"""List every cached backend avalible
|
|
107
|
+
|
|
108
|
+
.. note::
|
|
109
|
+
You can also use the 'python3 -m piwave list' command.
|
|
110
|
+
"""
|
|
111
|
+
if not backends:
|
|
112
|
+
Log.warning("No backends available")
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
backend_info = {}
|
|
116
|
+
for name, backend_class in backends.items():
|
|
117
|
+
backend = backend_class()
|
|
118
|
+
min_freq, max_freq = backend.frequency_range
|
|
119
|
+
rds = "Yes" if backend.supports_rds else "No"
|
|
120
|
+
backend_info[name] = {
|
|
121
|
+
'frequency_range': f"{min_freq}-{max_freq}MHz",
|
|
122
|
+
'rds_support': rds
|
|
123
|
+
}
|
|
124
|
+
Log.info(f"{name}: {min_freq}-{max_freq}MHz, RDS: {rds}")
|
|
125
|
+
|
|
126
|
+
return backend_info
|
piwave/backends/base.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/backend/base.py : backends backbone
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import subprocess
|
|
8
|
+
import os
|
|
9
|
+
import signal
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
class BackendError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class Backend(ABC):
|
|
17
|
+
def __init__(self, frequency: float = 90.0, ps: str = "PiWave",
|
|
18
|
+
rt: str = "", pi: str = "FFFF"):
|
|
19
|
+
self.frequency = frequency
|
|
20
|
+
self.ps = ps[:8] # Max 8 chars
|
|
21
|
+
self.rt = rt[:64] # Max 64 chars
|
|
22
|
+
self.pi = pi[:4] # Max 4 chars
|
|
23
|
+
self.current_process: Optional[subprocess.Popen] = None
|
|
24
|
+
self.is_available_cached: Optional[bool] = None
|
|
25
|
+
self._executable_path: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def name(self):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def frequency_range(self):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def supports_rds(self):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def supports_live_streaming(self):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def cache_file(self):
|
|
49
|
+
return Path(__file__).parent.parent / f"{self.name}_path"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def required_executable(self):
|
|
53
|
+
if self._executable_path:
|
|
54
|
+
return self._executable_path
|
|
55
|
+
|
|
56
|
+
self._executable_path = self._find_executable()
|
|
57
|
+
return self._executable_path
|
|
58
|
+
|
|
59
|
+
def _find_executable(self) -> str:
|
|
60
|
+
# Check cache first
|
|
61
|
+
if self.cache_file.exists():
|
|
62
|
+
try:
|
|
63
|
+
cached_path = self.cache_file.read_text().strip()
|
|
64
|
+
if self._is_valid_executable(cached_path):
|
|
65
|
+
return cached_path
|
|
66
|
+
else:
|
|
67
|
+
self.cache_file.unlink()
|
|
68
|
+
except Exception:
|
|
69
|
+
self.cache_file.unlink(missing_ok=True)
|
|
70
|
+
|
|
71
|
+
# Only then search for it
|
|
72
|
+
found_path = self._search_executable()
|
|
73
|
+
if found_path:
|
|
74
|
+
try:
|
|
75
|
+
self.cache_file.write_text(found_path)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return found_path
|
|
79
|
+
|
|
80
|
+
raise BackendError(f"Could not find path for {self.name}. Please manually add one with python3 -m piwave add {self.name} <path>")
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def _get_executable_name(self):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def _get_search_paths(self):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def _search_executable(self):
|
|
91
|
+
executable_name = self._get_executable_name()
|
|
92
|
+
|
|
93
|
+
# Try at first system $PATH
|
|
94
|
+
system_path = shutil.which(executable_name)
|
|
95
|
+
if system_path and self._is_valid_executable(system_path):
|
|
96
|
+
return system_path
|
|
97
|
+
|
|
98
|
+
# Then for the dirs
|
|
99
|
+
search_paths = self._get_search_paths()
|
|
100
|
+
|
|
101
|
+
for search_path in search_paths:
|
|
102
|
+
if not Path(search_path).exists():
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
for root, dirs, files in os.walk(search_path):
|
|
107
|
+
if executable_name in files:
|
|
108
|
+
executable_path = Path(root) / executable_name
|
|
109
|
+
if self._is_valid_executable(str(executable_path)):
|
|
110
|
+
return str(executable_path)
|
|
111
|
+
except (PermissionError, OSError):
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# valid executable part start
|
|
117
|
+
|
|
118
|
+
def _is_valid_executable(self, path: str) -> bool:
|
|
119
|
+
|
|
120
|
+
path_obj = Path(path)
|
|
121
|
+
if not (path_obj.exists() and path_obj.is_file() and os.access(path, os.X_OK)):
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
# test any approach, if only one is valid, its ok
|
|
125
|
+
validation_methods = [
|
|
126
|
+
self._ve_try_help_flag,
|
|
127
|
+
self._ve_try_version_flag,
|
|
128
|
+
self._ve_try_no_args,
|
|
129
|
+
self._ve_try_invalid_flag
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
for method in validation_methods:
|
|
133
|
+
try:
|
|
134
|
+
if method(path):
|
|
135
|
+
return True
|
|
136
|
+
except Exception:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def _ve_try_help_flag(self, path: str) -> bool:
|
|
142
|
+
try:
|
|
143
|
+
result = subprocess.run(
|
|
144
|
+
[path, "--help"],
|
|
145
|
+
stdout=subprocess.PIPE,
|
|
146
|
+
stderr=subprocess.PIPE,
|
|
147
|
+
timeout=3
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return result.returncode in [0, 1, 2]
|
|
151
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
def _ve_try_version_flag(self, path: str) -> bool:
|
|
155
|
+
for flag in ["--version", "-v", "-V"]:
|
|
156
|
+
try:
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
[path, flag],
|
|
159
|
+
stdout=subprocess.PIPE,
|
|
160
|
+
stderr=subprocess.PIPE,
|
|
161
|
+
timeout=3
|
|
162
|
+
)
|
|
163
|
+
if result.returncode in [0, 1]:
|
|
164
|
+
return True
|
|
165
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
166
|
+
continue
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def _ve_try_no_args(self, path: str) -> bool:
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
[path],
|
|
173
|
+
stdout=subprocess.PIPE,
|
|
174
|
+
stderr=subprocess.PIPE,
|
|
175
|
+
timeout=1
|
|
176
|
+
)
|
|
177
|
+
return True
|
|
178
|
+
except subprocess.TimeoutExpired:
|
|
179
|
+
return True
|
|
180
|
+
except (FileNotFoundError, PermissionError):
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def _ve_try_invalid_flag(self, path: str) -> bool:
|
|
184
|
+
try:
|
|
185
|
+
result = subprocess.run(
|
|
186
|
+
[path, "--piwave-test-invalid-flag"],
|
|
187
|
+
stdout=subprocess.PIPE,
|
|
188
|
+
stderr=subprocess.PIPE,
|
|
189
|
+
timeout=3
|
|
190
|
+
)
|
|
191
|
+
return result.returncode in [1, 2]
|
|
192
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
# valid executable part end
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def is_available(self):
|
|
199
|
+
if self.is_available_cached is not None:
|
|
200
|
+
return self.is_available_cached
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
self.required_executable
|
|
204
|
+
self.is_available_cached = True
|
|
205
|
+
return True
|
|
206
|
+
except BackendError:
|
|
207
|
+
self.is_available_cached = False
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def build_command(self, wav_file: str):
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def build_live_command(self):
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
def validate_settings(self):
|
|
219
|
+
min_freq, max_freq = self.frequency_range
|
|
220
|
+
return min_freq <= self.frequency <= max_freq
|
|
221
|
+
|
|
222
|
+
def play_file(self, wav_file: str) -> subprocess.Popen:
|
|
223
|
+
if not self.validate_settings():
|
|
224
|
+
min_freq, max_freq = self.frequency_range
|
|
225
|
+
raise BackendError(f"{self.name} supports {min_freq}-{max_freq}MHz, got {self.frequency}MHz")
|
|
226
|
+
|
|
227
|
+
cmd = self.build_command(wav_file)
|
|
228
|
+
self.current_process = subprocess.Popen(
|
|
229
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
230
|
+
preexec_fn=os.setsid
|
|
231
|
+
)
|
|
232
|
+
return self.current_process
|
|
233
|
+
|
|
234
|
+
def stop(self):
|
|
235
|
+
if self.current_process:
|
|
236
|
+
try:
|
|
237
|
+
os.killpg(os.getpgid(self.current_process.pid), signal.SIGTERM)
|
|
238
|
+
self.current_process.wait(timeout=5)
|
|
239
|
+
except (ProcessLookupError, subprocess.TimeoutExpired):
|
|
240
|
+
try:
|
|
241
|
+
os.killpg(os.getpgid(self.current_process.pid), signal.SIGKILL)
|
|
242
|
+
except ProcessLookupError:
|
|
243
|
+
pass
|
|
244
|
+
finally:
|
|
245
|
+
self.current_process = None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/backends/fm_transmitter.py : fm_transmitter backend, doesn't supports RDS, can broadcast on freqs 1 to 250
|
|
4
|
+
# needs https://github.com/markondej/fm_transmitter installed on the system to work !
|
|
5
|
+
|
|
6
|
+
from .base import Backend
|
|
7
|
+
|
|
8
|
+
class FmTransmitterBackend(Backend):
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def name(self):
|
|
12
|
+
return "fm_transmitter"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def frequency_range(self):
|
|
16
|
+
return (1.0, 250.0) # fm transmitter supports a pretty good range, tho be aware of performance issues
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def supports_rds(self):
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def supports_live_streaming(self):
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
def _get_executable_name(self):
|
|
27
|
+
return "fm_transmitter"
|
|
28
|
+
|
|
29
|
+
def _get_search_paths(self):
|
|
30
|
+
return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
|
|
31
|
+
|
|
32
|
+
def build_command(self, wav_file: str):
|
|
33
|
+
return [
|
|
34
|
+
'sudo', self.required_executable,
|
|
35
|
+
'-f', str(self.frequency),
|
|
36
|
+
wav_file
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def build_live_command(self):
|
|
40
|
+
return [
|
|
41
|
+
'sudo', self.required_executable,
|
|
42
|
+
'-f', str(self.frequency),
|
|
43
|
+
'-'
|
|
44
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/backends/pi_fm_rds.py : PiFmRds backend, supports RDS on freqs 80 to 108
|
|
4
|
+
# needs https://github.com/ChristopheJacquet/PiFmRds installed on the system to work !
|
|
5
|
+
|
|
6
|
+
from .base import Backend, BackendError
|
|
7
|
+
|
|
8
|
+
class PiFmRdsBackend(Backend):
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def name(self):
|
|
12
|
+
return "pi_fm_rds"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def frequency_range(self):
|
|
16
|
+
return (80.0, 108.0) # standard fm band with a bit less bcs it still kinda works
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def supports_rds(self):
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def supports_live_streaming(self):
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def _get_executable_name(self):
|
|
27
|
+
return "pi_fm_rds"
|
|
28
|
+
|
|
29
|
+
def _get_search_paths(self):
|
|
30
|
+
return ["/opt/PiWave/PiFmRds", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
|
|
31
|
+
|
|
32
|
+
def build_command(self, wav_file: str) -> list:
|
|
33
|
+
cmd = [
|
|
34
|
+
'sudo', self.required_executable,
|
|
35
|
+
'-freq', str(self.frequency),
|
|
36
|
+
'-audio', wav_file
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if self.ps:
|
|
40
|
+
cmd.extend(['-ps', self.ps])
|
|
41
|
+
if self.rt:
|
|
42
|
+
cmd.extend(['-rt', self.rt])
|
|
43
|
+
if self.pi:
|
|
44
|
+
cmd.extend(['-pi', self.pi])
|
|
45
|
+
|
|
46
|
+
return cmd
|
|
47
|
+
|
|
48
|
+
def build_live_command(self):
|
|
49
|
+
return None # not supported sadly
|
piwave/logger.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/Logger.py : Main logging manager
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
class Log:
|
|
8
|
+
COLORS = { # absolutely not taken from stackoverflow trust
|
|
9
|
+
'reset': '\033[0m',
|
|
10
|
+
'bold': '\033[1m',
|
|
11
|
+
'underline': '\033[4m',
|
|
12
|
+
'red': '\033[31m',
|
|
13
|
+
'green': '\033[32m',
|
|
14
|
+
'yellow': '\033[33m',
|
|
15
|
+
'blue': '\033[34m',
|
|
16
|
+
'magenta': '\033[35m',
|
|
17
|
+
'cyan': '\033[36m',
|
|
18
|
+
'white': '\033[37m',
|
|
19
|
+
'bright_red': '\033[91m',
|
|
20
|
+
'bright_green': '\033[92m',
|
|
21
|
+
'bright_yellow': '\033[93m',
|
|
22
|
+
'bright_blue': '\033[94m',
|
|
23
|
+
'bright_magenta': '\033[95m',
|
|
24
|
+
'bright_cyan': '\033[96m',
|
|
25
|
+
'bright_white': '\033[97m',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ICONS = {
|
|
29
|
+
'success': 'OK',
|
|
30
|
+
'error': 'ERR',
|
|
31
|
+
'warning': 'WARN',
|
|
32
|
+
'info': 'INFO',
|
|
33
|
+
'client': 'CLIENT',
|
|
34
|
+
'server': 'SERVER',
|
|
35
|
+
'file': 'FILE',
|
|
36
|
+
'broadcast': 'BCAST',
|
|
37
|
+
'version': 'VER',
|
|
38
|
+
'update': 'UPD',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
SILENT = False
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def config(cls, silent: bool = False):
|
|
45
|
+
cls.SILENT = silent
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def print(cls, message: str, style: str = '', icon: str = '', end: str = '\n'):
|
|
49
|
+
|
|
50
|
+
if cls.SILENT: return
|
|
51
|
+
|
|
52
|
+
color = cls.COLORS.get(style, '')
|
|
53
|
+
icon_char = cls.ICONS.get(icon, '')
|
|
54
|
+
if icon_char:
|
|
55
|
+
if color:
|
|
56
|
+
print(f"{color}[{icon_char}]\033[0m {message}", end=end)
|
|
57
|
+
else:
|
|
58
|
+
print(f"[{icon_char}] {message}", end=end)
|
|
59
|
+
else:
|
|
60
|
+
if color:
|
|
61
|
+
print(f"{color}{message}\033[0m", end=end)
|
|
62
|
+
else:
|
|
63
|
+
print(f"{message}", end=end)
|
|
64
|
+
sys.stdout.flush()
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def header(cls, text: str):
|
|
68
|
+
cls.print(text, 'bright_blue', end='\n\n')
|
|
69
|
+
sys.stdout.flush()
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def section(cls, text: str):
|
|
73
|
+
cls.print(f" {text} ", 'bright_blue', end='')
|
|
74
|
+
cls.print("─" * (len(text) + 2), 'blue', end='\n\n')
|
|
75
|
+
sys.stdout.flush()
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def success(cls, message: str):
|
|
79
|
+
cls.print(message, 'bright_green', 'success')
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def error(cls, message: str):
|
|
83
|
+
cls.print(message, 'bright_red', 'error')
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def warning(cls, message: str):
|
|
87
|
+
cls.print(message, 'bright_yellow', 'warning')
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def info(cls, message: str):
|
|
91
|
+
cls.print(message, 'bright_cyan', 'info')
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def file_message(cls, message: str):
|
|
95
|
+
cls.print(message, 'yellow', 'file')
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def broadcast_message(cls, message: str):
|
|
99
|
+
cls.print(message, 'bright_magenta', 'broadcast')
|