Compare commits

...

177 Commits

Author SHA1 Message Date
Junzi Sun
b4be55116c git workflow 2024-08-25 13:38:06 +02:00
Junzi Sun
fdfa2b3fab fix typing 2024-08-25 12:45:24 +02:00
Junzi Sun
2f5dbdf08d update version and dependencies 2024-08-25 12:37:50 +02:00
Junzi Sun
b22734ad51 fix typing 2024-08-25 12:37:37 +02:00
Junzi Sun
e2de7bc9ac fix typing 2024-08-25 12:33:28 +02:00
gituser283
74ea61fb11
fix decoder and uncertainty issues (#146)
* Created branch mods20230421a

* nic_v2 NIC return is set to None when a KeyError exception is raised

* process_raw receives additional return values from nuc_p, nic_v2, nuc_v, nac_v, nac_p .  hum44, p44, temp44, turb44, wind44, tk50, gs50, roc60baro, roc60ins, tc, and icao added to acs dictionary.

---------

Co-authored-by: Mike M <>
2024-08-25 12:27:12 +02:00
Junzi Sun
08218f558d
Merge pull request #151 from junzis/quantities
Some annotations for physical quantities
2024-08-25 12:23:36 +02:00
Junzi Sun
8acbdf186c
Merge pull request #170 from chrislanzara/master
Fixed "Megnatic" typo
2024-08-25 12:23:03 +02:00
dependabot[bot]
d8dd978730
Bump snok/install-poetry from 1.3.4 to 1.4.0 (#171)
Bumps [snok/install-poetry](https://github.com/snok/install-poetry) from 1.3.4 to 1.4.0.
- [Release notes](https://github.com/snok/install-poetry/releases)
- [Commits](https://github.com/snok/install-poetry/compare/v1.3.4...v1.4.0)

---
updated-dependencies:
- dependency-name: snok/install-poetry
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-25 12:22:23 +02:00
kirth123
01ceb8114a
deleted all the previous commits that were causing problems and added a new commit with better comments (#172) 2024-08-25 12:20:51 +02:00
Chris
4a7c719978
Fixed "Magnetic" typo 2024-07-03 22:49:24 +01:00
Xavier Olive
93794b64ca fix build for py312 2024-03-03 17:22:16 +01:00
Xavier Olive
9646c2fbea bump version 2024-02-02 09:51:44 +01:00
dependabot[bot]
97a50a5b13
Bump actions/cache from 3 to 4 (#166)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 20:04:17 +01:00
dependabot[bot]
81b54accb5
Bump codecov/codecov-action from 3 to 4 (#165)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 20:03:43 +01:00
dependabot[bot]
942c68dc78
Bump actions/setup-python from 4 to 5 (#163) 2024-01-01 16:29:45 +01:00
Xavier Olive
36fb0de92c upgrade poetry version 2023-12-19 09:59:35 +01:00
Flyer350
582a0d2188
DI = 1 correction for uplink messages (#157)
* Update uplink.py

Adding lockout for DI = 1

* Update uplink.py
2023-12-18 23:03:34 +01:00
Junzi Sun
45ae55d965
Merge pull request #161 from Flyer350/patch-11
Update allcall.py
2023-12-18 23:02:16 +01:00
Junzi Sun
4cf024f144
Merge pull request #162 from paulmadejong/altitude_none_return
Properly return None if altitude could not be decoded
2023-12-18 23:00:38 +01:00
Paul de Jong
eea09227b0 c_common.py: return -999999 (extremely low, physically impossible) altitude in case altitude is unknown or invalid, bds04.py: convert -999999 altitude to None to better signal an invalid/unknown decoded altitude to the user 2023-12-18 16:40:44 +01:00
Flyer350
852315ed2d
Update allcall.py
Modified a mistake in the interpretation of CA field 7.
See ICAO Annex 10, Vol IV., 3.1.2.5.2.2.1
2023-12-04 14:33:36 +01:00
dependabot[bot]
77273153cb
Bump actions/checkout from 3 to 4 (#158)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 18:36:54 +02:00
dependabot[bot]
b4c81db151
Bump snok/install-poetry from 1.3.3 to 1.3.4 (#156)
Bumps [snok/install-poetry](https://github.com/snok/install-poetry) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/snok/install-poetry/releases)
- [Commits](https://github.com/snok/install-poetry/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: snok/install-poetry
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-02 13:45:09 +09:00
Xavier Olive
b6608b2f78 loosen dependency requirements 2023-08-22 22:38:50 +02:00
Xavier Olive
070dc80bf4 include pyd files to wheel 2023-08-13 10:57:20 +02:00
Xavier Olive
8d9a8df9d9 minor fix 2023-07-18 13:47:06 +00:00
Xavier Olive
458a78c30b add the decoding of the gps status indicator 2023-07-18 13:42:16 +00:00
Junzi Sun
57aa7a7d9c update test 2023-07-18 13:41:54 +00:00
Xavier Olive
b1f8f6ed62
Some annotations for quantities 2023-06-14 22:50:33 +02:00
Junzi Sun
8e2051af68 update test 2023-05-22 17:05:43 +02:00
Paul de Jong
50864b56aa
Remove rounding in pyModeS (#147)
* Update bds50.py: round roll to 3 places instead of 1

Provide a bit more detail for smaller roll angles.

* remove rounding

* update poetry

* update poetry

* update test

* update bds06

* update workflow

---------

Co-authored-by: Junzi Sun <junzisun@gmail.com>
2023-05-22 17:00:58 +02:00
dependabot[bot]
8403cb61e4
Bump actions/checkout from 2 to 3 (#145)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-02 14:45:28 +01:00
dependabot[bot]
a5b8d670d3
Bump actions/setup-python from 2 to 4 (#144)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-02 14:45:05 +01:00
Xavier Olive
4e19ecdfa9 minor changes, new release 2023-02-14 18:39:52 +01:00
Nicholas (Nick) Kruzan
0d7a310007
Update py_common.py (#143) 2023-02-12 13:20:42 +01:00
Xavier Olive
66c29840b0 fix build issues 2023-01-19 15:18:16 +01:00
Xavier Olive
99dc2a4f40 limit wheels to windows, otherwise build from src 2022-12-31 00:42:50 +01:00
Xavier Olive
39936e4472 fix publish 2022-12-30 19:53:33 +01:00
Xavier Olive
144ee1710d fix publish 2022-12-30 19:46:15 +01:00
Xavier Olive
09ed997f91 fix publish 2022-12-30 19:40:25 +01:00
Xavier Olive
e437931254 fix publish 2022-12-30 19:36:16 +01:00
Xavier Olive
72fcc794e7 clean dependencies 2022-12-30 19:22:10 +01:00
Xavier Olive
ff12d819da fix publish source 2022-12-30 14:17:36 +01:00
Xavier Olive
dc9d7ea91a fix test runner.os 2022-12-30 14:10:22 +01:00
Xavier Olive
5bc11cd78c fix test runner.os 2022-12-30 14:08:44 +01:00
Xavier Olive
9e6f9c3ffe upload source 2022-12-30 14:05:38 +01:00
dependabot[bot]
e1017bc2e5
Bump codecov/codecov-action from 2 to 3 (#136)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2 to 3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-30 14:02:05 +01:00
Xavier Olive
ebe1f76cd9 dependabot.yml 2022-12-30 13:57:57 +01:00
Xavier Olive
8830ef3dfe fix poetry PATH for windows 2022-12-30 13:50:43 +01:00
Xavier Olive
d999851da5 fix typo and && 2022-12-30 13:42:52 +01:00
Xavier Olive
6d0406c6b9 update version 2022-12-30 13:28:36 +01:00
Xavier Olive
6140f84058 update yml 2022-12-30 00:59:30 +01:00
Xavier Olive
a8f3b9c811 attempt to fix poetry windows cache issue 2022-12-28 00:11:52 +01:00
Xavier Olive
8f098671a0
Update .gitignore 2022-12-28 00:05:07 +01:00
Xavier Olive
4cf99e8927
switch to poetry (#135) 2022-12-27 23:58:28 +01:00
Xavier Olive
5a703a0d3c
Merge pull request #134 from junzis/pr_flarm
FLARM decoder
2022-12-04 23:41:18 +01:00
Xavier Olive
90f35a8f29 FLARM decoder 2022-12-04 00:08:25 +01:00
Xavier Olive
87e6f50486 FLARM decoder 2022-12-04 00:04:55 +01:00
Xavier Olive
0667c1b3c3 include Python 3.11 2022-11-01 20:45:47 +09:00
Xavier Olive
9d82f9b9c9 compatibility with Python 3.7 2022-11-01 20:38:29 +09:00
Xavier Olive
51222372b1 fully annotated code 2022-11-01 16:32:42 +09:00
Xavier Olive
d3607a16c1
Merge pull request #132 from JWyman482/bds62_baro_rounding_fix
Bds62 baro rounding fix
2022-08-15 23:39:15 +02:00
John Wyman
6a427c08b5 Update display to remove units if baro == none 2022-08-14 00:45:13 -07:00
John Wyman
afd53af87b Increase padding on _print to fit baro 2022-08-14 00:34:17 -07:00
John Wyman
6b4a5bdaf3 move round() call to baro == None else clause 2022-08-14 00:27:09 -07:00
Xavier Olive
3e0d7ceec5
Merge pull request #131 from dforsi/fix/imports 2022-08-11 19:01:54 +02:00
Daniele Forsi
130491e641 Add missing import
Found with pylint.

Fixes:
pyModeS/streamer/decode.py:290:21: E0602: Undefined variable 'traceback' (undefined-variable)
2022-08-08 19:56:34 +02:00
Xavier Olive
1f620d6534
Merge pull request #129 from dforsi/fix/description 2022-08-08 13:57:22 +02:00
Daniele Forsi
b4d4ef8f1f Fix description 2022-08-08 10:21:57 +02:00
Junzi Sun
bb8f83e832 fix RCv for position with GNSS height 2022-07-09 12:11:28 +02:00
Junzi Sun
b90932e51d add missing parameters for dumpto file 2022-07-09 10:09:41 +02:00
Xavier Olive
b36540e2a1
fix corner case 2022-05-31 09:55:07 +02:00
Junzi Sun
c02414417d new release 2.11 2022-04-18 20:50:24 +02:00
Xavier Olive
13f4fe77bb tests with coverage 2022-04-18 20:40:49 +02:00
Xavier Olive
401f3f5e95 ignore for windows+github actions 2022-04-18 20:28:56 +02:00
Xavier Olive
73c3a75234 more os 2022-04-18 18:50:38 +02:00
Xavier Olive
06941bac2e fix tests 2022-04-18 18:47:12 +02:00
Xavier Olive
6e33ac0288 with mypy 2022-04-18 18:32:33 +02:00
Xavier Olive
65ce1a62c4 test ci and fix #125 2022-04-18 17:59:30 +02:00
Junzi Sun
5dc74a5548 new release 2022-04-18 11:59:14 +02:00
Junzi Sun
e7edba9e25
Merge pull request #121 from Flyer350/patch-9
Update bds20.py
2022-04-18 11:28:15 +02:00
Junzi Sun
ce5adbafe5
Update bds20.py 2022-04-18 11:27:53 +02:00
Flyer350
b52431ca51
Update bds20.py
#message with empty callsign can still be BDS2,0

Example messages:
a900189f20000000000000d0d739
a100000020000000000000a81785
2022-01-24 10:38:17 +01:00
Junzi Sun
00f04a8886
Merge pull request #119 from Flyer350/patch-8
fix bug in squawk decoding for BDS61
2021-11-22 10:23:32 +01:00
Flyer350
0eb2d1f6a2
Update bds61.py
Fix the Mode A decoder. The ZERO is already part of  the binary data inside the message and does not need to be added.

See ED-102A, 2.2.3.2.7.8.1.2 b. Starting with ―ME‖ bit 12, the code sequence shall be C1, A1, C2, A2, C4, A4, ZERO, B1, D1, B2, D2, B4, D4.
2021-11-18 11:24:42 +01:00
Junzi Sun
26ef5d3ad9
Merge pull request #116 from dforsi/fix/typos
Fix typos in doc strings
2021-11-02 22:10:54 +01:00
Daniele Forsi
b90c2e740d Fix typos in print() strings 2021-10-09 23:17:31 +02:00
Daniele Forsi
e39baf38ed Fix typos 2021-10-09 22:50:28 +02:00
Junzi Sun
b9471d7fcb move functions to py_common 2021-10-09 00:02:33 +02:00
Junzi Sun
a350050e6e
Merge pull request #112 from maranov/bugfix/airborne_velocity_trk
Fix UnboundLocalError in airborne_velocity when velocity is zero
2021-07-07 22:47:51 +02:00
Junzi Sun
01574a9f01
Merge pull request #111 from maranov/bugfix/tcpclient_disconnect
Fix invalid socket disconnect call in TcpClient
2021-07-07 22:46:21 +02:00
Junzi Sun
cdb7aef82e
Merge pull request #108 from dforsi/fix/typos
Fix typos
2021-07-07 22:45:22 +02:00
Novak, Marek
a0a8c9b2f7 Fix UnboundLocalError in airborne_velocity when velocity is zero 2021-06-28 17:21:33 +02:00
maranov
0b2648bfe0 Fix invalid socket disconnect call in TcpClient
The testing main function now also stops the Client explicitly
2021-06-21 17:26:58 +02:00
Daniele Forsi
69ce39ab39 Fix typos 2021-05-09 14:28:46 +02:00
Junzi Sun
83e22892ba
Merge pull request #98 from TimA346/implement_tc29
Decoder for ADS-B TC=29 (target state and status message)
2021-03-12 18:07:19 +01:00
Junzi Sun
7434fc9ed3
Merge pull request #103 from amhirsch/spelling-magnetic
Fixed spelling of "magnetic_north" in docstring
2021-03-12 13:37:27 +01:00
Junzi Sun
7260bff7e9
Merge pull request #101 from Flyer350/patch-7
Update uplink.py
2021-03-12 13:34:19 +01:00
Xander Hirsch
dd1fd596f8 Fixed spelling of "magnetic_north" in docstring 2021-03-07 23:27:08 -08:00
Flyer350
b9089d55d2
Update uplink.py 2021-02-24 15:28:59 +01:00
TimA346
6ab147bffe Added test for target state and status message subtype 1 2021-01-23 11:51:31 +01:00
TimA346
a308b9a7e0 implemented tc29
BDS6,2
2021-01-23 03:05:48 +01:00
TimA346
96f49a00e4
Merge pull request #1 from junzis/master
Update
2021-01-23 02:52:40 +01:00
Junzi Sun
b8f8f4dbc0 add github action for publishing on pypi 2021-01-23 01:35:04 +01:00
Junzi Sun
a4ce3bfaf1 number format 2021-01-23 01:22:19 +01:00
Junzi Sun
3bb8c361e9 bug fix: altiude and TAS 2021-01-23 01:21:54 +01:00
Junzi Sun
89e67fae31
Merge pull request #92 from junzis/pr_readme
suggestions for installation
2021-01-11 15:32:05 +01:00
Xavier Olive
50cef7d424 minor packaging additions 2021-01-11 14:45:32 +01:00
Xavier Olive
20801883d5 suggestions for installation 2021-01-06 16:46:20 +01:00
Junzi Sun
56d70b03d2
Update README.rst 2021-01-06 12:40:04 +01:00
Junzi Sun
47de4d1163
Merge pull request #87 from Flyer350/patch-5
Fix comments in uplink.py
2020-12-02 11:38:14 +01:00
Xavier Olive
2978329d03 add the pip install pyModeS[fast] option for the Cython dependency 2020-12-01 14:09:39 +01:00
Flyer350
4471c836d9
Update uplink.py 2020-11-27 10:28:20 +01:00
Flyer350
d16484bf72
Update interrogator() in allcall.py (#86) 2020-11-26 10:35:16 +01:00
Junzi Sun
436e9ce2c8
Merge pull request #82 from Flyer350/patch-3
Adding more uplink decoding funtions.
2020-11-26 10:34:03 +01:00
Junzi Sun
4a6341bcc0 import allcall and surv module 2020-11-14 23:04:27 +01:00
Junzi Sun
90e60a74b5
Merge pull request #84 from boringow/master
Implement all-call and short roll-call replies
2020-11-14 22:10:37 +01:00
Junzi Sun
f65ffd13f7 add tests 2020-11-14 22:05:48 +01:00
Junzi Sun
a3e5e7e141
Update surv.py 2020-11-14 21:57:34 +01:00
Junzi Sun
668d6580d0
Update allcall.py 2020-11-14 21:57:08 +01:00
boringow
eacc828b75
Update surv.py
I have completed the allcall.py and the surv.py, but I can not commit the two files at the same time. Hope you like how it looks, If necessary, tell me what could I change. Have a nice weekend!
2020-11-14 09:37:08 +01:00
boringow
4ba900f500
Update allcall.py 2020-11-12 14:49:52 +01:00
boringow
35c0bfa513
Update allcall.py 2020-11-12 14:46:33 +01:00
Flyer350
beb50fc788
Update uplink.py 2020-11-11 00:12:47 +01:00
Flyer350
fc135eacef
Update uplink.py
additional decoded fields added
2020-11-11 00:08:30 +01:00
boringow
84c6255567
Update allcall.py
Tested, it is all functioning well, I created a new decoder since I couldn't understand yours. It may not be the most optimum, I am quite new into coding so it may no be optimal, but for sure it is working :) Hope you like it!
2020-11-10 20:44:32 +01:00
Junzi Sun
224a0b8a67 remove print statement 2020-11-04 17:21:07 +01:00
Junzi Sun
c3976f1ca3 update cap17() 2020-10-16 22:32:03 +02:00
Junzi Sun
dd3a869c08
fix typos 2020-09-24 22:16:08 +02:00
Junzi Sun
7e966090ab
update meteorological reports decoding example 2020-09-24 22:13:13 +02:00
Junzi Sun
7a4c465f7d remove mandatory pyrtlsdr dependency 2020-08-17 01:19:16 +02:00
Junzi Sun
0ced97e3eb fix exception buffer bug 2020-08-17 00:53:49 +02:00
Junzi Sun
2bdd638cef bug fix in is60() 2020-08-14 23:50:54 +02:00
Junzi Sun
93fc536926 improve BDS 50 and 60 inference 2020-08-05 18:38:12 +02:00
Junzi Sun
1f15a953e3 minor fix in surface speed calculation 2020-07-29 00:53:05 +02:00
Junzi Sun
0d7f628f3c fix modeslive --help bug 2020-07-13 18:52:42 +02:00
Junzi Sun
7348a10f1b minor bug fix 2020-06-10 00:07:26 +02:00
Junzi Sun
c9d2c4c6bc Merge branch 'tuftedocelot-bds-61' 2020-05-23 21:09:58 +02:00
Junzi Sun
0c5539be3a update BDS61 decoder 2020-05-23 21:07:23 +02:00
Junzi Sun
1cf4d37e56 Merge branch 'master' into tuftedocelot-bds-61 2020-05-23 20:30:30 +02:00
Junzi Sun
2fc376abae update squawk decoding 2020-05-23 20:29:18 +02:00
Junzi Sun
9c660578ad Merge branch 'bds-61' of git://github.com/tuftedocelot/pyModeS into tuftedocelot-bds-61 2020-05-23 19:55:30 +02:00
Junzi Sun
ba84cd7c39 update test procedue in Makefile 2020-05-23 19:49:07 +02:00
Junzi Sun
2085a2c432 merge annotations 2020-05-23 19:34:26 +02:00
Junzi Sun
4126dedd19 fix altitude decoding 2020-05-23 19:17:58 +02:00
Chris Lajoie
7892aac4de
Continue reading from socket if no data received (#70)
Without this change, `modeslive` crashes if no data is available on the socket, which can happen during normal operation of a receiver if no aircraft are in range.

Tested on Python 3.7
2020-05-20 11:56:27 +02:00
Xavier Olive
7dcff01910 first annotations 2020-05-07 11:13:57 +02:00
Junzi Sun
228306dd5f version 2.8 2020-05-04 21:35:29 +02:00
Junzi Sun
1c6b39322f rename common file 2020-05-04 20:34:25 +02:00
Junzi Sun
9dde1b63d6 fix missing bin2hex() 2020-05-04 20:30:33 +02:00
Junzi Sun
560b737739 update api link 2020-05-04 01:51:33 +02:00
Junzi Sun
d50f05d00d Merge branch 'master' of github.com:junzis/pyModeS 2020-05-04 01:09:40 +02:00
Junzi Sun
11a560dbb5 update API doc 2020-05-04 01:07:02 +02:00
Junzi Sun
b51facbdc6 update docstring 2020-05-04 00:56:36 +02:00
Junzi Sun
d77d1f7f6b update docstring 2020-05-04 00:11:29 +02:00
Junzi Sun
5286355bf6 move common package to root level 2020-05-03 23:27:17 +02:00
Junzi Sun
fe9e033a64 update comments 2020-05-03 20:13:22 +02:00
Junzi Sun
57307109e6 remove python 2 imports 2020-05-02 19:32:54 +02:00
tuftedocelot
dc8f194e6e Add support and tests for BDS 6,1 emergency messages
ACAS RA messages (subtype 2) are not yet implemented. These messages
are the same format as BDS3,0 starting at byte 9
2020-04-30 20:35:19 -05:00
Junzi Sun
6571fe6fc0
Update README.rst 2020-04-04 20:48:23 +02:00
Junzi Sun
aa64d4e7a9
update README 2020-04-04 20:44:46 +02:00
wrobell
db57d7419f
Improve doc for surface position decoding 2020-03-11 22:08:04 +01:00
Junzi Sun
ed18352c0c update NL() function in CPR decoding 2020-03-03 12:39:39 +01:00
Junzi Sun
a75d6fd050 install c module 2020-02-26 00:55:25 +01:00
Junzi Sun
0ff628bb8e update imports 2020-02-26 00:54:49 +01:00
Junzi Sun
ea7653ef79 update test 2020-02-26 00:25:11 +01:00
Junzi Sun
2046b1de07
speedup (#59)
* remove unused functions

* cythonize common

* add bds05

* separate cleanly cython and python, bds05, bds06, modulo issues

* bds08

* bds09

* optimisations in bds09

* "make" things easier

* clean up useless stuff

* add make options

* fix hidden altitude() call

* minor updates to C code

* update tests

* update benchmark

* consolidation

* update clean script

* reduce complexity and change default type to str

Co-authored-by: Xavier Olive <1360812+xoolive@users.noreply.github.com>
2020-02-26 00:16:48 +01:00
Junzi Sun
768b80df8e improve modeslive 2020-02-25 21:30:42 +01:00
Junzi Sun
e52d43f963 update rtlsdr reader 2020-02-14 17:53:56 +01:00
Junzi Sun
b60b31d4fb release 2.5 2020-02-14 10:47:34 +01:00
Flyer350
cb67c63326 Create uplink.py (#55)
First basic functionality for uplink messages - UF and ICAO address.
2019-12-31 11:16:55 +01:00
Xavier Olive
bddaf9aec6 compatibility with bytes (#57) 2019-12-31 11:15:25 +01:00
Junzi Sun
00fc1475ff
Merge pull request #53 from dforsi/fix-typos
Fix typos
2019-11-19 11:17:41 +01:00
Daniele Forsi
489c405de0 Fix typos
Fixed with the command:
  codespell --write-changes --ignore-words-list hve,vas
codespell is available at https://github.com/lucasdemarchi/codespell
2019-10-31 13:38:46 +01:00
Junzi Sun
ec2721cfdc
Merge pull request #52 from Flyer350/patch-1
Update README.rst
2019-10-30 20:42:19 +01:00
Flyer350
2f13524a7c
Update README.rst
fixed typecodes for position messages
2019-10-30 15:54:37 +01:00
112 changed files with 5517 additions and 1881 deletions

12
.coveragerc Normal file
View File

@ -0,0 +1,12 @@
[run]
branch = True
include = */pyModeS/*
omit = *tests*
[report]
exclude_lines =
coverage: ignore
raise NotImplementedError
if TYPE_CHECKING:
ignore_errors = True

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

16
.github/workflows/build-publish.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: build and publish
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and publish to pypi
uses: JRubics/poetry-publish@v2.0
with:
poetry_version: "==1.8.2"
pypi_token: ${{ secrets.PYPI_API_TOKEN_PYMODES }}

70
.github/workflows/run-tests.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: tests
on:
push:
pull_request_target:
workflow_dispatch:
env:
POETRY_VERSION: "1.6.1"
jobs:
deploy:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
env:
PYTHON_VERSION: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
# virtualenv cache should depends on OS, Python version and `poetry.lock` (and optionally workflow files).
- name: Cache Packages
uses: actions/cache@v4
if: ${{ !startsWith(runner.os, 'windows') }}
with:
path: |
~/.local
.venv
key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }}
- name: Add poetry to windows path
if: "startsWith(runner.os, 'windows')"
run: |
echo "C:\Users\runneradmin\.local\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install and configure Poetry
uses: snok/install-poetry@v1.4.0
with:
version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true
virtualenvs-in-project: true
- name: Display Python version
run: poetry run python -c "import sys; print(sys.version)"
- name: Install dependencies
run: |
poetry install
- name: Type checking
run: |
poetry run mypy pyModeS
- name: Run tests
run: |
poetry run pytest tests --cov --cov-report term-missing
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
env_vars: PYTHON_VERSION

26
Makefile Normal file
View File

@ -0,0 +1,26 @@
install:
pip install . --upgrade
uninstall:
pip uninstall pyModeS -y
ext:
python setup.py build_ext --inplace
test:
make clean
@echo ""
@echo "[Test with py_common]"
python -m pytest tests
@echo ""
@echo "[Test with c_common]"
python setup.py build_ext --inplace
python -m pytest tests
clean:
find pyModeS -type f -name '*.c' -delete
find pyModeS -type f -name '*.so' -delete
find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf
rm -rf *.egg-info
rm -rf .pytest_cache
rm -rf build/*

View File

@ -1,6 +1,42 @@
The Python ADS-B/Mode-S Decoder
===============================
PyModeS is a Python library designed to decode Mode-S (including ADS-B) messages. It can be imported to your python project or used as a standalone tool to view and save live traffic data.
This is a project created by Junzi Sun, who works at `TU Delft <https://www.tudelft.nl/en/>`_, `Aerospace Engineering Faculty <https://www.tudelft.nl/en/ae/>`_, `CNS/ATM research group <http://cs.lr.tudelft.nl/atm/>`_. It is supported by many `contributors <https://github.com/junzis/pyModeS/graphs/contributors>`_ from different institutions.
Introduction
------------
pyModeS supports the decoding of following types of messages:
- DF4 / DF20: Altitude code
- DF5 / DF21: Identity code (squawk code)
- DF17 / DF18: Automatic Dependent Surveillance-Broadcast (ADS-B)
- TC=1-4 / BDS 0,8: Aircraft identification and category
- TC=5-8 / BDS 0,6: Surface position
- TC=9-18 / BDS 0,5: Airborne position
- TC=19 / BDS 0,9: Airborne velocity
- TC=28 / BDS 6,1: Airborne status [to be implemented]
- TC=29 / BDS 6,2: Target state and status information [to be implemented]
- TC=31 / BDS 6,5: Aircraft operational status [to be implemented]
- DF20 / DF21: Mode-S Comm-B messages
- BDS 1,0: Data link capability report
- BDS 1,7: Common usage GICB capability report
- BDS 2,0: Aircraft identification
- BDS 3,0: ACAS active resolution advisory
- BDS 4,0: Selected vertical intention
- BDS 4,4: Meteorological routine air report (experimental)
- BDS 4,5: Meteorological hazard report (experimental)
- BDS 5,0: Track and turn report
- BDS 6,0: Heading and speed report
If you find this project useful for your research, please considering cite this tool as::
@article{sun2019pymodes,
@ -14,40 +50,6 @@ If you find this project useful for your research, please considering cite this
Introduction
---------------------
PyModeS is a Python library designed to decode Mode-S (including ADS-B) message. It can be imported to your python project or be used as a standalone tool to view and save live traffic data.
Messages with following Downlink Formats (DF) are supported:
**DF17 / DF18: Automatic Dependent Surveillance-Broadcast (ADS-B)**
- TC=1-4 / BDS 0,8: Aircraft identification and category
- TC=5-8 / BDS 0,6: Surface position
- TC=9-18 / BDS 0,5: Airborne position
- TC=19 / BDS 0,9: Airborne velocity
- TC=28 / BDS 6,1: Airborne status [to be implemented]
- TC=29 / BDS 6,2: Target state and status information [to be implemented]
- TC=31 / BDS 6,5: Aircraft operational status [to be implemented]
**DF20 / DF21: Mode-S Comm-B replies**
- BDS 1,0: Data link capability report
- BDS 1,7: Common usage GICB capability report
- BDS 2,0: Aircraft identification
- BDS 3,0: ACAS active resolution advisory
- BDS 4,0: Selected vertical intention
- BDS 4,4: Meteorological routine air report (experimental)
- BDS 4,5: Meteorological hazard report (experimental)
- BDS 5,0: Track and turn report
- BDS 6,0: Heading and speed report
**DF4 / DF20: Altitude code**
**DF5 / DF21: Identity code (squawk code)**
Resources
-----------
@ -55,26 +57,50 @@ Check out and contribute to this open-source project at:
https://github.com/junzis/pyModeS
Detailed manual on Mode-S decoding is published at:
https://mode-s.org/decode.
https://mode-s.org/decode
The API documentation of pyModeS is at:
http://pymodes.readthedocs.io
https://mode-s.org/api
Install
-------
Basic installation
-------------------
Installation examples::
# stable version
pip install pyModeS
# conda (compiled) version
conda install -c conda-forge pymodes
# development version
pip install git+https://github.com/junzis/pyModeS
Dependencies ``numpy``, ``pyzmq`` and ``pyrtlsdr`` are installed automatically during previous installations processes.
Dependencies ``numpy``, and ``pyzmq`` are installed automatically during previous installations processes.
If you need to connect pyModeS to a RTL-SDR receiver, ``pyrtlsdr`` need to be installed manually::
pip install pyrtlsdr
Advanced installation (using c modules)
------------------------------------------
If you want to make use of the (faster) c module, install ``pyModeS`` as follows::
# conda (compiled) version
conda install -c conda-forge pymodes
# stable version
pip install pyModeS
# development version
git clone https://github.com/junzis/pyModeS
cd pyModeS
poetry install -E rtlsdr
View live traffic (modeslive)
@ -100,7 +126,7 @@ General usage::
Live with RTL-SDR
*******************
If you have an RTL-SDR receiver plugged to the computer, you can connect it with ``rtlsdr`` source switch, shown as follows::
If you have an RTL-SDR receiver connected to your computer, you can use the ``rtlsdr`` source switch (require ``pyrtlsdr`` package), with command::
$ modeslive --source rtlsdr
@ -141,7 +167,7 @@ Common functions
pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer
pms.gray2int(str) # Convert grey code to interger
pms.gray2int(str) # Convert grey code to integer
Core functions for ADS-B decoding
@ -155,7 +181,7 @@ Core functions for ADS-B decoding
# Typecode 1-4
pms.adsb.callsign(msg)
# Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 9-18 (airborne, GNSS height)
# Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 20-22 (airborne, GNSS height)
pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None)
pms.adsb.airborne_position(msg_even, msg_odd, t_even, t_odd)
pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref)
@ -249,8 +275,20 @@ Mode-S Enhanced Surveillance (EHS)
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
Meteorological routine air report (MRAR) [Experimental]
********************************************************
Meteorological reports [Experimental]
**************************************
To identify BDS 4,4 and 4,5 codes, you must set ``mrar`` argument to ``True`` in the ``infer()`` function:
.. code:: python
pms.bds.infer(msg. mrar=True)
Once the correct MRAR and MHR messages are identified, decode them as follows:
Meteorological routine air report (MRAR)
+++++++++++++++++++++++++++++++++++++++++
.. code:: python
@ -261,8 +299,8 @@ Meteorological routine air report (MRAR) [Experimental]
pms.commb.hum44(msg) # Humidity (%)
Meteorological hazard air report (MHR) [Experimental]
*******************************************************
Meteorological hazard air report (MHR)
+++++++++++++++++++++++++++++++++++++++++
.. code:: python
@ -321,8 +359,19 @@ Here is an example:
Unit test
---------
To perform unit tests. First, install ``tox`` through pip. Then, run the following commands:
To perform unit tests, ``pytest`` must be install first.
.. code:: bash
Build Cython extensions
::
$ tox
$ make ext
Run unit tests
::
$ make test
Clean build files
::
$ make clean

67
build.py Normal file
View File

@ -0,0 +1,67 @@
import os
import shutil
import sys
# import pip
from Cython.Build import cythonize
from setuptools import Distribution, Extension
from setuptools.command import build_ext
def build() -> None:
compile_args = []
if sys.platform == "linux":
compile_args += ["-Wno-pointer-sign", "-Wno-unused-variable"]
extensions = [
Extension(
"pyModeS.c_common",
["pyModeS/c_common.pyx"],
extra_compile_args=compile_args,
),
Extension(
"pyModeS.decoder.flarm.decode",
[
"pyModeS/decoder/flarm/decode.pyx",
"pyModeS/decoder/flarm/core.c",
],
extra_compile_args=compile_args,
include_dirs=["pyModeS/decoder/flarm"],
),
# Extension(
# "pyModeS.extra.demod2400.core",
# [
# "pyModeS/extra/demod2400/core.pyx",
# "pyModeS/extra/demod2400/demod2400.c",
# ],
# extra_compile_args=compile_args,
# include_dirs=["pyModeS/extra/demod2400"],
# libraries=["m"],
# ),
]
ext_modules = cythonize(
extensions,
compiler_directives={"binding": True, "language_level": 3},
)
distribution = Distribution({"name": "extended", "ext_modules": ext_modules})
distribution.package_dir = "extended" # type: ignore
cmd = build_ext.build_ext(distribution)
cmd.verbose = True # type: ignore
cmd.ensure_finalized() # type: ignore
cmd.run()
# Copy built extensions back to the project
for output in cmd.get_output_mapping():
relative_extension = os.path.relpath(output, cmd.build_lib)
shutil.copyfile(output, relative_extension)
mode = os.stat(relative_extension).st_mode
mode |= (mode & 0o444) >> 2
os.chmod(relative_extension, mode)
if __name__ == "__main__":
build()

View File

@ -17,5 +17,5 @@ help:
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
rm -f source/pyModeS*.rst source/modules.rst
sphinx-apidoc -f -e -M -o source/ ../pyModeS
sphinx-apidoc -f -e -M -o source/ ../pyModeS ../pyModeS/decoder/ehs.py ../pyModeS/decoder/els.py ../pyModeS/streamer ../pyModeS/extra
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -0,0 +1,17 @@
{% extends "!layout.html" %}
{% block footer %}
{{ super() }}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-74700456-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-74700456-1');
</script>
{% endblock %}

View File

@ -14,19 +14,20 @@
#
import os
import sys
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information -----------------------------------------------------
project = 'pyModeS'
copyright = '2019, Junzi Sun'
author = 'Junzi Sun'
project = "pyModeS"
copyright = "2019, Junzi Sun"
author = "Junzi Sun"
# The short X.Y version
version = ''
version = ""
# The full version, including alpha/beta/rc tags
release = ''
release = ""
# -- General configuration ---------------------------------------------------
@ -39,26 +40,24 @@ release = ''
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.mathjax',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'sphinx.ext.napoleon',
"sphinx.ext.autodoc",
"sphinx.ext.mathjax",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"sphinx.ext.napoleon",
]
# Add any paths that contain templates here, relative to this directory.
# templates_path = ['']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -82,6 +81,10 @@ pygments_style = None
# a list of builtin themes.
#
# html_theme = 'alabaster'
html_theme = "neo_rtd_theme"
import sphinx_theme
html_theme_path = [sphinx_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@ -108,7 +111,7 @@ pygments_style = None
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyModeSdoc'
htmlhelp_basename = "pyModeSdoc"
# -- Options for LaTeX output ------------------------------------------------
@ -117,15 +120,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -135,8 +135,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pyModeS.tex', 'pyModeS Documentation',
'Junzi Sun', 'manual'),
(master_doc, "pyModeS.tex", "pyModeS Documentation", "Junzi Sun", "manual")
]
@ -144,10 +143,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pymodes', 'pyModeS Documentation',
[author], 1)
]
man_pages = [(master_doc, "pymodes", "pyModeS Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
@ -156,9 +152,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pyModeS', 'pyModeS Documentation',
author, 'pyModeS', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
"pyModeS",
"pyModeS Documentation",
author,
"pyModeS",
"One line description of project.",
"Miscellaneous",
)
]
@ -177,7 +179,7 @@ epub_title = project
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------

View File

@ -9,11 +9,49 @@ Welcome to pyModeS documentation!
The source code can be found at: https://github.com/junzis/pyModeS
.. toctree::
:maxdepth: 3
:caption: Core modules
:maxdepth: 2
pyModeS.decoder.adsb
pyModeS.decoder.commb
.. toctree::
:caption: ADS-B messages
:maxdepth: 2
pyModeS.decoder.bds.bds05
pyModeS.decoder.bds.bds06
pyModeS.decoder.bds.bds08
pyModeS.decoder.bds.bds09
.. toctree::
:caption: ELS - elementary surveillance
:maxdepth: 2
pyModeS.decoder.bds.bds10
pyModeS.decoder.bds.bds17
pyModeS.decoder.bds.bds20
pyModeS.decoder.bds.bds30
.. toctree::
:caption: EHS - enhanced surveillance
:maxdepth: 2
pyModeS.decoder.bds.bds40
pyModeS.decoder.bds.bds50
pyModeS.decoder.bds.bds60
.. toctree::
:caption: MRAR / MHR
:maxdepth: 2
pyModeS.decoder.bds.bds44
pyModeS.decoder.bds.bds45
pyModeS.decoder
pyModeS.streamer
pyModeS.extra
----

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@ -0,0 +1,7 @@
pyModeS.c\_common module
========================
.. automodule:: pyModeS.c_common
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,7 @@
pyModeS.common module
=====================
.. automodule:: pyModeS.common
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.acas module
===========================
.. automodule:: pyModeS.decoder.acas
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.adsb module
===========================
.. automodule:: pyModeS.decoder.adsb
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.allcall module
==============================
.. automodule:: pyModeS.decoder.allcall
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds05 module
================================
.. automodule:: pyModeS.decoder.bds.bds05
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds06 module
================================
.. automodule:: pyModeS.decoder.bds.bds06
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds08 module
================================
.. automodule:: pyModeS.decoder.bds.bds08
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds09 module
================================
.. automodule:: pyModeS.decoder.bds.bds09
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds10 module
================================
.. automodule:: pyModeS.decoder.bds.bds10
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds17 module
================================
.. automodule:: pyModeS.decoder.bds.bds17
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds20 module
================================
.. automodule:: pyModeS.decoder.bds.bds20
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds30 module
================================
.. automodule:: pyModeS.decoder.bds.bds30
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds40 module
================================
.. automodule:: pyModeS.decoder.bds.bds40
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds44 module
================================
.. automodule:: pyModeS.decoder.bds.bds44
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds45 module
================================
.. automodule:: pyModeS.decoder.bds.bds45
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds50 module
================================
.. automodule:: pyModeS.decoder.bds.bds50
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds53 module
================================
.. automodule:: pyModeS.decoder.bds.bds53
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.bds.bds60 module
================================
.. automodule:: pyModeS.decoder.bds.bds60
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,14 +2,15 @@ pyModeS.decoder.bds package
===========================
.. automodule:: pyModeS.decoder.bds
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 4
pyModeS.decoder.bds.bds05
pyModeS.decoder.bds.bds06
@ -25,4 +26,3 @@ Submodules
pyModeS.decoder.bds.bds50
pyModeS.decoder.bds.bds53
pyModeS.decoder.bds.bds60

View File

@ -2,6 +2,6 @@ pyModeS.decoder.commb module
============================
.. automodule:: pyModeS.decoder.commb
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,7 +0,0 @@
pyModeS.decoder.common module
=============================
.. automodule:: pyModeS.decoder.common
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,7 +0,0 @@
pyModeS.decoder.ehs module
==========================
.. automodule:: pyModeS.decoder.ehs
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,7 +0,0 @@
pyModeS.decoder.els module
==========================
.. automodule:: pyModeS.decoder.els
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,29 +2,28 @@ pyModeS.decoder package
=======================
.. automodule:: pyModeS.decoder
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 4
pyModeS.decoder.bds
pyModeS.decoder.bds
Submodules
----------
.. toctree::
:maxdepth: 4
pyModeS.decoder.acas
pyModeS.decoder.adsb
pyModeS.decoder.allcall
pyModeS.decoder.commb
pyModeS.decoder.common
pyModeS.decoder.ehs
pyModeS.decoder.els
pyModeS.decoder.surv
pyModeS.decoder.uncertainty
pyModeS.decoder.uplink

View File

@ -2,6 +2,6 @@ pyModeS.decoder.surv module
===========================
.. automodule:: pyModeS.decoder.surv
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,6 +2,6 @@ pyModeS.decoder.uncertainty module
==================================
.. automodule:: pyModeS.decoder.uncertainty
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,7 @@
pyModeS.decoder.uplink module
=============================
.. automodule:: pyModeS.decoder.uplink
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,7 +0,0 @@
pyModeS.extra.aero module
=========================
.. automodule:: pyModeS.extra.aero
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,16 +0,0 @@
pyModeS.extra package
=====================
.. automodule:: pyModeS.extra
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
pyModeS.extra.aero
pyModeS.extra.tcpclient

View File

@ -1,7 +0,0 @@
pyModeS.extra.tcpclient module
==============================
.. automodule:: pyModeS.extra.tcpclient
:members:
:undoc-members:
:show-inheritance:

View File

@ -2,16 +2,23 @@ pyModeS package
===============
.. automodule:: pyModeS
:members:
:undoc-members:
:show-inheritance:
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 4
pyModeS.decoder
pyModeS.extra
pyModeS.streamer
pyModeS.decoder
Submodules
----------
.. toctree::
:maxdepth: 4
pyModeS.c_common
pyModeS.common

View File

@ -1,16 +0,0 @@
pyModeS.streamer package
========================
.. automodule:: pyModeS.streamer
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
pyModeS.streamer.screen
pyModeS.streamer.stream

View File

@ -1,7 +0,0 @@
pyModeS.streamer.screen module
==============================
.. automodule:: pyModeS.streamer.screen
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,7 +0,0 @@
pyModeS.streamer.stream module
==============================
.. automodule:: pyModeS.streamer.stream
:members:
:undoc-members:
:show-inheritance:

0
doc/warnings Normal file
View File

881
poetry.lock generated Normal file
View File

@ -0,0 +1,881 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "black"
version = "24.8.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
{file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"},
{file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
{file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
{file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
{file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"},
{file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
{file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
{file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
{file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"},
{file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
{file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
{file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
{file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"},
{file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
{file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
{file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
{file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"},
{file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
{file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
{file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
name = "cffi"
version = "1.17.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"},
{file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"},
{file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"},
{file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"},
{file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
{file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
{file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
{file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
{file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
{file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
{file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
{file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
{file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
{file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
{file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
{file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
{file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"},
{file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"},
{file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"},
{file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"},
{file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"},
{file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"},
{file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"},
{file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
]
[package.dependencies]
pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "codecov"
version = "2.1.13"
description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"},
{file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"},
]
[package.dependencies]
coverage = "*"
requests = ">=2.7.9"
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.6.1"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
{file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
{file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
{file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
{file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
{file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
{file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
{file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
{file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
{file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
{file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
{file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
{file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
{file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
{file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
{file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
{file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
{file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
{file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
{file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
{file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
{file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
{file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
{file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
{file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
{file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
{file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
{file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
{file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli"]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "flake8"
version = "7.1.1"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.8.1"
files = [
{file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"},
{file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.12.0,<2.13.0"
pyflakes = ">=3.2.0,<3.3.0"
[[package]]
name = "idna"
version = "3.8"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy"
version = "1.11.2"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
{file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
{file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
{file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
{file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
{file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
{file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
{file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
{file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
{file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
{file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
{file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
{file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
{file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
{file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
{file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
{file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
{file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
{file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
{file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
{file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
{file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
{file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
{file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
{file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
{file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
{file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "numpy"
version = "2.0.1"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"},
{file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"},
{file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"},
{file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"},
{file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"},
{file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"},
{file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"},
{file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"},
{file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"},
{file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"},
{file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"},
{file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"},
{file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"},
{file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"},
{file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"},
{file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"},
{file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"},
{file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"},
{file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"},
{file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"},
{file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"},
{file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"},
{file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"},
{file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"},
{file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"},
{file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"},
{file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"},
{file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"},
{file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"},
{file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"},
{file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"},
{file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"},
{file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"},
{file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"},
{file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"},
{file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"},
{file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"},
{file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"},
{file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"},
{file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"},
{file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"},
{file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"},
{file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"},
{file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"},
{file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pycodestyle"
version = "2.12.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"},
{file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"},
]
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pyflakes"
version = "3.2.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
]
[[package]]
name = "pyrtlsdr"
version = "0.3.0"
description = "A Python wrapper for librtlsdr (a driver for Realtek RTL2832U based SDR's)"
optional = true
python-versions = "*"
files = [
{file = "pyrtlsdr-0.3.0-py2.py3-none-any.whl", hash = "sha256:4967df42eb89e6bd70602337ae355c9b1231eb1d517fd8cc30f7b862fd1392f6"},
{file = "pyrtlsdr-0.3.0.tar.gz", hash = "sha256:fb3e583ba073b861e8e0bc5e62f66f07365e9147f5d36491de4ad62f23e45362"},
]
[package.extras]
lib = ["pyrtlsdrlib"]
[[package]]
name = "pytest"
version = "8.3.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "5.0.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
]
[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pyzmq"
version = "26.2.0"
description = "Python bindings for 0MQ"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"},
{file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"},
{file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"},
{file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"},
{file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"},
{file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"},
{file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"},
{file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"},
{file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"},
{file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"},
{file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"},
{file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"},
{file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"},
{file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"},
{file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"},
{file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"},
{file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"},
{file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"},
{file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"},
{file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"},
{file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"},
{file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"},
{file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"},
{file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"},
{file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"},
{file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"},
{file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"},
{file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"},
{file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"},
{file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"},
{file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"},
{file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"},
{file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"},
{file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"},
{file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"},
{file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"},
{file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"},
{file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"},
{file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"},
{file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"},
{file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"},
{file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"},
{file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"},
{file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"},
{file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"},
{file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"},
{file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"},
{file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"},
{file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"},
{file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"},
{file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"},
{file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"},
{file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"},
{file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"},
{file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"},
{file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"},
{file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"},
{file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"},
{file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"},
{file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"},
{file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"},
{file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"},
{file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"},
{file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"},
{file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"},
{file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"},
{file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"},
{file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"},
{file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"},
{file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"},
{file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"},
{file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"},
{file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"},
{file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"},
{file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"},
{file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"},
{file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"},
{file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"},
{file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"},
{file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"},
{file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"},
{file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"},
{file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"},
{file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"},
{file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"},
{file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"},
{file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"},
{file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"},
{file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"},
{file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"},
{file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"},
{file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"},
{file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"},
{file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"},
{file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"},
{file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"},
{file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"},
{file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"},
{file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"},
{file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"},
{file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"},
{file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"},
{file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"},
{file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"},
{file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"},
{file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"},
{file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"},
{file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"},
{file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"},
]
[package.dependencies]
cffi = {version = "*", markers = "implementation_name == \"pypy\""}
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "urllib3"
version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[extras]
rtlsdr = ["pyrtlsdr"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9"
content-hash = "f92c8f8bdcbe5a3351cbe1c026b19b913c9b4502e6ecc3a35b3cbef4d48fee7d"

3
pyModeS/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
decoder/flarm/decode.c
extra/demod2400/core.c
c_common.c

View File

@ -1,17 +1,35 @@
from __future__ import absolute_import, print_function, division
import os
import warnings
from .decoder.common import *
try:
from . import c_common as common
from .c_common import *
except Exception:
from . import py_common as common # type: ignore
from .py_common import * # type: ignore
from .decoder import tell
from .decoder import adsb
from .decoder import commb
from .decoder import common
from .decoder import allcall
from .decoder import surv
from .decoder import bds
from .extra import aero
from .extra import tcpclient
__all__ = [
"common",
"tell",
"adsb",
"commb",
"allcall",
"surv",
"bds",
"aero",
"tcpclient",
]
warnings.simplefilter("once", DeprecationWarning)
dirpath = os.path.dirname(os.path.realpath(__file__))

28
pyModeS/c_common.pxd Normal file
View File

@ -0,0 +1,28 @@
# cython: language_level=3
cdef int char_to_int(unsigned char binstr)
cdef unsigned char int_to_char(unsigned char i)
cpdef str hex2bin(str hexstr)
cpdef long bin2int(str binstr)
cpdef long hex2int(str hexstr)
cpdef str bin2hex(str binstr)
cpdef unsigned char df(str msg)
cpdef long crc(str msg, bint encode=*)
cpdef long floor(double x)
cpdef str icao(str msg)
cpdef bint is_icao_assigned(str icao)
cpdef int typecode(str msg)
cpdef int cprNL(double lat)
cpdef str idcode(str msg)
cpdef str squawk(str binstr)
cpdef int altcode(str msg)
cpdef int altitude(str binstr)
cpdef str data(str msg)
cpdef bint allzeros(str msg)

18
pyModeS/c_common.pyi Normal file
View File

@ -0,0 +1,18 @@
def hex2bin(hexstr: str) -> str: ...
def bin2int(binstr: str) -> int: ...
def hex2int(hexstr: str) -> int: ...
def bin2hex(binstr: str) -> str: ...
def df(msg: str) -> int: ...
def crc(msg: str, encode: bool = False) -> int: ...
def floor(x: float) -> float: ...
def icao(msg: str) -> str: ...
def is_icao_assigned(icao: str) -> bool: ...
def typecode(msg: str) -> int: ...
def cprNL(lat: float) -> int: ...
def idcode(msg: str) -> str: ...
def squawk(binstr: str) -> str: ...
def altcode(msg: str) -> int: ...
def altitude(binstr: str) -> int: ...
def data(msg: str) -> str: ...
def allzeros(msg: str) -> bool: ...
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ...

405
pyModeS/c_common.pyx Normal file
View File

@ -0,0 +1,405 @@
# cython: language_level=3
cimport cython
from cpython cimport array
from cpython.bytes cimport PyBytes_GET_SIZE
from cpython.bytearray cimport PyByteArray_GET_SIZE
from libc.math cimport abs, cos, acos, fabs, M_PI as pi, floor as c_floor
cdef int char_to_int(unsigned char binstr):
if 48 <= binstr <= 57: # 0 to 9
return binstr - 48
if 97 <= binstr <= 102: # a to f
return binstr - 97 + 10
if 65 <= binstr <= 70: # A to F
return binstr - 65 + 10
return 0
cdef unsigned char int_to_char(unsigned char i):
if i < 10:
return 48 + i # "0" + i
return 97 - 10 + i # "a" - 10 + i
@cython.boundscheck(False)
@cython.overflowcheck(False)
cpdef str hex2bin(str hexstr):
"""Convert a hexadecimal string to binary string, with zero fillings."""
# num_of_bits = len(hexstr) * 4
cdef hexbytes = bytes(hexstr.encode())
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
# binstr = bin(int(hexbytes, 16))[2:].zfill(int(num_of_bits))
cdef bytearray _binstr = bytearray(4 * len_hexstr)
cdef unsigned char[:] binstr = _binstr
cdef unsigned char int_
cdef Py_ssize_t i
for i in range(len_hexstr):
int_ = char_to_int(hexbytes[i])
binstr[4*i] = int_to_char((int_ >> 3) & 1)
binstr[4*i+1] = int_to_char((int_ >> 2) & 1)
binstr[4*i+2] = int_to_char((int_ >> 1) & 1)
binstr[4*i+3] = int_to_char((int_) & 1)
return _binstr.decode()
@cython.boundscheck(False)
cpdef long bin2int(str binstr):
"""Convert a binary string to integer."""
# return int(binstr, 2)
cdef bytearray binbytes = bytearray(binstr.encode())
cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes)
cdef long cumul = 0
cdef unsigned char[:] v_binstr = binbytes
for i in range(len_):
cumul = 2*cumul + char_to_int(v_binstr[i])
return cumul
@cython.boundscheck(False)
cpdef long hex2int(str hexstr):
"""Convert a binary string to integer."""
# return int(hexstr, 2)
cdef bytearray binbytes = bytearray(hexstr.encode())
cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes)
cdef long cumul = 0
cdef unsigned char[:] v_hexstr = binbytes
for i in range(len_):
cumul = 16*cumul + char_to_int(v_hexstr[i])
return cumul
@cython.boundscheck(False)
cpdef str bin2hex(str binstr):
return "{0:X}".format(int(binstr, 2))
@cython.boundscheck(False)
cpdef unsigned char df(str msg):
"""Decode Downlink Format value, bits 1 to 5."""
cdef str dfbin = hex2bin(msg[:2])
# return min(bin2int(dfbin[0:5]), 24)
cdef long df = bin2int(dfbin[0:5])
if df > 24:
return 24
return df
# the CRC generator
# G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000])
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef long crc(str msg, bint encode=False):
"""Mode-S Cyclic Redundancy Check.
Detect if bit error occurs in the Mode-S message. When encode option is on,
the checksum is generated.
Args:
msg (string): 28 bytes hexadecimal message string
encode (bool): True to encode the date only and return the checksum
Returns:
int: message checksum, or partity bits (encoder)
"""
# the CRC generator
# G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
# cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000])
cdef long[4] G = _G
# msgbin_split = wrap(msgbin, 8)
# mbytes = list(map(bin2int, msgbin_split))
cdef bytearray _msgbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] msgbin = _msgbin
cdef Py_ssize_t len_msgbin = PyByteArray_GET_SIZE(_msgbin)
cdef Py_ssize_t len_mbytes = len_msgbin // 8
cdef Py_ssize_t i
if encode:
for i in range(len_msgbin - 24, len_msgbin):
msgbin[i] = 0
cdef array.array _mbytes = array.array(
'l', [bin2int(_msgbin[8*i:8*i+8].decode()) for i in range(len_mbytes)]
)
cdef long[:] mbytes = _mbytes
cdef long bits, mask
cdef Py_ssize_t ibyte, ibit
for ibyte in range(len_mbytes - 3):
for ibit in range(8):
mask = 0x80 >> ibit
bits = mbytes[ibyte] & mask
if bits > 0:
mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit)
mbytes[ibyte + 1] = mbytes[ibyte + 1] ^ (
0xFF & ((G[0] << 8 - ibit) | (G[1] >> ibit))
)
mbytes[ibyte + 2] = mbytes[ibyte + 2] ^ (
0xFF & ((G[1] << 8 - ibit) | (G[2] >> ibit))
)
mbytes[ibyte + 3] = mbytes[ibyte + 3] ^ (
0xFF & ((G[2] << 8 - ibit) | (G[3] >> ibit))
)
cdef long result = (mbytes[len_mbytes-3] << 16) | (mbytes[len_mbytes-2] << 8) | mbytes[len_mbytes-1]
return result
cpdef long floor(double x):
"""Mode-S floor function.
Defined as the greatest integer value k, such that k <= x
For example: floor(3.6) = 3 and floor(-3.6) = -4
"""
return <long> c_floor(x)
cpdef str icao(str msg):
"""Calculate the ICAO address from an Mode-S message."""
cdef unsigned char DF = df(msg)
cdef long c0, c1
if DF in (11, 17, 18):
addr = msg[2:8]
elif DF in (0, 4, 5, 16, 20, 21):
c0 = crc(msg, encode=True)
c1 = hex2int(msg[-6:])
addr = "%06X" % (c0 ^ c1)
else:
addr = None
return addr
cpdef bint is_icao_assigned(str icao):
"""Check whether the ICAO address is assigned (Annex 10, Vol 3)."""
if (icao is None) or (not isinstance(icao, str)) or (len(icao) != 6):
return False
cdef long icaoint = hex2int(icao)
if 0x200000 < icaoint < 0x27FFFF:
return False # AFI
if 0x280000 < icaoint < 0x28FFFF:
return False # SAM
if 0x500000 < icaoint < 0x5FFFFF:
return False # EUR, NAT
if 0x600000 < icaoint < 0x67FFFF:
return False # MID
if 0x680000 < icaoint < 0x6F0000:
return False # ASIA
if 0x900000 < icaoint < 0x9FFFFF:
return False # NAM, PAC
if 0xB00000 < icaoint < 0xBFFFFF:
return False # CAR
if 0xD00000 < icaoint < 0xDFFFFF:
return False # future
if 0xF00000 < icaoint < 0xFFFFFF:
return False # future
return True
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int typecode(str msg):
"""Type code of ADS-B message"""
if df(msg) not in (17, 18):
return -1
# return None
cdef str tcbin = hex2bin(msg[8:10])
return bin2int(tcbin[0:5])
@cython.cdivision(True)
cpdef int cprNL(double lat):
"""NL() function in CPR decoding."""
if abs(lat) <= 1e-08:
return 59
elif abs(abs(lat) - 87) <= 1e-08 + 1e-05 * 87:
return 2
elif lat > 87 or lat < -87:
return 1
cdef int nz = 15
cdef double a = 1 - cos(pi / (2 * nz))
cdef double b = cos(pi / 180 * fabs(lat)) ** 2
cdef double nl = 2 * pi / (acos(1 - a / b))
NL = floor(nl)
return NL
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef str idcode(str msg):
"""Compute identity (squawk code)."""
if df(msg) not in [5, 21]:
raise RuntimeError("Message must be Downlink Format 5 or 21.")
squawk_code = squawk(hex2bin(msg)[19:32])
return squawk_code
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef str squawk(str binstr):
"""Compute identity (squawk code)."""
if len(binstr) != 13 or set(binstr) != set('01'):
raise RuntimeError("Input must be 13 bits binary string")
cdef bytearray _mbin = bytearray(binstr.encode())
cdef unsigned char[:] mbin = _mbin
cdef bytearray _idcode = bytearray(4)
cdef unsigned char[:] idcode = _idcode
cdef unsigned char C1 = mbin[0]
cdef unsigned char A1 = mbin[1]
cdef unsigned char C2 = mbin[2]
cdef unsigned char A2 = mbin[3]
cdef unsigned char C4 = mbin[4]
cdef unsigned char A4 = mbin[5]
# X = mbin[6]
cdef unsigned char B1 = mbin[7]
cdef unsigned char D1 = mbin[8]
cdef unsigned char B2 = mbin[9]
cdef unsigned char D2 = mbin[10]
cdef unsigned char B4 = mbin[11]
cdef unsigned char D4 = mbin[12]
idcode[0] = int_to_char((char_to_int(A4)*2 + char_to_int(A2))*2 + char_to_int(A1))
idcode[1] = int_to_char((char_to_int(B4)*2 + char_to_int(B2))*2 + char_to_int(B1))
idcode[2] = int_to_char((char_to_int(C4)*2 + char_to_int(C2))*2 + char_to_int(C1))
idcode[3] = int_to_char((char_to_int(D4)*2 + char_to_int(D2))*2 + char_to_int(D1))
return _idcode.decode()
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int altcode(str msg):
"""Compute the altitude."""
if df(msg) not in [0, 4, 16, 20]:
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
alt = altitude(hex2bin(msg)[19:32])
return alt
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int altitude(str binstr):
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
raise RuntimeError("Input must be 13 bits binary string")
cdef bytearray _mbin = bytearray(binstr.encode())
cdef unsigned char[:] mbin = _mbin
cdef char Mbit = binstr[6]
cdef char Qbit = binstr[8]
cdef int alt = 0
cdef bytearray vbin
cdef bytearray _graybytes = bytearray(11)
cdef unsigned char[:] graybytes = _graybytes
if bin2int(binstr) == 0:
# altitude unknown or invalid
alt = -999999
elif Mbit == 48: # unit in ft, "0" -> 48
if Qbit == 49: # 25ft interval, "1" -> 49
vbin = _mbin[:6] + _mbin[7:8] + _mbin[9:]
alt = bin2int(vbin.decode()) * 25 - 1000
if Qbit == 48: # 100ft interval, above 50175ft, "0" -> 48
graybytes[8] = mbin[0]
graybytes[2] = mbin[1]
graybytes[9] = mbin[2]
graybytes[3] = mbin[3]
graybytes[10] = mbin[4]
graybytes[4] = mbin[5]
# M = mbin[6]
graybytes[5] = mbin[7]
# Q = mbin[8]
graybytes[6] = mbin[9]
graybytes[0] = mbin[10]
graybytes[7] = mbin[11]
graybytes[1] = mbin[12]
alt = gray2alt(_graybytes.decode())
elif Mbit == 49: # unit in meter, "1" -> 49
vbin = _mbin[:6] + _mbin[7:]
alt = int(bin2int(vbin.decode()) * 3.28084) # convert to ft
return alt
cpdef int gray2alt(str codestr):
cdef str gc500 = codestr[:8]
cdef int n500 = gray2int(gc500)
# in 100-ft step must be converted first
cdef str gc100 = codestr[8:]
cdef int n100 = gray2int(gc100)
if n100 in [0, 5, 6]:
return -1
#return None
if n100 == 7:
n100 = 5
if n500 % 2:
n100 = 6 - n100
alt = (n500 * 500 + n100 * 100) - 1300
return alt
cdef int gray2int(str graystr):
"""Convert greycode to binary."""
cdef int num = bin2int(graystr)
num ^= num >> 8
num ^= num >> 4
num ^= num >> 2
num ^= num >> 1
return num
cpdef str data(str msg):
"""Return the data frame in the message, bytes 9 to 22."""
return msg[8:-6]
cpdef bint allzeros(str msg):
"""Check if the data bits are all zeros."""
d = hex2bin(data(msg))
if bin2int(d) > 0:
return False
else:
return True
def wrongstatus(data, sb, msb, lsb):
"""Check if the status bit and field bits are consistency.
This Function is used for checking BDS code versions.
"""
# status bit, most significant bit, least significant bit
status = int(data[sb - 1])
value = bin2int(data[msb - 1 : lsb])
if not status:
if value != 0:
return True
return False

22
pyModeS/common.pyi Normal file
View File

@ -0,0 +1,22 @@
from typing import Optional
def hex2bin(hexstr: str) -> str: ...
def bin2int(binstr: str) -> int: ...
def hex2int(hexstr: str) -> int: ...
def bin2hex(binstr: str) -> str: ...
def df(msg: str) -> int: ...
def crc(msg: str, encode: bool = False) -> int: ...
def floor(x: float) -> float: ...
def icao(msg: str) -> Optional[str]: ...
def is_icao_assigned(icao: str) -> bool: ...
def typecode(msg: str) -> Optional[int]: ...
def cprNL(lat: float) -> int: ...
def idcode(msg: str) -> str: ...
def squawk(binstr: str) -> str: ...
def altcode(msg: str) -> Optional[int]: ...
def altitude(binstr: str) -> Optional[int]: ...
def gray2alt(binstr: str) -> Optional[int]: ...
def gray2int(binstr: str) -> int: ...
def data(msg: str) -> str: ...
def allzeros(msg: str) -> bool: ...
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ...

View File

@ -1,11 +1,8 @@
from __future__ import absolute_import, print_function, division
def tell(msg: str) -> None:
from .. import common, adsb, commb, bds
from pyModeS.decoder import adsb, commb, common, bds
def tell(msg):
def _print(label, value, unit=None):
print("%20s: " % label, end="")
print("%28s: " % label, end="")
print("%s " % value, end="")
if unit:
print(unit)
@ -20,16 +17,21 @@ def tell(msg):
_print("Downlink Format", df)
if df == 17:
_print("Protocal", "Mode-S Extended Squitter (ADS-B)")
_print("Protocol", "Mode-S Extended Squitter (ADS-B)")
tc = common.typecode(msg)
if tc is None:
_print("ERROR", "Unknown typecode")
return
if 1 <= tc <= 4: # callsign
callsign = adsb.callsign(msg)
_print("Type", "Identitification and category")
_print("Type", "Identification and category")
_print("Callsign:", callsign)
if 5 <= tc <= 8: # surface position
_print("Type", "Surface postition")
_print("Type", "Surface position")
oe = adsb.oe_flag(msg)
msgbin = common.hex2bin(msg)
cprlat = common.bin2int(msgbin[54:71]) / 131072.0
@ -55,12 +57,14 @@ def tell(msg):
if tc == 19:
_print("Type", "Airborne velocity")
spd, trk, vr, t = adsb.velocity(msg)
types = {"GS": "Ground speed", "TAS": "True airspeed"}
_print("Speed", spd, "knots")
_print("Track", trk, "degrees")
_print("Vertical rate", vr, "feet/minute")
_print("Type", types[t])
velocity = adsb.velocity(msg)
if velocity is not None:
spd, trk, vr, t = velocity
types = {"GS": "Ground speed", "TAS": "True airspeed"}
_print("Speed", spd, "knots")
_print("Track", trk, "degrees")
_print("Vertical rate", vr, "feet/minute")
_print("Type", types[t])
if 20 <= tc <= 22: # airborne position
_print("Type", "Airborne position (with GNSS altitude)")
@ -74,12 +78,100 @@ def tell(msg):
_print("CPR Longitude", cprlon)
_print("Altitude", alt, "feet")
if tc == 29: # target state and status
_print("Type", "Target State and Status")
subtype = common.bin2int((common.hex2bin(msg)[32:])[5:7])
_print("Subtype", subtype)
tcas_operational = adsb.tcas_operational(msg)
types_29 = {0: "Not Engaged", 1: "Engaged"}
tcas_operational_types = {0: "Not Operational", 1: "Operational"}
if subtype == 0:
emergency_types = {
0: "No emergency",
1: "General emergency",
2: "Lifeguard/medical emergency",
3: "Minimum fuel",
4: "No communications",
5: "Unlawful interference",
6: "Downed aircraft",
7: "Reserved",
}
vertical_horizontal_types = {
1: "Acquiring mode",
2: "Capturing/Maintaining mode",
}
tcas_ra_types = {0: "Not active", 1: "Active"}
alt, alt_source, alt_ref = adsb.target_altitude(msg)
angle, angle_type, angle_source = adsb.target_angle(msg)
vertical_mode = adsb.vertical_mode(msg)
horizontal_mode = adsb.horizontal_mode(msg)
tcas_ra = adsb.tcas_ra(msg)
emergency_status = adsb.emergency_status(msg)
_print("Target altitude", alt, "feet")
_print("Altitude source", alt_source)
_print("Altitude reference", alt_ref)
_print("Angle", angle, "°")
_print("Angle Type", angle_type)
_print("Angle Source", angle_source)
if vertical_mode is not None:
_print(
"Vertical mode",
vertical_horizontal_types[vertical_mode],
)
if horizontal_mode is not None:
_print(
"Horizontal mode",
vertical_horizontal_types[horizontal_mode],
)
_print(
"TCAS/ACAS",
tcas_operational_types[tcas_operational]
if tcas_operational
else None,
)
_print("TCAS/ACAS RA", tcas_ra_types[tcas_ra])
_print("Emergency status", emergency_types[emergency_status])
else:
alt, alt_source = adsb.selected_altitude(msg) # type: ignore
baro = adsb.baro_pressure_setting(msg)
hdg = adsb.selected_heading(msg)
autopilot = adsb.autopilot(msg)
vnav = adsb.vnav_mode(msg)
alt_hold = adsb.altitude_hold_mode(msg)
app = adsb.approach_mode(msg)
lnav = adsb.lnav_mode(msg)
_print("Selected altitude", alt, "feet")
_print("Altitude source", alt_source)
_print(
"Barometric pressure setting",
baro,
"" if baro is None else "millibars",
)
_print("Selected Heading", hdg, "°")
if not (common.bin2int((common.hex2bin(msg)[32:])[46]) == 0):
_print(
"Autopilot", types_29[autopilot] if autopilot else None
)
_print("VNAV mode", types_29[vnav] if vnav else None)
_print(
"Altitude hold mode",
types_29[alt_hold] if alt_hold else None,
)
_print("Approach mode", types_29[app] if app else None)
_print(
"TCAS/ACAS",
tcas_operational_types[tcas_operational]
if tcas_operational
else None,
)
_print("LNAV mode", types_29[lnav] if lnav else None)
if df == 20:
_print("Protocal", "Mode-S Comm-B altitude reply")
_print("Protocol", "Mode-S Comm-B altitude reply")
_print("Altitude", common.altcode(msg), "feet")
if df == 21:
_print("Protocal", "Mode-S Comm-B identity reply")
_print("Protocol", "Mode-S Comm-B identity reply")
_print("Squawk code", common.idcode(msg))
if df == 20 or df == 21:
@ -97,7 +189,7 @@ def tell(msg):
}
BDS = bds.infer(msg, mrar=True)
if BDS in labels.keys():
if BDS is not None and BDS in labels.keys():
_print("BDS", "%s (%s)" % (BDS, labels[BDS]))
else:
_print("BDS", BDS)
@ -119,7 +211,7 @@ def tell(msg):
_print("True airspeed", commb.tas50(msg), "knots")
if BDS == "BDS60":
_print("Megnatic Heading", commb.hdg60(msg), "degrees")
_print("Magnetic Heading", commb.hdg60(msg), "degrees")
_print("Indicated airspeed", commb.ias60(msg), "knots")
_print("Mach number", commb.mach60(msg))
_print("Vertical rate (Baro)", commb.vr60baro(msg), "feet/minute")

View File

@ -1,23 +1,5 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Decoding Air-Air Surveillance (ACAS) DF=0/16
[To be implemented]
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common

View File

@ -1,88 +1,151 @@
# Copyright (C) 2015 Junzi Sun (TU Delft)
"""ADS-B module.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
The ADS-B module also imports functions from the following modules:
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""ADS-B Wrapper.
The ADS-B wrapper also imports functions from the following modules:
- pyModeS.decoder.bds.bds05
Functions: ``airborne_position``, ``airborne_position_with_ref``, ``altitude``
- pyModeS.decoder.bds.bds06
Functions: ``surface_position``, ``surface_position_with_ref``, ``surface_velocity``
- pyModeS.decoder.bds.bds08
Functions: ``category``, ``callsign``
- pyModeS.decoder.bds.bds09
Functions: ``airborne_velocity``, ``altitude_diff``
- bds05: ``airborne_position()``, ``airborne_position_with_ref()``,
``altitude()``
- bds06: ``surface_position()``, ``surface_position_with_ref()``,
``surface_velocity()``
- bds08: ``category()``, ``callsign()``
- bds09: ``airborne_velocity()``, ``altitude_diff()``
"""
from __future__ import absolute_import, print_function, division
from __future__ import annotations
import pyModeS as pms
from pyModeS.decoder import common
from pyModeS.decoder import uncertainty
from datetime import datetime
# from pyModeS.decoder.bds import bds05, bds06, bds09
from pyModeS.decoder.bds.bds05 import (
airborne_position,
airborne_position_with_ref,
altitude,
)
from pyModeS.decoder.bds.bds06 import (
from .. import common
from . import uncertainty
from .bds.bds05 import airborne_position, airborne_position_with_ref
from .bds.bds05 import altitude as altitude05
from .bds.bds06 import (
surface_position,
surface_position_with_ref,
surface_velocity,
)
from pyModeS.decoder.bds.bds08 import category, callsign
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
from .bds.bds08 import callsign, category
from .bds.bds09 import airborne_velocity, altitude_diff
from .bds.bds61 import emergency_squawk, emergency_state, is_emergency
from .bds.bds62 import (
altitude_hold_mode,
approach_mode,
autopilot,
baro_pressure_setting,
emergency_status,
horizontal_mode,
lnav_mode,
selected_altitude,
selected_heading,
target_altitude,
target_angle,
tcas_operational,
tcas_ra,
vertical_mode,
vnav_mode,
)
__all__ = [
"airborne_position",
"airborne_position_with_ref",
"altitude05",
"surface_position",
"surface_position_with_ref",
"surface_velocity",
"callsign",
"category",
"airborne_velocity",
"altitude_diff",
"emergency_squawk",
"emergency_state",
"is_emergency",
"df",
"icao",
"typecode",
"position",
"position_with_ref",
"altitude",
"velocity",
"speed_heading",
"oe_flag",
"version",
"nuc_p",
"nuc_v",
"nic_v1",
"nic_v2",
"nic_s",
"nic_a_c",
"nic_b",
"nac_p",
"nac_v",
"sil",
"selected_altitude",
"target_altitude",
"vertical_mode",
"horizontal_mode",
"selected_heading",
"target_angle",
"baro_pressure_setting",
"autopilot",
"vnav_mode",
"altitude_hold_mode",
"approach_mode",
"lnav_mode",
"tcas_operational",
"tcas_ra",
"emergency_status",
]
def df(msg):
def df(msg: str) -> int:
return common.df(msg)
def icao(msg):
def icao(msg: str) -> None | str:
return common.icao(msg)
def typecode(msg):
def typecode(msg: str) -> None | int:
return common.typecode(msg)
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
"""Decode position from a pair of even and odd position message
(works with both airborne and surface position messages)
def position(
msg0: str,
msg1: str,
t0: int | datetime,
t1: int | datetime,
lat_ref: None | float = None,
lon_ref: None | float = None,
) -> None | tuple[float, float]:
"""Decode surface or airborne position from a pair of even and odd
position messages.
Note, that to decode surface position using the position message pair,
the reference position has to be provided.
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
msg0 (string): even message (28 hexdigits)
msg1 (string): odd message (28 hexdigits)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
lat_ref (float): latitude of reference position
lon_ref (float): longitude of reference position
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
tc0 = typecode(msg0)
tc1 = typecode(msg1)
if tc0 is None or tc1 is None:
raise RuntimeError("Incorrect or inconsistent message types")
if 5 <= tc0 <= 8 and 5 <= tc1 <= 8:
if (not lat_ref) or (not lon_ref):
if lat_ref is None or lon_ref is None:
raise RuntimeError(
"Surface position encountered, a reference \
position lat/lon required. Location of \
receiver can be used."
"Surface position encountered, a reference position"
" lat/lon required. Location of receiver can be used."
)
else:
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
@ -96,19 +159,20 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
return airborne_position(msg0, msg1, t0, t1)
else:
raise RuntimeError("incorrect or inconsistant message types")
raise RuntimeError("Incorrect or inconsistent message types")
def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message,
knowing reference nearby location, such as previously
calculated location, ground station, or airport location, etc.
Works with both airborne and surface position messages.
The reference position shall be with in 180NM (airborne) or 45NM (surface)
def position_with_ref(msg: str, lat_ref: float, lon_ref: float) -> tuple[float, float]:
"""Decode position with only one message.
A reference position is required, which can be previously
calculated location, ground station, or airport location.
The function works with both airborne and surface position messages.
The reference position shall be within 180NM (airborne) or 45NM (surface)
of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
msg (str): even message (28 hexdigits)
lat_ref: previous known latitude
lon_ref: previous known longitude
@ -118,6 +182,9 @@ def position_with_ref(msg, lat_ref, lon_ref):
tc = typecode(msg)
if tc is None:
raise RuntimeError("incorrect or inconsistent message types")
if 5 <= tc <= 8:
return surface_position_with_ref(msg, lat_ref, lon_ref)
@ -125,93 +192,94 @@ def position_with_ref(msg, lat_ref, lon_ref):
return airborne_position_with_ref(msg, lat_ref, lon_ref)
else:
raise RuntimeError("incorrect or inconsistant message types")
raise RuntimeError("incorrect or inconsistent message types")
def altitude(msg):
"""Decode aircraft altitude
def altitude(msg: str) -> None | float:
"""Decode aircraft altitude.
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: altitude in feet
"""
"""
tc = typecode(msg)
if tc < 5 or tc == 19 or tc > 22:
if tc is None or tc < 5 or tc == 19 or tc > 22:
raise RuntimeError("%s: Not a position message" % msg)
if tc >= 5 and tc <= 8:
elif tc >= 5 and tc <= 8:
# surface position, altitude 0
return 0
msgbin = common.hex2bin(msg)
q = msgbin[47]
if q:
n = common.bin2int(msgbin[40:47] + msgbin[48:52])
alt = n * 25 - 1000
return alt
else:
return None
# airborn position
return altitude05(msg)
def velocity(msg, rtn_sources=False):
def velocity(
msg: str, source: bool = False
) -> None | tuple[None | float, None | float, None | int, str]:
"""Calculate the speed, heading, and vertical rate
(handles both airborne or surface message)
(handles both airborne or surface message).
Args:
msg (string): 28 bytes hexadecimal message string
rtn_source (boolean): If the function will return
the sources for direction of travel and vertical
rate. This will change the return value from a four
element array to a six element array.
msg (str): 28 hexdigits string
source (boolean): Include direction and vertical rate sources in return.
Default to False.
If set to True, the function will return six value instead of four.
Returns:
(int, float, int, string, string, string): speed (kt),
ground track or heading (degree),
rate of climb/descent (ft/min), speed type
('GS' for ground speed, 'AS' for airspeed),
direction source ('true_north' for ground track / true north
as refrence, 'mag_north' for magnetic north as reference),
rate of climb/descent source ('Baro' for barometer, 'GNSS'
for GNSS constellation).
In the case of surface messages, None will be put in place
for vertical rate and its respective sources.
int, float, int, string, [string], [string]:
- Speed (kt)
- Angle (degree), either ground track or heading
- Vertical rate (ft/min)
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
- [Optional] Vertical rate source ('BARO' or 'GNSS')
For surface messages, vertical rate and its respective sources are set
to None.
"""
tc = typecode(msg)
error = "incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
if tc is None:
raise RuntimeError(error)
if 5 <= typecode(msg) <= 8:
return surface_velocity(msg, rtn_sources)
if 5 <= tc <= 8:
return surface_velocity(msg, source)
elif typecode(msg) == 19:
return airborne_velocity(msg, rtn_sources)
elif tc == 19:
return airborne_velocity(msg, source)
else:
raise RuntimeError(
"incorrect or inconsistant message types, expecting 4<TC<9 or TC=19"
)
raise RuntimeError(error)
def speed_heading(msg):
def speed_heading(msg: str) -> None | tuple[None | float, None | float]:
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
(int, float): speed (kt), ground track or heading (degree)
"""
spd, trk_or_hdg, rocd, tag = velocity(msg)
decoded = velocity(msg)
if decoded is None:
return None
spd, trk_or_hdg, rocd, tag = decoded
return spd, trk_or_hdg
def oe_flag(msg):
def oe_flag(msg: str) -> int:
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: 0 or 1, for even or odd frame
"""
@ -219,11 +287,11 @@ def oe_flag(msg):
return int(msgbin[53])
def version(msg):
def version(msg: str) -> int:
"""ADS-B Version
Args:
msg (string): 28 bytes hexadecimal message string, TC = 31
msg (str): 28 hexdigits string, TC = 31
Returns:
int: version number
@ -241,13 +309,15 @@ def version(msg):
return version
def nuc_p(msg):
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
def nuc_p(msg: str) -> tuple[int, None | float, None | int, None | int]:
"""Calculate NUCp, Navigation Uncertainty Category - Position
(ADS-B version 1)
Args:
msg (string): 28 bytes hexadecimal message string,
msg (str): 28 hexdigits string,
Returns:
int: NUCp, Navigation Uncertainty Category (position)
int: Horizontal Protection Limit
int: 95% Containment Radius - Horizontal (meters)
int: 95% Containment Radius - Vertical (meters)
@ -255,7 +325,7 @@ def nuc_p(msg):
"""
tc = typecode(msg)
if typecode(msg) < 5 or typecode(msg) > 22:
if tc is None or tc < 5 or tc is None or tc > 22:
raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \
@ -263,27 +333,36 @@ def nuc_p(msg):
% msg
)
try:
NUCp = uncertainty.TC_NUCp_lookup[tc]
HPL = uncertainty.NUCp[NUCp]["HPL"]
RCu = uncertainty.NUCp[NUCp]["RCu"]
RCv = uncertainty.NUCp[NUCp]["RCv"]
except KeyError:
NUCp = uncertainty.TC_NUCp_lookup[tc]
index = uncertainty.NUCp.get(NUCp, None)
if index is not None:
HPL = index["HPL"]
RCu = index["RCu"]
RCv = index["RCv"]
else:
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
if tc in [20, 21]:
RCv = uncertainty.NA
RCv = uncertainty.NA
return HPL, RCu, RCv
# RCv only available for GNSS height
if tc == 20:
RCv = 4
elif tc == 21:
RCv = 15
return NUCp, HPL, RCu, RCv
def nuc_v(msg):
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
def nuc_v(msg: str) -> tuple[int, None | float, None | float]:
"""Calculate NUCv, Navigation Uncertainty Category - Velocity
(ADS-B version 1)
Args:
msg (string): 28 bytes hexadecimal message string,
msg (str): 28 hexdigits string,
Returns:
int: NUCv, Navigation Uncertainty Category (velocity)
int or string: 95% Horizontal Velocity Error
int or string: 95% Vertical Velocity Error
"""
@ -296,28 +375,31 @@ def nuc_v(msg):
msgbin = common.hex2bin(msg)
NUCv = common.bin2int(msgbin[42:45])
index = uncertainty.NUCv.get(NUCv, None)
try:
HVE = uncertainty.NUCv[NUCv]["HVE"]
VVE = uncertainty.NUCv[NUCv]["VVE"]
except KeyError:
if index is not None:
HVE = index["HVE"]
VVE = index["VVE"]
else:
HVE, VVE = uncertainty.NA, uncertainty.NA
return HVE, VVE
return NUCv, HVE, VVE
def nic_v1(msg, NICs):
def nic_v1(msg: str, NICs: int) -> tuple[int, None | float, None | float]:
"""Calculate NIC, navigation integrity category, for ADS-B version 1
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
NICs (int or string): NIC supplement
Returns:
int: NIC, Navigation Integrity Category
int or string: Horizontal Radius of Containment
int or string: Vertical Protection Limit
"""
if typecode(msg) < 5 or typecode(msg) > 22:
tc = typecode(msg)
if tc is None or tc < 5 or tc > 22:
raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \
@ -325,33 +407,37 @@ def nic_v1(msg, NICs):
% msg
)
tc = typecode(msg)
NIC = uncertainty.TC_NICv1_lookup[tc]
if isinstance(NIC, dict):
NIC = NIC[NICs]
try:
Rc = uncertainty.NICv1[NIC][NICs]["Rc"]
VPL = uncertainty.NICv1[NIC][NICs]["VPL"]
except KeyError:
Rc, VPL = uncertainty.NA, uncertainty.NA
d_index = uncertainty.NICv1.get(NIC, None)
Rc, VPL = uncertainty.NA, uncertainty.NA
return Rc, VPL
if d_index is not None:
index = d_index.get(NICs, None)
if index is not None:
Rc = index["Rc"]
VPL = index["VPL"]
return NIC, Rc, VPL
def nic_v2(msg, NICa, NICbc):
def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int | None, int | None]:
"""Calculate NIC, navigation integrity category, for ADS-B version 2
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
NICa (int or string): NIC supplement - A
NICbc (int or srting): NIC supplement - B or C
NICbc (int or string): NIC supplement - B or C
Returns:
int: NIC, Navigation Integrity Category
int or string: Horizontal Radius of Containment
"""
if typecode(msg) < 5 or typecode(msg) > 22:
tc = typecode(msg)
if tc is None or tc < 5 or tc > 22:
raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \
@ -359,7 +445,6 @@ def nic_v2(msg, NICa, NICbc):
% msg
)
tc = typecode(msg)
NIC = uncertainty.TC_NICv2_lookup[tc]
if 20 <= tc <= 22:
@ -370,19 +455,18 @@ def nic_v2(msg, NICa, NICbc):
try:
if isinstance(NIC, dict):
NIC = NIC[NICs]
Rc = uncertainty.NICv2[NIC][NICs]["Rc"]
except KeyError:
Rc = uncertainty.NA
return None, None
return Rc
return NIC, Rc # type: ignore
def nic_s(msg):
def nic_s(msg: str) -> int:
"""Obtain NIC supplement bit, TC=31 message
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: NICs number (0 or 1)
@ -400,11 +484,11 @@ def nic_s(msg):
return nic_s
def nic_a_c(msg):
def nic_a_c(msg: str) -> tuple[int, int]:
"""Obtain NICa/c, navigation integrity category supplements a and c
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
(int, int): NICa and NICc number (0 or 1)
@ -423,18 +507,18 @@ def nic_a_c(msg):
return nic_a, nic_c
def nic_b(msg):
def nic_b(msg: str) -> int:
"""Obtain NICb, navigation integrity category supplement-b
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: NICb number (0 or 1)
"""
tc = typecode(msg)
if tc < 9 or tc > 18:
if tc is None or tc < 9 or tc > 18:
raise RuntimeError(
"%s: Not a airborne position message, expecting 8<TC<19" % msg
)
@ -445,15 +529,18 @@ def nic_b(msg):
return nic_b
def nac_p(msg):
def nac_p(msg: str) -> tuple[int, int | None, int | None]:
"""Calculate NACp, Navigation Accuracy Category - Position
Args:
msg (string): 28 bytes hexadecimal message string, TC = 29 or 31
msg (str): 28 hexdigits string, TC = 29 or 31
Returns:
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
int: NACp, Navigation Accuracy Category (position)
int or string: 95% horizontal accuracy bounds,
Estimated Position Uncertainty
int or string: 95% vertical accuracy bounds,
Vertical Estimated Position Uncertainty
"""
tc = typecode(msg)
@ -477,18 +564,21 @@ def nac_p(msg):
except KeyError:
EPU, VEPU = uncertainty.NA, uncertainty.NA
return EPU, VEPU
return NACp, EPU, VEPU
def nac_v(msg):
def nac_v(msg: str) -> tuple[int, float | None, float | None]:
"""Calculate NACv, Navigation Accuracy Category - Velocity
Args:
msg (string): 28 bytes hexadecimal message string, TC = 19
msg (str): 28 hexdigits string, TC = 19
Returns:
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
int or string: 95% vertical accuracy bounds for velocity, Vertical Figure of Merit
int: NACv, Navigation Accuracy Category (velocity)
int or string: 95% horizontal accuracy bounds for velocity,
Horizontal Figure of Merit
int or string: 95% vertical accuracy bounds for velocity,
Vertical Figure of Merit
"""
tc = typecode(msg)
@ -506,25 +596,30 @@ def nac_v(msg):
except KeyError:
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
return HFOMr, VFOMr
return NACv, HFOMr, VFOMr
def sil(msg, version):
def sil(
msg: str,
version: None | int,
) -> tuple[float | None, float | None, str]:
"""Calculate SIL, Surveillance Integrity Level
Args:
msg (string): 28 bytes hexadecimal message string with TC = 29, 31
msg (str): 28 hexdigits string with TC = 29, 31
Returns:
int or string: Probability of exceeding Horizontal Radius of Containment RCu
int or string: Probability of exceeding Vertical Integrity Containment Region VPL
int or string:
Probability of exceeding Horizontal Radius of Containment RCu
int or string:
Probability of exceeding Vertical Integrity Containment Region VPL
string: SIL supplement based on per "hour" or "sample", or 'unknown'
"""
tc = typecode(msg)
if tc not in [29, 31]:
raise RuntimeError(
"%s: Not a target state and status messag, \
"%s: Not a target state and status message, \
or operation status message, expecting TC = 29 or 31"
% msg
)

View File

@ -1,23 +1,98 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Decoding all call replies DF=11
[To be implemented]
Decode all-call reply messages, with downlink format 11
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
from __future__ import annotations
from typing import Callable, TypeVar
from .. import common
T = TypeVar("T")
F = Callable[[str], T]
def _checkdf(func: F[T]) -> F[T]:
"""Ensure downlink format is 11."""
def wrapper(msg: str) -> T:
df = common.df(msg)
if df != 11:
raise RuntimeError(
"Incorrect downlink format, expect 11, got {}".format(df)
)
return func(msg)
return wrapper
@_checkdf
def icao(msg: str) -> None | str:
"""Decode transponder code (ICAO address).
Args:
msg (str): 14 hexdigits string
Returns:
string: ICAO address
"""
return common.icao(msg)
@_checkdf
def interrogator(msg: str) -> str:
"""Decode interrogator identifier code.
Args:
msg (str): 14 hexdigits string
Returns:
int: interrogator identifier code
"""
# the CRC remainder contains the CL and IC field.
# the top three bits are CL field and last four bits are IC field.
remainder = common.crc(msg)
if remainder > 79:
IC = "corrupt IC"
elif remainder < 16:
IC = "II" + str(remainder)
else:
IC = "SI" + str(remainder - 16)
return IC
@_checkdf
def capability(msg: str) -> tuple[int, None | str]:
"""Decode transponder capability.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: transponder capability, description
"""
msgbin = common.hex2bin(msg)
ca = common.bin2int(msgbin[5:8])
if ca == 0:
text = "level 1 transponder"
elif ca == 4:
text = "level 2 transponder, ability to set CA to 7, on ground"
elif ca == 5:
text = "level 2 transponder, ability to set CA to 7, airborne"
elif ca == 6:
text = (
"evel 2 transponder, ability to set CA to 7, "
"either airborne or ground"
)
elif ca == 7:
text = (
"Downlink Request value is not 0, "
"or the Flight Status is 2, 3, 4 or 5, "
"and either airborne or on the ground"
)
else:
text = None
return ca, text

View File

@ -18,16 +18,13 @@
Common functions for Mode-S decoding
"""
from __future__ import absolute_import, print_function, division
from typing import Optional
import numpy as np
from pyModeS.extra import aero
from pyModeS.decoder import common
from pyModeS.decoder.bds import (
bds05,
bds06,
bds08,
bds09,
from ... import common
from ...extra import aero
from . import ( # noqa: F401
bds10,
bds17,
bds20,
@ -36,22 +33,26 @@ from pyModeS.decoder.bds import (
bds44,
bds45,
bds50,
bds53,
bds60,
bds61,
bds62,
)
def is50or60(msg, spd_ref, trk_ref, alt_ref):
def is50or60(
msg: str, spd_ref: float, trk_ref: float, alt_ref: float
) -> Optional[str]:
"""Use reference ground speed and trk to determine BDS50 and DBS60.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
spd_ref (float): reference speed (ADS-B ground speed), kts
trk_ref (float): reference track (ADS-B track angle), deg
alt_ref (float): reference altitude (ADS-B altitude), ft
Returns:
String or None: BDS version, or possible versions, or None if nothing matches.
String or None: BDS version, or possible versions,
or None if nothing matches.
"""
@ -60,25 +61,34 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
vy = v * np.cos(np.radians(angle))
return vx, vy
# message must be both BDS 50 and 60 before processing
if not (bds50.is50(msg) and bds60.is60(msg)):
return None
h50 = bds50.trk50(msg)
v50 = bds50.gs50(msg)
if h50 is None or v50 is None:
return "BDS50,BDS60"
# --- assuming BDS60 ---
h60 = bds60.hdg60(msg)
m60 = bds60.mach60(msg)
i60 = bds60.ias60(msg)
# additional check now knowing the altitude
if (m60 is not None) and (i60 is not None):
ias_ = aero.mach2cas(m60, alt_ref * aero.ft) / aero.kts
if abs(i60 - ias_) > 20:
return "BDS50"
if h60 is None or (m60 is None and i60 is None):
return "BDS50,BDS60"
m60 = np.nan if m60 is None else m60
i60 = np.nan if i60 is None else i60
# --- assuming BDS50 ---
h50 = bds50.trk50(msg)
v50 = bds50.gs50(msg)
if h50 is None or v50 is None:
return "BDS50,BDS60"
XY5 = vxy(v50 * aero.kts, h50)
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
@ -104,15 +114,17 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
return BDS
def infer(msg, mrar=False):
def infer(msg: str, mrar: bool = False) -> Optional[str]:
"""Estimate the most likely BDS code of an message.
Args:
msg (String): 28 bytes hexadecimal message string
mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45). Defaults to False.
msg (str): 28 hexdigits string
mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45).
Defaults to False.
Returns:
String or None: BDS version, or possible versions, or None if nothing matches.
String or None: BDS version, or possible versions,
or None if nothing matches.
"""
df = common.df(msg)
@ -123,9 +135,11 @@ def infer(msg, mrar=False):
# For ADS-B / Mode-S extended squitter
if df == 17:
tc = common.typecode(msg)
if tc is None:
return None
if 1 <= tc <= 4:
return "BDS08" # indentification and category
return "BDS08" # identification and category
if 5 <= tc <= 8:
return "BDS06" # surface movement
if 9 <= tc <= 18:

View File

@ -1,36 +1,24 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,5
# ADS-B TC=9-18
# Airborn position
# Airborne position
# ------------------------------------------
from __future__ import annotations
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
from datetime import datetime
from ... import common
def airborne_position(msg0, msg1, t0, t1):
"""Decode airborn position from a pair of even and odd position message
def airborne_position(
msg0: str, msg1: str, t0: int | datetime, t1: int | datetime
) -> None | tuple[float, float]:
"""Decode airborne position from a pair of even and odd position message
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
msg0 (string): even message (28 hexdigits)
msg1 (string): odd message (28 hexdigits)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
@ -52,13 +40,13 @@ def airborne_position(msg0, msg1, t0, t1):
raise RuntimeError("Both even and odd CPR frames are required.")
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
cprlat_even = common.bin2int(mb0[22:39]) / 131072
cprlon_even = common.bin2int(mb0[39:56]) / 131072
cprlat_odd = common.bin2int(mb1[22:39]) / 131072
cprlon_odd = common.bin2int(mb1[39:56]) / 131072
air_d_lat_even = 360.0 / 60
air_d_lat_odd = 360.0 / 59
air_d_lat_even = 360 / 60
air_d_lat_odd = 360 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
@ -77,33 +65,36 @@ def airborne_position(msg0, msg1, t0, t1):
return None
# compute ni, longitude index m, and longitude
if t0 > t1:
# (people pass int+int or datetime+datetime)
if t0 > t1: # type: ignore
lat = lat_even
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even)
lon = (360 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 1, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_odd)
lon = (360 / ni) * (m % ni + cprlon_odd)
if lon > 180:
lon = lon - 360
return round(lat, 5), round(lon, 5)
return lat, lon
def airborne_position_with_ref(msg, lat_ref, lon_ref):
def airborne_position_with_ref(
msg: str, lat_ref: float, lon_ref: float
) -> tuple[float, float]:
"""Decode airborne position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be with in 180NM of the true position.
be within 180NM of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
msg (str): even message (28 hexdigits)
lat_ref: previous known latitude
lon_ref: previous known longitude
@ -113,11 +104,11 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0
cprlat = common.bin2int(mb[22:39]) / 131072
cprlon = common.bin2int(mb[39:56]) / 131072
i = int(mb[21])
d_lat = 360.0 / 59 if i else 360.0 / 60
d_lat = 360 / 59 if i else 360 / 60
j = common.floor(lat_ref / d_lat) + common.floor(
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
@ -128,9 +119,9 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
ni = common.cprNL(lat) - i
if ni > 0:
d_lon = 360.0 / ni
d_lon = 360 / ni
else:
d_lon = 360.0
d_lon = 360
m = common.floor(lon_ref / d_lon) + common.floor(
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
@ -138,14 +129,14 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5)
return lat, lon
def altitude(msg):
def altitude(msg: str) -> None | int:
"""Decode aircraft altitude
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: altitude in feet
@ -153,21 +144,19 @@ def altitude(msg):
tc = common.typecode(msg)
if tc < 9 or tc == 19 or tc > 22:
raise RuntimeError("%s: Not a airborn position message" % msg)
if tc is None or tc < 9 or tc == 19 or tc > 22:
raise RuntimeError("%s: Not an airborne position message" % msg)
mb = common.hex2bin(msg)[32:]
altbin = mb[8:20]
if tc < 19:
# barometric altitude
q = mb[15]
if q:
n = common.bin2int(mb[8:15] + mb[16:20])
alt = n * 25 - 1000
altcode = altbin[0:6] + "0" + altbin[6:]
alt = common.altitude(altcode)
if alt != -999999:
return alt
else:
alt = None
# return None if altitude is invalid
return None
else:
# GNSS altitude, meters -> feet
alt = common.bin2int(mb[8:20]) * 3.28084
return alt
return common.bin2int(altbin) * 3.28084 # type: ignore

View File

@ -1,37 +1,30 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,6
# ADS-B TC=5-8
# Surface position
# Surface movement
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
import math
from __future__ import annotations
from datetime import datetime
from ... import common
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
def surface_position(
msg0: str,
msg1: str,
t0: int | datetime,
t1: int | datetime,
lat_ref: float,
lon_ref: float,
) -> None | tuple[float, float]:
"""Decode surface position from a pair of even and odd position message,
the lat/lon of receiver must be provided to yield the correct solution.
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
msg0 (string): even message (28 hexdigits)
msg1 (string): odd message (28 hexdigits)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
lat_ref (float): latitude of the receiver
@ -45,13 +38,13 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
msgbin1 = common.hex2bin(msg1)
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072
air_d_lat_even = 90.0 / 60
air_d_lat_odd = 90.0 / 59
air_d_lat_even = 90 / 60
air_d_lat_odd = 90 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
@ -61,8 +54,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
# solution for north hemisphere
lat_even_s = lat_even_n - 90.0
lat_odd_s = lat_odd_n - 90.0
lat_even_s = lat_even_n - 90
lat_odd_s = lat_odd_n - 90
# chose which solution corrispondes to receiver location
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
@ -73,41 +66,44 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
return None
# compute ni, longitude index m, and longitude
if t0 > t1:
# (people pass int+int or datetime+datetime)
if t0 > t1: # type: ignore
lat = lat_even
nl = common.cprNL(lat_even)
ni = max(common.cprNL(lat_even) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_even)
lon = (90 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat_odd)
ni = max(common.cprNL(lat_odd) - 1, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_odd)
lon = (90 / ni) * (m % ni + cprlon_odd)
# four possible longitude solutions
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
lons = [lon, lon + 90, lon + 180, lon + 270]
# make sure lons are between -180 and 180
lons = [(l + 180) % 360 - 180 for l in lons]
lons = [(lon + 180) % 360 - 180 for lon in lons]
# the closest solution to receiver is the correct one
dls = [abs(lon_ref - l) for l in lons]
dls = [abs(lon_ref - lon) for lon in lons]
imin = min(range(4), key=dls.__getitem__)
lon = lons[imin]
return round(lat, 5), round(lon, 5)
return lat, lon
def surface_position_with_ref(msg, lat_ref, lon_ref):
def surface_position_with_ref(
msg: str, lat_ref: float, lon_ref: float
) -> tuple[float, float]:
"""Decode surface position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be with in 45NM of the true position.
be within 45NM of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
msg (str): even message (28 hexdigits)
lat_ref: previous known latitude
lon_ref: previous known longitude
@ -117,11 +113,11 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0
cprlat = common.bin2int(mb[22:39]) / 131072
cprlon = common.bin2int(mb[39:56]) / 131072
i = int(mb[21])
d_lat = 90.0 / 59 if i else 90.0 / 60
d_lat = 90 / 59 if i else 90 / 60
j = common.floor(lat_ref / d_lat) + common.floor(
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
@ -132,9 +128,9 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
ni = common.cprNL(lat) - i
if ni > 0:
d_lon = 90.0 / ni
d_lon = 90 / ni
else:
d_lon = 90.0
d_lon = 90
m = common.floor(lon_ref / d_lon) + common.floor(
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
@ -142,27 +138,32 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5)
return lat, lon
def surface_velocity(msg, rtn_sources=False):
"""Decode surface velocity from from a surface position message
def surface_velocity(
msg: str, source: bool = False
) -> tuple[None | float, None | float, int, str]:
"""Decode surface velocity from a surface position message
Args:
msg (string): 28 bytes hexadecimal message string
rtn_source (boolean): If the function will return
the sources for direction of travel and vertical
rate. This will change the return value from a four
element array to a six element array.
msg (str): 28 hexdigits string
source (boolean): Include direction and vertical rate sources in return.
Default to False.
If set to True, the function will return six value instead of four.
Returns:
(int, float, int, string, string, None): speed (kt),
ground track (degree), None for rate of climb/descend (ft/min),
and speed type ('GS' for ground speed), direction source
('true_north' for ground track / true north as reference),
None rate of climb/descent source.
"""
int, float, int, string, [string], [string]:
- Speed (kt)
- Angle (degree), ground track
- Vertical rate, always 0
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- [Optional] Direction source ('TRUE_NORTH')
- [Optional] Vertical rate source (None)
if common.typecode(msg) < 5 or common.typecode(msg) > 8:
"""
tc = common.typecode(msg)
if tc is None or tc < 5 or tc > 8:
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
mb = common.hex2bin(msg)[32:]
@ -170,29 +171,27 @@ def surface_velocity(msg, rtn_sources=False):
# ground track
trk_status = int(mb[12])
if trk_status == 1:
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
trk = round(trk, 1)
trk = common.bin2int(mb[13:20]) * 360 / 128
else:
trk = None
# ground movment / speed
# ground movement / speed
mov = common.bin2int(mb[5:12])
if mov == 0 or mov > 124:
spd = None
elif mov == 1:
spd = 0
spd = 0.0
elif mov == 124:
spd = 175
spd = 175.0
else:
movs = [2, 9, 13, 39, 94, 109, 124]
kts = [0.125, 1, 2, 15, 70, 100, 175]
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
step = (kts[i] - kts[i - 1]) * 1.0 / (movs[i] - movs[i - 1])
spd = kts[i - 1] + (mov - movs[i - 1]) * step
spd = round(spd, 2)
mov_lb = [2, 9, 13, 39, 94, 109, 124]
kts_lb: list[float] = [0.125, 1, 2, 15, 70, 100, 175]
step: list[float] = [0.125, 0.25, 0.5, 1, 2, 5]
i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov)
spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1]
if rtn_sources:
return spd, trk, 0, "GS", "true_north", None
if source:
return spd, trk, 0, "GS", "TRUE_NORTH", None # type: ignore
else:
return spd, trk, 0, "GS"

View File

@ -1,40 +1,24 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,8
# ADS-B TC=1-4
# Aircraft identitification and category
# Aircraft identification and category
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
from ... import common
def category(msg):
def category(msg: str) -> int:
"""Aircraft category number
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: category number
"""
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
tc = common.typecode(msg)
if tc is None or tc < 1 or tc > 4:
raise RuntimeError("%s: Not a identification message" % msg)
msgbin = common.hex2bin(msg)
@ -42,17 +26,18 @@ def category(msg):
return common.bin2int(mebin[5:8])
def callsign(msg):
def callsign(msg: str) -> str:
"""Aircraft callsign
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
string: callsign
"""
tc = common.typecode(msg)
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
if tc is None or tc < 1 or tc > 4:
raise RuntimeError("%s: Not a identification message" % msg)
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"

View File

@ -1,53 +1,41 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,9
# ADS-B TC=19
# Aircraft Airborn velocity
# Aircraft Airborne velocity
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
from __future__ import annotations
import math
from ... import common
def airborne_velocity(msg, rtn_sources=False):
"""Calculate the speed, track (or heading), and vertical rate
def airborne_velocity(
msg: str, source: bool = False
) -> None | tuple[None | int, None | float, None | int, str]:
"""Decode airborne velocity.
Args:
msg (string): 28 bytes hexadecimal message string
rtn_source (boolean): If the function will return
the sources for direction of travel and vertical
rate. This will change the return value from a four
element array to a six element array.
msg (str): 28 hexdigits string
source (boolean): Include direction and vertical rate sources in return.
Default to False.
If set to True, the function will return six value instead of four.
Returns:
(int, float, int, string, string, string): speed (kt),
ground track or heading (degree),
rate of climb/descent (ft/min), speed type
('GS' for ground speed, 'AS' for airspeed),
direction source ('true_north' for ground track / true north
as refrence, 'mag_north' for magnetic north as reference),
rate of climb/descent source ('Baro' for barometer, 'GNSS'
for GNSS constellation).
"""
int, float, int, string, [string], [string]:
- Speed (kt)
- Angle (degree), either ground track or heading
- Vertical rate (ft/min)
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
- [Optional] Vertical rate source ('BARO' or 'GNSS')
"""
if common.typecode(msg) != 19:
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
raise RuntimeError(
"%s: Not a airborne velocity message, expecting TC=19" % msg
)
mb = common.hex2bin(msg)[32:]
@ -56,77 +44,98 @@ def airborne_velocity(msg, rtn_sources=False):
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
return None
trk_or_hdg: None | float
spd: None | float
if subtype in (1, 2):
v_ew_sign = -1 if mb[13] == "1" else 1
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
if subtype == 2: # Supersonic
v_ew *= 4
v_ew = common.bin2int(mb[14:24])
v_ns = common.bin2int(mb[25:35])
v_ns_sign = -1 if mb[24] == "1" else 1
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
if subtype == 2: # Supersonic
v_ns *= 4
if v_ew == 0 or v_ns == 0:
spd = None
trk_or_hdg = None
vs = None
else:
v_ew_sign = -1 if mb[13] == "1" else 1
v_ew = v_ew - 1 # east-west velocity
if subtype == 2: # Supersonic
v_ew *= 4
v_we = v_ew_sign * v_ew
v_sn = v_ns_sign * v_ns
v_ns_sign = -1 if mb[24] == "1" else 1
v_ns = v_ns - 1 # north-south velocity
if subtype == 2: # Supersonic
v_ns *= 4
spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
spd = int(spd)
v_we = v_ew_sign * v_ew
v_sn = v_ns_sign * v_ns
trk = math.atan2(v_we, v_sn)
trk = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val
spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
spd = int(spd)
tag = "GS"
trk_or_hdg = round(trk, 2)
dir_type = "true_north"
trk = math.atan2(v_we, v_sn)
trk = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val
trk_or_hdg = trk
spd_type = "GS"
dir_type = "TRUE_NORTH"
else:
if mb[13] == "0":
hdg = None
else:
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
hdg = round(hdg, 2)
hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
trk_or_hdg = hdg
spd = common.bin2int(mb[25:35])
spd = None if spd == 0 else spd - 1
if subtype == 4: # Supersonic
if subtype == 4 and spd is not None: # Supersonic
spd *= 4
if mb[24] == "0":
tag = "IAS"
spd_type = "IAS"
else:
tag = "TAS"
spd_type = "TAS"
dir_type = "mag_north"
dir_type = "MAGNETIC_NORTH"
vr_source = "GNSS" if mb[35] == "0" else "Baro"
vr_source = "GNSS" if mb[35] == "0" else "BARO"
vr_sign = -1 if mb[36] == "1" else 1
vr = common.bin2int(mb[37:46])
rocd = None if vr == 0 else int(vr_sign * (vr - 1) * 64)
vs = None if vr == 0 else int(vr_sign * (vr - 1) * 64)
if rtn_sources:
return spd, trk_or_hdg, rocd, tag, dir_type, vr_source
if source:
return ( # type: ignore
spd,
trk_or_hdg,
vs,
spd_type,
dir_type,
vr_source,
)
else:
return spd, trk_or_hdg, rocd, tag
return spd, trk_or_hdg, vs, spd_type
def altitude_diff(msg):
"""Decode the differece between GNSS and barometric altitude
def altitude_diff(msg: str) -> None | float:
"""Decode the differece between GNSS and barometric altitude.
Args:
msg (string): 28 bytes hexadecimal message string, TC=19
msg (str): 28 hexdigits string, TC=19
Returns:
int: Altitude difference in ft. Negative value indicates GNSS altitude
below barometric altitude.
int: Altitude difference in feet. Negative value indicates GNSS altitude
below barometric altitude.
"""
tc = common.typecode(msg)
if tc != 19:
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
if tc is None or tc != 19:
raise RuntimeError(
"%s: Not a airborne velocity message, expecting TC=19" % msg
)
msgbin = common.hex2bin(msg)
sign = -1 if int(msgbin[80]) else 1

View File

@ -1,68 +1,53 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 1,0
# Data link capability report
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from ... import common
def is10(msg):
def is10(msg: str) -> bool:
"""Check if a message is likely to be BDS code 1,0
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# first 8 bits must be 0x10
if d[0:8] != "00010000":
return False
# bit 10 to 14 are reserved
if bin2int(d[9:14]) != 0:
if common.bin2int(d[9:14]) != 0:
return False
# overlay capabilty conflict
if d[14] == "1" and bin2int(d[16:23]) < 5:
# overlay capability conflict
if d[14] == "1" and common.bin2int(d[16:23]) < 5:
return False
if d[14] == "0" and bin2int(d[16:23]) > 4:
if d[14] == "0" and common.bin2int(d[16:23]) > 4:
return False
return True
def ovc10(msg):
def ovc10(msg: str) -> int:
"""Return the overlay control capability
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: Whether the transponder is OVC capable
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
return int(d[14])

View File

@ -1,45 +1,29 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 1,7
# Common usage GICB capability report
# ------------------------------------------
from typing import List
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from ... import common
def is17(msg):
def is17(msg: str) -> bool:
"""Check if a message is likely to be BDS code 1,7
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if bin2int(d[28:56]) != 0:
if common.bin2int(d[24:56]) != 0:
return False
caps = cap17(msg)
@ -56,14 +40,14 @@ def is17(msg):
return True
def cap17(msg):
def cap17(msg: str) -> List[str]:
"""Extract capacities from BDS 1,7 message
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
list: list of suport BDS codes
list: list of supported BDS codes
"""
allbds = [
"05",
@ -90,14 +74,10 @@ def cap17(msg):
"56",
"5F",
"60",
"NA",
"NA",
"E1",
"E2",
]
d = hex2bin(data(msg))
idx = [i for i, v in enumerate(d[:28]) if v == "1"]
capacity = ["BDS" + allbds[i] for i in idx if allbds[i] is not "NA"]
d = common.hex2bin(common.data(msg))
idx = [i for i, v in enumerate(d[:24]) if v == "1"]
capacity = ["BDS" + allbds[i] for i in idx]
return capacity

View File

@ -1,74 +1,60 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 2,0
# Aircraft identification
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from ... import common
def is20(msg):
def is20(msg: str) -> bool:
"""Check if a message is likely to be BDS code 2,0
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0:8] != "00100000":
return False
cs = cs20(msg)
# allow empty callsign
if common.bin2int(d[8:56]) == 0:
return True
if "#" in cs:
if "#" in cs20(msg):
return False
return True
def cs20(msg):
def cs20(msg: str) -> str:
"""Aircraft callsign
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
msg (str): 28 hexdigits string
Returns:
string: callsign, max. 8 chars
"""
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
cs = ""
cs += chars[bin2int(d[8:14])]
cs += chars[bin2int(d[14:20])]
cs += chars[bin2int(d[20:26])]
cs += chars[bin2int(d[26:32])]
cs += chars[bin2int(d[32:38])]
cs += chars[bin2int(d[38:44])]
cs += chars[bin2int(d[44:50])]
cs += chars[bin2int(d[50:56])]
cs += chars[common.bin2int(d[8:14])]
cs += chars[common.bin2int(d[14:20])]
cs += chars[common.bin2int(d[20:26])]
cs += chars[common.bin2int(d[26:32])]
cs += chars[common.bin2int(d[32:38])]
cs += chars[common.bin2int(d[38:44])]
cs += chars[common.bin2int(d[44:50])]
cs += chars[common.bin2int(d[50:56])]
return cs

View File

@ -1,41 +1,25 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 3,0
# ACAS active resolution advisory
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from ... import common
def is30(msg):
"""Check if a message is likely to be BDS code 2,0
def is30(msg: str) -> bool:
"""Check if a message is likely to be BDS code 3,0
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0:8] != "00110000":
return False
@ -45,7 +29,7 @@ def is30(msg):
return False
# reserved for ACAS III, in far future
if bin2int(d[15:22]) >= 48:
if common.bin2int(d[15:22]) >= 48:
return False
return True

View File

@ -1,136 +1,124 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 4,0
# Selected vertical intention
# ------------------------------------------
from __future__ import absolute_import, print_function, division
import warnings
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from typing import Optional
from ... import common
def is40(msg):
def is40(msg: str) -> bool:
"""Check if a message is likely to be BDS code 4,0
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# status bit 1, 14, and 27
if wrongstatus(d, 1, 2, 13):
if common.wrongstatus(d, 1, 2, 13):
return False
if wrongstatus(d, 14, 15, 26):
if common.wrongstatus(d, 14, 15, 26):
return False
if wrongstatus(d, 27, 28, 39):
if common.wrongstatus(d, 27, 28, 39):
return False
if wrongstatus(d, 48, 49, 51):
if common.wrongstatus(d, 48, 49, 51):
return False
if wrongstatus(d, 54, 55, 56):
if common.wrongstatus(d, 54, 55, 56):
return False
# bits 40-47 and 52-53 shall all be zero
if bin2int(d[39:47]) != 0:
if common.bin2int(d[39:47]) != 0:
return False
if bin2int(d[51:53]) != 0:
if common.bin2int(d[51:53]) != 0:
return False
return True
def selalt40mcp(msg):
def selalt40mcp(msg: str) -> Optional[int]:
"""Selected altitude, MCP/FCU
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
msg (str): 28 hexdigits string
Returns:
int: altitude in feet
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0] == "0":
return None
alt = bin2int(d[1:13]) * 16 # ft
alt = common.bin2int(d[1:13]) * 16 # ft
return alt
def selalt40fms(msg):
def selalt40fms(msg: str) -> Optional[int]:
"""Selected altitude, FMS
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
msg (str): 28 hexdigits string
Returns:
int: altitude in feet
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[13] == "0":
return None
alt = bin2int(d[14:26]) * 16 # ft
alt = common.bin2int(d[14:26]) * 16 # ft
return alt
def p40baro(msg):
def p40baro(msg: str) -> Optional[float]:
"""Barometric pressure setting
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
msg (str): 28 hexdigits string
Returns:
float: pressure in millibar
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[26] == "0":
return None
p = bin2int(d[27:39]) * 0.1 + 800 # millibar
p = common.bin2int(d[27:39]) * 0.1 + 800 # millibar
return p
def alt40mcp(msg):
def alt40mcp(msg: str) -> Optional[int]:
warnings.warn(
"alt40mcp() has been renamed to selalt40mcp(). It will be removed in the future.",
"""alt40mcp() has been renamed to selalt40mcp().
It will be removed in the future.""",
DeprecationWarning,
)
return selalt40mcp(msg)
def alt40fms(msg):
def alt40fms(msg: str) -> Optional[int]:
warnings.warn(
"alt40fms() has been renamed to selalt40fms(). It will be removed in the future.",
"""alt40fms() has been renamed to selalt40fms().
It will be removed in the future.""",
DeprecationWarning,
)
return selalt40mcp(msg)
return selalt40fms(msg)

View File

@ -1,59 +1,45 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 4,4
# Meteorological routine air report
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from typing import Optional, Tuple
from ... import common
def is44(msg):
def is44(msg: str) -> bool:
"""Check if a message is likely to be BDS code 4,4.
Meteorological routine air report
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# status bit 5, 35, 47, 50
if wrongstatus(d, 5, 6, 23):
if common.wrongstatus(d, 5, 6, 23):
return False
if wrongstatus(d, 35, 36, 46):
if common.wrongstatus(d, 35, 36, 46):
return False
if wrongstatus(d, 47, 48, 49):
if common.wrongstatus(d, 47, 48, 49):
return False
if wrongstatus(d, 50, 51, 56):
if common.wrongstatus(d, 50, 51, 56):
return False
# Bits 1-4 indicate source, values > 4 reserved and should not occur
if bin2int(d[0:4]) > 4:
if common.bin2int(d[0:4]) > 4:
return False
vw, dw = wind44(msg)
@ -67,111 +53,109 @@ def is44(msg):
return True
def wind44(msg):
def wind44(msg: str) -> Tuple[Optional[int], Optional[float]]:
"""Wind speed and direction.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
(int, float): speed (kt), direction (degree)
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
status = int(d[4])
if not status:
return None, None
speed = bin2int(d[5:14]) # knots
direction = bin2int(d[14:23]) * 180.0 / 256.0 # degree
speed = common.bin2int(d[5:14]) # knots
direction = common.bin2int(d[14:23]) * 180 / 256 # degree
return round(speed, 0), round(direction, 1)
return speed, direction
def temp44(msg):
def temp44(msg: str) -> Tuple[float, float]:
"""Static air temperature.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
float, float: temperature and alternative temperature in Celsius degree.
Note: Two values returns due to what seems to be an inconsistancy
Note: Two values returns due to what seems to be an inconsistency
error in ICAO 9871 (2008) Appendix A-67.
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
sign = int(d[23])
value = bin2int(d[24:34])
value = common.bin2int(d[24:34])
if sign:
value = value - 1024
temp = value * 0.25 # celsius
temp = round(temp, 2)
temp_alternative = value * 0.125 # celsius
temp_alternative = round(temp_alternative, 3)
return temp, temp_alternative
def p44(msg):
def p44(msg: str) -> Optional[int]:
"""Static pressure.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: static pressure in hPa
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[34] == "0":
return None
p = bin2int(d[35:46]) # hPa
p = common.bin2int(d[35:46]) # hPa
return p
def hum44(msg):
def hum44(msg: str) -> Optional[float]:
"""humidity
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
float: percentage of humidity, [0 - 100] %
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[49] == "0":
return None
hm = bin2int(d[50:56]) * 100.0 / 64 # %
hm = common.bin2int(d[50:56]) * 100 / 64 # %
return round(hm, 1)
return hm
def turb44(msg):
"""Turblence.
def turb44(msg: str) -> Optional[int]:
"""Turbulence.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[46] == "0":
return None
turb = bin2int(d[47:49])
turb = common.bin2int(d[47:49])
return turb

View File

@ -1,71 +1,57 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 4,5
# Meteorological hazard report
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from typing import Optional
from ... import common
def is45(msg):
def is45(msg: str) -> bool:
"""Check if a message is likely to be BDS code 4,5.
Meteorological hazard report
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# status bit 1, 4, 7, 10, 13, 16, 27, 39
if wrongstatus(d, 1, 2, 3):
if common.wrongstatus(d, 1, 2, 3):
return False
if wrongstatus(d, 4, 5, 6):
if common.wrongstatus(d, 4, 5, 6):
return False
if wrongstatus(d, 7, 8, 9):
if common.wrongstatus(d, 7, 8, 9):
return False
if wrongstatus(d, 10, 11, 12):
if common.wrongstatus(d, 10, 11, 12):
return False
if wrongstatus(d, 13, 14, 15):
if common.wrongstatus(d, 13, 14, 15):
return False
if wrongstatus(d, 16, 17, 26):
if common.wrongstatus(d, 16, 17, 26):
return False
if wrongstatus(d, 27, 28, 38):
if common.wrongstatus(d, 27, 28, 38):
return False
if wrongstatus(d, 39, 40, 51):
if common.wrongstatus(d, 39, 40, 51):
return False
# reserved
if bin2int(d[51:56]) != 0:
if common.bin2int(d[51:56]) != 0:
return False
temp = temp45(msg)
@ -76,149 +62,148 @@ def is45(msg):
return True
def turb45(msg):
def turb45(msg: str) -> Optional[int]:
"""Turbulence.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: Turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0] == "0":
return None
turb = bin2int(d[1:3])
turb = common.bin2int(d[1:3])
return turb
def ws45(msg):
def ws45(msg: str) -> Optional[int]:
"""Wind shear.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: Wind shear level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[3] == "0":
return None
ws = bin2int(d[4:6])
ws = common.bin2int(d[4:6])
return ws
def mb45(msg):
def mb45(msg: str) -> Optional[int]:
"""Microburst.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: Microburst level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[6] == "0":
return None
mb = bin2int(d[7:9])
mb = common.bin2int(d[7:9])
return mb
def ic45(msg):
def ic45(msg: str) -> Optional[int]:
"""Icing.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: Icing level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[9] == "0":
return None
ic = bin2int(d[10:12])
ic = common.bin2int(d[10:12])
return ic
def wv45(msg):
def wv45(msg: str) -> Optional[int]:
"""Wake vortex.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: Wake vortex level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[12] == "0":
return None
ws = bin2int(d[13:15])
ws = common.bin2int(d[13:15])
return ws
def temp45(msg):
def temp45(msg: str) -> Optional[float]:
"""Static air temperature.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
float: tmeperature in Celsius degree
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
sign = int(d[16])
value = bin2int(d[17:26])
value = common.bin2int(d[17:26])
if sign:
value = value - 512
temp = value * 0.25 # celsius
temp = round(temp, 1)
return temp
def p45(msg):
def p45(msg: str) -> Optional[int]:
"""Average static pressure.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: static pressure in hPa
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[26] == "0":
return None
p = bin2int(d[27:38]) # hPa
p = common.bin2int(d[27:38]) # hPa
return p
def rh45(msg):
def rh45(msg: str) -> Optional[int]:
"""Radio height.
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: radio height in ft
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[38] == "0":
return None
rh = bin2int(d[39:51]) * 16
rh = common.bin2int(d[39:51]) * 16
return rh

View File

@ -1,62 +1,48 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 5,0
# Track and turn report
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from typing import Optional
from ... import common
def is50(msg):
def is50(msg: str) -> bool:
"""Check if a message is likely to be BDS code 5,0
(Track and turn report)
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# status bit 1, 12, 24, 35, 46
if wrongstatus(d, 1, 3, 11):
if common.wrongstatus(d, 1, 3, 11):
return False
if wrongstatus(d, 12, 13, 23):
if common.wrongstatus(d, 12, 13, 23):
return False
if wrongstatus(d, 24, 25, 34):
if common.wrongstatus(d, 24, 25, 34):
return False
if wrongstatus(d, 35, 36, 45):
if common.wrongstatus(d, 35, 36, 45):
return False
if wrongstatus(d, 46, 47, 56):
if common.wrongstatus(d, 46, 47, 56):
return False
roll = roll50(msg)
if (roll is not None) and abs(roll) > 60:
if (roll is not None) and abs(roll) > 50:
return False
gs = gs50(msg)
@ -73,88 +59,88 @@ def is50(msg):
return True
def roll50(msg):
def roll50(msg: str) -> Optional[float]:
"""Roll angle, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
msg (str): 28 hexdigits string
Returns:
float: angle in degrees,
negative->left wing down, positive->right wing down
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0] == "0":
return None
sign = int(d[1]) # 1 -> left wing down
value = bin2int(d[2:11])
value = common.bin2int(d[2:11])
if sign:
value = value - 512
angle = value * 45.0 / 256.0 # degree
return round(angle, 1)
angle = value * 45 / 256 # degree
return angle
def trk50(msg):
def trk50(msg: str) -> Optional[float]:
"""True track angle, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
msg (str): 28 hexdigits string
Returns:
float: angle in degrees to true north (from 0 to 360)
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[11] == "0":
return None
sign = int(d[12]) # 1 -> west
value = bin2int(d[13:23])
value = common.bin2int(d[13:23])
if sign:
value = value - 1024
trk = value * 90.0 / 512.0
trk = value * 90 / 512.0
# convert from [-180, 180] to [0, 360]
if trk < 0:
trk = 360 + trk
return round(trk, 3)
return trk
def gs50(msg):
def gs50(msg: str) -> Optional[float]:
"""Ground speed, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
msg (str): 28 hexdigits string
Returns:
int: ground speed in knots
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[23] == "0":
return None
spd = bin2int(d[24:34]) * 2 # kts
spd = common.bin2int(d[24:34]) * 2 # kts
return spd
def rtrk50(msg):
def rtrk50(msg: str) -> Optional[float]:
"""Track angle rate, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
msg (str): 28 hexdigits string
Returns:
float: angle rate in degrees/second
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[34] == "0":
return None
@ -163,27 +149,27 @@ def rtrk50(msg):
return None
sign = int(d[35]) # 1 -> negative value, two's complement
value = bin2int(d[36:45])
value = common.bin2int(d[36:45])
if sign:
value = value - 512
angle = value * 8.0 / 256.0 # degree / sec
return round(angle, 3)
angle = value * 8 / 256 # degree / sec
return angle
def tas50(msg):
def tas50(msg: str) -> Optional[float]:
"""Aircraft true airspeed, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
msg (str): 28 hexdigits string
Returns:
int: true airspeed in knots
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[45] == "0":
return None
tas = bin2int(d[46:56]) * 2 # kts
tas = common.bin2int(d[46:56]) * 2 # kts
return tas

View File

@ -1,58 +1,44 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 5,3
# Air-referenced state vector
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from typing import Optional
from ... import common
def is53(msg):
def is53(msg: str) -> bool:
"""Check if a message is likely to be BDS code 5,3
(Air-referenced state vector)
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# status bit 1, 13, 24, 34, 47
if wrongstatus(d, 1, 3, 12):
if common.wrongstatus(d, 1, 3, 12):
return False
if wrongstatus(d, 13, 14, 23):
if common.wrongstatus(d, 13, 14, 23):
return False
if wrongstatus(d, 24, 25, 33):
if common.wrongstatus(d, 24, 25, 33):
return False
if wrongstatus(d, 34, 35, 46):
if common.wrongstatus(d, 34, 35, 46):
return False
if wrongstatus(d, 47, 49, 56):
if common.wrongstatus(d, 47, 49, 56):
return False
ias = ias53(msg)
@ -74,105 +60,105 @@ def is53(msg):
return True
def hdg53(msg):
def hdg53(msg: str) -> Optional[float]:
"""Magnetic heading, BDS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message (BDS53) string
msg (str): 28 hexdigits string
Returns:
float: angle in degrees to true north (from 0 to 360)
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0] == "0":
return None
sign = int(d[1]) # 1 -> west
value = bin2int(d[2:12])
value = common.bin2int(d[2:12])
if sign:
value = value - 1024
hdg = value * 90.0 / 512.0 # degree
hdg = value * 90 / 512 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
hdg = 360 + hdg
return round(hdg, 3)
return hdg
def ias53(msg):
def ias53(msg: str) -> Optional[float]:
"""Indicated airspeed, DBS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message
msg (str): 28 hexdigits
Returns:
int: indicated arispeed in knots
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[12] == "0":
return None
ias = bin2int(d[13:23]) # knots
ias = common.bin2int(d[13:23]) # knots
return ias
def mach53(msg):
def mach53(msg: str) -> Optional[float]:
"""MACH number, DBS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message
msg (str): 28 hexdigits
Returns:
float: MACH number
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[23] == "0":
return None
mach = bin2int(d[24:33]) * 0.008
return round(mach, 3)
mach = common.bin2int(d[24:33]) * 0.008
return mach
def tas53(msg):
def tas53(msg: str) -> Optional[float]:
"""Aircraft true airspeed, BDS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message
msg (str): 28 hexdigits
Returns:
float: true airspeed in knots
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[33] == "0":
return None
tas = bin2int(d[34:46]) * 0.5 # kts
return round(tas, 1)
tas = common.bin2int(d[34:46]) * 0.5 # kts
return tas
def vr53(msg):
def vr53(msg: str) -> Optional[int]:
"""Vertical rate
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
msg (str): 28 hexdigits (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[46] == "0":
return None
sign = int(d[47]) # 1 -> negative value, two's complement
value = bin2int(d[48:56])
value = common.bin2int(d[48:56])
if value == 0 or value == 255: # all zeros or all ones
return 0

View File

@ -1,57 +1,44 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 6,0
# Heading and speed report
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from typing import Optional
from ... import common
from ...extra import aero
def is60(msg):
def is60(msg: str) -> bool:
"""Check if a message is likely to be BDS code 6,0
Args:
msg (String): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
bool: True or False
"""
if allzeros(msg):
if common.allzeros(msg):
return False
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
# status bit 1, 13, 24, 35, 46
if wrongstatus(d, 1, 2, 12):
if common.wrongstatus(d, 1, 2, 12):
return False
if wrongstatus(d, 13, 14, 23):
if common.wrongstatus(d, 13, 14, 23):
return False
if wrongstatus(d, 24, 25, 34):
if common.wrongstatus(d, 24, 25, 34):
return False
if wrongstatus(d, 35, 36, 45):
if common.wrongstatus(d, 35, 36, 45):
return False
if wrongstatus(d, 46, 47, 56):
if common.wrongstatus(d, 46, 47, 56):
return False
ias = ias60(msg)
@ -70,90 +57,98 @@ def is60(msg):
if vr_ins is not None and abs(vr_ins) > 6000:
return False
# additional check knowing altitude
if (mach is not None) and (ias is not None) and (common.df(msg) == 20):
alt = common.altcode(msg)
if alt is not None:
ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts
if abs(ias - ias_) > 20:
return False
return True
def hdg60(msg):
def hdg60(msg: str) -> Optional[float]:
"""Megnetic heading of aircraft
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
msg (str): 28 hexdigits string
Returns:
float: heading in degrees to megnetic north (from 0 to 360)
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[0] == "0":
return None
sign = int(d[1]) # 1 -> west
value = bin2int(d[2:12])
value = common.bin2int(d[2:12])
if sign:
value = value - 1024
hdg = value * 90 / 512.0 # degree
hdg = value * 90 / 512 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
hdg = 360 + hdg
return round(hdg, 3)
return hdg
def ias60(msg):
def ias60(msg: str) -> Optional[float]:
"""Indicated airspeed
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
msg (str): 28 hexdigits string
Returns:
int: indicated airspeed in knots
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[12] == "0":
return None
ias = bin2int(d[13:23]) # kts
ias = common.bin2int(d[13:23]) # kts
return ias
def mach60(msg):
def mach60(msg: str) -> Optional[float]:
"""Aircraft MACH number
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
msg (str): 28 hexdigits string
Returns:
float: MACH number
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[23] == "0":
return None
mach = bin2int(d[24:34]) * 2.048 / 512.0
return round(mach, 3)
mach = common.bin2int(d[24:34]) * 2.048 / 512.0
return mach
def vr60baro(msg):
def vr60baro(msg: str) -> Optional[int]:
"""Vertical rate from barometric measurement, this value may be very noisy.
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
msg (str): 28 hexdigits string
Returns:
int: vertical rate in feet/minutes
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[34] == "0":
return None
sign = int(d[35]) # 1 -> negative value, two's complement
value = bin2int(d[36:45])
value = common.bin2int(d[36:45])
if value == 0 or value == 511: # all zeros or all ones
return 0
@ -164,22 +159,22 @@ def vr60baro(msg):
return roc
def vr60ins(msg):
"""Vertical rate messured by onbard equiments (IRS, AHRS)
def vr60ins(msg: str) -> Optional[int]:
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
msg (str): 28 hexdigits string
Returns:
int: vertical rate in feet/minutes
"""
d = hex2bin(data(msg))
d = common.hex2bin(common.data(msg))
if d[45] == "0":
return None
sign = int(d[46]) # 1 -> negative value, two's complement
value = bin2int(d[47:56])
value = common.bin2int(d[47:56])
if value == 0 or value == 511: # all zeros or all ones
return 0

View File

@ -0,0 +1,87 @@
# ------------------------------------------
# BDS 6,1
# ADS-B TC=28
# Aircraft Airborne status
# ------------------------------------------
from ... import common
def is_emergency(msg: str) -> bool:
"""Check if the aircraft is reporting an emergency.
Non-emergencies are either a subtype of zero (no information) or
subtype of one and a value of zero (no emergency).
Subtype = 2 indicates an ACAS RA broadcast, look in BDS 3,0
:param msg: 28 bytes hexadecimal message string
:return: if the aircraft has declared an emergency
"""
if common.typecode(msg) != 28:
raise RuntimeError(
"%s: Not an airborne status message, expecting TC=28" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:8])
if subtype == 2:
raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented")
emergency_state = common.bin2int(mb[8:11])
if subtype == 1 and emergency_state == 1:
return True
else:
return False
def emergency_state(msg: str) -> int:
"""Decode aircraft emergency state.
Value Meaning
----- -----------------------
0 No emergency
1 General emergency
2 Lifeguard/Medical
3 Minimum fuel
4 No communications
5 Unlawful communications
6-7 Reserved
:param msg: 28 bytes hexadecimal message string
:return: emergency state
"""
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:8])
if subtype == 2:
raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented")
emergency_state = common.bin2int(mb[8:11])
return emergency_state
def emergency_squawk(msg: str) -> str:
"""Decode squawk code.
Emergency value 1: squawk 7700.
Emergency value 4: squawk 7600.
Emergency value 5: squawk 7500.
:param msg: 28 bytes hexadecimal message string
:return: aircraft squawk code
"""
if common.typecode(msg) != 28:
raise RuntimeError(
"%s: Not an airborne status message, expecting TC=28" % msg
)
msgbin = common.hex2bin(msg)
# construct the 13 bits Mode A ID code
idcode = msgbin[43:56]
squawk = common.squawk(idcode)
return squawk

View File

@ -0,0 +1,552 @@
# ------------------------------------------
# BDS 6,2
# ADS-B TC=29
# Target State and Status
# ------------------------------------------
from __future__ import annotations
from ... import common
def selected_altitude(msg: str) -> tuple[None | float, str]:
"""Decode selected altitude.
Args:
msg (str): 28 hexdigits string
Returns:
int: Selected altitude (ft)
string: Source ('MCP/FCU' or 'FMS')
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not"
" contain selected altitude, use target altitude instead" % msg
)
alt = common.bin2int(mb[9:20])
if alt == 0:
return None, "N/A"
alt = (alt - 1) * 32
alt_source = "MCP/FCU" if int(mb[8]) == 0 else "FMS"
return alt, alt_source
def target_altitude(msg: str) -> tuple[None | int, str, str]:
"""Decode target altitude.
Args:
msg (str): 28 hexdigits string
Returns:
int: Target altitude (ft)
string: Source ('MCP/FCU', 'Holding mode' or 'FMS/RNAV')
string: Altitude reference, either pressure altitude or barometric
corrected altitude ('FL' or 'MSL')
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError(
"%s: ADS-B version 2 target state and status message does not"
" contain target altitude, use selected altitude instead" % msg
)
alt_avail = common.bin2int(mb[7:9])
if alt_avail == 0:
return None, "N/A", ""
elif alt_avail == 1:
alt_source = "MCP/FCU"
elif alt_avail == 2:
alt_source = "Holding mode"
else:
alt_source = "FMS/RNAV"
alt_ref = "FL" if int(mb[9]) == 0 else "MSL"
alt = -1000 + common.bin2int(mb[15:25]) * 100
return alt, alt_source, alt_ref
def vertical_mode(msg: str) -> None | int:
"""Decode vertical mode.
Value Meaning
----- -----------------------
1 "Acquiring" mode
2 "Capturing" or "Maintaining" mode
3 Reserved
Args:
msg (str): 28 hexdigits string
Returns:
int: Vertical mode
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError(
"%s: ADS-B version 2 target state and status message does not"
" contain vertical mode, use vnav mode instead" % msg
)
vertical_mode = common.bin2int(mb[13:15])
if vertical_mode == 0:
return None
return vertical_mode
def horizontal_mode(msg: str) -> None | int:
"""Decode horizontal mode.
Value Meaning
----- -----------------------
1 "Acquiring" mode
2 "Capturing" or "Maintaining" mode
3 Reserved
Args:
msg (str): 28 hexdigits string
Returns:
int: Horizontal mode
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError(
"%s: ADS-B version 2 target state and status message does not "
"contain horizontal mode, use lnav mode instead" % msg
)
horizontal_mode = common.bin2int(mb[25:27])
if horizontal_mode == 0:
return None
return horizontal_mode
def selected_heading(msg: str) -> None | float:
"""Decode selected heading.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
float: Selected heading (degree)
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain selected heading, use target angle instead" % msg
)
if int(mb[29]) == 0:
return None
else:
hdg_sign = int(mb[30])
hdg = (hdg_sign + 1) * common.bin2int(mb[31:39]) * (180 / 256)
return hdg
def target_angle(msg: str) -> tuple[None | int, str, str]:
"""Decode target heading/track angle.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
int: Target angle (degree)
string: Angle type ('Heading' or 'Track')
string: Source ('MCP/FCU', 'Autopilot Mode' or 'FMS/RNAV')
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError(
"%s: ADS-B version 2 target state and status message does not "
"contain target angle, use selected heading instead" % msg
)
angle_avail = common.bin2int(mb[25:27])
if angle_avail == 0:
return None, "", "N/A"
else:
angle = common.bin2int(mb[27:36])
if angle_avail == 1:
angle_source = "MCP/FCU"
elif angle_avail == 2:
angle_source = "Autopilot mode"
else:
angle_source = "FMS/RNAV"
angle_type = "Heading" if int(mb[36]) else "Track"
return angle, angle_type, angle_source
def baro_pressure_setting(msg: str) -> None | float:
"""Decode barometric pressure setting.
Args:
msg (str): 28 hexdigits string
Returns:
float: Barometric pressure setting (millibars)
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain barometric pressure setting" % msg
)
baro = common.bin2int(mb[20:29])
if baro == 0:
return None
return 800 + (baro - 1) * 0.8
def autopilot(msg) -> None | bool:
"""Decode autopilot engagement.
Args:
msg (str): 28 hexdigits string
Returns:
bool: Autopilot engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain autopilot engagement" % msg
)
if int(mb[46]) == 0:
return None
autopilot = True if int(mb[47]) == 1 else False
return autopilot
def vnav_mode(msg) -> None | bool:
"""Decode VNAV mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: VNAV mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain vnav mode, use vertical mode instead" % msg
)
if int(mb[46]) == 0:
return None
vnav_mode = True if int(mb[48]) == 1 else False
return vnav_mode
def altitude_hold_mode(msg) -> None | bool:
"""Decode altitude hold mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: Altitude hold mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain altitude hold mode" % msg
)
if int(mb[46]) == 0:
return None
alt_hold_mode = True if int(mb[49]) == 1 else False
return alt_hold_mode
def approach_mode(msg) -> None | bool:
"""Decode approach mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: Approach mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain approach mode" % msg
)
if int(mb[46]) == 0:
return None
app_mode = True if int(mb[51]) == 1 else False
return app_mode
def lnav_mode(msg) -> None | bool:
"""Decode LNAV mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: LNAV mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError(
"%s: ADS-B version 1 target state and status message does not "
"contain lnav mode, use horizontal mode instead" % msg
)
if int(mb[46]) == 0:
return None
lnav_mode = True if int(mb[53]) == 1 else False
return lnav_mode
def tcas_operational(msg) -> None | bool:
"""Decode TCAS/ACAS operational.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
bool: TCAS/ACAS operational
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
tcas = True if int(mb[51]) == 0 else False
else:
tcas = True if int(mb[52]) == 1 else False
return tcas
def tcas_ra(msg) -> bool:
"""Decode TCAS/ACAS Resolution advisory.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
bool: TCAS/ACAS Resolution advisory active
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError(
"%s: ADS-B version 2 target state and status message does not "
"contain TCAS/ACAS RA" % msg
)
tcas_ra = True if int(mb[52]) == 1 else False
return tcas_ra
def emergency_status(msg) -> int:
"""Decode aircraft emergency status.
Value Meaning
----- -----------------------
0 No emergency
1 General emergency
2 Lifeguard/medical emergency
3 Minimum fuel
4 No communications
5 Unlawful interference
6 Downed aircraft
7 Reserved
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
int: Emergency status
"""
if common.typecode(msg) != 29:
raise RuntimeError(
"%s: Not a target state and status message, expecting TC=29" % msg
)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError(
"%s: ADS-B version 2 target state and status message does not "
"contain emergency status" % msg
)
return common.bin2int(mb[53:56])

View File

@ -1,37 +1,88 @@
"""Comm-B Wrapper.
"""Comm-B module.
The Comm-B wrapper imports all functions from the following modules:
The Comm-B module imports all functions from the following modules:
**ELS - elementary surveillance**
- pyModeS.decoder.bds.bds10
- pyModeS.decoder.bds.bds17
- pyModeS.decoder.bds.bds20
- pyModeS.decoder.bds.bds30
ELS - elementary surveillance
**EHS - enhanced surveillance**
- pyModeS.decoder.bds.bds40
- pyModeS.decoder.bds.bds50
- pyModeS.decoder.bds.bds60
- pyModeS.decoder.bds.bds10
- pyModeS.decoder.bds.bds17
- pyModeS.decoder.bds.bds20
- pyModeS.decoder.bds.bds30
**MRAR and MHR**
- pyModeS.decoder.bds.bds44
- pyModeS.decoder.bds.bds45
EHS - enhanced surveillance
- pyModeS.decoder.bds.bds40
- pyModeS.decoder.bds.bds50
- pyModeS.decoder.bds.bds60
MRAR and MHR
- pyModeS.decoder.bds.bds44
- pyModeS.decoder.bds.bds45
"""
from __future__ import absolute_import, print_function, division
# ELS - elementary surveillance
from pyModeS.decoder.bds.bds10 import *
from pyModeS.decoder.bds.bds17 import *
from pyModeS.decoder.bds.bds20 import *
from pyModeS.decoder.bds.bds30 import *
from .bds.bds10 import is10, ovc10
from .bds.bds17 import is17, cap17
from .bds.bds20 import is20, cs20
from .bds.bds30 import is30
# ELS - enhanced surveillance
from pyModeS.decoder.bds.bds40 import *
from pyModeS.decoder.bds.bds50 import *
from pyModeS.decoder.bds.bds60 import *
from .bds.bds40 import (
is40,
selalt40fms,
selalt40mcp,
p40baro,
alt40fms,
alt40mcp,
)
from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50
from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins
# MRAR and MHR
from pyModeS.decoder.bds.bds44 import *
from pyModeS.decoder.bds.bds45 import *
from .bds.bds44 import is44, wind44, temp44, p44, hum44, turb44
from .bds.bds45 import is45, turb45, ws45, mb45, ic45, wv45, temp45, p45, rh45
__all__ = [
"is10",
"ovc10",
"is17",
"cap17",
"is20",
"cs20",
"is30",
"is40",
"selalt40fms",
"selalt40mcp",
"p40baro",
"alt40fms",
"alt40mcp",
"is50",
"roll50",
"trk50",
"gs50",
"rtrk50",
"tas50",
"is60",
"hdg60",
"ias60",
"mach60",
"vr60baro",
"vr60ins",
"is44",
"wind44",
"temp44",
"p44",
"hum44",
"turb44",
"is45",
"turb45",
"ws45",
"mb45",
"ic45",
"wv45",
"temp45",
"p45",
"rh45",
]

View File

@ -9,28 +9,58 @@ The EHS wrapper imports all functions from the following modules:
"""
from __future__ import absolute_import, print_function, division
import warnings
from pyModeS.decoder.bds.bds40 import *
from pyModeS.decoder.bds.bds50 import *
from pyModeS.decoder.bds.bds60 import *
from pyModeS.decoder.bds import infer
from .bds.bds40 import (
is40,
selalt40fms,
selalt40mcp,
p40baro,
alt40fms,
alt40mcp,
)
from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50
from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins
from .bds import infer
__all__ = [
"is40",
"selalt40fms",
"selalt40mcp",
"p40baro",
"alt40fms",
"alt40mcp",
"is50",
"roll50",
"trk50",
"gs50",
"rtrk50",
"tas50",
"is60",
"hdg60",
"ias60",
"mach60",
"vr60baro",
"vr60ins",
"infer",
]
warnings.simplefilter("once", DeprecationWarning)
warnings.warn(
"pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning
"pms.ehs module is deprecated. Please use pms.commb instead.",
DeprecationWarning,
)
def BDS(msg):
warnings.warn(
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.",
DeprecationWarning,
)
return infer(msg)
def icao(msg):
from pyModeS.decoder.common import icao
from . import common
return icao(msg)
return common.icao(msg)

View File

@ -10,16 +10,26 @@ The ELS wrapper imports all functions from the following modules:
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.bds.bds10 import *
from pyModeS.decoder.bds.bds17 import *
from pyModeS.decoder.bds.bds20 import *
from pyModeS.decoder.bds.bds30 import *
import warnings
from .bds.bds10 import is10, ovc10
from .bds.bds17 import cap17, is17
from .bds.bds20 import cs20, is20
from .bds.bds30 import is30
warnings.simplefilter("once", DeprecationWarning)
warnings.warn(
"pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning
"pms.els module is deprecated. Please use pms.commb instead.",
DeprecationWarning,
)
__all__ = [
"is10",
"ovc10",
"is17",
"cap17",
"is20",
"cs20",
"is30",
]

View File

@ -0,0 +1,26 @@
from typing import TypedDict
from typing_extensions import Annotated
from .decode import flarm as flarm_decode
__all__ = ["DecodedMessage", "flarm"]
class DecodedMessage(TypedDict):
timestamp: int
icao24: str
latitude: float
longitude: float
altitude: Annotated[int, "m"]
vertical_speed: Annotated[float, "m/s"]
groundspeed: int
track: int
type: str
sensorLatitude: float
sensorLongitude: float
isIcao24: bool
noTrack: bool
stealth: bool
flarm = flarm_decode

View File

@ -0,0 +1,124 @@
#include "core.h"
/*
*
* https://pastebin.com/YK2f8bfm
*
* NEW ENCRYPTION
*
* Swiss glider anti-colission system moved to a new encryption scheme: XXTEA
* The algorithm encrypts all the packet after the header: total 20 bytes or 5 long int words of data
*
* XXTEA description and code are found here: http://en.wikipedia.org/wiki/XXTEA
* The system uses 6 iterations of the main loop.
*
* The system version 6 sends two type of packets: position and ... some unknown data
* The difference is made by bit 0 of byte 3 of the packet: for position data this bit is zero.
*
* For position data the key used depends on the time and transmitting device address.
* The key is as well obscured by a weird algorithm.
* The code to generate the key is:
*
* */
void make_key(int *key, long time, long address)
{
const long key1[4] = {0xe43276df, 0xdca83759, 0x9802b8ac, 0x4675a56b};
const long key1b[4] = {0xfc78ea65, 0x804b90ea, 0xb76542cd, 0x329dfa32};
const long *table = ((((time >> 23) & 255) & 0x01) != 0) ? key1b : key1;
for (int i = 0; i < 4; i++)
{
key[i] = obscure(table[i] ^ ((time >> 6) ^ address), 0x045D9F3B) ^ 0x87B562F4;
}
}
long obscure(long key, unsigned long seed)
{
unsigned int m1 = seed * (key ^ (key >> 16));
unsigned int m2 = seed * (m1 ^ (m1 >> 16));
return m2 ^ (m2 >> 16);
}
/*
* NEW PACKET FORMAT:
*
* Byte Bits
* 0 AAAA AAAA device address
* 1 AAAA AAAA
* 2 AAAA AAAA
* 3 00aa 0000 aa = 10 or 01
*
* 4 vvvv vvvv vertical speed
* 5 xxxx xxvv
* 6 gggg gggg GPS status
* 7 tttt gggg plane type
*
* 8 LLLL LLLL Latitude
* 9 LLLL LLLL
* 10 aaaa aLLL
* 11 aaaa aaaa Altitude
*
* 12 NNNN NNNN Longitude
* 13 NNNN NNNN
* 14 xxxx NNNN
* 15 FFxx xxxx multiplying factor
*
* 16 SSSS SSSS as in version 4
* 17 ssss ssss
* 18 KKKK KKKK
* 19 kkkk kkkk
*
* 20 EEEE EEEE
* 21 eeee eeee
* 22 PPPP PPPP
* 24 pppp pppp
* */
/*
* https://en.wikipedia.org/wiki/XXTEA
*/
void btea(uint32_t *v, int n, uint32_t const key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
if (n > 1)
{ /* Coding Part */
/* Unused, should remove? */
rounds = 6 + 52 / n;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < (unsigned)n - 1; p++)
{
y = v[p + 1];
z = v[p] += MX;
}
y = v[0];
z = v[n - 1] += MX;
} while (--rounds);
}
else if (n < -1)
{ /* Decoding Part */
n = -n;
rounds = 6; // + 52 / n;
sum = rounds * DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p = n - 1; p > 0; p--)
{
z = v[p - 1];
y = v[p] -= MX;
}
z = v[n - 1];
y = v[0] -= MX;
sum -= DELTA;
} while (--rounds);
}
}

View File

@ -0,0 +1,13 @@
#ifndef __CORE_H__
#define __CORE_H__
#include <stdint.h>
#define DELTA 0x9e3779b9
#define MX (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))
void make_key(int *key, long time, long address);
long obscure(long key, unsigned long seed);
void btea(uint32_t *v, int n, uint32_t const key[4]);
#endif

View File

@ -0,0 +1,4 @@
cdef extern from "core.h":
void make_key(int*, long time, long address)
void btea(int*, int, int*)

View File

@ -0,0 +1,14 @@
from typing import Any
from . import DecodedMessage
AIRCRAFT_TYPES: list[str]
def flarm(
timestamp: int,
msg: str,
refLat: float,
refLon: float,
**kwargs: Any,
) -> DecodedMessage: ...

View File

@ -0,0 +1,147 @@
from cpython cimport array
from .core cimport make_key as c_make_key, btea as c_btea
import array
import math
from ctypes import c_byte
from textwrap import wrap
AIRCRAFT_TYPES = [
"Unknown", # 0
"Glider", # 1
"Tow-Plane", # 2
"Helicopter", # 3
"Parachute", # 4
"Parachute Drop-Plane", # 5
"Hangglider", # 6
"Paraglider", # 7
"Aircraft", # 8
"Jet", # 9
"UFO", # 10
"Balloon", # 11
"Airship", # 12
"UAV", # 13
"Reserved", # 14
"Static Obstacle", # 15
]
cdef long bytearray2int(str icao24):
return (
(int(icao24[4:6], 16) & 0xFF)
| ((int(icao24[2:4], 16) & 0xFF) << 8)
| ((int(icao24[:2], 16) & 0xFF) << 16)
)
cpdef array.array make_key(long timestamp, str icao24):
cdef long addr = bytearray2int(icao24)
cdef array.array a = array.array('i', [0, 0, 0, 0])
c_make_key(a.data.as_ints, timestamp, (addr << 8) & 0xffffff)
return a
cpdef array.array btea(long timestamp, str msg):
cdef int p
cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2]
cdef array.array key = make_key(timestamp, icao24)
pieces = wrap(msg[8:], 8)
cdef array.array toDecode = array.array('i', len(pieces) * [0])
for i, piece in enumerate(pieces):
p = 0
for elt in wrap(piece, 2)[::-1]:
p = (p << 8) + int(elt, 16)
toDecode[i] = p
c_btea(toDecode.data.as_ints, -5, key.data.as_ints)
return toDecode
cdef float velocity(int ns, int ew):
return math.hypot(ew / 4, ns / 4)
def heading(ns, ew, velocity):
if velocity < 1e-6:
velocity = 1
return (math.atan2(ew / velocity / 4, ns / velocity / 4) / 0.01745) % 360
def turningRate(a1, a2):
return ((((a2 - a1)) + 540) % 360) - 180
def flarm(long timestamp, str msg, float refLat, float refLon, **kwargs):
"""Decode a FLARM message.
Args:
timestamp (int)
msg (str)
refLat (float): the receiver's location
refLon (float): the receiver's location
Returns:
a dictionary with all decoded fields. Any extra keyword argument passed
is included in the output dictionary.
"""
cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2]
cdef int magic = int(msg[6:8], 16)
if magic != 0x10 and magic != 0x20:
return None
cdef array.array decoded = btea(timestamp, msg)
cdef int aircraft_type = (decoded[0] >> 28) & 0xF
cdef int gps = (decoded[0] >> 16) & 0xFFF
cdef int raw_vs = c_byte(decoded[0] & 0x3FF).value
noTrack = ((decoded[0] >> 14) & 0x1) == 1
stealth = ((decoded[0] >> 13) & 0x1) == 1
cdef int altitude = (decoded[1] >> 19) & 0x1FFF
cdef int lat = decoded[1] & 0x7FFFF
cdef int mult_factor = 1 << ((decoded[2] >> 30) & 0x3)
cdef int lon = decoded[2] & 0xFFFFF
ns = list(
c_byte((decoded[3] >> (i * 8)) & 0xFF).value * mult_factor
for i in range(4)
)
ew = list(
c_byte((decoded[4] >> (i * 8)) & 0xFF).value * mult_factor
for i in range(4)
)
cdef int roundLat = int(refLat * 1e7) >> 7
lat = (lat - roundLat) % 0x080000
if lat >= 0x040000:
lat -= 0x080000
lat = (((lat + roundLat) << 7) + 0x40)
roundLon = int(refLon * 1e7) >> 7
lon = (lon - roundLon) % 0x100000
if lon >= 0x080000:
lon -= 0x100000
lon = (((lon + roundLon) << 7) + 0x40)
speed = sum(velocity(n, e) for n, e in zip(ns, ew)) / 4
heading4 = heading(ns[0], ew[0], speed)
heading8 = heading(ns[1], ew[1], speed)
return dict(
timestamp=timestamp,
icao24=icao24,
latitude=lat * 1e-7,
longitude=lon * 1e-7,
geoaltitude=altitude,
vertical_speed=raw_vs * mult_factor / 10,
groundspeed=speed,
track=heading4 - 4 * turningRate(heading4, heading8) / 4,
type=AIRCRAFT_TYPES[aircraft_type],
sensorLatitude=refLat,
sensorLongitude=refLon,
isIcao24=magic==0x10,
noTrack=noTrack,
stealth=stealth,
gps=gps,
**kwargs
)

View File

@ -1,23 +1,138 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Warpper for short roll call surveillance replies DF=4/5
[To be implemented]
Decode short roll call surveillance replies, with downlink format 4 or 5
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
from __future__ import annotations
from typing import Callable, TypeVar
from .. import common
T = TypeVar("T")
F = Callable[[str], T]
def _checkdf(func: F[T]) -> F[T]:
"""Ensure downlink format is 4 or 5."""
def wrapper(msg: str) -> T:
df = common.df(msg)
if df not in [4, 5]:
raise RuntimeError(
"Incorrect downlink format, expect 4 or 5, got {}".format(df)
)
return func(msg)
return wrapper
@_checkdf
def fs(msg: str) -> tuple[int, str]:
"""Decode flight status.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: flight status, description
"""
msgbin = common.hex2bin(msg)
fs = common.bin2int(msgbin[5:8])
text = ""
if fs == 0:
text = "no alert, no SPI, aircraft is airborne"
elif fs == 1:
text = "no alert, no SPI, aircraft is on-ground"
elif fs == 2:
text = "alert, no SPI, aircraft is airborne"
elif fs == 3:
text = "alert, no SPI, aircraft is on-ground"
elif fs == 4:
text = "alert, SPI, aircraft is airborne or on-ground"
elif fs == 5:
text = "no alert, SPI, aircraft is airborne or on-ground"
return fs, text
@_checkdf
def dr(msg: str) -> tuple[int, str]:
"""Decode downlink request.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: downlink request, description
"""
msgbin = common.hex2bin(msg)
dr = common.bin2int(msgbin[8:13])
text = ""
if dr == 0:
text = "no downlink request"
elif dr == 1:
text = "request to send Comm-B message"
elif dr == 4:
text = "Comm-B broadcast 1 available"
elif dr == 5:
text = "Comm-B broadcast 2 available"
elif dr >= 16:
text = "ELM downlink segments available: {}".format(dr - 15)
return dr, text
@_checkdf
def um(msg: str) -> tuple[int, int, None | str]:
"""Decode utility message.
Utility message contains interrogator identifier and reservation type.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: interrogator identifier code that triggered the reply, and
reservation type made by the interrogator
"""
msgbin = common.hex2bin(msg)
iis = common.bin2int(msgbin[13:17])
ids = common.bin2int(msgbin[17:19])
if ids == 0:
ids_text = None
if ids == 1:
ids_text = "Comm-B interrogator identifier code"
if ids == 2:
ids_text = "Comm-C interrogator identifier code"
if ids == 3:
ids_text = "Comm-D interrogator identifier code"
return iis, ids, ids_text
@_checkdf
def altitude(msg: str) -> None | int:
"""Decode altitude.
Args:
msg (String): 14 hexdigits string
Returns:
int: altitude in ft
"""
return common.altcode(msg)
@_checkdf
def identity(msg: str) -> str:
"""Decode squawk code.
Args:
msg (String): 14 hexdigits string
Returns:
string: squawk code
"""
return common.idcode(msg)

View File

@ -1,8 +1,16 @@
"""Uncertainty parameters.
See source code at: https://github.com/junzis/pyModeS/blob/master/pyModeS/decoder/uncertainty.py
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 8):
from typing_extensions import TypedDict
else:
from typing import TypedDict
NA = None
TC_NUCp_lookup = {
@ -26,7 +34,7 @@ TC_NUCp_lookup = {
22: 0,
}
TC_NICv1_lookup = {
TC_NICv1_lookup: dict[int, int | dict[int, int]] = {
5: 11,
6: 10,
7: 9,
@ -46,7 +54,7 @@ TC_NICv1_lookup = {
22: 0,
}
TC_NICv2_lookup = {
TC_NICv2_lookup: dict[int, int | dict[int, int]] = {
5: 11,
6: 10,
7: {2: 9, 0: 8},
@ -67,7 +75,13 @@ TC_NICv2_lookup = {
}
NUCp = {
class NUCpEntry(TypedDict):
HPL: None | float
RCu: None | int
RCv: None | int
NUCp: dict[int, NUCpEntry] = {
9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
8: {"HPL": 25, "RCu": 10, "RCv": 15},
7: {"HPL": 185, "RCu": 93, "RCv": NA},
@ -80,7 +94,13 @@ NUCp = {
0: {"HPL": NA, "RCu": NA, "RCv": NA},
}
NUCv = {
class NUCvEntry(TypedDict):
HVE: None | float
VVE: None | float
NUCv: dict[int, NUCvEntry] = {
0: {"HVE": NA, "VVE": NA},
1: {"HVE": 10, "VVE": 15.2},
2: {"HVE": 3, "VVE": 4.5},
@ -88,7 +108,13 @@ NUCv = {
4: {"HVE": 0.3, "VVE": 0.46},
}
NACp = {
class NACpEntry(TypedDict):
EPU: None | int
VEPU: None | int
NACp: dict[int, NACpEntry] = {
11: {"EPU": 3, "VEPU": 4},
10: {"EPU": 10, "VEPU": 15},
9: {"EPU": 30, "VEPU": 45},
@ -103,7 +129,13 @@ NACp = {
0: {"EPU": NA, "VEPU": NA},
}
NACv = {
class NACvEntry(TypedDict):
HFOMr: None | float
VFOMr: None | float
NACv: dict[int, NACvEntry] = {
0: {"HFOMr": NA, "VFOMr": NA},
1: {"HFOMr": 10, "VFOMr": 15.2},
2: {"HFOMr": 3, "VFOMr": 4.5},
@ -111,7 +143,13 @@ NACv = {
4: {"HFOMr": 0.3, "VFOMr": 0.46},
}
SIL = {
class SILEntry(TypedDict):
PE_RCu: None | float
PE_VPL: None | float
SIL: dict[int, SILEntry] = {
3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
@ -119,7 +157,12 @@ SIL = {
}
NICv1 = {
class NICv1Entry(TypedDict):
Rc: None | float
VPL: None | float
NICv1: dict[int, dict[int, NICv1Entry]] = {
# NIC is used as the index at second Level
11: {0: {"Rc": 7.5, "VPL": 11}},
10: {0: {"Rc": 25, "VPL": 37.5}},
@ -135,7 +178,12 @@ NICv1 = {
0: {0: {"Rc": NA, "VPL": NA}},
}
NICv2 = {
class NICv2Entry(TypedDict):
Rc: None | float
NICv2: dict[int, dict[int, NICv2Entry]] = {
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
11: {0: {"Rc": 7.5}},
10: {0: {"Rc": 25}},

229
pyModeS/decoder/uplink.py Normal file
View File

@ -0,0 +1,229 @@
from typing import Optional
from .. import common
from textwrap import wrap
def uplink_icao(msg: str) -> str:
"Calculate the ICAO address from a Mode-S interrogation (uplink message)"
p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4)
data = int(msg[:-6], 16)
PA = int(msg[-6:], 16)
ad = 0
topbit = 0b1 << (len(msg) * 4 - 25)
for j in range(0, len(msg) * 4, 1):
if data & topbit:
data ^= p_gen
data = (data << 1) + ((PA >> 23) & 1)
PA = PA << 1
if j > (len(msg) * 4 - 26):
ad = ad + ((data >> (len(msg) * 4 - 25)) & 1)
ad = ad << 1
return "%06X" % (ad >> 2)
def uf(msg: str) -> int:
"""Decode Uplink Format value, bits 1 to 5."""
ufbin = common.hex2bin(msg[:2])
return min(common.bin2int(ufbin[0:5]), 24)
def bds(msg: str) -> Optional[str]:
"Decode requested BDS register from selective (Roll Call) interrogation."
UF = uf(msg)
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
if UF in {4, 5, 20, 21}:
di = mbytes[1] & 0x7 # DI - Designator Identification
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16
if di == 7:
RRS = mbytes[2] & 0x0F
BDS2 = RRS
elif di == 3:
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
BDS2 = RRS
else:
# for other values of DI, the BDS2 is assumed 0
# (as per ICAO Annex 10 Vol IV)
BDS2 = 0
return str(format(BDS1,"X")) + str(format(BDS2,"X"))
else:
return None
else:
return None
def pr(msg: str) -> Optional[int]:
"""Decode PR (probability of reply) field from All Call interrogation.
Interpretation:
0 signifies reply with probability of 1
1 signifies reply with probability of 1/2
2 signifies reply with probability of 1/4
3 signifies reply with probability of 1/8
4 signifies reply with probability of 1/16
5, 6, 7 not assigned
8 signifies disregard lockout, reply with probability of 1
9 signifies disregard lockout, reply with probability of 1/2
10 signifies disregard lockout, reply with probability of 1/4
11 signifies disregard lockout, reply with probability of 1/8
12 signifies disregard lockout, reply with probability of 1/16
13, 14, 15 not assigned.
"""
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
if uf(msg) == 11:
return ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7)
else:
return None
def ic(msg: str) -> Optional[str]:
"""Decode IC (interrogator code) from a ground-based interrogation."""
UF = uf(msg)
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
IC = None
if UF == 11:
codeLabel = mbytes[1] & 0x7
icField = (mbytes[1] >> 3) & 0xF
# Store the Interogator Code
ic_switcher = {
0: "II" + str(icField),
1: "SI" + str(icField),
2: "SI" + str(icField + 16),
3: "SI" + str(icField + 32),
4: "SI" + str(icField + 48),
}
IC = ic_switcher.get(codeLabel, "")
if UF in {4, 5, 20, 21}:
di = mbytes[1] & 0x7
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16 # noqa: F841
if di == 0 or di == 1 or di == 7:
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
elif di == 3:
# SI
SI = (mbytes[2] >> 2) & 0x3F
IC = "SI" + str(SI)
return IC
def lockout(msg):
"""Decode the lockout command from selective (Roll Call) interrogation."""
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
if uf(msg) in {4, 5, 20, 21}:
lockout = False
di = mbytes[1] & 0x7
if (di == 1 or di == 7):
# LOS
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
elif di == 3:
# LSS
if ((mbytes[2] & 0x2) >> 1) == 1:
lockout = True
return lockout
else:
return None
def uplink_fields(msg):
"""Decode individual fields of a ground-based interrogation."""
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
PR = ""
IC = ""
lockout = False
di = ""
RR = ""
RRS = ""
BDS = ""
if uf(msg) == 11:
# Probability of Reply decoding
PR = ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7)
# Get cl and ic bit fields from the data
# Decode the SI or II interrogator code
codeLabel = mbytes[1] & 0x7
icField = (mbytes[1] >> 3) & 0xF
# Store the Interogator Code
ic_switcher = {
0: "II" + str(icField),
1: "SI" + str(icField),
2: "SI" + str(icField + 16),
3: "SI" + str(icField + 32),
4: "SI" + str(icField + 48),
}
IC = ic_switcher.get(codeLabel, "")
if uf(msg) in {4, 5, 20, 21}:
# Decode the DI and get the lockout information conveniently
# (LSS or LOS)
# DI - Designator Identification
di = mbytes[1] & 0x7
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16
BDS2 = 0
if di == 0:
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
elif di == 1:
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
elif di == 7:
# LOS
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
RRS = mbytes[2] & 0x0F
BDS2 = RRS
elif di == 3:
# LSS
if ((mbytes[2] & 0x2) >> 1) == 1:
lockout = True
# SI
SI = (mbytes[2] >> 2) & 0x3F
IC = "SI" + str(SI)
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
BDS2 = RRS
if RR > 15:
BDS = str(format(BDS1,"X")) + str(format(BDS2,"X"))
return {
"DI": di,
"IC": IC,
"LOS": lockout,
"PR": PR,
"RR": RR,
"RRS": RRS,
"BDS": BDS,
}

View File

@ -1 +0,0 @@
from __future__ import absolute_import, print_function, division

View File

@ -9,15 +9,15 @@ International Standard Atmosphere
p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m]
a = vsound(H) # speed of sound [m/s] as function of H[m]
p = pressure(H) # calls atmos but retruns only pressure [Pa]
p = pressure(H) # calls atmos but returns only pressure [Pa]
T = temperature(H) # calculates temperature [K]
rho = density(H) # calls atmos but retruns only pressure [Pa]
rho = density(H) # calls atmos but returns only pressure [Pa]
Speed conversion at altitude H[m] in ISA
::
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion
Vtas = mach2tas(Mach,H) # mach number to true airspeed (Vtas) conversion
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
@ -35,18 +35,18 @@ ft = 0.3048 # ft -> m
fpm = 0.00508 # ft/min -> m/s
inch = 0.0254 # inch -> m
sqft = 0.09290304 # 1 square foot
nm = 1852.0 # nautical mile -> m
nm = 1852 # nautical mile -> m
lbs = 0.453592 # pound -> kg
g0 = 9.80665 # m/s2, Sea level gravity constant
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
p0 = 101325.0 # Pa, air pressure, sea level ISA
p0 = 101325 # Pa, air pressure, sea level ISA
rho0 = 1.225 # kg/m3, air density, sea level ISA
T0 = 288.15 # K, temperature, sea level ISA
gamma = 1.40 # cp/cv for air
gamma1 = 0.2 # (gamma-1)/2 for air
gamma2 = 3.5 # gamma/(gamma-1) for air
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
r_earth = 6371000.0 # m, average earth radius
r_earth = 6371000 # m, average earth radius
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
@ -94,8 +94,8 @@ def distance(lat1, lon1, lat2, lon2, H=0):
"""
# phi = 90 - latitude
phi1 = np.radians(90.0 - lat1)
phi2 = np.radians(90.0 - lat2)
phi1 = np.radians(90 - lat1)
phi2 = np.radians(90 - lat2)
# theta = longitude
theta1 = np.radians(lon1)
@ -158,16 +158,16 @@ def tas2eas(Vtas, H):
def cas2tas(Vcas, H):
"""Calibrated Airspeed to True Airspeed"""
p, rho, T = atmos(H)
qdyn = p0 * ((1.0 + rho0 * Vcas * Vcas / (7.0 * p0)) ** 3.5 - 1.0)
Vtas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
qdyn = p0 * ((1 + rho0 * Vcas * Vcas / (7 * p0)) ** 3.5 - 1.0)
Vtas = np.sqrt(7 * p / rho * ((1 + qdyn / p) ** (2 / 7.0) - 1.0))
return Vtas
def tas2cas(Vtas, H):
"""True Airspeed to Calibrated Airspeed"""
p, rho, T = atmos(H)
qdyn = p * ((1.0 + rho * Vtas * Vtas / (7.0 * p)) ** 3.5 - 1.0)
Vcas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
qdyn = p * ((1 + rho * Vtas * Vtas / (7 * p)) ** 3.5 - 1.0)
Vcas = np.sqrt(7 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2 / 7.0) - 1.0))
return Vcas

View File

@ -1,93 +1,132 @@
from __future__ import annotations
import time
import traceback
import numpy as np
import pyModeS as pms
from rtlsdr import RtlSdr
import time
modes_sample_rate = 2e6
from typing import Any
import_msg = """
---------------------------------------------------------------------
Warning: pyrtlsdr not installed (required for using RTL-SDR devices)!
---------------------------------------------------------------------"""
try:
import rtlsdr # type: ignore
except ImportError:
print(import_msg)
sampling_rate = 2e6
smaples_per_microsec = 2
modes_frequency = 1090e6
buffer_size = 1024 * 100
read_size = 1024 * 20
buffer_size = 1024 * 200
read_size = 1024 * 100
pbits = 8
fbits = 112
preamble = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]
th_amp = 0.2 # signal amplitude threshold for 0 and 1 bit
th_amp_diff = 0.8 # signal amplitude threshold difference between 0 and 1 bit
class RtlReader(object):
def __init__(self, **kwargs):
def __init__(self, **kwargs) -> None:
super(RtlReader, self).__init__()
self.signal_buffer = []
self.sdr = RtlSdr()
self.sdr.sample_rate = modes_sample_rate
self.signal_buffer: list[float] = [] # amplitude of the sample only
self.sdr = rtlsdr.RtlSdr()
self.sdr.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency
self.sdr.gain = "auto"
# sdr.freq_correction = 75
self.debug = kwargs.get("debug", False)
self.raw_pipe_in = None
self.stop_flag = False
self.noise_floor = 1e6
def _process_buffer(self):
self.exception_queue = None
def _calc_noise(self) -> float:
"""Calculate noise floor"""
window = smaples_per_microsec * 100
total_len = len(self.signal_buffer)
means = (
np.array(self.signal_buffer[: total_len // window * window])
.reshape(-1, window)
.mean(axis=1)
)
return min(means)
def _process_buffer(self) -> list[list[Any]]:
"""process raw IQ data in the buffer"""
# update noise floor
self.noise_floor = min(self._calc_noise(), self.noise_floor)
# set minimum signal amplitude
min_sig_amp = 3.162 * self.noise_floor # 10 dB SNR
# Mode S messages
messages = []
# signal_array = np.array(self.signal_buffer)
# pulses_array = np.where(np.array(self.signal_buffer) < th_amp, 0, 1)
# pulses = "".join(str(x) for x in pulses_array)
buffer_length = len(self.signal_buffer)
i = 0
while i < buffer_length:
if self.signal_buffer[i] < th_amp:
if self.signal_buffer[i] < min_sig_amp:
i += 1
continue
# if pulses[i : i + pbits * 2] == preamble:
if self._check_preamble(self.signal_buffer[i : i + pbits * 2]):
frame_start = i + pbits * 2
frame_end = i + pbits * 2 + (fbits + 1) * 2
frame_start = i + pbits * 2
if self._check_preamble(self.signal_buffer[i:frame_start]):
frame_length = (fbits + 1) * 2
frame_end = frame_start + frame_length
frame_pulses = self.signal_buffer[frame_start:frame_end]
msgbin = ""
threshold = max(frame_pulses) * 0.2
msgbin: list[int] = []
for j in range(0, frame_length, 2):
p2 = frame_pulses[j : j + 2]
j_2 = j + 2
p2 = frame_pulses[j:j_2]
if len(p2) < 2:
break
if p2[0] < th_amp and p2[1] < th_amp:
if p2[0] < threshold and p2[1] < threshold:
break
elif p2[0] >= p2[1]:
c = "1"
c = 1
elif p2[0] < p2[1]:
c = "0"
c = 0
else:
msgbin = ""
msgbin = []
break
msgbin += c
msgbin.append(c)
# advance i with a jump
i = frame_start + j
if len(msgbin) > 0:
msghex = pms.bin2hex(msgbin)
msghex = pms.bin2hex("".join([str(i) for i in msgbin]))
if self._check_msg(msghex):
messages.append([msghex, time.time()])
if self.debug:
self._debug_msg(msghex)
elif i > buffer_length - 500:
# save some for next process
break
# elif i > buffer_length - 500:
# # save some for next process
# break
else:
i += 1
# keep reminder of buffer for next iteration
# reset the buffer
self.signal_buffer = self.signal_buffer[i:]
return messages
def _check_preamble(self, pulses):
def _check_preamble(self, pulses) -> bool:
if len(pulses) != 16:
return False
@ -97,7 +136,7 @@ class RtlReader(object):
return True
def _check_msg(self, msg):
def _check_msg(self, msg) -> bool:
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
@ -107,8 +146,9 @@ class RtlReader(object):
return True
elif df in [4, 5, 11] and msglen == 14:
return True
return False
def _debug_msg(self, msg):
def _debug_msg(self, msg) -> None:
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
@ -121,35 +161,42 @@ class RtlReader(object):
# print("[*]", msg)
pass
def _read_callback(self, data, rtlsdr_obj):
# scaling signal (imporatant)
def _read_callback(self, data, rtlsdr_obj) -> None:
amp = np.absolute(data)
amp_norm = np.interp(amp, (amp.min(), amp.max()), (0, 1))
self.signal_buffer.extend(amp_norm.tolist())
self.signal_buffer.extend(amp.tolist())
if len(self.signal_buffer) >= buffer_size:
messages = self._process_buffer()
self.handle_messages(messages)
def handle_messages(self, messages):
def handle_messages(self, messages) -> None:
"""re-implement this method to handle the messages"""
for msg, t in messages:
# print("%15.9f %s" % (t, msg))
pass
def stop(self, *args, **kwargs):
self.sdr.cancel_read_async()
def stop(self, *args, **kwargs) -> None:
self.sdr.close()
def run(self, raw_pipe_in=None, stop_flag=None):
def run(
self, raw_pipe_in=None, stop_flag=None, exception_queue=None
) -> None:
self.raw_pipe_in = raw_pipe_in
self.exception_queue = exception_queue
self.stop_flag = stop_flag
self.sdr.read_samples_async(self._read_callback, read_size)
# count = 1
# while count < 1000:
# count += 1
# data = self.sdr.read_samples(read_size)
# self._read_callback(data, None)
try:
# raise RuntimeError("test exception")
while True:
data = self.sdr.read_samples(read_size)
self._read_callback(data, None)
except Exception as e:
tb = traceback.format_exc()
if self.exception_queue is not None:
self.exception_queue.put(tb)
raise e
if __name__ == "__main__":

View File

@ -1,17 +1,12 @@
"""Stream beast raw data from a TCP server, convert to mode-s messages."""
from __future__ import print_function, division
import os
import sys
import time
import pyModeS as pms
import traceback
import zmq
if sys.version_info > (3, 0):
PY_VERSION = 3
else:
PY_VERSION = 2
import math
class TcpClient(object):
@ -29,6 +24,8 @@ class TcpClient(object):
self.raw_pipe_in = None
self.stop_flag = False
self.exception_queue = None
def connect(self):
self.socket = zmq.Context().socket(zmq.STREAM)
self.socket.setsockopt(zmq.LINGER, 0)
@ -36,7 +33,7 @@ class TcpClient(object):
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
def stop(self):
self.socket.disconnect()
self.socket.close()
def read_raw_buffer(self):
""" Read raw ADS-B data type.
@ -153,6 +150,103 @@ class TcpClient(object):
messages.append([msg, ts])
return messages
def read_beast_buffer_rssi_piaware(self):
"""Handle mode-s beast data type.
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
2 byte Mode-AC
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
7 byte Mode-S short frame
<esc> "3" : 6 byte MLAT timestamp, 1 byte signal level,
14 byte Mode-S long frame
<esc> "4" : 6 byte MLAT timestamp, status data, DIP switch
configuration settings (not on Mode-S Beast classic)
<esc><esc>: true 0x1a
<esc> is 0x1a, and "1", "2" and "3" are 0x31, 0x32 and 0x33
timestamp:
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
"""
messages_mlat = []
msg = []
i = 0
# process the buffer until the last divider <esc> 0x1a
# then, reset the self.buffer with the remainder
while i < len(self.buffer):
if self.buffer[i : i + 2] == [0x1A, 0x1A]:
msg.append(0x1A)
i += 1
elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1A):
# special case where the last bit is 0x1a
msg.append(0x1A)
elif self.buffer[i] == 0x1A:
if i == len(self.buffer) - 1:
# special case where the last bit is 0x1a
msg.append(0x1A)
elif len(msg) > 0:
messages_mlat.append(msg)
msg = []
else:
msg.append(self.buffer[i])
i += 1
# save the reminder for next reading cycle, if not empty
if len(msg) > 0:
reminder = []
for i, m in enumerate(msg):
if (m == 0x1A) and (i < len(msg) - 1):
# rewind 0x1a, except when it is at the last bit
reminder.extend([m, m])
else:
reminder.append(m)
self.buffer = [0x1A] + msg
else:
self.buffer = []
# extract messages
messages = []
for mm in messages_mlat:
ts = time.time()
msgtype = mm[0]
# print(''.join('%02X' % i for i in mm))
if msgtype == 0x32:
# Mode-S Short Message, 7 byte, 14-len hexstr
msg = "".join("%02X" % i for i in mm[8:15])
elif msgtype == 0x33:
# Mode-S Long Message, 14 byte, 28-len hexstr
msg = "".join("%02X" % i for i in mm[8:22])
else:
# Other message tupe
continue
if len(msg) not in [14, 28]:
continue
'''
we get the raw 0-255 byte value (raw_rssi = mm[7])
we scale it to 0.0 - 1.0 (voltage = raw_rssi / 255)
we convert it to a dBFS power value (rolling the squaring of the voltage into the dB calculation)
'''
df = pms.df(msg)
raw_rssi = mm[7] # eighth byte of Mode-S message should contain RSSI value
rssi_ratio = raw_rssi / 255
signalLevel = rssi_ratio ** 2
dbfs_rssi = 10 * math.log10(signalLevel)
# skip incomplete message
if df in [0, 4, 5, 11] and len(msg) != 14:
continue
if df in [16, 17, 18, 19, 20, 21, 24] and len(msg) != 28:
continue
messages.append([msg, dbfs_rssi, ts])
return messages
def read_skysense_buffer(self):
"""Skysense stream format.
@ -170,7 +264,7 @@ class TcpClient(object):
Start character '$'
MS field - Payload
Postion 1 through 14:
Position 1 through 14:
14 bytes = 112 bits
Mode-S payload
In case of DF types that only carry 7 bytes of information
@ -254,8 +348,9 @@ class TcpClient(object):
for msg, t in messages:
print("%15.9f %s" % (t, msg))
def run(self, raw_pipe_in=None, stop_flag=None):
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self.raw_pipe_in = raw_pipe_in
self.exception_queue = exception_queue
self.stop_flag = stop_flag
self.connect()
@ -263,17 +358,9 @@ class TcpClient(object):
try:
received = [i for i in self.socket.recv(4096)]
if PY_VERSION == 2:
received = [ord(i) for i in received]
self.buffer.extend(received)
# print(''.join(x.encode('hex') for x in self.buffer))
# process self.buffer when it is longer enough
# if len(self.buffer) < 2048:
# continue
# -- Removed!! Cause delay in low data rate scenario --
if self.datatype == "beast":
messages = self.read_beast_buffer()
elif self.datatype == "raw":
@ -286,22 +373,15 @@ class TcpClient(object):
else:
self.handle_messages(messages)
except Exception as e:
# Provides the user an option to supply the environment
# variable PYMODES_DEBUG to halt the execution
# for debugging purposes
debug_intent = os.environ.get("PYMODES_DEBUG", "false")
if debug_intent.lower() == "true":
traceback.print_exc()
sys.exit()
else:
print("Unexpected Error:", e)
# raise RuntimeError("test exception")
try:
sock = self.connect()
time.sleep(1)
except Exception as e:
print("Unexpected Error:", e)
except zmq.error.Again:
continue
except Exception as e:
tb = traceback.format_exc()
if self.exception_queue is not None:
self.exception_queue.put(tb)
raise e
if __name__ == "__main__":
@ -310,4 +390,7 @@ if __name__ == "__main__":
port = int(sys.argv[2])
datatype = sys.argv[3]
client = TcpClient(host=host, port=port, datatype=datatype)
client.run()
try:
client.run()
finally:
client.stop()

0
pyModeS/py.typed Normal file
View File

View File

@ -1,61 +1,46 @@
from __future__ import absolute_import, print_function, division
from typing import Optional
import numpy as np
from textwrap import wrap
def hex2bin(hexstr):
"""Convert a hexdecimal string to binary string, with zero fillings."""
def hex2bin(hexstr: str) -> str:
"""Convert a hexadecimal string to binary string, with zero fillings."""
num_of_bits = len(hexstr) * 4
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
return binstr
def hex2int(hexstr):
"""Convert a hexdecimal string to integer."""
def hex2int(hexstr: str) -> int:
"""Convert a hexadecimal string to integer."""
return int(hexstr, 16)
def int2hex(n):
"""Convert a integer to hexadecimal string."""
# strip 'L' for python 2
return hex(n)[2:].rjust(6, "0").upper().rstrip("L")
def bin2int(binstr):
def bin2int(binstr: str) -> int:
"""Convert a binary string to integer."""
return int(binstr, 2)
def bin2hex(hexstr):
"""Convert a hexdecimal string to integer."""
return int2hex(bin2int(hexstr))
def bin2hex(binstr: str) -> str:
"""Convert a binary string to hexadecimal string."""
return "{0:X}".format(int(binstr, 2))
def bin2np(binstr):
"""Convert a binary string to numpy array."""
return np.array([int(i) for i in binstr])
def np2bin(npbin):
"""Convert a binary numpy array to string."""
return np.array2string(npbin, separator="")[1:-1]
def df(msg):
"""Decode Downlink Format vaule, bits 1 to 5."""
def df(msg: str) -> int:
"""Decode Downlink Format value, bits 1 to 5."""
dfbin = hex2bin(msg[:2])
return min(bin2int(dfbin[0:5]), 24)
def crc(msg, encode=False):
def crc(msg: str, encode: bool = False) -> int:
"""Mode-S Cyclic Redundancy Check.
Detect if bit error occurs in the Mode-S message. When encode option is on,
the checksum is generated.
Args:
msg (string): 28 bytes hexadecimal message string
encode (bool): True to encode the date only and return the checksum
msg: 28 bytes hexadecimal message string
encode: True to encode the date only and return the checksum
Returns:
int: message checksum, or partity bits (encoder)
@ -92,7 +77,7 @@ def crc(msg, encode=False):
return result
def crc_legacy(msg, encode=False):
def crc_legacy(msg: str, encode: bool = False) -> int:
"""Mode-S Cyclic Redundancy Check. (Legacy code, 2x slow)."""
# the polynominal generattor code for CRC [1111111111111010000001001]
generator = np.array(
@ -100,7 +85,7 @@ def crc_legacy(msg, encode=False):
)
ng = len(generator)
msgnpbin = bin2np(hex2bin(msg))
msgnpbin = np.array([int(i) for i in hex2bin(msg)])
if encode:
msgnpbin[-24:] = [0] * 24
@ -114,11 +99,13 @@ def crc_legacy(msg, encode=False):
msgnpbin[i : i + ng] = np.bitwise_xor(msgnpbin[i : i + ng], generator)
# last 24 bits
reminder = bin2int(np2bin(msgnpbin[-24:]))
msgbin = np.array2string(msgnpbin[-24:], separator="")[1:-1]
reminder = bin2int(msgbin)
return reminder
def floor(x):
def floor(x: float) -> int:
"""Mode-S floor function.
Defined as the greatest integer value k, such that k <= x
@ -128,7 +115,7 @@ def floor(x):
return int(np.floor(x))
def icao(msg):
def icao(msg: str) -> Optional[str]:
"""Calculate the ICAO address from an Mode-S message.
Applicable only with DF4, DF5, DF20, DF21 messages.
@ -140,13 +127,14 @@ def icao(msg):
String: ICAO address in 6 bytes hexadecimal string
"""
addr: Optional[str]
DF = df(msg)
if DF in (11, 17, 18):
addr = msg[2:8]
elif DF in (0, 4, 5, 16, 20, 21):
c0 = crc(msg, encode=True)
c1 = hex2int(msg[-6:])
c1 = int(msg[-6:], 16)
addr = "%06X" % (c0 ^ c1)
else:
addr = None
@ -154,12 +142,12 @@ def icao(msg):
return addr
def is_icao_assigned(icao):
def is_icao_assigned(icao: str) -> bool:
"""Check whether the ICAO address is assigned (Annex 10, Vol 3)."""
if (icao is None) or (not isinstance(icao, str)) or (len(icao) != 6):
return False
icaoint = hex2int(icao)
icaoint = int(icao, 16)
if 0x200000 < icaoint < 0x27FFFF:
return False # AFI
@ -183,7 +171,7 @@ def is_icao_assigned(icao):
return True
def typecode(msg):
def typecode(msg: str) -> Optional[int]:
"""Type code of ADS-B message
Args:
@ -199,31 +187,26 @@ def typecode(msg):
return bin2int(tcbin[0:5])
def cprNL(lat):
def cprNL(lat: float) -> int:
"""NL() function in CPR decoding."""
if lat == 0:
if np.isclose(lat, 0):
return 59
if lat == 87 or lat == -87:
elif np.isclose(abs(lat), 87):
return 2
if lat > 87 or lat < -87:
elif lat > 87 or lat < -87:
return 1
nz = 15
a = 1 - np.cos(np.pi / (2 * nz))
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
b = np.cos(np.pi / 180 * abs(lat)) ** 2
nl = 2 * np.pi / (np.arccos(1 - a / b))
NL = floor(nl)
return NL
def idcode(msg):
"""Compute identity (squawk code).
Applicable only for DF5 or DF21 messages, bit 20-32.
credit: @fbyrkjeland
def idcode(msg: str) -> str:
"""Compute identity code (squawk) encoded in DF5 or DF21 message.
Args:
msg (String): 28 bytes hexadecimal message string
@ -236,20 +219,37 @@ def idcode(msg):
raise RuntimeError("Message must be Downlink Format 5 or 21.")
mbin = hex2bin(msg)
idcodebin = mbin[19:32]
C1 = mbin[19]
A1 = mbin[20]
C2 = mbin[21]
A2 = mbin[22]
C4 = mbin[23]
A4 = mbin[24]
# _ = mbin[25]
B1 = mbin[26]
D1 = mbin[27]
B2 = mbin[28]
D2 = mbin[29]
B4 = mbin[30]
D4 = mbin[31]
return squawk(idcodebin)
def squawk(binstr: str) -> str:
"""Decode 13 bits identity (squawk) code.
Args:
binstr (String): 13 bits binary string
Returns:
string: squawk code
"""
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
raise RuntimeError("Input must be 13 bits binary string")
C1 = binstr[0]
A1 = binstr[1]
C2 = binstr[2]
A2 = binstr[3]
C4 = binstr[4]
A4 = binstr[5]
# X = binstr[6]
B1 = binstr[7]
D1 = binstr[8]
B2 = binstr[9]
D2 = binstr[10]
B4 = binstr[11]
D4 = binstr[12]
byte1 = int(A4 + A2 + A1, 2)
byte2 = int(B4 + B2 + B1, 2)
@ -259,11 +259,8 @@ def idcode(msg):
return str(byte1) + str(byte2) + str(byte3) + str(byte4)
def altcode(msg):
"""Compute the altitude.
Applicable only for DF4 or DF20 message, bit 20-32.
credit: @fbyrkjeland
def altcode(msg: str) -> Optional[int]:
"""Compute altitude encoded in DF4 or DF20 message.
Args:
msg (String): 28 bytes hexadecimal message string
@ -272,50 +269,78 @@ def altcode(msg):
int: altitude in ft
"""
alt: Optional[int]
if df(msg) not in [0, 4, 16, 20]:
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
# Altitude code, bit 20-32
mbin = hex2bin(msg)
mbit = mbin[25] # M bit: 26
qbit = mbin[27] # Q bit: 28
altitude_code = mbin[19:32]
if mbit == "0": # unit in ft
if qbit == "1": # 25ft interval
vbin = mbin[19:25] + mbin[26] + mbin[28:32]
alt = altitude(altitude_code)
return alt
def altitude(binstr: str) -> Optional[int]:
"""Decode 13 bits altitude code.
Args:
binstr (String): 13 bits binary string
Returns:
int: altitude in ft
"""
alt: Optional[int]
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
raise RuntimeError("Input must be 13 bits binary string")
Mbit = binstr[6]
Qbit = binstr[8]
if bin2int(binstr) == 0:
# altitude unknown or invalid
alt = None
elif Mbit == "0": # unit in ft
if Qbit == "1": # 25ft interval
vbin = binstr[:6] + binstr[7] + binstr[9:]
alt = bin2int(vbin) * 25 - 1000
if qbit == "0": # 100ft interval, above 50175ft
C1 = mbin[19]
A1 = mbin[20]
C2 = mbin[21]
A2 = mbin[22]
C4 = mbin[23]
A4 = mbin[24]
# _ = mbin[25]
B1 = mbin[26]
# D1 = mbin[27] # always zero
B2 = mbin[28]
D2 = mbin[29]
B4 = mbin[30]
D4 = mbin[31]
if Qbit == "0": # 100ft interval, above 50187.5ft
C1 = binstr[0]
A1 = binstr[1]
C2 = binstr[2]
A2 = binstr[3]
C4 = binstr[4]
A4 = binstr[5]
# M = binstr[6]
B1 = binstr[7]
# Q = binstr[8]
B2 = binstr[9]
D2 = binstr[10]
B4 = binstr[11]
D4 = binstr[12]
graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
alt = gray2alt(graystr)
if mbit == "1": # unit in meter
vbin = mbin[19:25] + mbin[26:31]
if Mbit == "1": # unit in meter
vbin = binstr[:6] + binstr[7:]
alt = int(bin2int(vbin) * 3.28084) # convert to ft
return alt
def gray2alt(codestr):
gc500 = codestr[:8]
def gray2alt(binstr: str) -> Optional[int]:
gc500 = binstr[:8]
n500 = gray2int(gc500)
# in 100-ft step must be converted first
gc100 = codestr[8:]
gc100 = binstr[8:]
n100 = gray2int(gc100)
if n100 in [0, 5, 6]:
@ -331,9 +356,9 @@ def gray2alt(codestr):
return alt
def gray2int(graystr):
def gray2int(binstr: str) -> int:
"""Convert greycode to binary."""
num = bin2int(graystr)
num = bin2int(binstr)
num ^= num >> 8
num ^= num >> 4
num ^= num >> 2
@ -341,12 +366,12 @@ def gray2int(graystr):
return num
def data(msg):
def data(msg: str) -> str:
"""Return the data frame in the message, bytes 9 to 22."""
return msg[8:-6]
def allzeros(msg):
def allzeros(msg: str) -> bool:
"""Check if the data bits are all zeros.
Args:
@ -364,7 +389,7 @@ def allzeros(msg):
return True
def wrongstatus(data, sb, msb, lsb):
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool:
"""Check if the status bit and field bits are consistency.
This Function is used for checking BDS code versions.
@ -379,3 +404,85 @@ def wrongstatus(data, sb, msb, lsb):
return True
return False
def fs(msg):
"""Decode flight status for DF 4, 5, 20, and 21.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: flight status, description
"""
msgbin = hex2bin(msg)
fs = bin2int(msgbin[5:8])
text = None
if fs == 0:
text = "no alert, no SPI, aircraft is airborne"
elif fs == 1:
text = "no alert, no SPI, aircraft is on-ground"
elif fs == 2:
text = "alert, no SPI, aircraft is airborne"
elif fs == 3:
text = "alert, no SPI, aircraft is on-ground"
elif fs == 4:
text = "alert, SPI, aircraft is airborne or on-ground"
elif fs == 5:
text = "no alert, SPI, aircraft is airborne or on-ground"
return fs, text
def dr(msg):
"""Decode downlink request for DF 4, 5, 20, and 21.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: downlink request, description
"""
msgbin = hex2bin(msg)
dr = bin2int(msgbin[8:13])
text = None
if dr == 0:
text = "no downlink request"
elif dr == 1:
text = "request to send Comm-B message"
elif dr == 4:
text = "Comm-B broadcast 1 available"
elif dr == 5:
text = "Comm-B broadcast 2 available"
elif dr >= 16:
text = "ELM downlink segments available: {}".format(dr - 15)
return dr, text
def um(msg):
"""Decode utility message for DF 4, 5, 20, and 21.
Utility message contains interrogator identifier and reservation type.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: interrogator identifier code that triggered the reply, and
reservation type made by the interrogator
"""
msgbin = hex2bin(msg)
iis = bin2int(msgbin[13:17])
ids = bin2int(msgbin[17:19])
if ids == 0:
ids_text = None
if ids == 1:
ids_text = "Comm-B interrogator identifier code"
if ids == 2:
ids_text = "Comm-C interrogator identifier code"
if ids == 3:
ids_text = "Comm-D interrogator identifier code"
return iis, ids, ids_text

View File

@ -1,6 +1,6 @@
from __future__ import absolute_import, print_function, division
import os
import time
import traceback
import datetime
import csv
import pyModeS as pms
@ -27,7 +27,7 @@ class Decode:
self.dumpto = None
def process_raw(self, adsb_ts, adsb_msg, commb_ts, commb_msg, tnow=None):
"""process a chunk of adsb and commb messages recieved in the same
"""process a chunk of adsb and commb messages received in the same
time period.
"""
if tnow is None:
@ -73,8 +73,15 @@ class Decode:
"VFOMr": None,
"PE_RCu": None,
"PE_VPL": None,
"hum44" : None,
"p44" : None,
"temp44" : None,
"turb44" : None,
"wind44" : None,
}
self.acs[icao]["tc"] = tc
self.acs[icao]["icao"] = icao
self.acs[icao]["t"] = t
self.acs[icao]["live"] = int(t)
@ -155,7 +162,7 @@ class Decode:
ac["nic_bc"] = pms.adsb.nic_b(msg)
if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22):
ac["HPL"], ac["RCu"], ac["RCv"] = pms.adsb.nuc_p(msg)
ac["NUCp"], ac["HPL"], ac["RCu"], ac["RCv"] = pms.adsb.nuc_p(msg)
if (ac["ver"] == 1) and ("nic_s" in ac.keys()):
ac["Rc"], ac["VPL"] = pms.adsb.nic_v1(msg, ac["nic_s"])
@ -164,20 +171,20 @@ class Decode:
and ("nic_a" in ac.keys())
and ("nic_bc" in ac.keys())
):
ac["Rc"] = pms.adsb.nic_v2(msg, ac["nic_a"], ac["nic_bc"])
ac["NIC"], ac["Rc"] = pms.adsb.nic_v2(msg, ac["nic_a"], ac["nic_bc"])
if tc == 19:
ac["HVE"], ac["VVE"] = pms.adsb.nuc_v(msg)
ac["NUCv"], ac["HVE"], ac["VVE"] = pms.adsb.nuc_v(msg)
if ac["ver"] in [1, 2]:
ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg)
ac["NACv"], ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg)
if tc == 29:
ac["PE_RCu"], ac["PE_VPL"], ac["base"] = pms.adsb.sil(msg, ac["ver"])
ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
if tc == 31:
ac["ver"] = pms.adsb.version(msg)
ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
ac["PE_RCu"], ac["PE_VPL"], ac["sil_base"] = pms.adsb.sil(
msg, ac["ver"]
)
@ -194,6 +201,8 @@ class Decode:
if icao not in self.acs:
continue
self.acs[icao]["icao"] = icao
self.acs[icao]["t"] = t
self.acs[icao]["live"] = int(t)
bds = pms.bds.infer(msg)
@ -217,8 +226,10 @@ class Decode:
output_buffer.append([t, icao, "rtrk50", rtrk50])
if trk50:
self.acs[icao]["trk50"] = trk50
output_buffer.append([t, icao, "trk50", trk50])
if gs50:
self.acs[icao]["gs50"] = gs50
output_buffer.append([t, icao, "gs50", gs50])
elif bds == "BDS60":
@ -232,16 +243,29 @@ class Decode:
self.acs[icao]["t60"] = t
if ias60:
self.acs[icao]["ias"] = ias60
output_buffer.append([t, icao, "ias60", ias60])
if hdg60:
self.acs[icao]["hdg"] = hdg60
output_buffer.append([t, icao, "hdg60", hdg60])
if mach60:
self.acs[icao]["mach"] = mach60
output_buffer.append([t, icao, "mach60", mach60])
if roc60baro:
self.acs[icao]["roc60baro"] = roc60baro
output_buffer.append([t, icao, "roc60baro", roc60baro])
if roc60ins:
self.acs[icao]["roc60ins"] = roc60ins
output_buffer.append([t, icao, "roc60ins", roc60ins])
elif bds == "BDS44":
if(pms.commb.is44(msg)):
self.acs[icao]["hum44"] = pms.commb.hum44(msg)
self.acs[icao]["p44"] = pms.commb.p44(msg)
self.acs[icao]["temp44"] = pms.commb.temp44(msg)
self.acs[icao]["turb44"] = pms.commb.turb44(msg)
self.acs[icao]["wind44"] = pms.commb.wind44(msg)
# clear up old data
for icao in list(self.acs.keys()):
if self.t - self.acs[icao]["live"] > self.cache_timeout:
@ -259,26 +283,31 @@ class Decode:
return
def get_aircraft(self):
"""all aircraft that are stored in memeory"""
"""all aircraft that are stored in memory"""
acs = self.acs
return acs
def run(self, raw_pipe_out, ac_pipe_in):
def run(self, raw_pipe_out, ac_pipe_in, exception_queue):
local_buffer = []
while True:
while raw_pipe_out.poll():
data = raw_pipe_out.recv()
local_buffer.append(data)
try:
while raw_pipe_out.poll():
data = raw_pipe_out.recv()
local_buffer.append(data)
for data in local_buffer:
self.process_raw(
data["adsb_ts"],
data["adsb_msg"],
data["commb_ts"],
data["commb_msg"],
)
local_buffer = []
for data in local_buffer:
self.process_raw(
data["adsb_ts"],
data["adsb_msg"],
data["commb_ts"],
data["commb_msg"],
)
local_buffer = []
acs = self.get_aircraft()
ac_pipe_in.send(acs)
time.sleep(0.001)
acs = self.get_aircraft()
ac_pipe_in.send(acs)
time.sleep(0.001)
except Exception as e:
tb = traceback.format_exc()
exception_queue.put((e, tb))

View File

@ -1,134 +0,0 @@
#!/usr/bin/env python
from __future__ import print_function, division
import os
import sys
import argparse
import curses
import signal
import multiprocessing
from pyModeS.streamer.decode import Decode
from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, "w")
support_rawtypes = ["raw", "beast", "skysense"]
parser = argparse.ArgumentParser()
parser.add_argument(
"--source",
help='Choose data source, "rtlsdr" or "net"',
required=True,
default="net",
)
parser.add_argument(
"--connect",
help="Define server, port and data type. Supported data types are: %s"
% support_rawtypes,
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
required=False,
)
parser.add_argument(
"--latlon",
help="Receiver latitude and longitude, needed for the surface position, default none",
nargs=2,
metavar=("LAT", "LON"),
default=None,
required=False,
)
parser.add_argument(
"--show-uncertainty",
dest="uncertainty",
help="Display uncertainty values, default off",
action="store_true",
required=False,
default=False,
)
parser.add_argument(
"--dumpto",
help="Folder to dump decoded output, default none",
required=False,
default=None,
)
args = parser.parse_args()
SOURCE = args.source
LATLON = args.latlon
UNCERTAINTY = args.uncertainty
DUMPTO = args.dumpto
if SOURCE == "rtlsdr":
pass
elif SOURCE == "net":
if args.connect is None:
print("Error: --connect argument must not be empty.")
else:
SERVER, PORT, DATATYPE = args.connect
if DATATYPE not in support_rawtypes:
print("Data type not supported, avaiable ones are %s" % support_rawtypes)
else:
print('Source must be "rtlsdr" or "net".')
sys.exit(1)
if DUMPTO is not None:
# append to current folder except root is given
if DUMPTO[0] != "/":
DUMPTO = os.getcwd() + "/" + DUMPTO
if not os.path.isdir(DUMPTO):
print("Error: dump folder (%s) does not exist" % DUMPTO)
sys.exit(1)
# raw_event = multiprocessing.Event()
# ac_event = multiprocessing.Event()
# raw_queue = multiprocessing.Queue()
# ac_queue = multiprocessing.Queue()
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
stop_flag = multiprocessing.Value("b", False)
if SOURCE == "net":
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
elif SOURCE == "rtlsdr":
source = RtlSdrSource()
recv_process = multiprocessing.Process(target=source.run, args=(raw_pipe_in, stop_flag))
decode = Decode(latlon=LATLON, dumpto=DUMPTO)
decode_process = multiprocessing.Process(
target=decode.run, args=(raw_pipe_out, ac_pipe_in)
)
screen = Screen(uncertainty=UNCERTAINTY)
screen_process = multiprocessing.Process(target=screen.run, args=(ac_pipe_out,))
def closeall(signal, frame):
print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal))
stop_flag.value = True
curses.endwin()
recv_process.terminate()
decode_process.terminate()
screen_process.terminate()
recv_process.join()
decode_process.join()
screen_process.join()
exit(0)
signal.signal(signal.SIGINT, closeall)
recv_process.start()
decode_process.start()
screen_process.start()

155
pyModeS/streamer/modeslive.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python
import os
import sys
import time
import argparse
import curses
import signal
import multiprocessing
from pyModeS.streamer.decode import Decode
from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource # , RtlSdrSource24
def main():
support_rawtypes = ["raw", "beast", "skysense"]
parser = argparse.ArgumentParser()
parser.add_argument(
"--source",
help='Choose data source, "rtlsdr", "rtlsdr24" or "net"',
required=True,
default="net",
)
parser.add_argument(
"--connect",
help="Define server, port and data type. Supported data types are: {}".format(
support_rawtypes
),
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
required=False,
)
parser.add_argument(
"--latlon",
help="Receiver latitude and longitude, needed for the surface position, default none",
nargs=2,
metavar=("LAT", "LON"),
default=None,
required=False,
)
parser.add_argument(
"--show-uncertainty",
dest="uncertainty",
help="Display uncertainty values, default off",
action="store_true",
required=False,
default=False,
)
parser.add_argument(
"--dumpto",
help="Folder to dump decoded output, default none",
required=False,
default=None,
)
args = parser.parse_args()
SOURCE = args.source
LATLON = args.latlon
UNCERTAINTY = args.uncertainty
DUMPTO = args.dumpto
if SOURCE in ["rtlsdr", "rtlsdr24"]:
pass
elif SOURCE == "net":
if args.connect is None:
print("Error: --connect argument must not be empty.")
else:
SERVER, PORT, DATATYPE = args.connect
if DATATYPE not in support_rawtypes:
print(
"Data type not supported, available ones are %s"
% support_rawtypes
)
else:
print('Source must be "rtlsdr" or "net".')
sys.exit(1)
if DUMPTO is not None:
# append to current folder except root is given
if DUMPTO[0] != "/":
DUMPTO = os.getcwd() + "/" + DUMPTO
if not os.path.isdir(DUMPTO):
print("Error: dump folder (%s) does not exist" % DUMPTO)
sys.exit(1)
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, "w")
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
exception_queue = multiprocessing.Queue()
stop_flag = multiprocessing.Value("b", False)
if SOURCE == "net":
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
elif SOURCE == "rtlsdr":
source = RtlSdrSource()
# elif SOURCE == "rtlsdr24":
# source = RtlSdrSource24()
recv_process = multiprocessing.Process(
target=source.run, args=(raw_pipe_in, stop_flag, exception_queue)
)
decode = Decode(latlon=LATLON, dumpto=DUMPTO)
decode_process = multiprocessing.Process(
target=decode.run, args=(raw_pipe_out, ac_pipe_in, exception_queue)
)
screen = Screen(uncertainty=UNCERTAINTY)
screen_process = multiprocessing.Process(
target=screen.run, args=(ac_pipe_out, exception_queue)
)
def shutdown():
stop_flag.value = True
curses.endwin()
sys.stdout = sys.__stdout__
recv_process.terminate()
decode_process.terminate()
screen_process.terminate()
recv_process.join()
decode_process.join()
screen_process.join()
def closeall(signal, frame):
print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal))
shutdown()
sys.exit(0)
signal.signal(signal.SIGINT, closeall)
recv_process.start()
decode_process.start()
screen_process.start()
while True:
if (
(not recv_process.is_alive())
or (not decode_process.is_alive())
or (not screen_process.is_alive())
):
shutdown()
while not exception_queue.empty():
trackback = exception_queue.get()
print(trackback)
sys.exit(1)
time.sleep(0.01)

View File

@ -1,8 +1,8 @@
from __future__ import print_function, division
import curses
import numpy as np
import time
import threading
import traceback
COLUMNS = [
("call", 10),
@ -187,24 +187,32 @@ class Screen(object):
self.screen.refresh()
self.draw_frame()
def run(self, ac_pipe_out):
def run(self, ac_pipe_out, exception_queue):
local_buffer = []
key_thread = threading.Thread(target=self.kye_handling)
key_thread.daemon = True
key_thread.start()
while True:
while ac_pipe_out.poll():
acs = ac_pipe_out.recv()
local_buffer.append(acs)
for acs in local_buffer:
self.update_ac(acs)
local_buffer = []
try:
# raise RuntimeError("test exception")
while ac_pipe_out.poll():
acs = ac_pipe_out.recv()
local_buffer.append(acs)
for acs in local_buffer:
self.update_ac(acs)
local_buffer = []
self.update()
except:
except curses.error:
pass
except Exception as e:
tb = traceback.format_exc()
exception_queue.put(tb)
time.sleep(0.1)
raise e
time.sleep(0.001)

60
pyproject.toml Normal file
View File

@ -0,0 +1,60 @@
[tool.poetry]
name = "pyModeS"
version = "2.18"
description = "Python Mode-S and ADS-B Decoder"
authors = ["Junzi Sun <j.sun-1@tudelft.nl>"]
license = "GNU GPL v3"
readme = "README.rst"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3",
"Typing :: Typed",
]
packages = [{ include = "pyModeS", from = "." }]
include = [
"LICENSE",
"*.pyx",
"*.pxd",
"*.pyi",
"py.typed",
{ path = "pyModeS/**/*.so", format = "wheel" },
{ path = "pyModeS/**/*.pyd", format = "wheel" },
]
build = "build.py"
[tool.poetry.scripts]
modeslive = "pyModeS.streamer.modeslive:main"
[tool.poetry.dependencies]
python = ">=3.9"
numpy = ">=1.26"
pyzmq = ">=24.0"
pyrtlsdr = { version = ">=0.2.93", optional = true }
[tool.poetry.group.dev.dependencies]
mypy = ">=0.991"
flake8 = ">=5.0.0"
black = ">=22.12.0"
isort = ">=5.11.4"
pytest = ">=7.2.0"
pytest-cov = ">=4.0.0"
codecov = ">=2.1.12"
[tool.poetry.extras]
rtlsdr = ["pyrtlsdr"]
[tool.black]
line-length = 80
target_version = ['py39', 'py310', 'py311', 'py312']
include = '\.pyi?$'
[tool.isort]
line_length = 80
profile = "black"
[build-system]
requires = ["poetry-core>=1.0.0", "Cython>=0.29.32", "setuptools>=69.1.1"]
build-backend = "poetry.core.masonry.api"

101
setup.py
View File

@ -1,101 +0,0 @@
"""A setuptools based setup module.
See:
https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject
Steps for deploying a new verison:
1. Increase the version number
2. remove the old deployment under [dist] and [build] folder
3. run: python setup.py sdist
run: python setup.py bdist_wheel --universal
4. twine upload dist/*
"""
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# To use a consistent encoding
from codecs import open
from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(here, "README.rst"), encoding="utf-8") as f:
long_description = f.read()
setup(
name="pyModeS",
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version="2.4",
description="Python Mode-S and ADS-B Decoder",
long_description=long_description,
# The project's main homepage.
url="https://github.com/junzis/pyModeS",
# Author details
author="Junzi Sun",
author_email="j.sun-1@tudelft.nl",
# Choose your license
license="GNU GPL v3",
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 4 - Beta",
# Indicate who your project is intended for
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
# Pick your license as you wish (should match "license" above)
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
],
# What does your project relate to?
keywords="Mode-S ADS-B EHS ELS Comm-B",
# You can just specify the packages manually here if your project is
# simple. Or you can use find_packages().
packages=find_packages(exclude=["contrib", "docs", "tests"]),
# Alternatively, if you want to distribute just a my_module.py, uncomment
# this:
# py_modules=["my_module"],
# List run-time dependencies here. These will be installed by pip when
# your project is installed. For an analysis of "install_requires" vs pip's
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=["numpy", "argparse", "pyzmq", "pyrtlsdr"],
# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,
# for example:
# $ pip install -e .[dev,test]
# extras_require={
# 'dev': ['check-manifest'],
# 'test': ['coverage'],
# },
# If there are data files included in your packages that need to be
# installed, specify them here. If using Python 2.6 or less, then these
# have to be included in MANIFEST.in as well.
# package_data={
# 'sample': ['package_data.dat'],
# },
# Although 'package_data' is the preferred approach, in some case you may
# need to place data files outside of your packages. See:
# http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
# data_files=[('my_data', ['data/data_file'])],
# To provide executable scripts, use entry points in preference to the
# "scripts" keyword. Entry points provide cross-platform support and allow
# pip to create the appropriate form of executable for the target platform.
# entry_points={
# 'console_scripts': [
# 'sample=sample:main',
# ],
# },
scripts=["pyModeS/streamer/modeslive"],
)

Some files were not shown because too many files have changed in this diff Show More