faketelemetry 0.1.0__tar.gz
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.
- faketelemetry-0.1.0/LICENSE +21 -0
- faketelemetry-0.1.0/PKG-INFO +111 -0
- faketelemetry-0.1.0/README.md +78 -0
- faketelemetry-0.1.0/faketelemetry/__init__.py +6 -0
- faketelemetry-0.1.0/faketelemetry/enums.py +16 -0
- faketelemetry-0.1.0/faketelemetry/multi_channel.py +33 -0
- faketelemetry-0.1.0/faketelemetry/noise_injector.py +21 -0
- faketelemetry-0.1.0/faketelemetry/telemetry_generator.py +119 -0
- faketelemetry-0.1.0/faketelemetry.egg-info/PKG-INFO +111 -0
- faketelemetry-0.1.0/faketelemetry.egg-info/SOURCES.txt +15 -0
- faketelemetry-0.1.0/faketelemetry.egg-info/dependency_links.txt +1 -0
- faketelemetry-0.1.0/faketelemetry.egg-info/top_level.txt +1 -0
- faketelemetry-0.1.0/pyproject.toml +3 -0
- faketelemetry-0.1.0/setup.cfg +4 -0
- faketelemetry-0.1.0/setup.py +36 -0
- faketelemetry-0.1.0/tests/test_generator.py +101 -0
- faketelemetry-0.1.0/tests/test_noise.py +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Adam Billekvist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faketelemetry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time Fake Telemetry Stream Generator
|
|
5
|
+
Home-page: https://github.com/adkvi/faketelemetry
|
|
6
|
+
Author: Adam Billekvist
|
|
7
|
+
Author-email: adam.billeqvist@example.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/adkvi/faketelemetry#readme
|
|
9
|
+
Project-URL: Source, https://github.com/adkvi/faketelemetry
|
|
10
|
+
Project-URL: Tracker, https://github.com/adkvi/faketelemetry/issues
|
|
11
|
+
Keywords: telemetry,signal,generator,iot,testing,simulation,waveform,python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.7
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: keywords
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: project-url
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/faketelemetry/)
|
|
35
|
+
[](https://opensource.org/licenses/MIT)
|
|
36
|
+
[](https://github.com/adkvi/faketelemetry/actions)
|
|
37
|
+
|
|
38
|
+
# FakeTelemetry
|
|
39
|
+
|
|
40
|
+
**A minimal Python package to generate real-time, timestamped fake telemetry streams for testing, simulation, and development.**
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
- Sine, cosine, square, sawtooth, triangle, pulse, and random noise waveforms
|
|
44
|
+
- Custom user-defined waveform support
|
|
45
|
+
- Adjustable amplitude, frequency, offset, and sample rate
|
|
46
|
+
- Optional Gaussian noise injection
|
|
47
|
+
- Real-time streaming of (datetime, value) tuples
|
|
48
|
+
- Multi-channel (parallel) telemetry generation
|
|
49
|
+
- Well-tested and extensible
|
|
50
|
+
|
|
51
|
+
## Example Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from faketelemetry import TelemetryGenerator, WaveformType, NoiseInjector, MultiChannelTelemetryGenerator
|
|
55
|
+
|
|
56
|
+
# Single channel with noise (sine wave)
|
|
57
|
+
noise = NoiseInjector(noise_level=0.2)
|
|
58
|
+
gen = TelemetryGenerator(
|
|
59
|
+
waveform=WaveformType.SINE,
|
|
60
|
+
frequency=1.0,
|
|
61
|
+
amplitude=1.0,
|
|
62
|
+
offset=0.0,
|
|
63
|
+
noise_injector=noise
|
|
64
|
+
)
|
|
65
|
+
for timestamp, value in gen.stream(sampling_rate=2.0, duration=3):
|
|
66
|
+
print(timestamp, value)
|
|
67
|
+
|
|
68
|
+
# Multi-channel example (sine and cosine)
|
|
69
|
+
ch1 = TelemetryGenerator(WaveformType.SINE, frequency=1.0)
|
|
70
|
+
ch2 = TelemetryGenerator(WaveformType.COSINE, frequency=0.5)
|
|
71
|
+
multi = MultiChannelTelemetryGenerator([ch1, ch2])
|
|
72
|
+
for sample in multi.stream(sampling_rate=2.0, duration=3):
|
|
73
|
+
print({k: (v[0], v[1]) for k, v in sample.items()})
|
|
74
|
+
|
|
75
|
+
# Triangle and pulse waveforms
|
|
76
|
+
tri = TelemetryGenerator(WaveformType.TRIANGLE, frequency=1.0)
|
|
77
|
+
pulse = TelemetryGenerator(WaveformType.PULSE, frequency=1.0)
|
|
78
|
+
print('Triangle:', next(tri.stream(1)))
|
|
79
|
+
print('Pulse:', next(pulse.stream(1)))
|
|
80
|
+
|
|
81
|
+
# Custom waveform (e.g., constant 42)
|
|
82
|
+
def custom_func(t):
|
|
83
|
+
return 42.0
|
|
84
|
+
gen_custom = TelemetryGenerator(WaveformType.CUSTOM, custom_func=custom_func)
|
|
85
|
+
print('Custom:', next(gen_custom.stream(1)))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
Clone this repo and install locally:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pip install .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or install in editable/development mode:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
pip install -e .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
Run all tests:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
python -m unittest discover -s tests
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[](https://pypi.org/project/faketelemetry/)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
[](https://github.com/adkvi/faketelemetry/actions)
|
|
4
|
+
|
|
5
|
+
# FakeTelemetry
|
|
6
|
+
|
|
7
|
+
**A minimal Python package to generate real-time, timestamped fake telemetry streams for testing, simulation, and development.**
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
- Sine, cosine, square, sawtooth, triangle, pulse, and random noise waveforms
|
|
11
|
+
- Custom user-defined waveform support
|
|
12
|
+
- Adjustable amplitude, frequency, offset, and sample rate
|
|
13
|
+
- Optional Gaussian noise injection
|
|
14
|
+
- Real-time streaming of (datetime, value) tuples
|
|
15
|
+
- Multi-channel (parallel) telemetry generation
|
|
16
|
+
- Well-tested and extensible
|
|
17
|
+
|
|
18
|
+
## Example Usage
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from faketelemetry import TelemetryGenerator, WaveformType, NoiseInjector, MultiChannelTelemetryGenerator
|
|
22
|
+
|
|
23
|
+
# Single channel with noise (sine wave)
|
|
24
|
+
noise = NoiseInjector(noise_level=0.2)
|
|
25
|
+
gen = TelemetryGenerator(
|
|
26
|
+
waveform=WaveformType.SINE,
|
|
27
|
+
frequency=1.0,
|
|
28
|
+
amplitude=1.0,
|
|
29
|
+
offset=0.0,
|
|
30
|
+
noise_injector=noise
|
|
31
|
+
)
|
|
32
|
+
for timestamp, value in gen.stream(sampling_rate=2.0, duration=3):
|
|
33
|
+
print(timestamp, value)
|
|
34
|
+
|
|
35
|
+
# Multi-channel example (sine and cosine)
|
|
36
|
+
ch1 = TelemetryGenerator(WaveformType.SINE, frequency=1.0)
|
|
37
|
+
ch2 = TelemetryGenerator(WaveformType.COSINE, frequency=0.5)
|
|
38
|
+
multi = MultiChannelTelemetryGenerator([ch1, ch2])
|
|
39
|
+
for sample in multi.stream(sampling_rate=2.0, duration=3):
|
|
40
|
+
print({k: (v[0], v[1]) for k, v in sample.items()})
|
|
41
|
+
|
|
42
|
+
# Triangle and pulse waveforms
|
|
43
|
+
tri = TelemetryGenerator(WaveformType.TRIANGLE, frequency=1.0)
|
|
44
|
+
pulse = TelemetryGenerator(WaveformType.PULSE, frequency=1.0)
|
|
45
|
+
print('Triangle:', next(tri.stream(1)))
|
|
46
|
+
print('Pulse:', next(pulse.stream(1)))
|
|
47
|
+
|
|
48
|
+
# Custom waveform (e.g., constant 42)
|
|
49
|
+
def custom_func(t):
|
|
50
|
+
return 42.0
|
|
51
|
+
gen_custom = TelemetryGenerator(WaveformType.CUSTOM, custom_func=custom_func)
|
|
52
|
+
print('Custom:', next(gen_custom.stream(1)))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Clone this repo and install locally:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pip install .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or install in editable/development mode:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Testing
|
|
70
|
+
|
|
71
|
+
Run all tests:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
python -m unittest discover -s tests
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WaveformType(Enum):
|
|
5
|
+
"""
|
|
6
|
+
Enumeration of supported waveform types for telemetry generation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
SINE = "sine"
|
|
10
|
+
COSINE = "cosine"
|
|
11
|
+
SQUARE = "square"
|
|
12
|
+
SAWTOOTH = "sawtooth"
|
|
13
|
+
TRIANGLE = "triangle"
|
|
14
|
+
PULSE = "pulse"
|
|
15
|
+
RANDOM_NOISE = "random_noise"
|
|
16
|
+
CUSTOM = "custom"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .telemetry_generator import TelemetryGenerator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MultiChannelTelemetryGenerator:
|
|
8
|
+
"""
|
|
9
|
+
Generate multiple telemetry streams (channels) in parallel.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, generators: list[TelemetryGenerator]):
|
|
13
|
+
"""
|
|
14
|
+
:param generators: List of TelemetryGenerator instances (one per channel)
|
|
15
|
+
"""
|
|
16
|
+
self.generators = generators
|
|
17
|
+
|
|
18
|
+
def stream(self, sampling_rate: float, duration: Optional[float] = None):
|
|
19
|
+
"""
|
|
20
|
+
Yield a dict of {channel_index: (datetime, value)} for each sample.
|
|
21
|
+
"""
|
|
22
|
+
interval = 1.0 / sampling_rate
|
|
23
|
+
start_time = time.time()
|
|
24
|
+
elapsed = 0.0
|
|
25
|
+
while duration is None or elapsed < duration:
|
|
26
|
+
now = time.time() - start_time
|
|
27
|
+
result = {}
|
|
28
|
+
for idx, gen in enumerate(self.generators):
|
|
29
|
+
value = gen.generate_point(now)
|
|
30
|
+
result[idx] = (datetime.now(), value)
|
|
31
|
+
yield result
|
|
32
|
+
time.sleep(interval)
|
|
33
|
+
elapsed = time.time() - start_time
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NoiseInjector:
|
|
5
|
+
"""
|
|
6
|
+
Injects Gaussian noise into a signal value.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, noise_level: float = 0.0):
|
|
10
|
+
"""
|
|
11
|
+
:param noise_level: Standard deviation of the Gaussian noise.
|
|
12
|
+
"""
|
|
13
|
+
self.noise_level = noise_level
|
|
14
|
+
|
|
15
|
+
def add_noise(self, value: float) -> float:
|
|
16
|
+
"""
|
|
17
|
+
Add Gaussian noise to the input value.
|
|
18
|
+
"""
|
|
19
|
+
if self.noise_level > 0:
|
|
20
|
+
return value + random.gauss(0, self.noise_level)
|
|
21
|
+
return value
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
import random
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Iterator, Tuple, Optional, Callable
|
|
6
|
+
from .enums import WaveformType
|
|
7
|
+
from .noise_injector import NoiseInjector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TelemetryGenerator:
|
|
11
|
+
"""
|
|
12
|
+
Generates real-time telemetry data based on mathematical waveforms.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
waveform: WaveformType,
|
|
18
|
+
frequency: float = 1.0,
|
|
19
|
+
amplitude: float = 1.0,
|
|
20
|
+
offset: float = 0.0,
|
|
21
|
+
noise_injector: Optional[NoiseInjector] = None,
|
|
22
|
+
custom_func: Optional[Callable[[float], float]] = None,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the telemetry generator.
|
|
26
|
+
:param waveform: Type of signal to generate
|
|
27
|
+
:param frequency: Frequency in Hz (must be >= 0)
|
|
28
|
+
:param amplitude: Peak amplitude (must be >= 0)
|
|
29
|
+
:param offset: Base offset (vertical shift)
|
|
30
|
+
:param noise_injector: Optional NoiseInjector
|
|
31
|
+
:param custom_func: Optional function for custom waveform, signature: (t: float) -> float
|
|
32
|
+
"""
|
|
33
|
+
if frequency < 0:
|
|
34
|
+
raise ValueError("Frequency must be non-negative.")
|
|
35
|
+
if amplitude < 0:
|
|
36
|
+
raise ValueError("Amplitude must be non-negative.")
|
|
37
|
+
if waveform == WaveformType.CUSTOM and not callable(custom_func):
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"A callable custom_func must be provided for CUSTOM waveform."
|
|
40
|
+
)
|
|
41
|
+
self.waveform = waveform
|
|
42
|
+
self.frequency = frequency
|
|
43
|
+
self.amplitude = amplitude
|
|
44
|
+
self.offset = offset
|
|
45
|
+
self.noise_injector = noise_injector
|
|
46
|
+
self.custom_func = custom_func
|
|
47
|
+
|
|
48
|
+
def generate_point(self, t: float) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Generate a signal value at time t (seconds).
|
|
51
|
+
"""
|
|
52
|
+
if t < 0:
|
|
53
|
+
raise ValueError("Time t must be non-negative.")
|
|
54
|
+
if self.waveform == WaveformType.SINE:
|
|
55
|
+
value = (
|
|
56
|
+
self.amplitude * math.sin(2 * math.pi * self.frequency * t)
|
|
57
|
+
+ self.offset
|
|
58
|
+
)
|
|
59
|
+
elif self.waveform == WaveformType.COSINE:
|
|
60
|
+
value = (
|
|
61
|
+
self.amplitude * math.cos(2 * math.pi * self.frequency * t)
|
|
62
|
+
+ self.offset
|
|
63
|
+
)
|
|
64
|
+
elif self.waveform == WaveformType.SQUARE:
|
|
65
|
+
value = (
|
|
66
|
+
self.amplitude
|
|
67
|
+
* (1 if math.sin(2 * math.pi * self.frequency * t) >= 0 else -1)
|
|
68
|
+
+ self.offset
|
|
69
|
+
)
|
|
70
|
+
elif self.waveform == WaveformType.SAWTOOTH:
|
|
71
|
+
value = (
|
|
72
|
+
self.amplitude
|
|
73
|
+
* (2 * (t * self.frequency - math.floor(0.5 + t * self.frequency)))
|
|
74
|
+
+ self.offset
|
|
75
|
+
)
|
|
76
|
+
elif self.waveform == WaveformType.TRIANGLE:
|
|
77
|
+
value = (
|
|
78
|
+
self.amplitude
|
|
79
|
+
* (
|
|
80
|
+
2
|
|
81
|
+
* abs(
|
|
82
|
+
2 * (t * self.frequency - math.floor(t * self.frequency + 0.5))
|
|
83
|
+
)
|
|
84
|
+
- 1
|
|
85
|
+
)
|
|
86
|
+
+ self.offset
|
|
87
|
+
)
|
|
88
|
+
elif self.waveform == WaveformType.PULSE:
|
|
89
|
+
value = (
|
|
90
|
+
self.amplitude * (1 if (t * self.frequency) % 1 < 0.1 else 0)
|
|
91
|
+
+ self.offset
|
|
92
|
+
)
|
|
93
|
+
elif self.waveform == WaveformType.RANDOM_NOISE:
|
|
94
|
+
value = self.amplitude * random.gauss(0, 1) + self.offset
|
|
95
|
+
elif self.waveform == WaveformType.CUSTOM:
|
|
96
|
+
if self.custom_func is None:
|
|
97
|
+
raise ValueError("Custom waveform requires a custom_func argument.")
|
|
98
|
+
value = self.custom_func(t)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"Unsupported waveform: {self.waveform}")
|
|
101
|
+
if self.noise_injector:
|
|
102
|
+
value = self.noise_injector.add_noise(value)
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
def stream(
|
|
106
|
+
self, sampling_rate: float, duration: Optional[float] = None
|
|
107
|
+
) -> Iterator[Tuple[datetime, float]]:
|
|
108
|
+
"""
|
|
109
|
+
Stream (datetime, value) tuples in real-time at the given sampling rate.
|
|
110
|
+
"""
|
|
111
|
+
interval = 1.0 / sampling_rate
|
|
112
|
+
start_time = time.time()
|
|
113
|
+
elapsed = 0.0
|
|
114
|
+
while duration is None or elapsed < duration:
|
|
115
|
+
now = time.time() - start_time
|
|
116
|
+
value = self.generate_point(now)
|
|
117
|
+
yield (datetime.now(), value)
|
|
118
|
+
time.sleep(interval)
|
|
119
|
+
elapsed = time.time() - start_time
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faketelemetry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time Fake Telemetry Stream Generator
|
|
5
|
+
Home-page: https://github.com/adkvi/faketelemetry
|
|
6
|
+
Author: Adam Billekvist
|
|
7
|
+
Author-email: adam.billeqvist@example.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/adkvi/faketelemetry#readme
|
|
9
|
+
Project-URL: Source, https://github.com/adkvi/faketelemetry
|
|
10
|
+
Project-URL: Tracker, https://github.com/adkvi/faketelemetry/issues
|
|
11
|
+
Keywords: telemetry,signal,generator,iot,testing,simulation,waveform,python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.7
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: keywords
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: project-url
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/faketelemetry/)
|
|
35
|
+
[](https://opensource.org/licenses/MIT)
|
|
36
|
+
[](https://github.com/adkvi/faketelemetry/actions)
|
|
37
|
+
|
|
38
|
+
# FakeTelemetry
|
|
39
|
+
|
|
40
|
+
**A minimal Python package to generate real-time, timestamped fake telemetry streams for testing, simulation, and development.**
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
- Sine, cosine, square, sawtooth, triangle, pulse, and random noise waveforms
|
|
44
|
+
- Custom user-defined waveform support
|
|
45
|
+
- Adjustable amplitude, frequency, offset, and sample rate
|
|
46
|
+
- Optional Gaussian noise injection
|
|
47
|
+
- Real-time streaming of (datetime, value) tuples
|
|
48
|
+
- Multi-channel (parallel) telemetry generation
|
|
49
|
+
- Well-tested and extensible
|
|
50
|
+
|
|
51
|
+
## Example Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from faketelemetry import TelemetryGenerator, WaveformType, NoiseInjector, MultiChannelTelemetryGenerator
|
|
55
|
+
|
|
56
|
+
# Single channel with noise (sine wave)
|
|
57
|
+
noise = NoiseInjector(noise_level=0.2)
|
|
58
|
+
gen = TelemetryGenerator(
|
|
59
|
+
waveform=WaveformType.SINE,
|
|
60
|
+
frequency=1.0,
|
|
61
|
+
amplitude=1.0,
|
|
62
|
+
offset=0.0,
|
|
63
|
+
noise_injector=noise
|
|
64
|
+
)
|
|
65
|
+
for timestamp, value in gen.stream(sampling_rate=2.0, duration=3):
|
|
66
|
+
print(timestamp, value)
|
|
67
|
+
|
|
68
|
+
# Multi-channel example (sine and cosine)
|
|
69
|
+
ch1 = TelemetryGenerator(WaveformType.SINE, frequency=1.0)
|
|
70
|
+
ch2 = TelemetryGenerator(WaveformType.COSINE, frequency=0.5)
|
|
71
|
+
multi = MultiChannelTelemetryGenerator([ch1, ch2])
|
|
72
|
+
for sample in multi.stream(sampling_rate=2.0, duration=3):
|
|
73
|
+
print({k: (v[0], v[1]) for k, v in sample.items()})
|
|
74
|
+
|
|
75
|
+
# Triangle and pulse waveforms
|
|
76
|
+
tri = TelemetryGenerator(WaveformType.TRIANGLE, frequency=1.0)
|
|
77
|
+
pulse = TelemetryGenerator(WaveformType.PULSE, frequency=1.0)
|
|
78
|
+
print('Triangle:', next(tri.stream(1)))
|
|
79
|
+
print('Pulse:', next(pulse.stream(1)))
|
|
80
|
+
|
|
81
|
+
# Custom waveform (e.g., constant 42)
|
|
82
|
+
def custom_func(t):
|
|
83
|
+
return 42.0
|
|
84
|
+
gen_custom = TelemetryGenerator(WaveformType.CUSTOM, custom_func=custom_func)
|
|
85
|
+
print('Custom:', next(gen_custom.stream(1)))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
Clone this repo and install locally:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pip install .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or install in editable/development mode:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
pip install -e .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
Run all tests:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
python -m unittest discover -s tests
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
faketelemetry/__init__.py
|
|
6
|
+
faketelemetry/enums.py
|
|
7
|
+
faketelemetry/multi_channel.py
|
|
8
|
+
faketelemetry/noise_injector.py
|
|
9
|
+
faketelemetry/telemetry_generator.py
|
|
10
|
+
faketelemetry.egg-info/PKG-INFO
|
|
11
|
+
faketelemetry.egg-info/SOURCES.txt
|
|
12
|
+
faketelemetry.egg-info/dependency_links.txt
|
|
13
|
+
faketelemetry.egg-info/top_level.txt
|
|
14
|
+
tests/test_generator.py
|
|
15
|
+
tests/test_noise.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
faketelemetry
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
here = pathlib.Path(__file__).parent.resolve()
|
|
5
|
+
long_description = (here / "README.md").read_text(encoding="utf-8")
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="faketelemetry",
|
|
9
|
+
version="0.1.0",
|
|
10
|
+
description="Real-time Fake Telemetry Stream Generator",
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
author="Adam Billekvist",
|
|
14
|
+
author_email="adam.billeqvist@example.com",
|
|
15
|
+
url="https://github.com/adkvi/faketelemetry",
|
|
16
|
+
project_urls={
|
|
17
|
+
"Documentation": "https://github.com/adkvi/faketelemetry#readme",
|
|
18
|
+
"Source": "https://github.com/adkvi/faketelemetry",
|
|
19
|
+
"Tracker": "https://github.com/adkvi/faketelemetry/issues",
|
|
20
|
+
},
|
|
21
|
+
keywords=[
|
|
22
|
+
"telemetry", "signal", "generator", "iot", "testing", "simulation", "waveform", "python"
|
|
23
|
+
],
|
|
24
|
+
packages=find_packages(),
|
|
25
|
+
install_requires=[],
|
|
26
|
+
python_requires=">=3.7",
|
|
27
|
+
classifiers=[
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
34
|
+
"Topic :: Software Development :: Testing",
|
|
35
|
+
],
|
|
36
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from faketelemetry import (
|
|
4
|
+
TelemetryGenerator,
|
|
5
|
+
WaveformType,
|
|
6
|
+
MultiChannelTelemetryGenerator,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestTelemetryGenerator(unittest.TestCase):
|
|
11
|
+
def test_sine_wave(self):
|
|
12
|
+
gen = TelemetryGenerator(
|
|
13
|
+
WaveformType.SINE, frequency=1.0, amplitude=2.0, offset=1.0
|
|
14
|
+
)
|
|
15
|
+
value = gen.generate_point(0)
|
|
16
|
+
self.assertAlmostEqual(value, 1.0)
|
|
17
|
+
value = gen.generate_point(0.25)
|
|
18
|
+
self.assertAlmostEqual(value, 3.0, places=1)
|
|
19
|
+
|
|
20
|
+
def test_cosine_wave(self):
|
|
21
|
+
gen = TelemetryGenerator(
|
|
22
|
+
WaveformType.COSINE, frequency=1.0, amplitude=1.0, offset=0.0
|
|
23
|
+
)
|
|
24
|
+
value = gen.generate_point(0)
|
|
25
|
+
self.assertAlmostEqual(value, 1.0)
|
|
26
|
+
|
|
27
|
+
def test_square_wave(self):
|
|
28
|
+
gen = TelemetryGenerator(
|
|
29
|
+
WaveformType.SQUARE, frequency=1.0, amplitude=1.0, offset=0.0
|
|
30
|
+
)
|
|
31
|
+
self.assertIn(gen.generate_point(0), [1.0, -1.0])
|
|
32
|
+
|
|
33
|
+
def test_sawtooth_wave(self):
|
|
34
|
+
gen = TelemetryGenerator(
|
|
35
|
+
WaveformType.SAWTOOTH, frequency=1.0, amplitude=1.0, offset=0.0
|
|
36
|
+
)
|
|
37
|
+
value = gen.generate_point(0)
|
|
38
|
+
self.assertAlmostEqual(value, 0.0)
|
|
39
|
+
|
|
40
|
+
def test_multichannel_stream(self):
|
|
41
|
+
gen1 = TelemetryGenerator(WaveformType.SINE, frequency=1.0)
|
|
42
|
+
gen2 = TelemetryGenerator(WaveformType.COSINE, frequency=2.0)
|
|
43
|
+
multi = MultiChannelTelemetryGenerator([gen1, gen2])
|
|
44
|
+
stream = multi.stream(sampling_rate=2.0, duration=1)
|
|
45
|
+
results = list(stream)
|
|
46
|
+
self.assertTrue(len(results) > 0)
|
|
47
|
+
for sample in results:
|
|
48
|
+
self.assertEqual(len(sample), 2)
|
|
49
|
+
for v in sample.values():
|
|
50
|
+
self.assertIsInstance(v[0], datetime)
|
|
51
|
+
self.assertIsInstance(v[1], float)
|
|
52
|
+
|
|
53
|
+
def test_triangle_wave(self):
|
|
54
|
+
gen = TelemetryGenerator(
|
|
55
|
+
WaveformType.TRIANGLE, frequency=1.0, amplitude=1.0, offset=0.0
|
|
56
|
+
)
|
|
57
|
+
self.assertAlmostEqual(gen.generate_point(0), -1.0, places=1)
|
|
58
|
+
self.assertAlmostEqual(gen.generate_point(0.25), 0.0, places=1)
|
|
59
|
+
self.assertAlmostEqual(gen.generate_point(0.5), 1.0, places=1)
|
|
60
|
+
self.assertAlmostEqual(gen.generate_point(0.75), 0.0, places=1)
|
|
61
|
+
|
|
62
|
+
def test_pulse_wave(self):
|
|
63
|
+
gen = TelemetryGenerator(
|
|
64
|
+
WaveformType.PULSE, frequency=1.0, amplitude=1.0, offset=0.0
|
|
65
|
+
)
|
|
66
|
+
self.assertEqual(gen.generate_point(0), 1.0)
|
|
67
|
+
self.assertEqual(gen.generate_point(0.11), 0.0)
|
|
68
|
+
self.assertEqual(gen.generate_point(1.0), 1.0)
|
|
69
|
+
|
|
70
|
+
def test_custom_wave(self):
|
|
71
|
+
def custom_func(t):
|
|
72
|
+
return 42.0
|
|
73
|
+
|
|
74
|
+
gen = TelemetryGenerator(WaveformType.CUSTOM, custom_func=custom_func)
|
|
75
|
+
self.assertEqual(gen.generate_point(0), 42.0)
|
|
76
|
+
self.assertEqual(gen.generate_point(1), 42.0)
|
|
77
|
+
|
|
78
|
+
def test_invalid_frequency(self):
|
|
79
|
+
with self.assertRaises(ValueError) as cm:
|
|
80
|
+
TelemetryGenerator(WaveformType.SINE, frequency=-1)
|
|
81
|
+
self.assertIn("Frequency must be non-negative", str(cm.exception))
|
|
82
|
+
|
|
83
|
+
def test_invalid_amplitude(self):
|
|
84
|
+
with self.assertRaises(ValueError) as cm:
|
|
85
|
+
TelemetryGenerator(WaveformType.SINE, amplitude=-1)
|
|
86
|
+
self.assertIn("Amplitude must be non-negative", str(cm.exception))
|
|
87
|
+
|
|
88
|
+
def test_invalid_custom_func(self):
|
|
89
|
+
with self.assertRaises(ValueError) as cm:
|
|
90
|
+
TelemetryGenerator(WaveformType.CUSTOM, custom_func=None)
|
|
91
|
+
self.assertIn("callable custom_func", str(cm.exception))
|
|
92
|
+
|
|
93
|
+
def test_negative_time(self):
|
|
94
|
+
gen = TelemetryGenerator(WaveformType.SINE)
|
|
95
|
+
with self.assertRaises(ValueError) as cm:
|
|
96
|
+
gen.generate_point(-1)
|
|
97
|
+
self.assertIn("Time t must be non-negative", str(cm.exception))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
unittest.main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from faketelemetry.noise_injector import NoiseInjector
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestNoiseInjector(unittest.TestCase):
|
|
6
|
+
def test_no_noise(self):
|
|
7
|
+
ni = NoiseInjector(noise_level=0.0)
|
|
8
|
+
self.assertEqual(ni.add_noise(5.0), 5.0)
|
|
9
|
+
|
|
10
|
+
def test_with_noise(self):
|
|
11
|
+
ni = NoiseInjector(noise_level=1.0)
|
|
12
|
+
values = [ni.add_noise(5.0) for _ in range(100)]
|
|
13
|
+
self.assertTrue(any(abs(v - 5.0) > 0.1 for v in values))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
unittest.main()
|