libasterix 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.
- libasterix-0.1.0/LICENSE +29 -0
- libasterix-0.1.0/PKG-INFO +651 -0
- libasterix-0.1.0/README.md +636 -0
- libasterix-0.1.0/pyproject.toml +33 -0
- libasterix-0.1.0/setup.cfg +4 -0
- libasterix-0.1.0/src/asterix/__init__.py +0 -0
- libasterix-0.1.0/src/asterix/base.py +1439 -0
- libasterix-0.1.0/src/asterix/generated.py +145674 -0
- libasterix-0.1.0/src/asterix/py.typed +0 -0
- libasterix-0.1.0/src/libasterix.egg-info/PKG-INFO +651 -0
- libasterix-0.1.0/src/libasterix.egg-info/SOURCES.txt +15 -0
- libasterix-0.1.0/src/libasterix.egg-info/dependency_links.txt +1 -0
- libasterix-0.1.0/src/libasterix.egg-info/requires.txt +3 -0
- libasterix-0.1.0/src/libasterix.egg-info/top_level.txt +1 -0
- libasterix-0.1.0/tests/test_asterix.py +757 -0
- libasterix-0.1.0/tests/test_bitstring.py +122 -0
- libasterix-0.1.0/tests/test_raw_datablock.py +75 -0
libasterix-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Copyright (c) 2024, Zoran Bošnjak
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright
|
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above
|
|
11
|
+
copyright notice, this list of conditions and the following
|
|
12
|
+
disclaimer in the documentation and/or other materials provided
|
|
13
|
+
with the distribution.
|
|
14
|
+
|
|
15
|
+
* Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived
|
|
17
|
+
from this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
20
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
21
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
22
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
23
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
24
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
25
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
26
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
27
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
28
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: libasterix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Asterix data processing library
|
|
5
|
+
Author-email: Zoran Bošnjak <zoran.bosnjak@via.si>
|
|
6
|
+
Project-URL: Homepage, https://zoranbosnjak.github.io/asterix-libs
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/zoranbosnjak/asterix-libs/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: typing_extensions; python_version < "3.10"
|
|
15
|
+
|
|
16
|
+
# Asterix data processing library for python
|
|
17
|
+
|
|
18
|
+
Features:
|
|
19
|
+
|
|
20
|
+
- asterix data parsing/decoding from bytes
|
|
21
|
+
- asterix data encoding/unparsing to bytes
|
|
22
|
+
- precise conversion functions for physical quantities
|
|
23
|
+
- support for many asterix categories and editions
|
|
24
|
+
- support for Reserved Expansion Fields (REF)
|
|
25
|
+
- support for categories with multiple UAPs, eg. cat001
|
|
26
|
+
- support for context dependent items, eg. I062/380/IAS
|
|
27
|
+
- pure python implementation
|
|
28
|
+
- type annotations for static type checking,
|
|
29
|
+
including subitem access by name
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
Encoding and decoding asterix example.
|
|
34
|
+
This example also includes type annotations for static
|
|
35
|
+
type checking with `mypy`. In a simple untyped environment,
|
|
36
|
+
the type annotations and assertions could be skipped.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from typing import *
|
|
40
|
+
from binascii import hexlify, unhexlify
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
|
|
43
|
+
from asterix.base import *
|
|
44
|
+
import asterix.generated as gen
|
|
45
|
+
|
|
46
|
+
# Select particular asterix categories and editions
|
|
47
|
+
Cat034 = gen.Cat_034_1_29
|
|
48
|
+
Cat048 = gen.Cat_048_1_32
|
|
49
|
+
|
|
50
|
+
# Example messages for this application
|
|
51
|
+
class Token:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class NorthMarker(Token):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class SectorCrossing(Token):
|
|
60
|
+
azimuth: float
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Plot(Token):
|
|
64
|
+
rho: float
|
|
65
|
+
theta: float
|
|
66
|
+
ssr: str
|
|
67
|
+
|
|
68
|
+
# example message to be encoded
|
|
69
|
+
tx_message = [
|
|
70
|
+
NorthMarker(),
|
|
71
|
+
SectorCrossing(0.0),
|
|
72
|
+
Plot(rho=10.0, theta=45.0, ssr='7777'),
|
|
73
|
+
SectorCrossing(45.0),
|
|
74
|
+
]
|
|
75
|
+
print('sending message:', tx_message)
|
|
76
|
+
|
|
77
|
+
# encode token to datablock
|
|
78
|
+
def encode(token: Token) -> bytes:
|
|
79
|
+
if isinstance(token, NorthMarker):
|
|
80
|
+
rec034 = Cat034.cv_record.create({
|
|
81
|
+
'000': 1, # North marker message
|
|
82
|
+
'010': (('SAC', 1), ('SIC', 2)),
|
|
83
|
+
})
|
|
84
|
+
datablock034 = Cat034.create([rec034])
|
|
85
|
+
return datablock034.unparse().to_bytes()
|
|
86
|
+
if isinstance(token, SectorCrossing):
|
|
87
|
+
rec034 = Cat034.cv_record.create({
|
|
88
|
+
'000': 2, # Sector crossing message
|
|
89
|
+
'010': (('SAC', 1), ('SIC', 2)),
|
|
90
|
+
'020': ((token.azimuth, "°")),
|
|
91
|
+
})
|
|
92
|
+
datablock034 = Cat034.create([rec034])
|
|
93
|
+
return datablock034.unparse().to_bytes()
|
|
94
|
+
if isinstance(token, Plot):
|
|
95
|
+
rec048 = Cat048.cv_record.create({
|
|
96
|
+
'010': (('SAC', 1), ('SIC', 2)),
|
|
97
|
+
'040': (('RHO', (token.rho, "NM")), ('THETA', (token.theta, "°"))),
|
|
98
|
+
'070': (0, 0, 0, 0, ('MODE3A', token.ssr)),
|
|
99
|
+
})
|
|
100
|
+
datablock048= Cat048.create([rec048])
|
|
101
|
+
return datablock048.unparse().to_bytes()
|
|
102
|
+
raise Exception('unexpected token', token)
|
|
103
|
+
|
|
104
|
+
datablocks = [encode(token) for token in tx_message]
|
|
105
|
+
tx = b''.join(datablocks)
|
|
106
|
+
print('bytes on the wire:', hexlify(tx))
|
|
107
|
+
|
|
108
|
+
assert hexlify(tx) == \
|
|
109
|
+
b'220007c0010201220008d00102020030000c9801020a0020000fff220008d001020220'
|
|
110
|
+
|
|
111
|
+
# decode bytes to message list
|
|
112
|
+
def decode(rx_bytes: bytes) -> List[Token]:
|
|
113
|
+
message: List[Token] = []
|
|
114
|
+
|
|
115
|
+
raw_datablocks = RawDatablock.parse(Bits.from_bytes(tx))
|
|
116
|
+
assert not isinstance(raw_datablocks, ValueError)
|
|
117
|
+
for db in raw_datablocks:
|
|
118
|
+
cat = db.get_category()
|
|
119
|
+
if cat == 34:
|
|
120
|
+
result034 = Cat034.cv_uap.parse(db.get_raw_records())
|
|
121
|
+
assert not isinstance(result034, ValueError)
|
|
122
|
+
for rec034 in result034:
|
|
123
|
+
i000 = rec034.get_item('000')
|
|
124
|
+
assert i000 is not None
|
|
125
|
+
val = i000.as_uint()
|
|
126
|
+
if val == 1:
|
|
127
|
+
message.append(NorthMarker())
|
|
128
|
+
elif val == 2:
|
|
129
|
+
i020 = rec034.get_item('020')
|
|
130
|
+
assert i020 is not None
|
|
131
|
+
azimuth = i020.variation.content.as_quantity("°")
|
|
132
|
+
message.append(SectorCrossing(azimuth = azimuth))
|
|
133
|
+
else:
|
|
134
|
+
pass
|
|
135
|
+
elif cat == 48:
|
|
136
|
+
result048 = Cat048.cv_uap.parse(db.get_raw_records())
|
|
137
|
+
assert not isinstance(result048, ValueError)
|
|
138
|
+
for rec048 in result048:
|
|
139
|
+
i040 = rec048.get_item('040')
|
|
140
|
+
i070 = rec048.get_item('070')
|
|
141
|
+
assert i040 is not None
|
|
142
|
+
assert i070 is not None
|
|
143
|
+
rho = i040.variation.get_item('RHO').variation.content.as_quantity("NM")
|
|
144
|
+
theta = i040.variation.get_item('THETA').variation.content.as_quantity("°")
|
|
145
|
+
ssr = i070.variation.get_item('MODE3A').variation.content.as_string()
|
|
146
|
+
message.append(Plot(rho = rho, theta = theta, ssr = ssr))
|
|
147
|
+
else:
|
|
148
|
+
pass
|
|
149
|
+
return message
|
|
150
|
+
|
|
151
|
+
rx = tx
|
|
152
|
+
rx_message = decode(rx)
|
|
153
|
+
|
|
154
|
+
# expect the same message
|
|
155
|
+
print('received message:', rx_message)
|
|
156
|
+
assert rx_message == tx_message
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Installation
|
|
160
|
+
|
|
161
|
+
Use any of the following methods:
|
|
162
|
+
|
|
163
|
+
### Method 1 - copy library files
|
|
164
|
+
|
|
165
|
+
The following files are required:
|
|
166
|
+
|
|
167
|
+
- [base.py](src/asterix/base.py)
|
|
168
|
+
- [generated.py](src/asterix/generated.py)
|
|
169
|
+
|
|
170
|
+
Download and copy files either alongside your project sources or
|
|
171
|
+
to some location where `python` can find it.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# check default python path
|
|
175
|
+
python3 -c "import sys; print('\n'.join(sys.path))"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Method 2 - install/update package with `pip`
|
|
179
|
+
|
|
180
|
+
Use `pip` to install or update:
|
|
181
|
+
|
|
182
|
+
``` bash
|
|
183
|
+
# prepare and activate virtual environment (optional)
|
|
184
|
+
sudo apt install python3-venv
|
|
185
|
+
python3 -m venv env
|
|
186
|
+
source env/bin/activate
|
|
187
|
+
|
|
188
|
+
# install or upgrade (from default branch)
|
|
189
|
+
pip install -e "git+https://github.com/zoranbosnjak/asterix-libs.git#egg=libasterix&subdirectory=libs/python"
|
|
190
|
+
|
|
191
|
+
# install or upgrade (from 'devel' branch)
|
|
192
|
+
pip install -e "git+https://github.com/zoranbosnjak/asterix-libs.git@devel#egg=libasterix&subdirectory=libs/python"
|
|
193
|
+
|
|
194
|
+
# deactivate virtual environment when done (if activated)
|
|
195
|
+
deactivate
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Tutorial
|
|
199
|
+
|
|
200
|
+
Check library installation.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
python3 -c "import asterix.base as base; print(base.AstSpec)"
|
|
204
|
+
python3 -c "import asterix.generated as gen; print(gen.manifest['CATS'].keys())"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Import
|
|
208
|
+
|
|
209
|
+
This tutorial assumes importing complete `asterix` module into the current
|
|
210
|
+
namespace. In practice however only the required objects could be imported
|
|
211
|
+
or the module might be imported to a dedicated namespace.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from asterix.base import *
|
|
215
|
+
from asterix.generated import *
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Error handling
|
|
219
|
+
|
|
220
|
+
Some operation (eg. parsing) can fail on unexpected input. In such case,
|
|
221
|
+
to indicate an error, this library will not raise an exception, but will
|
|
222
|
+
return `ValueError('problem description')` instead.
|
|
223
|
+
|
|
224
|
+
With this approach, a user can handle errors in a type safe way, for example:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
def parse_datablocks(s: bytes) -> List[RawDatablock]:
|
|
228
|
+
dbs = RawDatablock.parse(Bits.from_bytes(s))
|
|
229
|
+
if isinstance(dbs, ValueError):
|
|
230
|
+
return [] # or raise exception, or ...
|
|
231
|
+
return dbs
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
For clarity, the error handling part is skipped in some parts of this tutorial.
|
|
235
|
+
|
|
236
|
+
### Immutable objects
|
|
237
|
+
|
|
238
|
+
All operation on asterix objects are *immutable*.
|
|
239
|
+
|
|
240
|
+
For example:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from asterix.generated import *
|
|
244
|
+
|
|
245
|
+
Spec = Cat_002_1_1
|
|
246
|
+
|
|
247
|
+
# create empty record
|
|
248
|
+
rec0 = Spec.cv_record.create({})
|
|
249
|
+
|
|
250
|
+
# this operation does nothing (result is not stored)
|
|
251
|
+
rec0.set_item('000', 1)
|
|
252
|
+
assert rec0.get_item('000') is None
|
|
253
|
+
|
|
254
|
+
# store result to 'rec1'
|
|
255
|
+
rec1 = rec0.set_item('000', 1)
|
|
256
|
+
assert rec1.get_item('000') is not None
|
|
257
|
+
|
|
258
|
+
# use multiple updates in sequence
|
|
259
|
+
rec2a = rec0.set_item('000', 1).set_item('010', (('SAC', 1), ('SIC', 2)))
|
|
260
|
+
rec2b = Spec.cv_record.create({'000': 1, '010': (('SAC', 1), ('SIC', 2))})
|
|
261
|
+
assert rec2a.unparse() == rec2b.unparse()
|
|
262
|
+
|
|
263
|
+
# mutation can be simulated by replacing old object with the new one
|
|
264
|
+
# (using the same variable name)
|
|
265
|
+
rec0 = rec0.set_item('000', 1)
|
|
266
|
+
assert rec0.get_item('000') is not None
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Datagram
|
|
270
|
+
|
|
271
|
+
Datagram is a raw binary data as received for example from UDP socket.
|
|
272
|
+
This is represented with `bytes` data type in python.
|
|
273
|
+
|
|
274
|
+
### Raw Datablock
|
|
275
|
+
|
|
276
|
+
Raw datablock is asterix datablock in the form `cat|length|data` with the
|
|
277
|
+
correct byte size. A datagram can contain multiple datablocks.
|
|
278
|
+
|
|
279
|
+
This is represented in python with `class RawDatablock`.
|
|
280
|
+
|
|
281
|
+
In some cases it might be sufficient to work with raw datablocks, for example
|
|
282
|
+
in the case of asterix category filtering. In this case, it is not required
|
|
283
|
+
to fully parse asterix records.
|
|
284
|
+
|
|
285
|
+
**Example**: Category filter, drop datablocks if category == 1
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from binascii import hexlify, unhexlify
|
|
289
|
+
from asterix.base import *
|
|
290
|
+
|
|
291
|
+
def receive_from_udp(): # UDP rx text function
|
|
292
|
+
return unhexlify(''.join([
|
|
293
|
+
'01000401', # cat1 datablock
|
|
294
|
+
'02000402', # cat2 datablock
|
|
295
|
+
]))
|
|
296
|
+
|
|
297
|
+
def send_to_udp(s): # UDP tx test function
|
|
298
|
+
print(hexlify(s))
|
|
299
|
+
|
|
300
|
+
input_data = Bits.from_bytes(receive_from_udp())
|
|
301
|
+
raw_datablocks = RawDatablock.parse(input_data) # can fail on wrong input
|
|
302
|
+
valid_datablocks = [db.unparse().to_bytes() \
|
|
303
|
+
for db in raw_datablocks if db.get_category() != 1]
|
|
304
|
+
output_data = b''.join(valid_datablocks)
|
|
305
|
+
send_to_udp(output_data)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Datablock, Record
|
|
309
|
+
|
|
310
|
+
Datablock (represented as `class Datablock`) is a higher level, where we
|
|
311
|
+
have a guarantee that all containing records are semantically correct
|
|
312
|
+
(asterix is fully parsed or correctly constructed).
|
|
313
|
+
|
|
314
|
+
Datablock/Record is required to work with asterix items and subitems.
|
|
315
|
+
|
|
316
|
+
**Example**: Create 2 records and combine them to a single datablock
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from asterix.generated import *
|
|
320
|
+
|
|
321
|
+
Spec = Cat_002_1_1 # use cat002, edition 1.1
|
|
322
|
+
|
|
323
|
+
rec1 = Spec.cv_record.create({
|
|
324
|
+
'000': 1,
|
|
325
|
+
'010': (('SAC', 1), ('SIC', 2)),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
rec2 = Spec.cv_record.create({
|
|
329
|
+
'000': 2,
|
|
330
|
+
'010': (('SAC', 1), ('SIC', 2)),
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
db = Spec.create([rec1, rec2])
|
|
334
|
+
s = db.unparse().to_bytes() # ready to send over the network
|
|
335
|
+
print(hexlify(s))
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Example**: Parse datagram (from the example above) and extract message type
|
|
339
|
+
from each record
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from asterix.base import *
|
|
343
|
+
from asterix.generated import *
|
|
344
|
+
|
|
345
|
+
Spec = Cat_002_1_1 # use cat002, edition 1.1
|
|
346
|
+
|
|
347
|
+
s = unhexlify(b'02000bc0010201c0010202') # ... use data from the example above
|
|
348
|
+
raw_datablocks = RawDatablock.parse(Bits.from_bytes(s)) # can fail on wrong input
|
|
349
|
+
for db in raw_datablocks:
|
|
350
|
+
records = Spec.cv_uap.parse(db.get_raw_records()) # can fail on wrong input
|
|
351
|
+
for record in records:
|
|
352
|
+
i000 = record.get_item('000') # returns None if the item is not present
|
|
353
|
+
raw_value = i000.as_uint()
|
|
354
|
+
description = i000.variation.content.table_value()
|
|
355
|
+
print('{}: {}'.format(raw_value, description))
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Example**: Asterix filter, rewrite SAC/SIC code with random values.
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
import time
|
|
362
|
+
import random
|
|
363
|
+
from asterix.base import *
|
|
364
|
+
from asterix.generated import *
|
|
365
|
+
|
|
366
|
+
# categories/editions of interest
|
|
367
|
+
Specs = {
|
|
368
|
+
48: Cat_048_1_31,
|
|
369
|
+
62: Cat_062_1_19,
|
|
370
|
+
63: Cat_063_1_6,
|
|
371
|
+
# ...
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
def process_record(sac, sic, rec):
|
|
375
|
+
"""Process single record."""
|
|
376
|
+
return rec.set_item('010', (('SAC', sac), ('SIC', sic)))
|
|
377
|
+
|
|
378
|
+
def process_datablock(sac, sic, db):
|
|
379
|
+
"""Process single raw datablock."""
|
|
380
|
+
cat = db.get_category()
|
|
381
|
+
Spec = Specs.get(cat)
|
|
382
|
+
if Spec is None:
|
|
383
|
+
return db
|
|
384
|
+
# second level of parsing (records are valid)
|
|
385
|
+
records = Spec.cv_uap.parse(db.get_raw_records())
|
|
386
|
+
new_records = [process_record(sac, sic, rec) for rec in records]
|
|
387
|
+
return Spec.create(new_records)
|
|
388
|
+
|
|
389
|
+
def rewrite_sac_sic(sac : int, sic : int, s : bytes) -> bytes:
|
|
390
|
+
"""Process datagram."""
|
|
391
|
+
# first level of parsing (datablocks are valid)
|
|
392
|
+
raw_datablocks = RawDatablock.parse(Bits.from_bytes(s))
|
|
393
|
+
result = [process_datablock(sac, sic, db) for db in raw_datablocks]
|
|
394
|
+
output = b''.join([db.unparse().to_bytes() for db in result])
|
|
395
|
+
return output
|
|
396
|
+
|
|
397
|
+
def rx_bytes_from_the_network():
|
|
398
|
+
"""Dummy rx function (generate valid asterix datagram)."""
|
|
399
|
+
time.sleep(1)
|
|
400
|
+
Spec = Cat_048_1_31
|
|
401
|
+
rec = Spec.cv_record.create({'010': 0, '040': 0})
|
|
402
|
+
db1 = Spec.create([rec, rec]).unparse().to_bytes()
|
|
403
|
+
db2 = Spec.create([rec, rec]).unparse().to_bytes()
|
|
404
|
+
return b''.join([db1, db2])
|
|
405
|
+
|
|
406
|
+
def tx_bytes_to_the_network(s_output):
|
|
407
|
+
"""Dummy tx function."""
|
|
408
|
+
print(hexlify(s_output))
|
|
409
|
+
|
|
410
|
+
# main processing loop
|
|
411
|
+
while True:
|
|
412
|
+
s_input = rx_bytes_from_the_network()
|
|
413
|
+
new_sac = random.randint(0,127)
|
|
414
|
+
new_sic = random.randint(128,255)
|
|
415
|
+
try:
|
|
416
|
+
s_output = rewrite_sac_sic(new_sac, new_sic, s_input)
|
|
417
|
+
tx_bytes_to_the_network(s_output)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
print('Asterix exception: ', e)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### Reserved expansion fields
|
|
423
|
+
|
|
424
|
+
TODO: Add parsing/constructing expansion field example
|
|
425
|
+
|
|
426
|
+
#### Multiple UAP-s
|
|
427
|
+
|
|
428
|
+
Make sure to use appropriate UAP name, together with a correct UAP selector
|
|
429
|
+
value, for example for CAT001:
|
|
430
|
+
|
|
431
|
+
- `['020', 'TYP'] = 0` for `plot`
|
|
432
|
+
- `['020', 'TYP'] = 1` for `track`
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
from asterix.base import *
|
|
436
|
+
from asterix.generated import *
|
|
437
|
+
|
|
438
|
+
Cat1 = Cat_001_1_4
|
|
439
|
+
|
|
440
|
+
rec01_plot = Cat1.cv_uap.spec('plot').create({
|
|
441
|
+
'010': 0x0102,
|
|
442
|
+
'020': ((('TYP',0),0,0,0,0,0,None),),
|
|
443
|
+
'040': 0x01020304
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
rec01_track = Cat1.cv_uap.spec('track').create({
|
|
447
|
+
'010': 0x0102,
|
|
448
|
+
'020': ((('TYP',1),0,0,0,0,0,None),),
|
|
449
|
+
'040': 0x01020304,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
rec01_invalid = Cat1.cv_uap.spec('plot').create({
|
|
453
|
+
'010': 0x0102,
|
|
454
|
+
'020': ((('TYP',1),0,0,0,0,0,None),),
|
|
455
|
+
'040': 0x01020304
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
print(Cat1.create([rec01_plot]).unparse().to_bytes().hex())
|
|
459
|
+
print(Cat1.create([rec01_track]).unparse().to_bytes().hex())
|
|
460
|
+
print(Cat1.create([rec01_invalid]).unparse().to_bytes().hex())
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Library manifest
|
|
464
|
+
|
|
465
|
+
This library defines a `manifest` structure in the form:
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
manifest = {
|
|
469
|
+
'CATS': {
|
|
470
|
+
1: {
|
|
471
|
+
'1.2': CAT_001_1_2,
|
|
472
|
+
'1.3': CAT_001_1_3,
|
|
473
|
+
'1.4': CAT_001_1_4,
|
|
474
|
+
},
|
|
475
|
+
2: {
|
|
476
|
+
'1.0': CAT_002_1_0,
|
|
477
|
+
'1.1': CAT_002_1_1,
|
|
478
|
+
#...
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
This structure can be used to extract *latest* editions for each defined
|
|
482
|
+
category, for example:
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
from asterix.generated import *
|
|
486
|
+
|
|
487
|
+
def to_edition(ed):
|
|
488
|
+
"""Convert edition string to a tuple, for example "1.2" -> (1,2)"""
|
|
489
|
+
a,b = ed.split('.')
|
|
490
|
+
return (int(a), int(b))
|
|
491
|
+
|
|
492
|
+
def get_latest_edition(lst):
|
|
493
|
+
return sorted(lst, key=lambda pair: to_edition(pair[0]), reverse=True)[0]
|
|
494
|
+
|
|
495
|
+
Specs = {} # will be populated with latest editions
|
|
496
|
+
|
|
497
|
+
for cat in range(1,256):
|
|
498
|
+
editions = manifest['CATS'].get(cat)
|
|
499
|
+
if editions is None:
|
|
500
|
+
continue
|
|
501
|
+
latest = get_latest_edition(editions.items())
|
|
502
|
+
ed, cls = latest
|
|
503
|
+
Specs[cat] = cls
|
|
504
|
+
|
|
505
|
+
print(Specs)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Alternatively, a prefered way is to be explicit about each edition,
|
|
509
|
+
for example:
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
from asterix.generated import *
|
|
513
|
+
|
|
514
|
+
Specs = {
|
|
515
|
+
48: Cat_048_1_31,
|
|
516
|
+
62: Cat_062_1_19,
|
|
517
|
+
63: Cat_063_1_6,
|
|
518
|
+
# ...
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Generic asterix processing
|
|
523
|
+
|
|
524
|
+
*Generic processing* in this context means working with asterix data where
|
|
525
|
+
the subitem names and types are determined at runtime. That is: the explicit
|
|
526
|
+
subitem names are never mentioned in the application source code.
|
|
527
|
+
|
|
528
|
+
This is in contrast to *application specific processing*, where we are
|
|
529
|
+
explicit about subitems, for example ["010", "SAC"].
|
|
530
|
+
|
|
531
|
+
**Example**: Show raw content of all toplevel items of each record
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
from asterix.generated import *
|
|
535
|
+
|
|
536
|
+
Specs = {
|
|
537
|
+
48: Cat_048_1_31,
|
|
538
|
+
62: Cat_062_1_19,
|
|
539
|
+
63: Cat_063_1_6,
|
|
540
|
+
# ...
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# some test input bytes
|
|
544
|
+
s = unhexlify(''.join([
|
|
545
|
+
'3e00a5254327d835a95a0d0a2baf256af940e8a8d0caa1a594e1e525f2e32bc0448b',
|
|
546
|
+
'0e34c0b6211b5847038319d1b88d714b990a6e061589a414209d2e1d00ba5602248e',
|
|
547
|
+
'64092c2a0410138b2c030621c2043080fe06182ee40d2fa51078192cce70e9af5435',
|
|
548
|
+
'aeb2e3c74efc7107052ce9a0a721290cb5b2b566137911b5315fa412250031b95579',
|
|
549
|
+
'03ed2ef47142ed8a79165c82fb803c0e38c7f7d641c1a4a77740960737']))
|
|
550
|
+
|
|
551
|
+
def handle_nonspare(cat, name, nsp):
|
|
552
|
+
print('cat{}, item {}, {}'.format(cat, name, nsp.unparse()))
|
|
553
|
+
# depending on the application, we might want to display
|
|
554
|
+
# deep subitems, which is possible by examining 'nsp' object
|
|
555
|
+
|
|
556
|
+
for db in RawDatablock.parse(Bits.from_bytes(s)):
|
|
557
|
+
cat = db.get_category()
|
|
558
|
+
Spec = Specs.get(cat)
|
|
559
|
+
if Spec is None:
|
|
560
|
+
print('unsupported category', cat)
|
|
561
|
+
continue
|
|
562
|
+
for record in Spec.cv_uap.parse(db.get_raw_records()):
|
|
563
|
+
for (name, nsp) in record.items_regular.items():
|
|
564
|
+
handle_nonspare(cat, name, nsp)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Example**: Generate dummy single record datablock with all fixed items set to zero
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
from asterix.generated import *
|
|
571
|
+
|
|
572
|
+
# we could even randomly select a category/edition from the 'manifest',
|
|
573
|
+
# but for simplicity just use a particular spec
|
|
574
|
+
Spec = Cat_062_1_20
|
|
575
|
+
|
|
576
|
+
rec = Spec.cv_record.create({})
|
|
577
|
+
all_items = Spec.cv_record.cv_items_dict
|
|
578
|
+
for name in all_items:
|
|
579
|
+
if name is None:
|
|
580
|
+
continue
|
|
581
|
+
nsp = all_items[name]
|
|
582
|
+
var = nsp.cv_rule.cv_variation
|
|
583
|
+
if issubclass(var, Element):
|
|
584
|
+
rec = rec.set_item(name, 0)
|
|
585
|
+
elif issubclass(var, Group):
|
|
586
|
+
rec = rec.set_item(name, 0)
|
|
587
|
+
elif issubclass(var, Extended):
|
|
588
|
+
pass # skip for this test
|
|
589
|
+
elif issubclass(var, Repetitive):
|
|
590
|
+
pass # skip for this test
|
|
591
|
+
elif issubclass(var, Explicit):
|
|
592
|
+
pass # skip for this test
|
|
593
|
+
elif issubclass(var, Compound):
|
|
594
|
+
pass # skip for this test
|
|
595
|
+
else:
|
|
596
|
+
raise Exception('unexpected subclass')
|
|
597
|
+
|
|
598
|
+
s = Spec.create([rec]).unparse().to_bytes()
|
|
599
|
+
print(hexlify(s))
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Using `mypy` static code checker
|
|
603
|
+
|
|
604
|
+
**Note**: `mypy` version `0.991` or above is required for this library.
|
|
605
|
+
|
|
606
|
+
[mypy](https://www.mypy-lang.org/) is a static type checker for Python.
|
|
607
|
+
It is recommended to use the tool on asterix application code, to identify
|
|
608
|
+
some problems which would otherwise result in runtime errors.
|
|
609
|
+
|
|
610
|
+
Consider the following test program (`test.py`):
|
|
611
|
+
|
|
612
|
+
```python
|
|
613
|
+
from asterix.generated import *
|
|
614
|
+
|
|
615
|
+
Spec = Cat_008_1_3
|
|
616
|
+
rec = Spec.cv_record.create({'010': (('SA',1), ('SIC',2))})
|
|
617
|
+
print(rec.get_item('010').variation.get_item('SA').as_uint())
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
The program contains the following bugs:
|
|
621
|
+
- Misspelled item name, `SA` instead of `SAC`, on lines 4 and 5
|
|
622
|
+
- `get_item('010') result is not checked if the item
|
|
623
|
+
is actually present, which might result in runtime error
|
|
624
|
+
|
|
625
|
+
```
|
|
626
|
+
$ python test.py
|
|
627
|
+
... results in runtime error (wrong item name)
|
|
628
|
+
$ pip install mypy
|
|
629
|
+
$ mypy test.py
|
|
630
|
+
... detects all problems, without actually running the program
|
|
631
|
+
Found 3 errors in 1 file (checked 1 source file)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Correct version of this program is:
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
from asterix.generated import *
|
|
638
|
+
|
|
639
|
+
Spec = Cat_008_1_3
|
|
640
|
+
rec = Spec.cv_record.create({'010': (('SAC',1), ('SIC',2))})
|
|
641
|
+
i010 = rec.get_item('010')
|
|
642
|
+
if i010 is not None:
|
|
643
|
+
print(i010.variation.get_item('SAC').as_uint())
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
$ mypy test.py
|
|
648
|
+
Success: no issues found in 1 source file
|
|
649
|
+
$ python test.py
|
|
650
|
+
1
|
|
651
|
+
```
|