adsb-generator 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.
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .generator import ADSBSample, ADSBGenerator
4
+ from .channel import ChannelParams, ADSBChannel
5
+ from .encoder import TXParams, ADSBEncoder
6
+ from .message import MessageType, ADSBMessage
7
+
8
+ __all__ = ["ADSBSample", "ADSBGenerator", "ChannelParams", "ADSBChannel",
9
+ "TXParams", "ADSBEncoder", "MessageType", "ADSBMessage"]
@@ -0,0 +1,84 @@
1
+ import math
2
+
3
+ class ADSBAlgorithms():
4
+
5
+ CRC_POLY = 0xFFF409
6
+
7
+ @classmethod
8
+ def calculate_crc(cls, data: int) -> int:
9
+
10
+ packet = data << 24
11
+ remainder = packet
12
+
13
+ for bit in range(111, 23, -1):
14
+ if (remainder >> bit) & 1:
15
+ remainder ^= cls.CRC_POLY << (bit - 24)
16
+
17
+ crc = remainder & 0xFFFFFF
18
+
19
+ return packet | crc
20
+
21
+ _NL_TABLE = [
22
+ (87, 59), (83, 58), (79, 57), (76, 56), (73, 55),
23
+ (70, 54), (66, 53), (63, 52), (60, 51), (57, 50),
24
+ (53, 49), (50, 48), (46, 47), (43, 46), (40, 45),
25
+ (37, 44), (34, 43), (31, 42), (28, 41), (25, 40),
26
+ (22, 39), (19, 38), (16, 37), (13, 36), (9, 35),
27
+ (6, 34), (3, 33), (0, 32),
28
+ ]
29
+
30
+ @classmethod
31
+ def _nl(cls, lat: float) -> int:
32
+ if abs(lat) >= 87:
33
+ return 1
34
+ for lat_min, nl in cls._NL_TABLE:
35
+ if abs(lat) >= lat_min:
36
+ return nl
37
+ return 32
38
+
39
+ @classmethod
40
+ def encode_cpr(cls, lat: float, lon: float, odd: bool) -> tuple[int, int]:
41
+ lat = max(-90.0, min(90.0, lat))
42
+ lon = max(-180.0, min(180.0, lon))
43
+
44
+ NB = 17
45
+ factor = 1 << NB
46
+
47
+ nz_lat = 59 if odd else 60
48
+ dlat = 360.0 / nz_lat
49
+
50
+ zone_lat = math.floor(lat / dlat)
51
+ rem_lat = lat - zone_lat * dlat
52
+ yz = int(round((rem_lat / dlat) * factor)) & (factor - 1)
53
+
54
+ rlat = dlat * (yz / factor + zone_lat)
55
+ rlat = max(-90.0, min(90.0, rlat))
56
+
57
+ nl = cls._nl(rlat)
58
+
59
+ dlon = 360.0 / nl
60
+ zone_lon = math.floor(lon / dlon)
61
+ rem_lon = lon - zone_lon * dlon
62
+ xz = int(round((rem_lon / dlon) * factor)) & (factor - 1)
63
+
64
+ return yz, xz
65
+
66
+ @staticmethod
67
+ def encode_altitude(alt: int) -> int:
68
+ alt_clamped = max(-1000, min(50100, alt))
69
+ n = (alt_clamped + 1000) // 25
70
+
71
+ top_bits = (n >> 4) & 0x7F
72
+ bottom_bits = n & 0x0F
73
+
74
+ return (top_bits << 5) | (1 << 4) | bottom_bits
75
+
76
+ @staticmethod
77
+ def encode_ground_track(degrees: float, valid: bool = True) -> tuple[int, int]:
78
+ if not valid or degrees is None:
79
+ return 0, 0
80
+
81
+ degrees = degrees % 360.0
82
+ trk_code = round(degrees * 128.0 / 360.0)
83
+
84
+ return 1, trk_code
@@ -0,0 +1,248 @@
1
+ import random
2
+ import numpy as np
3
+ from enum import Enum
4
+
5
+ class ChannelParams(Enum):
6
+ """Supported channel impairment parameters for ADS-B signal simulation."""
7
+ SNR_DB = "snr_db"
8
+ NOISE_CORRELATION = "noise_correlation"
9
+
10
+ FREQUENCY_OFFSET = "frequency_offset"
11
+ PHASE_OFFSET = "phase_offset"
12
+ DC_OFFSET_I = "dc_offset_i"
13
+ DC_OFFSET_Q = "dc_offset_q"
14
+
15
+ IQ_GAIN_IMBALANCE = "iq_gain_imbalance"
16
+ IQ_PHASE_IMBALANCE = "iq_phase_imbalance"
17
+
18
+ class ADSBChannel:
19
+ """
20
+ Simulates realistic RF channel impairments on ADS-B baseband I/Q signals.
21
+
22
+ This class applies a series of channel effects—including IQ imbalance, DC offset,
23
+ frequency offset, phase offset, and additive white Gaussian noise (AWGN)—to
24
+ simulate the degradations encountered in real-world ADS-B reception. Each
25
+ impairment parameter is sampled from configurable probability distributions.
26
+
27
+ Attributes:
28
+ sample_rate (float): Sampling rate in samples per second.
29
+ channel_params_distributions (dict[ChannelParams | str, list[list[float]]]):
30
+ Mapping of channel parameters to their probability distributions defined
31
+ as intervals with associated weights.
32
+ seed (int | None): Seed for the internal random number generator to ensure
33
+ reproducible channel simulation.
34
+ """
35
+ def __init__(self, sample_rate: float = 2e6, channel_params_distributions: (dict[ChannelParams | str, list[list[float]]]) | None = None, seed: int | None = None):
36
+ """
37
+ Initializes the channel simulator with sampling parameters and impairment distributions.
38
+
39
+ Args:
40
+ sample_rate: Sampling rate in samples per second. Defaults to 2 MHz.
41
+ channel_params_distributions: A mapping of ChannelParams to probability
42
+ distributions defined as lists of [min_val, max_val, weight] intervals.
43
+ If None, defaults to distributions covering typical ADS-B reception
44
+ conditions for all eight channel parameters.
45
+ seed: Seed for the internal random number generator to ensure reproducible
46
+ channel simulation. If None, the generator is seeded from system randomness.
47
+
48
+ Raises:
49
+ ValueError: If `channel_params_distributions` contains invalid keys, invalid
50
+ intervals (min > max), weights that do not sum to 1.0, or noise correlation
51
+ values outside [-1.0, 1.0] (validated via `_validate_distributions`).
52
+ """
53
+
54
+ self.sample_rate = sample_rate
55
+
56
+ default_dists = {
57
+ ChannelParams.SNR_DB: [
58
+ [3.0, 8.0, 0.20],
59
+ [8.0, 15.0, 0.55],
60
+ [15.0, 25.0, 0.25],
61
+ ],
62
+ ChannelParams.NOISE_CORRELATION: [
63
+ [0.0, 0.0, 1.0],
64
+ ],
65
+ ChannelParams.FREQUENCY_OFFSET: [
66
+ [-1000.0, 1000.0, 0.80],
67
+ [-3000.0, 3000.0, 0.20],
68
+ ],
69
+ ChannelParams.PHASE_OFFSET: [
70
+ [-0.10, 0.10, 0.80],
71
+ [-0.30, 0.30, 0.20],
72
+ ],
73
+ ChannelParams.DC_OFFSET_I: [
74
+ [-0.01, 0.01, 0.90],
75
+ [-0.03, 0.03, 0.10],
76
+ ],
77
+ ChannelParams.DC_OFFSET_Q: [
78
+ [-0.01, 0.01, 0.90],
79
+ [-0.03, 0.03, 0.10],
80
+ ],
81
+ ChannelParams.IQ_GAIN_IMBALANCE: [
82
+ [0.00, 0.02, 0.80],
83
+ [0.02, 0.05, 0.20],
84
+ ],
85
+ ChannelParams.IQ_PHASE_IMBALANCE: [
86
+ [-1.0, 1.0, 0.80],
87
+ [-3.0, 3.0, 0.20],
88
+ ],
89
+ }
90
+
91
+ self.channel_params_dists = channel_params_distributions or default_dists
92
+
93
+ self._validate_distributions
94
+
95
+ self._seed = seed if seed is not None else random.randint(0, 2**32 - 1)
96
+ self._rng = random.Random(self._seed)
97
+ self._np_rng = np.random.default_rng(self._seed)
98
+
99
+ @property
100
+ def seed(self) -> int:
101
+ """Gets the seed value."""
102
+ return self._seed
103
+
104
+ def _validate_distributions(self):
105
+ valid_types = set(ChannelParams)
106
+ valid_values = {item.value for item in ChannelParams}
107
+
108
+ for channel_param, intervals in self.channel_params_dists.items():
109
+ if channel_param not in valid_types and channel_param not in valid_values:
110
+ raise ValueError(
111
+ f"Invalid channel param key: '{channel_param}'. "
112
+ f"Valid options are the ChannelParams enums or: {valid_values}"
113
+ )
114
+
115
+ enum_key = channel_param if isinstance(channel_param, ChannelParams) else ChannelParams(channel_param)
116
+
117
+ total_weights = 0.0
118
+ for min_val, max_val, weight in intervals:
119
+ if min_val > max_val:
120
+ raise ValueError(
121
+ f"Invalid range {min_val} > {max_val} in key '{channel_param}'"
122
+ )
123
+
124
+ if enum_key == ChannelParams.NOISE_CORRELATION:
125
+ if not (-1.0 <= min_val <= 1.0) or not (-1.0 <= max_val <= 1.0):
126
+ raise ValueError(
127
+ f"Noise correlation must be in range [-1.0, 1.0]. "
128
+ f"Got range [{min_val}, {max_val}] for key '{channel_param}'"
129
+ )
130
+ if min_val > max_val:
131
+ raise ValueError(
132
+ f"Invalid noise correlation range: {min_val} > {max_val}"
133
+ )
134
+ total_weights += weight
135
+
136
+ if not (0.99 <= total_weights <= 1.01):
137
+ raise ValueError(
138
+ f"Sum of weights for key '{channel_param}' must equal 1.0, got {total_weights:.2f}"
139
+ )
140
+
141
+ def _sample_channel_params(self) -> dict[ChannelParams, float]:
142
+ sampled_params = {}
143
+
144
+ default_values = {
145
+ ChannelParams.SNR_DB: 15.0,
146
+ ChannelParams.NOISE_CORRELATION: 0.0,
147
+ ChannelParams.FREQUENCY_OFFSET: 0.0,
148
+ ChannelParams.PHASE_OFFSET: 0.0,
149
+ ChannelParams.DC_OFFSET_I: 0.0,
150
+ ChannelParams.DC_OFFSET_Q: 0.0,
151
+ ChannelParams.IQ_GAIN_IMBALANCE: 0.0,
152
+ ChannelParams.IQ_PHASE_IMBALANCE: 0.0,
153
+ }
154
+
155
+ for param_key, intervals in self.channel_params_dists.items():
156
+ ranges = [(low, high) for low, high, _ in intervals]
157
+ weights = [w for _, _, w in intervals]
158
+
159
+ selected_range = self._rng.choices(ranges, weights, k=1)[0]
160
+
161
+ sampled_val = self._rng.uniform(selected_range[0], selected_range[1])
162
+
163
+ enum_key = param_key if isinstance(param_key, ChannelParams) else ChannelParams(param_key)
164
+ sampled_params[enum_key] = sampled_val
165
+
166
+ for param in ChannelParams:
167
+ if param not in sampled_params:
168
+ sampled_params[param] = default_values[param]
169
+
170
+ return sampled_params
171
+
172
+ def _apply_dc_offset(self, signal: np.ndarray, dc_offset_i: float, dc_offset_q: float) -> np.ndarray:
173
+ dc_offset = dc_offset_i + 1j * dc_offset_q
174
+ return signal + dc_offset
175
+
176
+ def _apply_freq_offset(self, signal: np.ndarray, freq_offset: float, sample_rate: float) -> np.ndarray:
177
+ t = np.arange(len(signal)) / sample_rate
178
+ return signal * np.exp(1j * 2 * np.pi * freq_offset * t)
179
+
180
+ def _apply_phase_offset(self, signal: np.ndarray, phase_offset: float) -> np.ndarray:
181
+ return signal * np.exp(1j * phase_offset)
182
+
183
+ def _apply_iq_imbalance(self, signal: np.ndarray, gain_imbalance: float, phase_imbalance: float) -> np.ndarray:
184
+ phi = np.deg2rad(phase_imbalance)
185
+
186
+ i = signal.real * (1 + gain_imbalance / 2)
187
+ q = signal.imag * (1 - gain_imbalance / 2)
188
+
189
+ i_out = i * np.cos(phi) + q * np.sin(phi)
190
+ q_out = i * np.sin(phi) + q * np.cos(phi)
191
+
192
+ return i_out + 1j * q_out
193
+
194
+ def _apply_gaussian_noise(self, signal: np.ndarray, snr_db: float, noise_correlation: float = 0) -> np.ndarray:
195
+ if len(signal) == 0:
196
+ return signal.copy()
197
+ signal_power = np.mean(np.abs(signal) ** 2)
198
+
199
+ snr_linear = 10 ** (snr_db / 10)
200
+
201
+ noise_power = signal_power / snr_linear
202
+ noise_std = np.sqrt(noise_power / 2)
203
+
204
+ if noise_correlation > 0:
205
+ cov_matrix = noise_power / 2 * np.array([
206
+ [1.0, noise_correlation],
207
+ [noise_correlation, 1.0]
208
+ ])
209
+
210
+ noise = self._np_rng.multivariate_normal(
211
+ mean=[0, 0],
212
+ cov=cov_matrix,
213
+ size=len(signal)
214
+ )
215
+ noise_complex = noise[:, 0] + 1j * noise[:, 1]
216
+ else:
217
+ noise_i = self._np_rng.normal(0, noise_std, len(signal))
218
+ noise_q = self._np_rng.normal(0, noise_std, len(signal))
219
+ noise_complex = noise_i + 1j * noise_q
220
+
221
+ return signal + noise_complex
222
+
223
+ def apply(self, signal: np.ndarray) -> tuple[np.ndarray, dict[ChannelParams, float]]:
224
+ """
225
+ Applies simulated channel impairments to an ADS-B baseband I/Q signal.
226
+
227
+ This method samples channel parameters from configured distributions and
228
+ applies impairments in the following order: IQ imbalance, DC offset,
229
+ frequency offset, phase offset, and finally additive Gaussian noise.
230
+
231
+ Args:
232
+ signal: Complex I/Q samples of the baseband signal (dtype=np.complex64).
233
+
234
+ Returns:
235
+ A tuple containing:
236
+ - np.ndarray: The impaired complex I/Q signal (dtype=np.complex64)
237
+ - dict[ChannelParams, float]: The channel parameters used for this operation
238
+ """
239
+ params = self._sample_channel_params()
240
+
241
+ signal = self._apply_iq_imbalance(signal, params[ChannelParams.IQ_GAIN_IMBALANCE], params[ChannelParams.IQ_PHASE_IMBALANCE])
242
+ signal = self._apply_dc_offset(signal, params[ChannelParams.DC_OFFSET_I], params[ChannelParams.DC_OFFSET_Q])
243
+ signal = self._apply_freq_offset(signal, params[ChannelParams.FREQUENCY_OFFSET], self.sample_rate)
244
+ signal = self._apply_phase_offset(signal, params[ChannelParams.PHASE_OFFSET])
245
+ signal = self._apply_gaussian_noise(signal, params[ChannelParams.SNR_DB], params[ChannelParams.NOISE_CORRELATION])
246
+
247
+ return signal, params
248
+
@@ -0,0 +1,155 @@
1
+ import random
2
+ import numpy as np
3
+ from enum import Enum
4
+
5
+ class TXParams(Enum):
6
+ AMPLITUDE = "amplitude"
7
+
8
+ class ADSBEncoder:
9
+ """
10
+ Converts ADS-B raw frames into baseband I/Q samples with randomized transmission parameters.
11
+
12
+ This class handles the encoding of 112-bit ADS-B messages into a complex baseband
13
+ signal representation, following the pulse-position modulation (PPM) encoding
14
+ scheme defined in the Mode S/ADS-B standard. The encoder adds configurable
15
+ random variations to transmission parameters (e.g., amplitude) to simulate
16
+ real-world signal variability.
17
+
18
+ Attributes:
19
+ sample_rate (float): Sampling rate in samples per second.
20
+ tx_params_distributions (dict[TXParams | str, list[list[float]]]): Mapping
21
+ of transmission parameters to their probability distributions defined
22
+ as intervals with associated weights.
23
+ seed (int | None): Seed for the internal random number generator to ensure
24
+ reproducible signal generation.
25
+ """
26
+ def __init__(self, sample_rate: float = 2e6, tx_params_distributions: (dict[TXParams | str, list[list[float]]]) | None = None, seed: int | None = None):
27
+ """
28
+ Initializes the encoder with sampling parameters and transmission parameter distributions.
29
+
30
+ Args:
31
+ sample_rate: Sampling rate in samples per second. Defaults to 2 MHz.
32
+ tx_params_distributions: A mapping of TXParams to probability distributions
33
+ defined as lists of [min_val, max_val, weight] intervals. If None,
34
+ defaults to a single amplitude distribution with three intervals
35
+ covering the range [0.05, 1.00].
36
+ seed: Seed for the internal random number generator to ensure reproducible
37
+ signal generation. If None, the generator is seeded from system randomness.
38
+
39
+ Raises:
40
+ ValueError: If `tx_params_distributions` contains invalid keys, invalid
41
+ intervals (min > max), or weights that do not sum to 1.0 (validated
42
+ via `_validate_distributions`).
43
+ """
44
+ self.sample_rate = sample_rate
45
+
46
+ default_dists = {
47
+ TXParams.AMPLITUDE: [
48
+ [0.05, 0.25, 0.5],
49
+ [0.25, 0.65, 0.3],
50
+ [0.65, 1.00, 0.2]
51
+ ]}
52
+
53
+ self.tx_params_dists = tx_params_distributions or default_dists
54
+
55
+ self._validate_distributions
56
+
57
+ self._seed = seed if seed is not None else random.randint(0, 2**32 - 1)
58
+ self._rng = random.Random(self._seed)
59
+
60
+ @property
61
+ def seed(self) -> int:
62
+ """Gets the seed value."""
63
+ return self._seed
64
+
65
+ def _validate_distributions(self):
66
+ valid_types = set(TXParams)
67
+ valid_values = {item.value for item in TXParams}
68
+
69
+ for tx_param, intervals in self.tx_params_dists.items():
70
+ if tx_param not in valid_types and tx_param not in valid_values:
71
+ raise ValueError(
72
+ f"Invalid tx param key: '{tx_param}'. "
73
+ f"Valid options are the TXParams enums or: {valid_values}"
74
+ )
75
+
76
+ total_weights = 0.0
77
+ for min_val, max_val, weight in intervals:
78
+ if min_val > max_val:
79
+ raise ValueError(
80
+ f"Invalid range {min_val} > {max_val} in key '{tx_param}'"
81
+ )
82
+ total_weights += weight
83
+
84
+ if not (0.99 <= total_weights <= 1.01):
85
+ raise ValueError(
86
+ f"Sum of weights for key '{tx_param}' must equal 1.0, got {total_weights:.2f}"
87
+ )
88
+
89
+ def _sample_tx_params(self) -> dict[TXParams, float]:
90
+ sampled_params = {}
91
+ for param_key, intervals in self.tx_params_dists.items():
92
+ ranges = [(low, high) for low, high, _ in intervals]
93
+ weights = [w for _, _, w in intervals]
94
+
95
+ selected_range = self._rng.choices(ranges, weights, k=1)[0]
96
+
97
+ sampled_val = self._rng.uniform(selected_range[0], selected_range[1])
98
+
99
+ enum_key = param_key if isinstance(param_key, TXParams) else TXParams(param_key)
100
+ sampled_params[enum_key] = sampled_val
101
+
102
+ return sampled_params
103
+
104
+ def encode(self, msg: int) -> tuple[np.ndarray, dict[TXParams, float]]:
105
+ """
106
+ Encodes a 112-bit ADS-B message into a complex baseband I/Q signal.
107
+
108
+ This method samples transmission parameters from configured distributions and
109
+ generates a 120 μs signal containing the preamble and 112 data bits encoded
110
+ using PPM at 1 Mbps.
111
+
112
+ The timing follows the ADS-B standard:
113
+ - Preamble: 8.0 μs with pulses at specific positions
114
+ - Data bits: 112 bits at 1 μs per bit starting at 8.0 μs
115
+ - Pulse width: 0.5 μs for both preamble and data pulses
116
+
117
+ Args:
118
+ msg: The 112-bit ADS-B message as an integer (LSB alignment).
119
+
120
+ Returns:
121
+ A tuple containing:
122
+ - np.ndarray: Complex I/Q samples of the baseband signal (dtype=np.complex64)
123
+ - dict[TXParams, float]: The transmission parameters used for this encode operation
124
+ """
125
+ params = self._sample_tx_params()
126
+ amplitude = params[TXParams.AMPLITUDE]
127
+
128
+ samples_per_us = self.sample_rate / 1e6
129
+ total_samples = int(round(120.0 * samples_per_us))
130
+
131
+ signal = np.zeros(total_samples, dtype=np.float32)
132
+
133
+ for start_us, end_us in ((0.0, 0.5), (1.0, 1.5), (3.5, 4.0), (4.5, 5.0)):
134
+ signal[
135
+ int(round(start_us * samples_per_us)):
136
+ int(round(end_us * samples_per_us))
137
+ ] = amplitude
138
+
139
+ bit_start_us = 8.0
140
+
141
+ for shift in range(111, -1, -1):
142
+ bit = (msg >> shift) & 1
143
+
144
+ pulse_offset_us = 0.0 if bit else 0.5
145
+
146
+ p_start = round((bit_start_us + pulse_offset_us) * samples_per_us)
147
+ p_end = round((bit_start_us + pulse_offset_us + 0.5) * samples_per_us)
148
+
149
+ signal[p_start:p_end] = amplitude
150
+
151
+ bit_start_us += 1.0
152
+
153
+ iq_samples = signal.astype(np.complex64)
154
+
155
+ return iq_samples, params
@@ -0,0 +1,85 @@
1
+ import random
2
+ import numpy as np
3
+ from dataclasses import dataclass
4
+ from .message import ADSBMessage, MessageType
5
+ from .encoder import ADSBEncoder, TXParams
6
+ from .channel import ADSBChannel, ChannelParams
7
+
8
+ @dataclass
9
+ class ADSBSample:
10
+ """A single ADS-B sample containing the raw message, encoded signal, and impaired signal."""
11
+ message: int
12
+ message_type: MessageType
13
+
14
+ clean_signal: np.ndarray
15
+ tx_params: dict[TXParams, float]
16
+
17
+ channel_signal: np.ndarray
18
+ channel_params: dict[ChannelParams, float]
19
+
20
+ class ADSBGenerator():
21
+ """
22
+ End-to-end ADS-B signal generator that produces synthetic I/Q samples with channel impairments.
23
+
24
+ This class orchestrates the full ADS-B signal generation pipeline: building random
25
+ 112-bit ADS-B messages, encoding them into baseband I/Q signals using PPM, and
26
+ applying realistic RF channel impairments. It is implemented as an infinite iterator
27
+ that yields ADSBSample instances on each iteration.
28
+
29
+ Attributes:
30
+ seed (int): Seed used for the internal random number generators.
31
+ """
32
+ def __init__(self, message_type_probs: dict[MessageType | str, float] | None = None , tx_params_distributions: dict[TXParams | str, list[list[float]]] | None = None,
33
+ channel_params_distributions: dict[ChannelParams | str, list[list[float]]] | None = None, sample_rate: float = 2e6, seed: int | None = None):
34
+ """
35
+ Initializes the generator with message, transmission, and channel distributions.
36
+
37
+ Args:
38
+ message_type_probs: Mapping of MessageType to their emission probabilities.
39
+ If None, defaults to an equal 25% distribution across all message types.
40
+ tx_params_distributions: Mapping of TXParams to probability distributions
41
+ defined as lists of [min_val, max_val, weight] intervals. If None,
42
+ defaults to a single amplitude distribution.
43
+ channel_params_distributions: Mapping of ChannelParams to probability
44
+ distributions defined as lists of [min_val, max_val, weight] intervals.
45
+ If None, defaults to distributions covering typical ADS-B reception
46
+ conditions.
47
+ sample_rate: Sampling rate in samples per second. Defaults to 2 MHz.
48
+ seed: Seed for the internal random number generators to ensure reproducible
49
+ output. If None, the generator is seeded from system randomness.
50
+ """
51
+
52
+ self._seed = seed if seed is not None else random.randint(0, 2**32 - 1)
53
+
54
+ self.builder = ADSBMessage(message_type_probs, self._seed)
55
+ self.encoder = ADSBEncoder(sample_rate, tx_params_distributions, self._seed)
56
+ self.channel = ADSBChannel(sample_rate, channel_params_distributions, self._seed)
57
+
58
+ @property
59
+ def seed(self) -> int:
60
+ """Gets the seed value."""
61
+ return self._seed
62
+
63
+ def __iter__(self):
64
+ """
65
+ Yields an infinite stream of ADS-B samples.
66
+
67
+ Each iteration builds a random ADS-B message, encodes it into a baseband
68
+ I/Q signal, applies channel impairments, and yields an ADSBSample containing
69
+ the raw message, clean signal, and impaired signal along with their parameters.
70
+ """
71
+ while True:
72
+ message, message_type = self.builder.build()
73
+
74
+ clean_signal, tx_params = self.encoder.encode(message)
75
+
76
+ channel_signal, channel_params = self.channel.apply(clean_signal)
77
+
78
+ yield ADSBSample(
79
+ message=message,
80
+ message_type=message_type,
81
+ clean_signal=clean_signal,
82
+ tx_params=tx_params,
83
+ channel_signal=channel_signal,
84
+ channel_params=channel_params,
85
+ )
@@ -0,0 +1,198 @@
1
+ import random
2
+ import string
3
+ from enum import Enum
4
+ from .algorithms import ADSBAlgorithms
5
+
6
+ class MessageType(Enum):
7
+ """Supported Automatic Dependent Surveillance-Broadcast (ADS-B) message types."""
8
+ IDENTIFICATION = "identification"
9
+ SURFACE_POSITION = "surface_position"
10
+ AIRBORNE_POSITION = "airborne_position"
11
+ AIRBORNE_VELOCITY = "airborne_velocity"
12
+
13
+ class ADSBMessage():
14
+ """
15
+ Generates synthetic 112-bit ADS-B (Mode S Downlink Format 17) raw frames.
16
+
17
+ This class handles the probabilistic generation and bit-level encoding of
18
+ various ADS-B message types—including aircraft identification, surface position,
19
+ airborne position, and airborne velocity—complete with 24-bit CRC parity generation.
20
+
21
+ Attributes:
22
+ message_type_probs (dict[MessageType | str, float]): Mapping of message
23
+ types to their selection probabilities.
24
+ seed: int | None = None: Seed for the internal random number generator to ensure reproducible
25
+ message output.
26
+ """
27
+ def __init__(self, message_type_probs: dict = None, seed: int | None = None):
28
+ """
29
+ Initializes the instance with message type probabilities.
30
+
31
+ Args:
32
+ message_type_probs: A mapping of MessageType to their respective
33
+ emission probabilities. If None, defaults to an equal 25% distribution
34
+ across IDENTIFICATION, SURFACE_POSITION, AIRBORNE_POSITION, and
35
+ AIRBORNE_VELOCITY.
36
+ seed: Seed for the internal random number generator to ensure reproducible
37
+ message output. If None, the generator is seeded from system randomness.
38
+
39
+ Raises:
40
+ ValueError: If `message_type_probs` contains invalid probability values
41
+ or does not sum to 1.0 (validated via `_validate_probabilities`).
42
+ """
43
+
44
+ default_probs = {
45
+ MessageType.IDENTIFICATION: 0.25,
46
+ MessageType.SURFACE_POSITION: 0.25,
47
+ MessageType.AIRBORNE_POSITION: 0.25,
48
+ MessageType.AIRBORNE_VELOCITY: 0.25
49
+ }
50
+
51
+ self.message_type_probs = message_type_probs or default_probs
52
+ self._validate_probabilities()
53
+
54
+ self._seed = seed if seed is not None else random.randint(0, 2**32 - 1)
55
+ self._rng = random.Random(self._seed)
56
+
57
+ self._CALLSIGN_CHARSET = {
58
+ " ": 0,
59
+ **{chr(ord("A") + i): i + 1 for i in range(26)},
60
+ **{str(i): 48 + i for i in range(10)},
61
+ }
62
+
63
+ @property
64
+ def seed(self) -> int:
65
+ """Gets the seed value."""
66
+ return self._seed
67
+
68
+
69
+ def _validate_probabilities(self):
70
+ valid_types = set(item for item in MessageType)
71
+ valid_values = set(item.value for item in MessageType)
72
+
73
+ for msg_type in self.message_type_probs.keys():
74
+ if msg_type not in valid_types and msg_type not in valid_values:
75
+ raise ValueError(
76
+ f"Invalid message type key: '{msg_type}'. "
77
+ f"Valid options are the MessageType enums or: {valid_values}"
78
+ )
79
+
80
+ total= sum(self.message_type_probs.values())
81
+ if not (0.99 <= total <= 1.01):
82
+ raise ValueError(
83
+ f"Sum of probabilities must equal 1.0, got {total}"
84
+ )
85
+
86
+ def _build_identification_message(self) -> int:
87
+ tc = self._rng.randint(1, 4)
88
+ ca = self._rng.randint(0, 7)
89
+
90
+ prefix = "".join(self._rng.choices(string.ascii_uppercase, k=3))
91
+ digits = str(self._rng.randint(10, 99999))
92
+ callsign = (prefix + digits).ljust(8, " ")[:8]
93
+
94
+ callsign_48bit = 0
95
+ for char in callsign:
96
+ code = self._CALLSIGN_CHARSET.get(char, 0)
97
+ callsign_48bit = (callsign_48bit << 6) | (code & 0x3F)
98
+
99
+ return (tc << 51) | (ca << 48) | callsign_48bit
100
+
101
+ def _build_surface_position_message(self) -> int:
102
+ tc = self._rng.randint(5, 8)
103
+ mov = self._rng.randint(0, 127)
104
+ s, trk = ADSBAlgorithms.encode_ground_track(self._rng.uniform(0, 360), self._rng.choice([True, False]))
105
+ t = self._rng.choices([1, 0], [0.9, 0.1], k=1)[0]
106
+
107
+ f = self._rng.randint(0, 1)
108
+ lat_cpr, lon_cpr = ADSBAlgorithms.encode_cpr(self._rng.uniform(-90, 90), self._rng.uniform(-180, 180), (f == 1))
109
+
110
+ return ((tc << 51) | (mov << 44) | (s << 43) | (trk << 36) |
111
+ (t << 35) | (f << 34) | (lat_cpr << 17) | lon_cpr)
112
+
113
+ def _build_airborne_position_message(self) -> int:
114
+ tc = self._rng.choice([self._rng.randint(9, 18), self._rng.randint(20, 22)])
115
+ ss = self._rng.randint(0 ,3)
116
+ saf = self._rng.choice([0, 1])
117
+
118
+ alt = ADSBAlgorithms.encode_altitude(self._rng.randint(500, 45000))
119
+
120
+ t = self._rng.choices([1, 0], [0.9, 0.1], k=1)[0]
121
+
122
+ f = self._rng.randint(0, 1)
123
+ lat_cpr, lon_cpr = ADSBAlgorithms.encode_cpr(self._rng.uniform(-90, 90), self._rng.uniform(-180, 180), (f == 1))
124
+
125
+ return ((tc << 51) | (ss << 49) | (saf << 48) | (alt << 36) |
126
+ (t << 35) | (f << 34) | (lat_cpr << 17) | lon_cpr)
127
+
128
+ def _build_airborne_velocity_message(self) -> int:
129
+ tc = 19
130
+ st = self._rng.randint(1, 4)
131
+ ic = self._rng.choice([0, 1])
132
+ ifr = self._rng.choice([0, 1])
133
+ nucv = self._rng.randint(0, 4)
134
+
135
+ subtype = 0
136
+ if st in [1, 2]:
137
+ dew = self._rng.choice([0, 1])
138
+ vew = (self._rng.randint(0, 1021) + 1) if st == 1 else (int(self._rng.choice([x for x in range(0, 4085, 4)]) / 4) + 1)
139
+ dns = self._rng.choice([0, 1])
140
+ vns = (self._rng.randint(0, 1021) + 1) if st == 1 else (int(self._rng.choice([x for x in range(0, 4085, 4)]) / 4) + 1)
141
+
142
+ subtype = (dew << 21) | (vew << 11) | (dns << 10) | vns
143
+ else:
144
+ sh = self._rng.choice([0, 1])
145
+ hdg = round((self._rng.uniform(0.0, 360.0) * 1024) / 360) % 1024
146
+ t = self._rng.choice([0, 1])
147
+ ais = (self._rng.randint(0, 1021) + 1) if st == 3 else (int(self._rng.choice([x for x in range(0, 4085, 4)]) / 4) + 1)
148
+
149
+ subtype = (sh << 21) | (hdg << 11) | (t << 10) | ais
150
+
151
+ vrsrc = self._rng.choice([0, 1])
152
+ svr = self._rng.choice([0, 1])
153
+ vr = int(self._rng.choice([x for x in range(0, 32641, 64)]) / 64) + 1
154
+ res = 0
155
+ sdif = self._rng.choice([0, 1])
156
+ dalt = int(self._rng.choice([x for x in range(0, 3151, 25)]) / 25) + 1
157
+
158
+ return ((tc << 51) | (st << 48) | (ic << 47) | (ifr << 46) | (nucv << 43) |
159
+ (subtype << 21) | (vrsrc << 20) | (svr << 19) | (vr << 10) | (res << 8) | (sdif << 7) | dalt)
160
+
161
+
162
+ def build(self) -> tuple[int, MessageType]:
163
+ """
164
+ Generates a self._rng ADS-B message based on configured probabilities and calculates its CRC.
165
+
166
+ self._rngly selects an ADS-B message type using the configured probability distribution,
167
+ constructs the message fields (Downlink Format, Capability, ICAO address, and Message payload),
168
+ and computes the final parity checksum.
169
+
170
+ Returns:
171
+ The complete 112-bit ADS-B message encoded as an integer, including
172
+ the 24-bit Parity/Interrogation ID (CRC).
173
+
174
+ Message type.
175
+ """
176
+ types = list(self.message_type_probs.keys())
177
+ weights = list(self.message_type_probs.values())
178
+ selected_type = self._rng.choices(types, weights, k=1)[0]
179
+
180
+ selected_type = MessageType(selected_type) if isinstance(selected_type, str) else selected_type
181
+
182
+ df = 17
183
+ ca = self._rng.randint(0, 7)
184
+ icao = self._rng.getrandbits(24)
185
+ me = 0
186
+
187
+ generators = {
188
+ MessageType.IDENTIFICATION: self._build_identification_message,
189
+ MessageType.SURFACE_POSITION: self._build_surface_position_message,
190
+ MessageType.AIRBORNE_POSITION: self._build_airborne_position_message,
191
+ MessageType.AIRBORNE_VELOCITY: self._build_airborne_velocity_message,
192
+ }
193
+
194
+ me = generators[selected_type]()
195
+
196
+ data = (df << 83) | (ca << 80) | (icao << 56) | me
197
+
198
+ return ADSBAlgorithms.calculate_crc(data), selected_type
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: adsb-generator
3
+ Version: 0.1.0
4
+ Summary: `adsb-generator` produces realistic I/Q (In-phase/Quadrature) samples of clean and impaired ADS-B (Mode S Downlink Format 17) baseband signals, along with the raw 112-bit message and all applied parameters. It implements an infinite iterator that streams reproducible samples with full control over message type distributions, transmission parameters, and channel impairments.
5
+ Author-email: Adam Hamri <adamhamri9@proton.me>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/adamhamri9/adsb-datagen
8
+ Project-URL: Repository, https://github.com/adamhamri9/adsb-datagen
9
+ Project-URL: Issues, https://github.com/adamhamri9/adsb-datagen/issues
10
+ Keywords: adsb,mode-s,aviation,signal-generation,iq-samples,baseband
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Communications :: Ham Radio
22
+ Requires-Python: <3.14,>=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.26.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # adsb-generator
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
33
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-3776AB.svg)](https://www.python.org/downloads/)
34
+
35
+ **Synthetic ADS-B data generator.**
36
+
37
+ `adsb-generator` produces realistic I/Q (In-phase/Quadrature) samples of clean and impaired ADS-B (Mode S Downlink Format 17) baseband signals, along with the raw 112-bit message and all applied parameters. It implements an infinite iterator that streams reproducible samples with full control over message type distributions, transmission parameters, and channel impairments.
38
+
39
+ ## Key Features
40
+
41
+ - **End-to-end pipeline**: random message generation, PPM encoding, and RF channel simulation in a single call.
42
+ - **Four ADS-B message types**: identification, surface position, airborne position, and airborne velocity with configurable emission probabilities.
43
+ - **Realistic channel impairments**: Gaussian Noise(both AWGN and Correlated) , frequency & phase offset, IQ imbalance, and DC offset, all sampled from configurable probability distributions.
44
+ - **Reproducibility**: deterministic output via a shared seed across all pipeline stages.
45
+ - **Configurable distributions**: override any transmission or channel parameter distribution to model specific receiver conditions or hardware behavior.
46
+ - **NumPy-native**: all signals are `np.complex64` arrays, ready for direct use with any downstream processing tool.
47
+
48
+ ## Requirements
49
+
50
+ - Python 3.10+
51
+ - NumPy
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install adsb-generator
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ```python
62
+ from adsb_generator import ADSBGenerator
63
+
64
+ # Create a generator with a fixed seed for reproducibility
65
+ gen = ADSBGenerator(seed=42)
66
+
67
+ # Each iteration yields an ADSBSample with the raw message,
68
+ # clean signal, and channel-impaired signal
69
+ for sample in gen:
70
+ print(f"Message type : {sample.message_type.value}")
71
+ print(f"Raw message : {sample.message:#028x}")
72
+ print(f"Clean signal : {sample.clean_signal.shape} complex64 samples")
73
+ print(f"Noisy signal : {sample.channel_signal.shape} complex64 samples")
74
+ print(f"SNR (dB) : {sample.channel_params['snr_db']:.1f}")
75
+ print(f"Amplitude : {sample.tx_params['amplitude']:.3f}")
76
+ break
77
+ ```
78
+
79
+ ### Customizing Distributions
80
+
81
+ ```python
82
+ from adsb_generator import ADSBGenerator, MessageType, ChannelParams
83
+
84
+ # Favor airborne positions, restrict SNR to low-moderate range
85
+ gen = ADSBGenerator(
86
+ message_type_probs={
87
+ MessageType.AIRBORNE_POSITION: 0.60,
88
+ MessageType.AIRBORNE_VELOCITY: 0.20,
89
+ MessageType.IDENTIFICATION: 0.10,
90
+ MessageType.SURFACE_POSITION: 0.10,
91
+ },
92
+ channel_params_distributions={
93
+ ChannelParams.SNR_DB: [
94
+ [3.0, 8.0, 0.70],
95
+ [8.0, 15.0, 0.30],
96
+ ],
97
+ },
98
+ sample_rate=2e6,
99
+ seed=12345,
100
+ )
101
+
102
+ sample = next(gen)
103
+ ```
104
+
105
+ ## API Reference
106
+
107
+ ### `ADSBGenerator`
108
+
109
+ ```python
110
+ ADSBGenerator(
111
+ message_type_probs: dict[MessageType | str, float] | None = None,
112
+ tx_params_distributions: dict[TXParams | str, list[list[float]]] | None = None,
113
+ channel_params_distributions: dict[ChannelParams | str, list[list[float]]] | None = None,
114
+ sample_rate: float = 2e6,
115
+ seed: int | None = None,
116
+ )
117
+ ```
118
+
119
+ | Parameter | Type | Default | Description |
120
+ |---|---|---|---|
121
+ | `message_type_probs` | `dict` or `None` | Equal 25% per type | Mapping of `MessageType` to emission probabilities (must sum to 1.0). |
122
+ | `tx_params_distributions` | `dict` or `None` | Single amplitude band | Mapping of `TXParams` to `[[min, max, weight], ...]` intervals. |
123
+ | `channel_params_distributions` | `dict` or `None` | Typical ADS-B conditions | Mapping of `ChannelParams` to `[[min, max, weight], ...]` intervals. |
124
+ | `sample_rate` | `float` | `2e6` | Sampling rate in samples per second. |
125
+ | `seed` | `int` or `None` | Random | Seed for deterministic output across all pipeline stages. |
126
+
127
+ ---
128
+
129
+ ### `ADSBSample`
130
+
131
+ Dataclass returned by each iteration of `ADSBGenerator`.
132
+
133
+ | Field | Type | Description |
134
+ |---|---|---|
135
+ | `message` | `int` | Complete 112-bit ADS-B message (including 24-bit CRC) as an integer. |
136
+ | `message_type` | `MessageType` | The type of ADS-B message generated. |
137
+ | `clean_signal` | `np.ndarray` | Complex baseband I/Q signal before channel impairments (`complex64`). |
138
+ | `tx_params` | `dict[TXParams, float]` | Transmission parameters applied during encoding. |
139
+ | `channel_signal` | `np.ndarray` | Complex baseband I/Q signal after channel impairments (`complex64`). |
140
+ | `channel_params` | `dict[ChannelParams, float]` | Channel parameters applied to the signal. |
141
+
142
+ ---
143
+
144
+ ### `MessageType`
145
+
146
+ | Value | Description |
147
+ |---|---|
148
+ | `IDENTIFICATION` | Aircraft identification (callsign) messages (TC 1--4). |
149
+ | `SURFACE_POSITION` | Surface position messages (TC 5--8). |
150
+ | `AIRBORNE_POSITION` | Airborne position messages (TC 9--18, 20--22). |
151
+ | `AIRBORNE_VELOCITY` | Airborne velocity messages (TC 19). |
152
+
153
+ ---
154
+
155
+ ### `ADSBEncoder`
156
+
157
+ Encodes 112-bit messages into complex baseband I/Q samples using PPM per the Mode S standard.
158
+
159
+ ```python
160
+ ADSBEncoder(
161
+ sample_rate: float = 2e6,
162
+ tx_params_distributions: dict[TXParams | str, list[list[float]]] | None = None,
163
+ seed: int | None = None,
164
+ )
165
+ ```
166
+
167
+ | Method | Returns | Description |
168
+ |---|---|---|
169
+ | `encode(msg: int)` | `tuple[np.ndarray, dict[TXParams, float]]` | Encodes a 112-bit message into a 120-us baseband I/Q signal. |
170
+
171
+ ---
172
+
173
+ ### `ADSBChannel`
174
+
175
+ Simulates realistic RF channel impairments on baseband I/Q signals.
176
+
177
+ ```python
178
+ ADSBChannel(
179
+ sample_rate: float = 2e6,
180
+ channel_params_distributions: dict[ChannelParams | str, list[list[float]]] | None = None,
181
+ seed: int | None = None,
182
+ )
183
+ ```
184
+
185
+ | Method | Returns | Description |
186
+ |---|---|---|
187
+ | `apply(signal: np.ndarray)` | `tuple[np.ndarray, dict[ChannelParams, float]]` | Applies impairments: IQ imbalance, DC offset, frequency offset, phase offset, AWGN. |
188
+
189
+ ---
190
+
191
+ ### `ChannelParams`
192
+
193
+ | Value | Description |
194
+ |---|---|
195
+ | `SNR_DB` | Signal-to-noise ratio in dB. |
196
+ | `NOISE_CORRELATION` | I/Q noise correlation coefficient (-1.0 to 1.0). |
197
+ | `FREQUENCY_OFFSET` | Carrier frequency offset in Hz. |
198
+ | `PHASE_OFFSET` | Phase offset in radians. |
199
+ | `DC_OFFSET_I` | DC offset on the in-phase component. |
200
+ | `DC_OFFSET_Q` | DC offset on the quadrature component. |
201
+ | `IQ_GAIN_IMBALANCE` | Gain imbalance between I and Q channels. |
202
+ | `IQ_PHASE_IMBALANCE` | Phase imbalance between I and Q channels (degrees). |
203
+
204
+ ---
205
+
206
+ ### `ADSBAlgorithms`
207
+
208
+ Static utility class providing ADS-B encoding algorithms.
209
+
210
+ | Method | Returns | Description |
211
+ |---|---|---|
212
+ | `calculate_crc(data: int)` | `int` | Computes 24-bit CRC parity using polynomial `0xFFF409`. |
213
+ | `encode_cpr(lat, lon, odd)` | `tuple[int, int]` | Encodes lat/lon into CPR 17-bit values. |
214
+ | `encode_altitude(alt: int)` | `int` | Encodes altitude in feet into 12-bit Gillham-coded format. |
215
+ | `encode_ground_track(degrees, valid)` | `tuple[int, int]` | Encodes ground track heading into the 7-bit format. |
216
+
217
+ ## Distribution Format
218
+
219
+ All configurable distributions use the format `[[min_val, max_val, weight], ...]` where weights for a given parameter must sum to `1.0` (+/- 0.01 tolerance). Keys can be enum members or their string values (e.g., `ChannelParams.SNR_DB` or `"snr_db"`).
220
+
221
+ ## License
222
+
223
+ [MIT](LICENSE) - Copyright (c) 2026 Adam Hamri (adamhamri9)
@@ -0,0 +1,11 @@
1
+ adsb_generator/__init__.py,sha256=AU84UFFLrdWoMK_uix9uCYodx7bh5YUJNrQf6IHrN54,352
2
+ adsb_generator/algorithms.py,sha256=LBcTkbK94gouSkFmDo48ZiZaC_-4mY9TtFJwes3shxw,2265
3
+ adsb_generator/channel.py,sha256=_5hUjqhpJyu_n8T8y1OdQycydrJUFEq11nTGZo1x79E,10416
4
+ adsb_generator/encoder.py,sha256=X4zUdVVU2ixI7x4xi9g5QFXV2r4CN9vFUcLMJDnLXNk,6235
5
+ adsb_generator/generator.py,sha256=RTdwA0DcWVc6P2IriJIfSNIqEz9bgDPrN9nV3xd8nU4,3774
6
+ adsb_generator/message.py,sha256=x9zIOKdtV-uikBkC1KtqiWnj-w61OTgvZuHQl2N2mqQ,8209
7
+ adsb_generator-0.1.0.dist-info/licenses/LICENSE,sha256=0Zc5Y-ihUySF2-U3su0-ERhEq-pYJPvcbfUMkONdrWY,1079
8
+ adsb_generator-0.1.0.dist-info/METADATA,sha256=chG4asTjaw4k5sUR0sOm4l0Nu1uK9nQD0d-q6whIPIE,8851
9
+ adsb_generator-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ adsb_generator-0.1.0.dist-info/top_level.txt,sha256=Dqxs5s64BNiCODLScEChyq31fa8AKNVZxQDDv6BDmKM,15
11
+ adsb_generator-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Hamri (adamhamri9)
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 @@
1
+ adsb_generator