├── .gitignore
├── COPYING
├── README.md
├── _modes.c
├── _modes.h
├── debian
├── changelog
├── compat
├── config-template
├── control
├── copyright
├── mlat-client.config
├── mlat-client.init
├── mlat-client.install
├── mlat-client.logrotate
├── mlat-client.postinst
├── mlat-client.postrm
├── mlat-client.templates
└── rules
├── flightaware
├── __init__.py
└── client
│ ├── __init__.py
│ ├── adeptclient.py
│ └── cli.py
├── mlat
├── __init__.py
├── client
│ ├── __init__.py
│ ├── cli.py
│ ├── coordinator.py
│ ├── jsonclient.py
│ ├── net.py
│ ├── options.py
│ ├── output.py
│ ├── receiver.py
│ ├── stats.py
│ ├── synthetic_es.py
│ ├── util.py
│ └── version.py
├── constants.py
├── geodesy.py
└── profile.py
├── modes_crc.c
├── modes_message.c
├── modes_reader.c
├── pyproject.toml
├── run-flake8.sh
├── setup.py
├── tools
└── compare-message-timing.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | build
3 | dist
4 | *.so
5 | test*.sh
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mlat-client
2 |
3 | This is a client that selectively forwards Mode S messages to a
4 | server that resolves the transmitter position by multilateration of the same
5 | message received by multiple clients.
6 |
7 | The corresponding server code is available at
8 | https://github.com/mutability/mlat-server.
9 |
10 | ## Building
11 |
12 | To build a Debian (or Ubuntu, Raspbian, etc) package that includes config
13 | and startup scripts:
14 |
15 | $ sudo apt-get install build-essential debhelper python3-dev
16 | $ dpkg-buildpackage -b -uc
17 |
18 | This will build a .deb package in the parent directory. Install it with dpkg:
19 |
20 | $ sudo dpkg -i ../mlat-client_(version)_(architecture).deb
21 |
22 | To build/install (client only) on other systems using pip (you might want
23 | to do this inside a virtualenv):
24 |
25 | $ pip install .
26 |
27 | Or using the legacy setup.py:
28 |
29 | $ ./setup.py install
30 |
31 | ## Running
32 |
33 | If you are connecting to a third party multilateration server, contact the
34 | server's administrator for configuration instructions.
35 |
36 | ## Supported receivers
37 |
38 | * Anything that produces Beast-format output with a 12MHz clock:
39 | * dump1090_mr, dump1090-mutability, FlightAware's dump1090
40 | * modesdeco (probably?)
41 | * an actual Mode-S Beast
42 | * airspy_adsb in Beast output mode
43 | * SBS receivers
44 | * Radarcape in 12MHz mode
45 | * Radarcape in GPS mode
46 |
47 | ## Unsupported receivers
48 |
49 | * The FlightRadar24 radarcape-based receiver. This produces a deliberately
50 | crippled timestamp in its output, making it useless for multilateration.
51 | If you have one of these, you should ask FR24 to fix this.
52 |
53 | ## License
54 |
55 | Copyright 2015, [Oliver Jowett](mailto:oliver@mutability.co.uk).
56 |
57 | This program is free software: you can redistribute it and/or modify
58 | it under the terms of the GNU General Public License as published by
59 | the Free Software Foundation, either version 3 of the License, or
60 | (at your option) any later version.
61 |
62 | This program is distributed in the hope that it will be useful,
63 | but WITHOUT ANY WARRANTY; without even the implied warranty of
64 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
65 | GNU General Public License for more details.
66 |
67 | You should have received [a copy of the GNU General Public License](COPYING)
68 | along with this program. If not, see .
69 |
--------------------------------------------------------------------------------
/_modes.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Part of mlat-client - an ADS-B multilateration client.
3 | * Copyright 2015, Oliver Jowett
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #include "_modes.h"
20 |
21 | /* would be nice to move this into the submodule init, but that needs python 3.5 for PyModule_AddFunctions */
22 | static PyMethodDef methods[] = {
23 | { "crc", modescrc_crc, METH_VARARGS, "Calculate the Mode S CRC over a buffer. Don't include the message's trailing CRC bytes in the provided buffer." },
24 | { "EventMessage", (PyCFunction)modesmessage_eventmessage, METH_VARARGS|METH_KEYWORDS, "Constructs a new event message with a given type, timestamp, and event data." },
25 | { NULL, NULL, 0, NULL }
26 | };
27 |
28 | static void free_modes(PyObject *);
29 |
30 | PyDoc_STRVAR(docstr, "C helpers to speed up ModeS message processing");
31 | static PyModuleDef module = {
32 | PyModuleDef_HEAD_INIT,
33 | "_modes", /* m_name */
34 | docstr, /* m_doc */
35 | -1, /* m_size */
36 | methods, /* m_methods */
37 | NULL, /* m_slots / m_reload */
38 | NULL, /* m_traverse */
39 | NULL, /* m_clear */
40 | (freefunc)free_modes /* m_free */
41 | };
42 |
43 | PyMODINIT_FUNC
44 | PyInit__modes(void)
45 | {
46 | PyObject *m = NULL;
47 |
48 | m = PyModule_Create(&module);
49 | if (m == NULL)
50 | return NULL;
51 |
52 | if (modescrc_module_init(m) < 0) {
53 | goto error;
54 | }
55 |
56 | if (modesmessage_module_init(m) < 0) {
57 | goto error;
58 | }
59 |
60 | if (modesreader_module_init(m) < 0) {
61 | goto error;
62 | }
63 |
64 | return m;
65 |
66 | error:
67 | Py_DECREF(m);
68 | return NULL;
69 | }
70 |
71 | void free_modes(PyObject *m)
72 | {
73 | modesreader_module_free(m);
74 | modesmessage_module_free(m);
75 | modescrc_module_free(m);
76 | }
77 |
--------------------------------------------------------------------------------
/_modes.h:
--------------------------------------------------------------------------------
1 | #ifndef _MODES_H
2 | #define _MODES_H
3 |
4 | /*
5 | * Part of mlat-client - an ADS-B multilateration client.
6 | * Copyright 2015, Oliver Jowett
7 | *
8 | * This program is free software: you can redistribute it and/or modify
9 | * it under the terms of the GNU General Public License as published by
10 | * the Free Software Foundation, either version 3 of the License, or
11 | * (at your option) any later version.
12 | *
13 | * This program is distributed in the hope that it will be useful,
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | * GNU General Public License for more details.
17 | *
18 | * You should have received a copy of the GNU General Public License
19 | * along with this program. If not, see .
20 | */
21 |
22 | #include
23 | #include
24 |
25 | #include
26 |
27 | /* a modesmessage object */
28 | typedef struct {
29 | PyObject_HEAD
30 |
31 | unsigned long long timestamp;
32 | unsigned int signal;
33 |
34 | unsigned int df;
35 | unsigned int nuc;
36 | char even_cpr;
37 | char odd_cpr;
38 | char valid;
39 | PyObject *crc;
40 | PyObject *address;
41 | PyObject *altitude;
42 |
43 | uint8_t *data;
44 | int datalen;
45 |
46 | PyObject *eventdata;
47 | } modesmessage;
48 |
49 | /* special DF types for non-Mode-S messages */
50 | #define DF_MODEAC 32
51 | #define DF_EVENT_TIMESTAMP_JUMP 33
52 | #define DF_EVENT_MODE_CHANGE 34
53 | #define DF_EVENT_EPOCH_ROLLOVER 35
54 | #define DF_EVENT_RADARCAPE_STATUS 36
55 | #define DF_EVENT_RADARCAPE_POSITION 37
56 |
57 | /* factory function to build a modesmessage from a provided buffer */
58 | PyObject *modesmessage_from_buffer(unsigned long long timestamp, unsigned signal, uint8_t *data, int datalen);
59 | /* factory function to build an event message */
60 | PyObject *modesmessage_new_eventmessage(int type, unsigned long long timestamp, PyObject *eventdata);
61 | /* python entry point */
62 | PyObject *modesmessage_eventmessage(PyObject *self, PyObject *args, PyObject *kwds);
63 |
64 | /* crc helpers */
65 | uint32_t modescrc_buffer_crc(uint8_t *buf, Py_ssize_t len); /* internal interface */
66 | PyObject *modescrc_crc(PyObject *self, PyObject *args); /* external interface */
67 |
68 | /* submodule init/cleanup */
69 | int modescrc_module_init(PyObject *m);
70 | void modescrc_module_free(PyObject *m);
71 | int modesreader_module_init(PyObject *m);
72 | void modesreader_module_free(PyObject *m);
73 | int modesmessage_module_init(PyObject *m);
74 | void modesmessage_module_free(PyObject *m);
75 |
76 | #endif
77 |
--------------------------------------------------------------------------------
/debian/changelog:
--------------------------------------------------------------------------------
1 | mlat-client (0.2.13) stable; urgency=medium
2 |
3 | * Support for Python 3.11 PyFloat_Unpack4 API change
4 | * Move the CLI scripts {fa-,}mlat-client into their respective packages;
5 | replace the existing scripts with setuptools APIs
6 | * Add a pyproject.toml with most of the build metadata. setup.py is still needed
7 | for building with older Python and for the extension module.
8 | * Switch package builds to use pybuild, since everything should be recent enough
9 | for that now.
10 |
11 | -- Oliver Jowett Mon, 23 Oct 2023 19:52:21 +0800
12 |
13 | mlat-client (0.2.12) stable; urgency=medium
14 |
15 | * Treat a wider range of timestamps as synthetic
16 | * Fix radarcape position sanity check (broken for 2+ years!)
17 |
18 | -- Oliver Jowett Mon, 20 Dec 2021 14:25:08 +0800
19 |
20 | mlat-client (0.2.11) stable; urgency=medium
21 |
22 | * Do a sanity check on reported radarcape positions
23 | * Adjust UDP datagram size to follow reported route MTU
24 | * Make setup.py use env to find python3 interpreter (PR #17)
25 |
26 | -- Oliver Jowett Mon, 30 Dec 2019 22:14:59 +0800
27 |
28 | mlat-client (0.2.10) stable; urgency=medium
29 |
30 | * Fix handling of special (synthetic mlat / zero) message timestamps
31 | * Fix handling of Radarcape UTC midnight timestamp rollover
32 | * Fix typo in feet/meters conversion
33 | * Increase listen queue size on listening output sockets
34 | * Add a message timing correlation script
35 |
36 | -- Oliver Jowett Thu, 02 Nov 2017 13:11:26 +0000
37 |
38 | mlat-client (0.2.9) stable; urgency=medium
39 |
40 | * Fix jsonclient result path (github issue #18)
41 |
42 | -- Oliver Jowett Mon, 03 Apr 2017 13:29:01 +0100
43 |
44 | mlat-client (0.2.8) stable; urgency=medium
45 |
46 | * Mode A/C support
47 | * DF18 trackfile identifier support for Mode A/C results
48 | * Radarcape position message support
49 | * Update Beast-format timestamp adjustments to match the behavior of
50 | the Beast/Radarcape. This effectively only changes Mode A/C.
51 |
52 | -- Oliver Jowett Mon, 16 Jan 2017 14:14:29 +0000
53 |
54 | mlat-client (0.2.7) stable; urgency=medium
55 |
56 | * Add --no-anon-results option
57 |
58 | -- Oliver Jowett Tue, 25 Oct 2016 23:42:40 +0100
59 |
60 | mlat-client (0.2.6) stable; urgency=medium
61 |
62 | * Fix missing METH_KEYWORDS that would cause a crash on amd64
63 | * Use sendto, not connect/send, for the UDP transport so that the
64 | UDP source address follows any routing changes that happen
65 |
66 | -- Oliver Jowett Wed, 27 Jul 2016 17:09:36 +0100
67 |
68 | mlat-client (0.2.5) stable; urgency=medium
69 |
70 | * Overhaul of the extension module to allow much more filtering to happen
71 | before messages are passed to the Python layer
72 | * Autodetect input format where possible
73 | * Generate results as DF18 TIS-B, not DF17
74 | * Support generating DF18 "anonymous address" results
75 | * Don't fail entirely if a result output listener can't be created
76 | * IPv6 support
77 | * Interpret Radarcape status messages and change clock mode based on GPS
78 | status
79 | * Send a settings message on Beast-format connections so that (if the
80 | other end understands them) we get only the specific messages we care
81 | about
82 |
83 | -- Oliver Jowett Fri, 15 Jul 2016 13:13:03 +0100
84 |
85 | mlat-client (0.2.4) unstable; urgency=medium
86 |
87 | * Better periodic stats logging.
88 | * Grab CLIENT_VERSION from the source in setup.py so it doesn't need to be
89 | in two places.
90 | * fa-mlat-client: explicitly log exceptions on exit to try to fix cxfreeze
91 | weirdness.
92 | * fa-mlat-client: log server status messages.
93 | * fa-mlat-client: periodically report UDP stats so the server can notice
94 | message loss.
95 |
96 | -- Oliver Jowett Sun, 27 Sep 2015 23:27:16 +0100
97 |
98 | mlat-client (0.2.3) unstable; urgency=medium
99 |
100 | * Produce integer speed/heading/vrate values in Basestation output, maybe
101 | this will make VRS happier.
102 |
103 | -- Oliver Jowett Thu, 09 Jul 2015 12:27:20 +0100
104 |
105 | mlat-client (0.2.2) unstable; urgency=medium
106 |
107 | * Big overhaul of option handling. Existing configs should be upgraded
108 | automatically. See new config questions or mlat-client --help for details.
109 |
110 | * Support both listen and connect ("push") modes for result connections.
111 |
112 | * Add Beast-format result connections that generate synthetic DF17 messages
113 | for result position and velocity. The synthetic messages have a special
114 | timestamps (FF 00 MLAT) to indicate they are mlat-generated. mlat-client
115 | will ignore any messages with this timestamp that it receives. Input
116 | messages from the receiver are _not_ looped through to result connections.
117 |
118 | * Support receiving mlat results via a Flightaware-style connections in
119 | fa-mlat-client.
120 |
121 | * Support the full range of input / result options in fa-mlat-client.
122 |
123 | -- Oliver Jowett Thu, 02 Jul 2015 16:48:45 +0100
124 |
125 | mlat-client (0.2.1) unstable; urgency=medium
126 |
127 | * Add Breaks/Replaces for piaware versions that also provide
128 | /usr/bin/fa-mlat-client (piaware 2.0-5 stops providing this)
129 |
130 | -- Oliver Jowett Sun, 28 Jun 2015 12:05:22 +0100
131 |
132 | mlat-client (0.2.0) unstable; urgency=medium
133 |
134 | * Integrate the Flightaware version of the client into the main release.
135 |
136 | -- Oliver Jowett Thu, 18 Jun 2015 20:33:17 +0100
137 |
138 | mlat-client (0.1.17) unstable; urgency=medium
139 |
140 | * Handle keepalives and other messages that have a timestamp of zero.
141 | * Log a better explanation when a ClockResetError is seen.
142 |
143 | -- Oliver Jowett Mon, 15 Jun 2015 20:17:11 +0100
144 |
145 | mlat-client (0.1.16) unstable; urgency=medium
146 |
147 | * Workaround for dump1090-mutability issue #47 (a bug when using --modeac that would
148 | trigger ClockResetErrors)
149 |
150 | -- Oliver Jowett Mon, 08 Jun 2015 17:13:03 +0100
151 |
152 | mlat-client (0.1.15) unstable; urgency=medium
153 |
154 | * Fix "TypeError: expected an object with the buffer interface" and
155 | timestamp problems when configured for a SBS receiver.
156 | * Added some debugging info to ClockResetError.
157 |
158 | -- Oliver Jowett Sun, 07 Jun 2015 11:16:30 +0100
159 |
160 | mlat-client (0.1.14) unstable; urgency=medium
161 |
162 | * Fix looping "_modes.ClockResetError" errors after dump1090 restart.
163 | * Skip 0.1.13 since the 0.1.12 release had the wrong client version
164 | embedded in the code.
165 |
166 | -- Oliver Jowett Sun, 31 May 2015 14:04:28 +0100
167 |
168 | mlat-client (0.1.12) unstable; urgency=medium
169 |
170 | * Log client version on startup.
171 | * Add an inactivity timeout to server connections.
172 | In particular, this should help with connections that silently die
173 | during the handshaking phase, where we would otherwise wait forever
174 | without writing.
175 | * Convert to Python 3.
176 | * Use time.monotonic() if available; emulate it if not available. This should
177 | help with cases where system time goes backwards.
178 | * More processing moved into the extension module. This seems to reduce the
179 | client CPU by around 20% overall.
180 |
181 | -- Oliver Jowett Wed, 27 May 2015 14:28:16 +0100
182 |
183 | mlat-client (0.1.11) unstable; urgency=medium
184 |
185 | * Bugfixes for SBS support.
186 | * Entirely clean up UDP state on server disconnect.
187 |
188 | -- Oliver Jowett Sun, 03 May 2015 23:52:01 +0100
189 |
190 | mlat-client (0.1.10) unstable; urgency=medium
191 |
192 | * Rename --clock-type to --input-type.
193 | * Add support for SBS "raw socket" inputs (--input-type sbs).
194 | * Clean up the Radarcape GPS rollover code.
195 | * Improvements to options help.
196 | * Remove --random-drop as it's no longer useful.
197 | * Added configuration-script support for --input-type.
198 |
199 | -- Oliver Jowett Tue, 28 Apr 2015 14:09:21 +0100
200 |
201 | mlat-client (0.1.9) unstable; urgency=medium
202 |
203 | * Support for Radarcape when in GPS timestamp mode.
204 | * Add --privacy command-line option.
205 |
206 | -- Oliver Jowett Sat, 25 Apr 2015 18:29:31 +0100
207 |
208 | mlat-client (0.1.8) unstable; urgency=medium
209 |
210 | * Don't explode if there is no covariance matrix in an ECEF result.
211 | * Log server message parsing failures with a bit more detail.
212 | * Fix server->client decompression in zlib2 when a partial packet is received.
213 | * Fix server->client path for zlib and "no" compression (oops).
214 |
215 | -- Oliver Jowett Fri, 24 Apr 2015 10:09:50 +0100
216 |
217 | mlat-client (0.1.7) unstable; urgency=medium
218 |
219 | * Remove --byteswap-timestamp option: the timestamps generated by a
220 | big-endian dump1090 are irretrievably broken.
221 | * Futureproofing: add support for "split sync" sync message
222 | * Futureproofing: add a sequence number to the UDP header, bump UDP version
223 | * Futureproofing: restructure the heartbeat message so more fields can be
224 | added later
225 | * Add support for ECEF-style return results.
226 |
227 | -- Oliver Jowett Thu, 23 Apr 2015 00:10:45 +0100
228 |
229 | mlat-client (0.1.6) unstable; urgency=medium
230 |
231 | * Raise better exceptions on input error to try to diagnose Radarcape problems.
232 | * Handle (and skip) the mystery '4' message that Radarcape systems generate.
233 | * Add --byteswap-timestamp for dealing with dump1090s that (incorrectly)
234 | generate little-endian timestamp values.
235 | * Use the normal reconnect delay when reconnecting after a timestamp problem is
236 | seen.
237 | * Don't forward DF17 sync messages with NUC<6.
238 | * Use UDP transport for mlat/sync messages if requested by the server. Can
239 | be disabled with --no-udp.
240 | * Send ADS-B position rate reports if requested by the server.
241 |
242 | -- Oliver Jowett Sat, 18 Apr 2015 09:31:50 +0100
243 |
244 | mlat-client (0.1.5) unstable; urgency=medium
245 |
246 | * Fix exception logging.
247 | * Fix server vs client confusion in the shutdown message.
248 | * Escape CSV fields that might have commas/quotes.
249 | * Add support for extended Basestation-like output format on a separate port via --sbs-ext-port.
250 |
251 | -- Oliver Jowett Sat, 04 Apr 2015 13:06:41 +0100
252 |
253 | mlat-client (0.1.4) unstable; urgency=medium
254 |
255 | * Emit ICAO addresses in SBS output in all-caps to make VRS happier.
256 | * If no Beast data (not even keepalives) is seen for 2.5 minutes, reconnect.
257 | * Clean up connection logging a little.
258 | * Re-add some periodic stats.
259 | * Increase the allowable backwards-timestamp value a little.
260 |
261 | -- Oliver Jowett Mon, 30 Mar 2015 22:59:10 +0100
262 |
263 | mlat-client (0.1.3) unstable; urgency=medium
264 |
265 | * If message timestamps go significantly backwards, drop the input connection
266 | and reconnect.
267 | * Ignore heartbeat messages with a zero timestamp.
268 | * Expire all current aircraft state if the input connetion is lost.
269 | * Handle connection errors a bit more reliably.
270 |
271 | -- Oliver Jowett Mon, 23 Mar 2015 10:44:51 +0000
272 |
273 | mlat-client (0.1.2) unstable; urgency=medium
274 |
275 | * Reset last-message-timestamp when reconnecting to input.
276 | Should fix the client getting stuck if dump1090 restarts.
277 |
278 | -- Oliver Jowett Wed, 18 Mar 2015 14:20:02 +0000
279 |
280 | mlat-client (0.1.1) unstable; urgency=low
281 |
282 | * Fix BaseStation time format to make VRS happier.
283 |
284 | -- Oliver Jowett Wed, 18 Mar 2015 10:59:30 +0000
285 |
286 | mlat-client (0.1) unstable; urgency=medium
287 |
288 | * Initial release.
289 |
290 | -- Oliver Jowett Tue, 17 Mar 2015 21:59:40 +0000
291 |
--------------------------------------------------------------------------------
/debian/compat:
--------------------------------------------------------------------------------
1 | 10
2 |
--------------------------------------------------------------------------------
/debian/config-template:
--------------------------------------------------------------------------------
1 | ## TEMPLATE FILE - This is used to create /etc/default/mlat-client ##
2 | ## The first three lines will be discarded ##
3 |
4 | # mlat-client configuration file
5 | # This is a POSIX shell fragment.
6 | # You can edit this file directly, or use
7 | # "dpkg-reconfigure mlat-client"
8 |
9 | # Start the client?
10 | START_CLIENT=
11 |
12 | # System user to run as.
13 | RUN_AS_USER=
14 |
15 | # User to log into the server as
16 | SERVER_USER=
17 |
18 | # Logfile to log to
19 | LOGFILE=
20 |
21 | # Input receiver type (dump1090, beast, radarcape_12mhz, radarcape_gps, sbs)
22 | INPUT_TYPE=
23 |
24 | # Input host:port to connect to for Beast-format messages
25 | INPUT_HOSTPORT=
26 |
27 | # Multilateration server host:port to provide data to
28 | SERVER_HOSTPORT=
29 |
30 | # Latitude of the receiver, in decimal degrees
31 | LAT=
32 |
33 | # Longitude of the receiver, in decimal degrees
34 | LON=
35 |
36 | # Altitude of the receiver, in metres
37 | ALT=
38 |
39 | # List of result connections/listeners to establish.
40 | # This should be a space-separated list of values suitable for passing to
41 | # the --results option (see mlat-client --help for syntax)
42 | RESULTS=
43 |
44 | # Other arguments to pass to mlat-client
45 | EXTRA_ARGS=
46 |
--------------------------------------------------------------------------------
/debian/control:
--------------------------------------------------------------------------------
1 | Source: mlat-client
2 | Section: embedded
3 | Priority: extra
4 | Maintainer: Oliver Jowett
5 | Build-Depends: debhelper(>=10), dh-python, python3-dev
6 | Standards-Version: 3.9.3
7 | Homepage: https://github.com/mutability/mlat-client
8 | Vcs-Git: https://github.com/mutability/mlat-client.git
9 |
10 | Package: mlat-client
11 | Architecture: any
12 | Depends: ${shlibs:Depends}, ${python3:Depends}, ${misc:Depends}, adduser
13 | Breaks: piaware (= 2.0-1), piaware (= 2.0-2), piaware (= 2.0-3), piaware (= 2.0-4)
14 | Replaces: piaware (= 2.0-1), piaware (= 2.0-2), piaware (= 2.0-3), piaware (= 2.0-4)
15 | Description: ADS-B multilateration client.
16 | mlat-client feeds selected messages from an ADS-B receiver
17 | to a multilateration server, and provides access to the results
18 | generated by the server.
19 |
--------------------------------------------------------------------------------
/debian/copyright:
--------------------------------------------------------------------------------
1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2 | Upstream-Name: mlat-client
3 | Upstream-Contact: Oliver Jowett
4 | Source: https://github.com/mutability/mlat-client
5 |
6 | Files: *
7 | Copyright: Copyright 2015, Oliver Jowett
8 | License: GPL-3+
9 |
10 | License: GPL-3+
11 | This program is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 | .
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 | .
21 | You should have received a copy of the GNU General Public License
22 | along with this program. If not, see .
23 | .
24 | On Debian systems, the full text of the GNU General Public
25 | License version 3 can be found in the file
26 | `/usr/share/common-licenses/GPL-3'.
27 |
--------------------------------------------------------------------------------
/debian/mlat-client.config:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | NAME=mlat-client
4 | CONFIGFILE=/etc/default/$NAME
5 | set -e
6 | . /usr/share/debconf/confmodule
7 |
8 | db_set_yn() {
9 | # $1 = db key
10 | # $2 = config file yes/no value
11 | if [ "x$2" = "xyes" ]; then
12 | db_set $1 true
13 | else
14 | db_set $1 false
15 | fi
16 | }
17 |
18 | # Load config file, if it exists.
19 | if [ -e $CONFIGFILE ]; then
20 | . $CONFIGFILE || true
21 |
22 | # upgrade old config
23 | if [ -z "$INPUT_HOSTPORT" ]
24 | then
25 | if [ -n "$INPUT_HOST" ] && [ -n "$INPUT_PORT" ]
26 | then
27 | INPUT_HOSTPORT=$INPUT_HOST:$INPUT_PORT
28 | fi
29 | fi
30 |
31 | if [ -z "$RESULTS" ]
32 | then
33 | if [ -n "$SBS_PORT" -a "$SBS_PORT" != 0 ]
34 | then
35 | RESULTS="$RESULTS basestation,listen,$SBS_PORT"
36 | fi
37 |
38 | if [ -n "$SBS_EXT_PORT" -a "$SBS_EXT_PORT" != 0 ]
39 | then
40 | RESULTS="$RESULTS basestation_ext,listen,$SBS_EXT_PORT"
41 | fi
42 |
43 | if [ -z "$RESULTS" ]
44 | then
45 | RESULTS="none"
46 | fi
47 | fi
48 |
49 | # Store values from config file into
50 | # debconf db.
51 |
52 | [ -n "$START_CLIENT" ] && db_set_yn $NAME/start-client "$START_CLIENT"
53 | [ -n "$RUN_AS_USER" ] && db_set $NAME/run-as-user "$RUN_AS_USER"
54 | [ -n "$SERVER_USER" ] && db_set $NAME/server-user "$SERVER_USER"
55 | [ -n "$LOGFILE" ] && db_set $NAME/log-file "$LOGFILE"
56 | [ -n "$INPUT_TYPE" ] && db_set $NAME/input-type "$INPUT_TYPE"
57 | [ -n "$INPUT_HOSTPORT" ] && db_set $NAME/input-hostport "$INPUT_HOSTPORT"
58 | [ -n "$SERVER_HOSTPORT" ] && db_set $NAME/server-hostport "$SERVER_HOSTPORT"
59 | [ -n "$LAT" ] && db_set $NAME/receiver-lat "$LAT"
60 | [ -n "$LON" ] && db_set $NAME/receiver-lon "$LON"
61 | [ -n "$ALT" ] && db_set $NAME/receiver-alt "$ALT"
62 | [ -n "$RESULTS" ] && db_set $NAME/results "$RESULTS"
63 |
64 | # this can be legitimately empty:
65 | db_set $NAME/extra-args "$EXTRA_ARGS"
66 | fi
67 |
68 | # Ask questions.
69 |
70 | db_input_verify() {
71 | # $1 = priority
72 | # $2 = db key
73 | # $3 = verification function, should return 0 if OK
74 | PRI=$1; KEY=$2; VERIFY=$3
75 |
76 | set +e
77 | db_input $PRI $KEY; RESULT=$?
78 | db_go
79 | set -e
80 | ASKED=0
81 | while :
82 | do
83 | db_get $KEY
84 | if $VERIFY "$RET"; then return 0; fi
85 | if [ $RESULT -ne 0 ]; then
86 | # db_input failed, and the existing value does not validate
87 | if [ $RESULT = 30 ] && [ $ASKED = 0 ]
88 | then
89 | # question was skipped, but existing value is invalid
90 | # bump priority and try again (once)
91 | PRI=high
92 | ASKED=1
93 | else
94 | # give up, use the default value
95 | db_reset $KEY
96 | return 0
97 | fi
98 | else
99 | # db_input was OK, but the value did not verify.
100 | # show an error message
101 | db_input high mlat-client/invalid-$VERIFY || true
102 | fi
103 |
104 | # try again
105 | set +e
106 | db_fset $KEY seen false
107 | db_input high $KEY; RESULT=$?
108 | db_go
109 | set -e
110 | done
111 | }
112 |
113 | is_unsigned_int() {
114 | if echo "$1" | grep -Eq '^(0|+?[1-9][0-9]*)$'; then return 0; else return 1; fi
115 | }
116 |
117 | is_number() {
118 | if echo "$1" | grep -Eq '^([+-]?[0-9][0-9]*)(\.[0-9]+)?$'; then return 0; else return 1; fi
119 | }
120 |
121 | is_not_empty() {
122 | if [ -z "$1" ]; then return 1; else return 0; fi
123 | }
124 |
125 | # "adduser: To avoid problems, the username should consist only of
126 | # letters, digits, underscores, full stops, at signs and dashes, and not start with
127 | # a dash (as defined by IEEE Std 1003.1-2001). For compatibility with Samba
128 | # machine accounts $ is also supported at the end of the username"
129 | is_non_root_user() {
130 | if [ -z "$1" ]; then return 1;
131 | elif [ "$1" = "root" ]; then return 1;
132 | elif echo "$1" | grep -Eq '^[a-zA-Z0-9_.@-]+\$?$'; then return 0;
133 | else return 1; fi
134 | }
135 |
136 | is_port_number() {
137 | if is_unsigned_int "$1"; then
138 | if [ "$1" -eq 0 ]; then return 0; fi
139 | if [ "$1" -lt 1024 ]; then return 1; fi
140 | if [ "$1" -gt 65535 ]; then return 1; fi
141 | return 0
142 | else
143 | return 1
144 | fi
145 | }
146 |
147 | db_input medium $NAME/start-client || true
148 | db_go || true
149 |
150 | db_get $NAME/start-client
151 | if [ "$RET" = "true" ]
152 | then
153 | db_input_verify low $NAME/run-as-user is_non_root_user || true
154 | db_input_verify medium $NAME/server-hostport is_not_empty || true
155 | db_input_verify high $NAME/server-user is_not_empty || true
156 | db_input high $NAME/input-type || true
157 | db_input_verify high $NAME/input-hostport is_not_empty || true
158 | db_input_verify high $NAME/receiver-lat is_number || true
159 | db_input_verify high $NAME/receiver-lon is_number || true
160 | db_input_verify high $NAME/receiver-alt is_number || true
161 | db_input medium $NAME/results || true
162 | db_input_verify low $NAME/log-file is_not_empty || true
163 | db_input low $NAME/extra-args || true
164 | db_go || true
165 | fi
166 |
167 | # Done.
168 | db_stop
169 |
--------------------------------------------------------------------------------
/debian/mlat-client.init:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ### BEGIN INIT INFO
3 | # Provides: mlat-client
4 | # Required-Start: $remote_fs
5 | # Required-Stop: $remote_fs
6 | # Default-Start: 2 3 4 5
7 | # Default-Stop: 0 1 6
8 | # Short-Description: Multilateration client
9 | # Description: Receives ADS-B messages from a receiver
10 | # and forwards them to a server for multilateration of
11 | # the aircraft position.
12 | ### END INIT INFO
13 |
14 | # Do NOT "set -e"
15 |
16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script
17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin
18 | DESC="mlat-client daemon"
19 | NAME=mlat-client
20 | DAEMON=/usr/bin/mlat-client
21 | ARGS=""
22 | PIDFILE=/var/run/$NAME.pid
23 | SCRIPTNAME=/etc/init.d/$NAME
24 |
25 | # Exit if the package is not installed
26 | [ -x "$DAEMON" ] || exit 0
27 |
28 | # Read configuration variable file if it is present
29 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME
30 |
31 | # work out daemon args
32 |
33 | # sanitize missing settings
34 | badconfig=""
35 | [ -z "$START_CLIENT" ] && START_CLIENT=no
36 | [ -z "$RUN_AS_USER" ] && RUN_AS_USER="missing-RUN_AS_USER-setting-in-config"
37 | [ -z "$SERVER_USER" ] && badconfig="$badconfig SERVER_USER"
38 | [ -z "$LAT" ] && badconfig="$badconfig LAT"
39 | [ -z "$LON" ] && badconfig="$badconfig LON"
40 | [ -z "$ALT" ] && badconfig="$badconfig ALT"
41 | [ -z "$INPUT_TYPE" ] && INPUT_TYPE=dump1090
42 | [ -z "$INPUT_HOSTPORT" ] && badconfig="$badconfig INPUT_HOSTPORT"
43 | [ -z "$SERVER_HOSTPORT" ] && badconfig="$badconfig SERVER_HOSTPORT"
44 |
45 | ARGS="--user $SERVER_USER --lat $LAT --lon $LON --alt $ALT --input-type $INPUT_TYPE --input-connect $INPUT_HOSTPORT --server $SERVER_HOSTPORT"
46 |
47 | for r in $RESULTS
48 | do
49 | if [ "$r" != "none" ]
50 | then
51 | ARGS="$ARGS --results $r"
52 | fi
53 | done
54 |
55 | ARGS="$ARGS $EXTRA_ARGS"
56 |
57 | # Load the VERBOSE setting and other rcS variables
58 | . /lib/init/vars.sh
59 |
60 | # Define LSB log_* functions.
61 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present
62 | # and status_of_proc is working.
63 | . /lib/lsb/init-functions
64 |
65 | #
66 | # Function that starts the daemon/service
67 | #
68 | do_start()
69 | {
70 | # Return
71 | # 0 if daemon has been started
72 | # 1 if daemon was already running
73 | # 2 if daemon could not be started
74 |
75 | if [ "x$START_CLIENT" != "xyes" ]; then
76 | log_warning_msg "Not starting $NAME daemon, disabled via /etc/default/$NAME"
77 | return 2
78 | fi
79 |
80 | if [ -n "$badconfig" ]; then
81 | log_warning_msg "Not starting $NAME daemon, missing configuration options ($badconfig)"
82 | return 2
83 | fi
84 |
85 | start-stop-daemon --start --quiet --pidfile $PIDFILE --user "$RUN_AS_USER" --startas $DAEMON --test > /dev/null \
86 | || return 1
87 |
88 | start-stop-daemon --start --nicelevel 5 --quiet --pidfile $PIDFILE --user "$RUN_AS_USER" --chuid "$RUN_AS_USER" --make-pidfile --background --no-close --startas $DAEMON -- \
89 | $ARGS >>$LOGFILE 2>&1 \
90 | || return 2
91 | sleep 1
92 | }
93 |
94 | #
95 | # Function that stops the daemon/service
96 | #
97 | do_stop()
98 | {
99 | # Return
100 | # 0 if daemon has been stopped
101 | # 1 if daemon was already stopped
102 | # 2 if daemon could not be stopped
103 | # other if a failure occurred
104 | start-stop-daemon --stop --retry=TERM/30/KILL/5 --pidfile $PIDFILE --user "$RUN_AS_USER"
105 | RETVAL="$?"
106 | [ "$RETVAL" = 2 ] && return 2
107 | sleep 1
108 | # Many daemons don't delete their pidfiles when they exit.
109 | rm -f $PIDFILE
110 | return "$RETVAL"
111 | }
112 |
113 | case "$1" in
114 | start)
115 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
116 | do_start
117 | case "$?" in
118 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
119 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
120 | esac
121 | ;;
122 | stop)
123 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
124 | do_stop
125 | case "$?" in
126 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
127 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
128 | esac
129 | ;;
130 | status)
131 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
132 | ;;
133 | restart|force-reload)
134 | log_daemon_msg "Restarting $DESC" "$NAME"
135 | do_stop
136 | case "$?" in
137 | 0|1)
138 | do_start
139 | case "$?" in
140 | 0) log_end_msg 0 ;;
141 | 1) log_end_msg 1 ;; # Old process is still running
142 | *) log_end_msg 1 ;; # Failed to start
143 | esac
144 | ;;
145 | *)
146 | # Failed to stop
147 | log_end_msg 1
148 | ;;
149 | esac
150 | ;;
151 | *)
152 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
153 | exit 3
154 | ;;
155 | esac
156 |
157 | :
158 |
--------------------------------------------------------------------------------
/debian/mlat-client.install:
--------------------------------------------------------------------------------
1 | debian/config-template usr/share/mlat-client
2 |
--------------------------------------------------------------------------------
/debian/mlat-client.logrotate:
--------------------------------------------------------------------------------
1 | /var/log/mlat-client.log {
2 | daily
3 | rotate 4
4 | copytruncate
5 | }
6 |
--------------------------------------------------------------------------------
/debian/mlat-client.postinst:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # postinst script for mlat-client
3 | #
4 | # see: dh_installdeb(1)
5 |
6 | set -e
7 |
8 | # summary of how this script can be called:
9 | # * `configure'
10 | # * `abort-upgrade'
11 | # * `abort-remove' `in-favour'
12 | #
13 | # * `abort-remove'
14 | # * `abort-deconfigure' `in-favour'
15 | # `removing'
16 | #
17 | # for details, see http://www.debian.org/doc/debian-policy/ or
18 | # the debian-policy package
19 |
20 | NAME=mlat-client
21 | CONFIGFILE=/etc/default/$NAME
22 | TEMPLATECONFIG=/usr/share/$NAME/config-template
23 | SEDSCRIPT=$CONFIGFILE.sed.tmp
24 |
25 | subvar_raw() {
26 | # $1 = db var value
27 | # $2 = config var name
28 |
29 | # if not present in the config file, add it
30 | if grep -Eq "^ *$2=" $CONFIGFILE
31 | then
32 | # Already present
33 | true
34 | else
35 | echo "# $2 added automatically on upgrade" >> $CONFIGFILE
36 | echo "$2=" >> $CONFIGFILE
37 | fi
38 |
39 | # add to the sedscript
40 | echo "s@^ *$2=.*@$2=\"$1\"@" >>$SEDSCRIPT
41 | }
42 |
43 | subvar() {
44 | # $1 = db var name
45 | # $2 = config var name
46 | db_get $NAME/$1
47 | subvar_raw "$RET" "$2"
48 | }
49 |
50 | subvar_yn() {
51 | # $1 = db var name
52 | # $2 = config var name
53 | db_get $NAME/$1
54 | if [ "$RET" = "true" ]; then subvar_raw "yes" "$2"; else subvar_raw "no" "$2"; fi
55 | }
56 |
57 | case "$1" in
58 | configure)
59 | . /usr/share/debconf/confmodule
60 |
61 | # Generate config file, if it doesn't exist.
62 | if [ ! -e $CONFIGFILE ]; then
63 | tail -n +4 $TEMPLATECONFIG >$CONFIGFILE
64 | fi
65 |
66 | rm -f $SEDSCRIPT
67 |
68 | subvar_yn start-client START_CLIENT
69 | subvar run-as-user RUN_AS_USER
70 | subvar server-hostport SERVER_HOSTPORT
71 | subvar server-user SERVER_USER
72 | subvar input-type INPUT_TYPE
73 | subvar input-hostport INPUT_HOSTPORT
74 | subvar receiver-lat LAT
75 | subvar receiver-lon LON
76 | subvar receiver-alt ALT
77 | subvar results RESULTS
78 | subvar log-file LOGFILE
79 | subvar extra-args EXTRA_ARGS
80 |
81 | cp -a -f $CONFIGFILE $CONFIGFILE.tmp
82 | sed -f $SEDSCRIPT < $CONFIGFILE > $CONFIGFILE.tmp
83 | mv -f $CONFIGFILE.tmp $CONFIGFILE
84 | rm $SEDSCRIPT
85 |
86 | db_get $NAME/run-as-user
87 | RUNAS="$RET"
88 | if ! getent passwd "$RUNAS" >/dev/null
89 | then
90 | adduser --system --home /usr/share/$NAME --no-create-home --quiet "$RUNAS"
91 | fi
92 |
93 | ;;
94 |
95 | abort-upgrade|abort-remove|abort-deconfigure)
96 | ;;
97 |
98 | *)
99 | echo "postinst called with unknown argument \`$1'" >&2
100 | exit 1
101 | ;;
102 | esac
103 |
104 | # dh_installdeb will replace this with shell code automatically
105 | # generated by other debhelper scripts.
106 |
107 | #DEBHELPER#
108 |
109 | if [ "$1" = "configure" ]; then db_stop; fi
110 | exit 0
111 |
--------------------------------------------------------------------------------
/debian/mlat-client.postrm:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # postrm script for #PACKAGE#
3 | #
4 | # see: dh_installdeb(1)
5 |
6 | set -e
7 |
8 | # summary of how this script can be called:
9 | # * `remove'
10 | # * `purge'
11 | # * `upgrade'
12 | # * `failed-upgrade'
13 | # * `abort-install'
14 | # * `abort-install'
15 | # * `abort-upgrade'
16 | # * `disappear'
17 | #
18 | # for details, see http://www.debian.org/doc/debian-policy/ or
19 | # the debian-policy package
20 |
21 |
22 | case "$1" in
23 | purge)
24 | rm -f /etc/default/mlat-client
25 | ;;
26 |
27 | remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
28 | ;;
29 |
30 | *)
31 | echo "postrm called with unknown argument \`$1'" >&2
32 | exit 1
33 | ;;
34 | esac
35 |
36 | # dh_installdeb will replace this with shell code automatically
37 | # generated by other debhelper scripts.
38 |
39 | #DEBHELPER#
40 |
41 | exit 0
42 |
--------------------------------------------------------------------------------
/debian/mlat-client.templates:
--------------------------------------------------------------------------------
1 | Template: mlat-client/start-client
2 | Description: Start the standalone multilateration client?
3 | The multilateration client can be started automatically in standalone mode
4 | based on the answers to these configuration questions.
5 | .
6 | Enable this if you are connecting to an independent mlat server and you
7 | have configuration details for that server.
8 | .
9 | Disable this if you are providing multilateration data to Flightaware
10 | via piaware, or if you will start mlat-client via some other mechanism.
11 | Type: boolean
12 | Default: false
13 |
14 | Template: mlat-client/run-as-user
15 | Description: User to run mlat-client as:
16 | When started automatically, mlat-client runs as an unprivileged system user.
17 | This user will be created if it does not yet exist.
18 | Type: string
19 | Default: mlat
20 |
21 | Template: mlat-client/server-hostport
22 | Description: Multilateration server (host:port) to connect to:
23 | The multilateration client connects to a server that processes the
24 | collected data. This setting controls which server the client will
25 | connect to. The administrator of the server will be able to provide
26 | this information.
27 | Type: string
28 | Default: mlat.mutability.co.uk:40147
29 |
30 | Template: mlat-client/server-user
31 | Description: User to log into the server as:
32 | The multilateration client identifies itself to the server via a short
33 | username. This can be anything you like, it's just used to identify who is
34 | connecting. Each receiver should have a different value.
35 | Type: string
36 | Default:
37 |
38 | Template: mlat-client/log-file
39 | Description: Path to log to:
40 | When started automatically, mlat-client will log its output somewhere. This
41 | log mostly contains not very interesting connection info.
42 | Type: string
43 | Default: /var/log/mlat-client.log
44 |
45 | Template: mlat-client/receiver-lat
46 | Description: Latitude of receiver, in decimal degrees:
47 | The multilateration server must know the location of the receiver. This should
48 | be the position of the antenna, ideally to within about 25m. A four-decimal-
49 | place GPS location is fine.
50 | Type: string
51 | Default:
52 |
53 | Template: mlat-client/receiver-lon
54 | Description: Longitude of receiver, in decimal degrees:
55 | The multilateration server must know the location of the receiver. This should
56 | be the position of the antenna, ideally to within about 25m. A four-decimal-
57 | place GPS location is fine.
58 | Type: string
59 | Default:
60 |
61 | Template: mlat-client/receiver-alt
62 | Description: Altitude of receiver, in metres (height above ellipsoid):
63 | The multilateration server must know the location of the receiver. This should
64 | be the position of the antenna, ideally to within about 25m. A GPS-derived
65 | altitude is fine; use the WGS84 uncorrected altitude, not the corrected AMSL
66 | altitude. This value should be in metres.
67 | Type: string
68 | Default:
69 |
70 | Template: mlat-client/input-type
71 | Description: Receiver type:
72 | This setting sets the type of receiver that Mode S messages will
73 | be read from.
74 | .
75 | dump1090:
76 | dump1090 (MalcolmRobb or mutability fork), or anything else
77 | that can generate Beast-format messages with a 12MHz clock.
78 | beast:
79 | The Mode-S Beast.
80 | radarcape_gps:
81 | Radarcape with GPS timestamps (preferred).
82 | radarcape_12mhz:
83 | Radarcape with legacy 12MHz timestamps.
84 | sbs:
85 | Kinetic Avionics SBS-1/SBS-3 systems.
86 | Type: select
87 | Choices: dump1090, beast, radarcape_gps, radarcape_12mhz, sbs
88 | Default: dump1090
89 |
90 | Template: mlat-client/input-hostport
91 | Description: Input host:port for Mode S traffic:
92 | The multilateration client needs to read Mode S messages from a
93 | receiver such as dump1090. Here you can configure the host and port that the
94 | receiver is running on.
95 | .
96 | For dump1090, you need Beast-format output. This is usually available on port 30005.
97 | For Radarcapes, use port 10002.
98 | For SBS, use port 30006.
99 | Type: string
100 | Default: localhost:30005
101 |
102 | Template: mlat-client/results
103 | Description: List of result connections/listeners:
104 | The multilateration client can return the calculated aircraft positions in
105 | various forms. This settings accepts a list of connections or listeners to
106 | establish. See the mlat-client --help option for the exact format.
107 | .
108 | A value of "none" disables the output of results.
109 | Type: string
110 | Default: basestation,listen,31003
111 |
112 | Template: mlat-client/extra-args
113 | Description: Extra arguments to pass to mlat-client:
114 | Here you can add any extra arguments you want to pass to mlat-client.
115 | Type: string
116 | Default:
117 |
118 | Template: mlat-client/invalid-is_not_empty
119 | Description: Value cannot be empty.
120 | Type: error
121 |
122 | Template: mlat-client/invalid-is_port_number
123 | Description: Value must be a valid port number (1024-65535), or zero to disable.
124 | Type: error
125 |
126 | Template: mlat-client/invalid-is_number
127 | Description: Value must be a decimal number
128 | Type: error
129 |
130 | Template: mlat-client/invalid-is_non_root_user
131 | Description: Value must be a username (without spaces) that isn't root.
132 | Type: error
133 |
--------------------------------------------------------------------------------
/debian/rules:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 | # -*- makefile -*-
3 |
4 |
5 | # We use pybuild now.
6 | export DH_VERBOSE=1
7 | export PYBUILD_NAME=foo
8 |
9 | %:
10 | dh $@ --with python3 --buildsystem=pybuild
11 |
--------------------------------------------------------------------------------
/flightaware/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mutability/mlat-client/fe70767be859100176983b948140046b6ecdd34a/flightaware/__init__.py
--------------------------------------------------------------------------------
/flightaware/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mutability/mlat-client/fe70767be859100176983b948140046b6ecdd34a/flightaware/client/__init__.py
--------------------------------------------------------------------------------
/flightaware/client/adeptclient.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | """
4 | The FlightAware adept protocol, client side.
5 | """
6 |
7 | import asyncore
8 | import socket
9 | import errno
10 | import sys
11 | import itertools
12 | import struct
13 |
14 | from mlat.client import net, util, stats, version
15 |
16 | # UDP protocol submessages
17 | # TODO: This needs merging with mlat-client's variant
18 | # (they are not quite identical so it'll need a new
19 | # udp protocol version - this version has the decoded
20 | # ICAO address at the start of MLAT/SYNC to ease the
21 | # work of the server doing fan-out)
22 |
23 | TYPE_SYNC = 1
24 | TYPE_MLAT_SHORT = 2
25 | TYPE_MLAT_LONG = 3
26 | # TYPE_SSYNC = 4
27 | TYPE_REBASE = 5
28 | TYPE_ABS_SYNC = 6
29 | TYPE_MLAT_MODEAC = 7
30 |
31 | STRUCT_HEADER = struct.Struct(">IHQ")
32 | STRUCT_SYNC = struct.Struct(">B3Bii14s14s")
33 | # STRUCT_SSYNC = struct.Struct(">Bi14s")
34 | STRUCT_MLAT_SHORT = struct.Struct(">B3Bi7s")
35 | STRUCT_MLAT_LONG = struct.Struct(">B3Bi14s")
36 | STRUCT_REBASE = struct.Struct(">BQ")
37 | STRUCT_ABS_SYNC = struct.Struct(">B3BQQ14s14s")
38 | STRUCT_MLAT_MODEAC = struct.Struct(">Bi2s")
39 |
40 |
41 | if sys.platform == 'linux':
42 | IP_MTU = 14 # not defined in the socket module, unfortunately
43 |
44 | def get_mtu(s):
45 | try:
46 | return s.getsockopt(socket.SOL_IP, IP_MTU)
47 | except OSError:
48 | return None
49 | except socket.error:
50 | return None
51 | else:
52 | def get_mtu(s):
53 | return None
54 |
55 |
56 | class UdpServerConnection:
57 | def __init__(self, host, port, key):
58 | self.host = host
59 | self.port = port
60 | self.key = key
61 |
62 | self.base_timestamp = None
63 | self.header_timestamp = None
64 | self.buf = bytearray(1500)
65 | self.used = 0
66 | self.seq = 0
67 | self.count = 0
68 | self.sock = None
69 | self.mtu = 1400
70 | self.route_mtu = -1
71 |
72 | def start(self):
73 | addrlist = socket.getaddrinfo(host=self.host,
74 | port=self.port,
75 | family=socket.AF_UNSPEC,
76 | type=socket.SOCK_DGRAM,
77 | proto=0,
78 | flags=socket.AI_NUMERICHOST)
79 |
80 | if len(addrlist) != 1:
81 | # expect exactly one result since we specify AI_NUMERICHOST
82 | raise IOError('unexpectedly got {0} results when resolving {1}'.format(len(addrlist), self.host))
83 | a_family, a_type, a_proto, a_canonname, a_sockaddr = addrlist[0]
84 | self.sock = socket.socket(a_family, a_type, a_proto)
85 | self.remote_address = a_sockaddr
86 | self.refresh_socket()
87 |
88 | def refresh_socket(self):
89 | try:
90 | self.sock.connect(self.remote_address)
91 | except OSError:
92 | pass
93 | except socket.error:
94 | pass
95 |
96 | new_mtu = get_mtu(self.sock)
97 | if new_mtu is not None and new_mtu != self.route_mtu:
98 | util.log('Route MTU changed to {0}', new_mtu)
99 | self.route_mtu = new_mtu
100 | self.mtu = max(100, self.route_mtu - 100)
101 |
102 | def prepare_header(self, timestamp):
103 | self.base_timestamp = timestamp
104 | STRUCT_HEADER.pack_into(self.buf, 0,
105 | self.key, self.seq, self.base_timestamp)
106 | self.used += STRUCT_HEADER.size
107 |
108 | def rebase(self, timestamp):
109 | self.base_timestamp = timestamp
110 | STRUCT_REBASE.pack_into(self.buf, self.used,
111 | TYPE_REBASE,
112 | self.base_timestamp)
113 | self.used += STRUCT_REBASE.size
114 |
115 | def send_mlat(self, message):
116 | if not self.used:
117 | self.prepare_header(message.timestamp)
118 |
119 | delta = message.timestamp - self.base_timestamp
120 | if abs(delta) > 0x7FFFFFF0:
121 | self.rebase(message.timestamp)
122 | delta = 0
123 |
124 | if len(message) == 2:
125 | STRUCT_MLAT_MODEAC.pack_into(self.buf, self.used,
126 | TYPE_MLAT_MODEAC,
127 | delta, bytes(message))
128 | self.used += STRUCT_MLAT_MODEAC.size
129 | elif len(message) == 7:
130 | STRUCT_MLAT_SHORT.pack_into(self.buf, self.used,
131 | TYPE_MLAT_SHORT,
132 | message.address >> 16,
133 | (message.address >> 8) & 255,
134 | message.address & 255,
135 | delta, bytes(message))
136 | self.used += STRUCT_MLAT_SHORT.size
137 |
138 | elif len(message) == 14:
139 | STRUCT_MLAT_LONG.pack_into(self.buf, self.used,
140 | TYPE_MLAT_LONG,
141 | message.address >> 16,
142 | (message.address >> 8) & 255,
143 | message.address & 255,
144 | delta, bytes(message))
145 | self.used += STRUCT_MLAT_LONG.size
146 |
147 | if self.used > self.mtu:
148 | self.flush()
149 |
150 | def send_sync(self, em, om):
151 | if not self.used:
152 | self.prepare_header(int((em.timestamp + om.timestamp) / 2))
153 |
154 | if abs(em.timestamp - om.timestamp) > 0xFFFFFFF0:
155 | # use abs sync
156 | STRUCT_ABS_SYNC.pack_into(self.buf, self.used,
157 | TYPE_ABS_SYNC,
158 | em.address >> 16,
159 | (em.address >> 8) & 255,
160 | em.address & 255,
161 | em.timestamp, om.timestamp, bytes(em), bytes(om))
162 | self.used += STRUCT_ABS_SYNC.size
163 | else:
164 | edelta = em.timestamp - self.base_timestamp
165 | odelta = om.timestamp - self.base_timestamp
166 | if abs(edelta) > 0x7FFFFFF0 or abs(odelta) > 0x7FFFFFF0:
167 | self.rebase(int((em.timestamp + om.timestamp) / 2))
168 | edelta = em.timestamp - self.base_timestamp
169 | odelta = om.timestamp - self.base_timestamp
170 |
171 | STRUCT_SYNC.pack_into(self.buf, self.used,
172 | TYPE_SYNC,
173 | em.address >> 16,
174 | (em.address >> 8) & 255,
175 | em.address & 255,
176 | edelta, odelta, bytes(em), bytes(om))
177 | self.used += STRUCT_SYNC.size
178 |
179 | if self.used > self.mtu:
180 | self.flush()
181 |
182 | def flush(self):
183 | if not self.used:
184 | return
185 |
186 | try:
187 | self.sock.send(memoryview(self.buf)[0:self.used])
188 | except socket.error:
189 | pass
190 |
191 | stats.global_stats.server_udp_bytes += self.used
192 |
193 | self.used = 0
194 | self.base_timestamp = None
195 | self.seq = (self.seq + 1) & 0xffff
196 | self.count += 1
197 |
198 | if self.count % 50 == 0:
199 | self.refresh_socket()
200 |
201 | def close(self):
202 | self.used = 0
203 | if self.sock:
204 | self.sock.close()
205 |
206 | def __str__(self):
207 | return '{0}:{1}'.format(self.host, self.port)
208 |
209 |
210 | class AdeptReader(asyncore.file_dispatcher, net.LoggingMixin):
211 | """Reads tab-separated key-value messages from stdin and dispatches them."""
212 |
213 | def __init__(self, connection, coordinator):
214 | super().__init__(sys.stdin)
215 |
216 | self.connection = connection
217 | self.coordinator = coordinator
218 | self.partial_line = b''
219 | self.closed = False
220 |
221 | self.handlers = {
222 | 'mlat_wanted': self.process_wanted_message,
223 | 'mlat_unwanted': self.process_unwanted_message,
224 | 'mlat_result': self.process_result_message,
225 | 'mlat_status': self.process_status_message
226 | }
227 |
228 | def readable(self):
229 | return True
230 |
231 | def writable(self):
232 | return False
233 |
234 | def handle_read(self):
235 | try:
236 | moredata = self.recv(16384)
237 | except socket.error as e:
238 | if e.errno == errno.EAGAIN:
239 | return
240 | raise
241 |
242 | if not moredata:
243 | self.close()
244 | return
245 |
246 | stats.global_stats.server_rx_bytes += len(moredata)
247 |
248 | data = self.partial_line + moredata
249 | lines = data.split(b'\n')
250 | for line in lines[:-1]:
251 | try:
252 | self.process_line(line.decode('ascii'))
253 | except IOError:
254 | raise
255 | except Exception:
256 | util.log_exc('Unexpected exception processing adept message')
257 |
258 | self.partial_line = lines[-1]
259 |
260 | def handle_close(self):
261 | self.close()
262 |
263 | def close(self):
264 | if not self.closed:
265 | self.closed = True
266 | super().close()
267 | self.connection.disconnect()
268 |
269 | def process_line(self, line):
270 | fields = line.split('\t')
271 | message = dict(zip(fields[0::2], fields[1::2]))
272 |
273 | handler = self.handlers.get(message['type'])
274 | if handler:
275 | handler(message)
276 |
277 | def parse_hexid_list(self, s):
278 | icao = set()
279 | modeac = set()
280 | if s != '':
281 | for x in s.split(' '):
282 | if x[0] == '@':
283 | modeac.add(int(x[1:], 16))
284 | else:
285 | icao.add(int(x, 16))
286 | return icao, modeac
287 |
288 | def process_wanted_message(self, message):
289 | wanted_icao, wanted_modeac = self.parse_hexid_list(message['hexids'])
290 | self.coordinator.server_start_sending(wanted_icao, wanted_modeac)
291 |
292 | def process_unwanted_message(self, message):
293 | unwanted_icao, unwanted_modeac = self.parse_hexid_list(message['hexids'])
294 | self.coordinator.server_stop_sending(unwanted_icao, unwanted_modeac)
295 |
296 | def process_result_message(self, message):
297 | self.coordinator.server_mlat_result(timestamp=None,
298 | addr=int(message['hexid'], 16),
299 | lat=float(message['lat']),
300 | lon=float(message['lon']),
301 | alt=float(message['alt']),
302 | nsvel=float(message['nsvel']),
303 | ewvel=float(message['ewvel']),
304 | vrate=float(message['fpm']),
305 | callsign=None,
306 | squawk=None,
307 | error_est=None,
308 | nstations=None,
309 | anon=bool(message.get('anon', 0)),
310 | modeac=bool(message.get('modeac', 0)))
311 |
312 | def process_status_message(self, message):
313 | s = message.get('status', 'unknown')
314 | r = int(message.get('receiver_sync_count', 0))
315 |
316 | if s == 'ok':
317 | self.connection.state = "synchronized with {} nearby receivers".format(r)
318 | elif s == 'unstable':
319 | self.connection.state = "clock unstable"
320 | elif s == 'no_sync':
321 | self.connection.state = "not synchronized with any nearby receivers"
322 | else:
323 | self.connection.state = "{} {}".format(s, r)
324 |
325 |
326 | class AdeptWriter(asyncore.file_dispatcher, net.LoggingMixin):
327 | """Writes tab-separated key-value messages to stdout."""
328 |
329 | def __init__(self, connection):
330 | super().__init__(sys.stdout)
331 | self.connection = connection
332 | self.writebuf = bytearray()
333 | self.closed = False
334 | self.last_position = None
335 |
336 | def readable(self):
337 | return False
338 |
339 | def writable(self):
340 | return len(self.writebuf) > 0
341 |
342 | def handle_write(self):
343 | if self.writebuf:
344 | sent = self.send(self.writebuf)
345 | del self.writebuf[:sent]
346 | stats.global_stats.server_tx_bytes += sent
347 | if len(self.writebuf) > 65536:
348 | raise IOError('Server write buffer overflow (too much unsent data)')
349 |
350 | def handle_close(self):
351 | self.close()
352 |
353 | def close(self):
354 | if not self.closed:
355 | self.closed = True
356 | super().close()
357 | self.connection.disconnect()
358 |
359 | def send_message(self, **kwargs):
360 | line = '\t'.join(itertools.chain.from_iterable(kwargs.items())) + '\n'
361 | self.writebuf += line.encode('ascii')
362 |
363 | def send_seen(self, aclist):
364 | self.send_message(type='mlat_seen',
365 | hexids=' '.join('{0:06X}'.format(icao) for icao in aclist))
366 |
367 | def send_lost(self, aclist):
368 | self.send_message(type='mlat_lost',
369 | hexids=' '.join('{0:06X}'.format(icao) for icao in aclist))
370 |
371 | def send_rate_report(self, report):
372 | self.send_message(type='mlat_rates',
373 | rates=' '.join('{0:06X} {1:.2f}'.format(icao, rate) for icao, rate in report.items()))
374 |
375 | def send_ready(self, allow_anon, allow_modeac):
376 | capabilities = []
377 | if allow_anon:
378 | capabilities.append('anon')
379 | if allow_modeac:
380 | capabilities.append('modeac')
381 | self.send_message(type='mlat_event', event='ready', mlat_client_version=version.CLIENT_VERSION,
382 | capabilities=' '.join(capabilities))
383 |
384 | def send_input_connected(self):
385 | self.send_message(type='mlat_event', event='connected')
386 |
387 | def send_input_disconnected(self):
388 | self.send_message(type='mlat_event', event='disconnected')
389 |
390 | def send_clock_reset(self, reason, frequency=None, epoch=None, mode=None):
391 | message = {
392 | 'type': 'mlat_event',
393 | 'event': 'clock_reset',
394 | 'reason': reason
395 | }
396 |
397 | if frequency is not None:
398 | message['frequency'] = str(frequency)
399 | message['epoch'] = 'none' if epoch is None else epoch
400 | message['mode'] = mode
401 |
402 | self.send_message(**message)
403 |
404 | def send_position_update(self, lat, lon, alt, altref):
405 | new_pos = (lat, lon, alt, altref)
406 | if self.last_position is None or self.last_position != new_pos:
407 | self.send_message(type='mlat_location_update',
408 | lat='{0:.5f}'.format(lat),
409 | lon='{0:.5f}'.format(lon),
410 | alt='{0:.0f}'.format(alt),
411 | altref=altref)
412 | self.last_position = new_pos
413 |
414 | def send_udp_report(self, count):
415 | self.send_message(type='mlat_udp_report', messages_sent=str(count))
416 |
417 |
418 | class AdeptConnection:
419 | UDP_REPORT_INTERVAL = 60.0
420 |
421 | def __init__(self, udp_transport=None, allow_anon=True, allow_modeac=True):
422 | if udp_transport is None:
423 | raise NotImplementedError('non-UDP transport not supported')
424 |
425 | self.reader = None
426 | self.writer = None
427 | self.coordinator = None
428 | self.closed = False
429 | self.udp_transport = udp_transport
430 | self.allow_anon = allow_anon
431 | self.allow_modeac = allow_modeac
432 | self.state = 'init'
433 |
434 | def start(self, coordinator):
435 | self.coordinator = coordinator
436 |
437 | self.reader = AdeptReader(self, coordinator)
438 | self.writer = AdeptWriter(self)
439 |
440 | self.udp_transport.start()
441 | self.send_mlat = self.udp_transport.send_mlat
442 | self.send_sync = self.udp_transport.send_sync
443 | self.send_split_sync = None
444 | self.send_seen = self.writer.send_seen
445 | self.send_lost = self.writer.send_lost
446 | self.send_rate_report = self.writer.send_rate_report
447 | self.send_clock_reset = self.writer.send_clock_reset
448 | self.send_input_connected = self.writer.send_input_connected
449 | self.send_input_disconnected = self.writer.send_input_disconnected
450 | self.send_position_update = self.writer.send_position_update
451 |
452 | self.state = 'connected'
453 | self.writer.send_ready(allow_anon=self.allow_anon, allow_modeac=self.allow_modeac)
454 | self.next_udp_report = util.monotonic_time() + self.UDP_REPORT_INTERVAL
455 | self.coordinator.server_connected()
456 |
457 | def disconnect(self, why=None):
458 | if not self.closed:
459 | self.closed = True
460 | self.state = 'closed'
461 | if self.reader:
462 | self.reader.close()
463 | if self.writer:
464 | self.writer.close()
465 | if self.udp_transport:
466 | self.udp_transport.close()
467 | if self.coordinator:
468 | self.coordinator.server_disconnected()
469 |
470 | def heartbeat(self, now):
471 | if self.udp_transport:
472 | self.udp_transport.flush()
473 |
474 | if now > self.next_udp_report:
475 | self.next_udp_report = now + self.UDP_REPORT_INTERVAL
476 | self.writer.send_udp_report(self.udp_transport.count)
477 |
--------------------------------------------------------------------------------
/flightaware/client/cli.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # FlightAware multilateration client
4 |
5 | import argparse
6 |
7 | import mlat.client.version
8 | from flightaware.client.adeptclient import AdeptConnection, UdpServerConnection
9 | from mlat.client.coordinator import Coordinator
10 | from mlat.client.util import log, log_exc
11 | from mlat.client import options
12 |
13 |
14 | def _main():
15 | # piaware will timestamp our log messages itself, suppress the normal logging timestamps
16 | mlat.client.util.suppress_log_timestamps = True
17 |
18 | parser = argparse.ArgumentParser(description="Client for multilateration.")
19 |
20 | options.make_inputs_group(parser)
21 | options.make_results_group(parser)
22 |
23 | parser.add_argument('--udp-transport',
24 | help="Provide UDP transport information. Expects an IP:port:key argument.",
25 | required=True)
26 |
27 | args = parser.parse_args()
28 |
29 | log("fa-mlat-client {version} starting up", version=mlat.client.version.CLIENT_VERSION)
30 |
31 | # udp_transport is IP:port:key
32 | # split backwards to handle IPv6 addresses in the host part, which themselves contain colons.
33 | parts = args.udp_transport.split(':')
34 | udp_key = int(parts[-1])
35 | udp_port = int(parts[-2])
36 | udp_host = ':'.join(parts[:-2])
37 | udp_transport = UdpServerConnection(udp_host, udp_port, udp_key)
38 | log("Using UDP transport to {host} port {port}", host=udp_host, port=udp_port)
39 |
40 | receiver = options.build_receiver_connection(args)
41 | adept = AdeptConnection(udp_transport, allow_anon=args.allow_anon_results, allow_modeac=args.allow_modeac_results)
42 | outputs = options.build_outputs(args)
43 |
44 | coordinator = Coordinator(receiver=receiver, server=adept, outputs=outputs, freq=options.clock_frequency(args),
45 | allow_anon=args.allow_anon_results, allow_modeac=args.allow_modeac_results)
46 | adept.start(coordinator)
47 | coordinator.run_until(lambda: adept.closed)
48 |
49 |
50 | def main():
51 | try:
52 | _main()
53 | except KeyboardInterrupt:
54 | log("Exiting on SIGINT")
55 | except Exception:
56 | log_exc("Exiting on exception")
57 | else:
58 | log("Exiting on connection loss")
59 |
60 | if __name__ == '__main__':
61 | main()
62 |
--------------------------------------------------------------------------------
/mlat/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mutability/mlat-client/fe70767be859100176983b948140046b6ecdd34a/mlat/__init__.py
--------------------------------------------------------------------------------
/mlat/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mutability/mlat-client/fe70767be859100176983b948140046b6ecdd34a/mlat/client/__init__.py
--------------------------------------------------------------------------------
/mlat/client/cli.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import argparse
20 |
21 | import mlat.client.version
22 |
23 | from mlat.client.util import log
24 | from mlat.client.receiver import ReceiverConnection
25 | from mlat.client.jsonclient import JsonServerConnection
26 | from mlat.client.coordinator import Coordinator
27 | from mlat.client import options
28 |
29 |
30 | def main():
31 | parser = argparse.ArgumentParser(description="Client for multilateration.")
32 |
33 | options.make_inputs_group(parser)
34 | options.make_results_group(parser)
35 |
36 | location = parser.add_argument_group('Receiver location')
37 | location.add_argument('--lat',
38 | type=options.latitude,
39 | help="Latitude of the receiver, in decimal degrees. Required.",
40 | required=True)
41 | location.add_argument('--lon',
42 | type=options.longitude,
43 | help="Longitude of the receiver, in decimal degrees. Required.",
44 | required=True)
45 | location.add_argument('--alt',
46 | type=options.altitude,
47 | help="""
48 | Altitude of the receiver (height above ellipsoid). Required. Defaults to metres, but units may
49 | specified with a 'ft' or 'm' suffix. (Except if they're negative due to option
50 | parser weirdness. Sorry!)""",
51 | required=True)
52 | location.add_argument('--privacy',
53 | help="""
54 | Sets the privacy flag for this receiver. Currently, this removes the receiver
55 | location pin from the coverage maps.""",
56 | action='store_true',
57 | default=False)
58 |
59 | server = parser.add_argument_group('Multilateration server connection')
60 | server.add_argument('--user',
61 | help="User information to give to the server. Used to get in touch if there are problems.",
62 | required=True)
63 | server.add_argument('--server',
64 | help="host:port of the multilateration server to connect to",
65 | type=options.hostport,
66 | default=('mlat.mutability.co.uk', 40147))
67 | server.add_argument('--no-udp',
68 | dest='udp',
69 | help="Don't offer to use UDP transport for sync/mlat messages",
70 | action='store_false',
71 | default=True)
72 |
73 | args = parser.parse_args()
74 |
75 | log("mlat-client {version} starting up", version=mlat.client.version.CLIENT_VERSION)
76 |
77 | outputs = options.build_outputs(args)
78 |
79 | receiver = ReceiverConnection(host=args.input_connect[0], port=args.input_connect[1],
80 | mode=options.connection_mode(args))
81 | server = JsonServerConnection(host=args.server[0], port=args.server[1],
82 | handshake_data={'lat': args.lat,
83 | 'lon': args.lon,
84 | 'alt': args.alt,
85 | 'user': args.user,
86 | 'clock_type': options.clock_type(args),
87 | 'clock_frequency': options.clock_frequency(args),
88 | 'clock_epoch': options.clock_epoch(args),
89 | 'privacy': args.privacy},
90 | offer_zlib=True,
91 | offer_udp=args.udp,
92 | return_results=(len(outputs) > 0))
93 |
94 | coordinator = Coordinator(receiver=receiver, server=server, outputs=outputs, freq=options.clock_frequency(args),
95 | allow_anon=args.allow_anon_results, allow_modeac=args.allow_modeac_results)
96 |
97 | server.start()
98 | coordinator.run_forever()
99 |
100 |
101 | if __name__ == '__main__':
102 | main()
103 |
--------------------------------------------------------------------------------
/mlat/client/coordinator.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | Core of the client: track aircraft and send data to the server as needed.
21 | """
22 |
23 | import asyncore
24 | import time
25 |
26 | import _modes
27 | import mlat.profile
28 | from mlat.client.util import monotonic_time, log
29 | from mlat.client.stats import global_stats
30 |
31 |
32 | class Aircraft:
33 | """One tracked aircraft."""
34 |
35 | def __init__(self, icao):
36 | self.icao = icao
37 | self.messages = 0
38 | self.last_message_time = 0
39 | self.last_position_time = 0
40 | self.even_message = None
41 | self.odd_message = None
42 | self.reported = False
43 | self.requested = True
44 | self.measurement_start = None
45 | self.rate_measurement_start = 0
46 | self.recent_adsb_positions = 0
47 |
48 |
49 | class Coordinator:
50 | update_interval = 5.0
51 | report_interval = 30.0
52 | stats_interval = 900.0
53 | position_expiry_age = 30.0
54 | expiry_age = 60.0
55 |
56 | def __init__(self, receiver, server, outputs, freq, allow_anon, allow_modeac):
57 | self.receiver = receiver
58 | self.server = server
59 | self.outputs = outputs
60 | self.freq = freq
61 | self.allow_anon = allow_anon
62 | self.allow_modeac = allow_modeac
63 |
64 | self.aircraft = {}
65 | self.requested_traffic = set()
66 | self.requested_modeac = set()
67 | self.reported = set()
68 | self.df_handlers = {
69 | _modes.DF_EVENT_MODE_CHANGE: self.received_mode_change_event,
70 | _modes.DF_EVENT_EPOCH_ROLLOVER: self.received_epoch_rollover_event,
71 | _modes.DF_EVENT_TIMESTAMP_JUMP: self.received_timestamp_jump_event,
72 | _modes.DF_EVENT_RADARCAPE_POSITION: self.received_radarcape_position_event,
73 | 0: self.received_df_misc,
74 | 4: self.received_df_misc,
75 | 5: self.received_df_misc,
76 | 16: self.received_df_misc,
77 | 20: self.received_df_misc,
78 | 21: self.received_df_misc,
79 | 11: self.received_df11,
80 | 17: self.received_df17,
81 | _modes.DF_MODEAC: self.received_modeac
82 | }
83 | self.next_report = None
84 | self.next_stats = monotonic_time() + self.stats_interval
85 | self.next_profile = monotonic_time()
86 | self.next_aircraft_update = self.last_aircraft_update = monotonic_time()
87 | self.recent_jumps = 0
88 |
89 | receiver.coordinator = self
90 | server.coordinator = self
91 |
92 | # internals
93 |
94 | def run_forever(self):
95 | self.run_until(lambda: False)
96 |
97 | def run_until(self, termination_condition):
98 | try:
99 | next_heartbeat = monotonic_time() + 0.5
100 | while not termination_condition():
101 | # maybe there are no active sockets and
102 | # we're just waiting on a timeout
103 | if asyncore.socket_map:
104 | asyncore.loop(timeout=0.1, count=5)
105 | else:
106 | time.sleep(0.5)
107 |
108 | now = monotonic_time()
109 | if now >= next_heartbeat:
110 | next_heartbeat = now + 0.5
111 | self.heartbeat(now)
112 |
113 | finally:
114 | self.receiver.disconnect('Client shutting down')
115 | self.server.disconnect('Client shutting down')
116 | for o in self.outputs:
117 | o.disconnect('Client shutting down')
118 |
119 | def heartbeat(self, now):
120 | self.receiver.heartbeat(now)
121 | self.server.heartbeat(now)
122 | for o in self.outputs:
123 | o.heartbeat(now)
124 |
125 | if now >= self.next_profile:
126 | self.next_profile = now + 30.0
127 | mlat.profile.dump_cpu_profiles()
128 |
129 | if now >= self.next_aircraft_update:
130 | self.next_aircraft_update = now + self.update_interval
131 | self.update_aircraft(now)
132 |
133 | # piggyback reporting on regular updates
134 | # as the reporting uses data produced by the update
135 | if self.next_report and now >= self.next_report:
136 | self.next_report = now + self.report_interval
137 | self.send_aircraft_report()
138 | self.send_rate_report(now)
139 |
140 | if now >= self.next_stats:
141 | self.next_stats = now + self.stats_interval
142 | self.periodic_stats(now)
143 |
144 | def update_aircraft(self, now):
145 | # process aircraft the receiver has seen
146 | # (we have not necessarily seen any messages,
147 | # due to the receiver filter)
148 | for icao in self.receiver.recent_aircraft():
149 | ac = self.aircraft.get(icao)
150 | if not ac:
151 | ac = Aircraft(icao)
152 | ac.requested = (icao in self.requested_traffic)
153 | ac.rate_measurement_start = now
154 | self.aircraft[icao] = ac
155 |
156 | if ac.last_message_time <= self.last_aircraft_update:
157 | # fudge it a bit, receiver has seen messages
158 | # but they were all filtered
159 | ac.messages += 1
160 | ac.last_message_time = now
161 |
162 | # expire aircraft we have not seen for a while
163 | for ac in list(self.aircraft.values()):
164 | if (now - ac.last_message_time) > self.expiry_age:
165 | del self.aircraft[ac.icao]
166 |
167 | self.last_aircraft_update = now
168 |
169 | def send_aircraft_report(self):
170 | all_aircraft = {x.icao for x in self.aircraft.values() if x.messages > 1}
171 | seen_ac = all_aircraft.difference(self.reported)
172 | lost_ac = self.reported.difference(all_aircraft)
173 |
174 | if seen_ac:
175 | self.server.send_seen(seen_ac)
176 | if lost_ac:
177 | self.server.send_lost(lost_ac)
178 |
179 | self.reported = all_aircraft
180 |
181 | def send_rate_report(self, now):
182 | # report ADS-B position rate stats
183 | rate_report = {}
184 | for ac in self.aircraft.values():
185 | interval = now - ac.rate_measurement_start
186 | if interval > 0 and ac.recent_adsb_positions > 0:
187 | rate = 1.0 * ac.recent_adsb_positions / interval
188 | ac.rate_measurement_start = now
189 | ac.recent_adsb_positions = 0
190 | rate_report[ac.icao] = rate
191 |
192 | if rate_report:
193 | self.server.send_rate_report(rate_report)
194 |
195 | def periodic_stats(self, now):
196 | log('Receiver status: {0}', self.receiver.state)
197 | log('Server status: {0}', self.server.state)
198 | global_stats.log_and_reset()
199 |
200 | adsb_req = adsb_total = modes_req = modes_total = 0
201 | now = monotonic_time()
202 | for ac in self.aircraft.values():
203 | if ac.messages < 2:
204 | continue
205 |
206 | if now - ac.last_position_time < self.position_expiry_age:
207 | adsb_total += 1
208 | if ac.requested:
209 | adsb_req += 1
210 | else:
211 | modes_total += 1
212 | if ac.requested:
213 | modes_req += 1
214 |
215 | log('Aircraft: {modes_req} of {modes_total} Mode S, {adsb_req} of {adsb_total} ADS-B used',
216 | modes_req=modes_req,
217 | modes_total=modes_total,
218 | adsb_req=adsb_req,
219 | adsb_total=adsb_total)
220 |
221 | if self.recent_jumps > 0:
222 | log('Out-of-order timestamps: {recent}', recent=self.recent_jumps)
223 | self.recent_jumps = 0
224 |
225 | # callbacks from server connection
226 |
227 | def server_connected(self):
228 | self.requested_traffic = set()
229 | self.requested_modeac = set()
230 | self.newly_seen = set()
231 | self.aircraft = {}
232 | self.reported = set()
233 | self.next_report = monotonic_time() + self.report_interval
234 | if self.receiver.state != 'ready':
235 | self.receiver.reconnect()
236 |
237 | def server_disconnected(self):
238 | self.receiver.disconnect('Lost connection to multilateration server, no need for input data')
239 | self.next_report = None
240 | self.next_rate_report = None
241 | self.next_expiry = None
242 |
243 | def server_mlat_result(self, timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
244 | callsign, squawk, error_est, nstations, anon, modeac):
245 | global_stats.mlat_positions += 1
246 |
247 | if anon and not self.allow_anon:
248 | return
249 |
250 | if modeac and not self.allow_modeac:
251 | return
252 |
253 | for o in self.outputs:
254 | o.send_position(timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
255 | callsign, squawk, error_est, nstations, anon, modeac)
256 |
257 | def server_start_sending(self, icao_set, modeac_set=set()):
258 | for icao in icao_set:
259 | ac = self.aircraft.get(icao)
260 | if ac:
261 | ac.requested = True
262 | self.requested_traffic.update(icao_set)
263 | if self.allow_modeac:
264 | self.requested_modeac.update(modeac_set)
265 | self.update_receiver_filter()
266 |
267 | def server_stop_sending(self, icao_set, modeac_set=set()):
268 | for icao in icao_set:
269 | ac = self.aircraft.get(icao)
270 | if ac:
271 | ac.requested = False
272 | self.requested_traffic.difference_update(icao_set)
273 | if self.allow_modeac:
274 | self.requested_modeac.difference_update(modeac_set)
275 | self.update_receiver_filter()
276 |
277 | def update_receiver_filter(self):
278 | now = monotonic_time()
279 |
280 | mlat = set()
281 | for icao in self.requested_traffic:
282 | ac = self.aircraft.get(icao)
283 | if not ac or (now - ac.last_position_time > self.position_expiry_age):
284 | # requested, and we have not seen a recent ADS-B message from it
285 | mlat.add(icao)
286 |
287 | self.receiver.update_filter(mlat)
288 | self.receiver.update_modeac_filter(self.requested_modeac)
289 |
290 | # callbacks from receiver input
291 |
292 | def input_connected(self):
293 | self.server.send_input_connected()
294 |
295 | def input_disconnected(self):
296 | self.server.send_input_disconnected()
297 | # expire everything
298 | self.aircraft.clear()
299 | self.server.send_lost(self.reported)
300 | self.reported.clear()
301 |
302 | @mlat.profile.trackcpu
303 | def input_received_messages(self, messages):
304 | now = monotonic_time()
305 | for message in messages:
306 | handler = self.df_handlers.get(message.df)
307 | if handler:
308 | handler(message, now)
309 |
310 | # handlers for input messages
311 |
312 | def received_mode_change_event(self, message, now):
313 | # decoder mode changed, clock parameters possibly changed
314 | self.freq = message.eventdata['frequency']
315 | self.recent_jumps = 0
316 | self.server.send_clock_reset(reason='Decoder mode changed to {mode}'.format(mode=message.eventdata['mode']),
317 | frequency=message.eventdata['frequency'],
318 | epoch=message.eventdata['epoch'],
319 | mode=message.eventdata['mode'])
320 | log("Input format changed to {mode}, {freq:.0f}MHz clock",
321 | mode=message.eventdata['mode'],
322 | freq=message.eventdata['frequency']/1e6)
323 |
324 | def received_epoch_rollover_event(self, message, now):
325 | # epoch rollover, reset clock
326 | self.server.send_clock_reset('Epoch rollover detected')
327 |
328 | def received_timestamp_jump_event(self, message, now):
329 | self.recent_jumps += 1
330 | if self.recent_jumps == 10:
331 | log("Warning: the timestamps provided by your receiver do not seem to be self-consistent. "
332 | "This can happen if you feed data from multiple receivers to a single mlat-client, which "
333 | "is not supported; use a separate mlat-client for each receiver.")
334 |
335 | def received_radarcape_position_event(self, message, now):
336 | lat, lon = message.eventdata['lat'], message.eventdata['lon']
337 | if lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180:
338 | self.server.send_position_update(lat, lon,
339 | message.eventdata['alt'],
340 | 'egm96_meters')
341 |
342 | def received_df_misc(self, message, now):
343 | ac = self.aircraft.get(message.address)
344 | if not ac:
345 | return False # not a known ICAO
346 |
347 | ac.messages += 1
348 | ac.last_message_time = now
349 |
350 | if ac.messages < 10:
351 | return # wait for more messages
352 | if not ac.requested:
353 | return
354 |
355 | # Candidate for MLAT
356 | if now - ac.last_position_time < self.position_expiry_age:
357 | return # reported position recently, no need for mlat
358 | self.server.send_mlat(message)
359 |
360 | def received_df11(self, message, now):
361 | ac = self.aircraft.get(message.address)
362 | if not ac:
363 | ac = Aircraft(message.address)
364 | ac.requested = (message.address in self.requested_traffic)
365 | ac.messages += 1
366 | ac.last_message_time = now
367 | ac.rate_measurement_start = now
368 | self.aircraft[message.address] = ac
369 | return # will need some more messages..
370 |
371 | ac.messages += 1
372 | ac.last_message_time = now
373 |
374 | if ac.messages < 10:
375 | return # wait for more messages
376 | if not ac.requested:
377 | return
378 |
379 | # Candidate for MLAT
380 | if now - ac.last_position_time < self.position_expiry_age:
381 | return # reported position recently, no need for mlat
382 | self.server.send_mlat(message)
383 |
384 | def received_df17(self, message, now):
385 | ac = self.aircraft.get(message.address)
386 | if not ac:
387 | ac = Aircraft(message.address)
388 | ac.requested = (message.address in self.requested_traffic)
389 | ac.messages += 1
390 | ac.last_message_time = now
391 | ac.rate_measurement_start = now
392 | self.aircraft[message.address] = ac
393 | return # wait for more messages
394 |
395 | ac.messages += 1
396 | ac.last_message_time = now
397 | if ac.messages < 10:
398 | return
399 |
400 | if not message.even_cpr and not message.odd_cpr:
401 | # not a position message
402 | return
403 |
404 | ac.last_position_time = now
405 |
406 | if message.altitude is None:
407 | return # need an altitude
408 | if message.nuc < 6:
409 | return # need NUCp >= 6
410 |
411 | ac.recent_adsb_positions += 1
412 |
413 | if self.server.send_split_sync:
414 | if not ac.requested:
415 | return
416 |
417 | # this is a useful reference message
418 | self.server.send_split_sync(message)
419 | else:
420 | if message.even_cpr:
421 | ac.even_message = message
422 | else:
423 | ac.odd_message = message
424 |
425 | if not ac.requested:
426 | return
427 | if not ac.even_message or not ac.odd_message:
428 | return
429 | if abs(ac.even_message.timestamp - ac.odd_message.timestamp) > 5 * self.freq:
430 | return
431 |
432 | # this is a useful reference message pair
433 | self.server.send_sync(ac.even_message, ac.odd_message)
434 |
435 | def received_modeac(self, message, now):
436 | if message.address not in self.requested_modeac:
437 | return
438 |
439 | self.server.send_mlat(message)
440 |
--------------------------------------------------------------------------------
/mlat/client/jsonclient.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | The JSON client/server protocol, client side.
21 | """
22 |
23 | import math
24 | import time
25 | import struct
26 | import zlib
27 | import socket
28 | import errno
29 | import json
30 |
31 | import mlat.client.version
32 | import mlat.client.net
33 | import mlat.profile
34 | import mlat.geodesy
35 |
36 | from mlat.client.util import log, monotonic_time
37 | from mlat.client.stats import global_stats
38 |
39 | DEBUG = False
40 |
41 | # UDP protocol submessages
42 |
43 | TYPE_SYNC = 1
44 | TYPE_MLAT_SHORT = 2
45 | TYPE_MLAT_LONG = 3
46 | TYPE_SSYNC = 4
47 | TYPE_REBASE = 5
48 | TYPE_ABS_SYNC = 6
49 |
50 | STRUCT_HEADER = struct.Struct(">IHQ")
51 | STRUCT_SYNC = struct.Struct(">Bii14s14s")
52 | STRUCT_SSYNC = struct.Struct(">Bi14s")
53 | STRUCT_MLAT_SHORT = struct.Struct(">Bi7s")
54 | STRUCT_MLAT_LONG = struct.Struct(">Bi14s")
55 | STRUCT_REBASE = struct.Struct(">BQ")
56 | STRUCT_ABS_SYNC = struct.Struct(">BQQ14s14s")
57 |
58 |
59 | class UdpServerConnection:
60 | def __init__(self, host, port, key):
61 | self.host = host
62 | self.port = port
63 | self.key = key
64 |
65 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
66 | self.sock.connect((host, port))
67 |
68 | self.base_timestamp = None
69 | self.header_timestamp = None
70 | self.buf = bytearray(1500)
71 | self.used = 0
72 | self.seq = 0
73 |
74 | def prepare_header(self, timestamp):
75 | self.base_timestamp = timestamp
76 | STRUCT_HEADER.pack_into(self.buf, 0,
77 | self.key, self.seq, self.base_timestamp)
78 | self.used += STRUCT_HEADER.size
79 |
80 | def rebase(self, timestamp):
81 | self.base_timestamp = timestamp
82 | STRUCT_REBASE.pack_into(self.buf, self.used,
83 | TYPE_REBASE,
84 | self.base_timestamp)
85 | self.used += STRUCT_REBASE.size
86 |
87 | def send_mlat(self, message):
88 | if not self.used:
89 | self.prepare_header(message.timestamp)
90 |
91 | delta = message.timestamp - self.base_timestamp
92 | if abs(delta) > 0x7FFFFFF0:
93 | self.rebase(message.timestamp)
94 | delta = 0
95 |
96 | if len(message) == 7:
97 | STRUCT_MLAT_SHORT.pack_into(self.buf, self.used,
98 | TYPE_MLAT_SHORT,
99 | delta, bytes(message))
100 | self.used += STRUCT_MLAT_SHORT.size
101 |
102 | else:
103 | STRUCT_MLAT_LONG.pack_into(self.buf, self.used,
104 | TYPE_MLAT_LONG,
105 | delta, bytes(message))
106 | self.used += STRUCT_MLAT_LONG.size
107 |
108 | if self.used > 1400:
109 | self.flush()
110 |
111 | def send_sync(self, em, om):
112 | if not self.used:
113 | self.prepare_header(int((em.timestamp + om.timestamp) / 2))
114 |
115 | if abs(em.timestamp - om.timestamp) > 0xFFFFFFF0:
116 | # use abs sync
117 | STRUCT_ABS_SYNC.pack_into(self.buf, self.used,
118 | TYPE_ABS_SYNC,
119 | em.timestamp, om.timestamp, bytes(em), bytes(om))
120 | self.used += STRUCT_ABS_SYNC.size
121 | else:
122 | edelta = em.timestamp - self.base_timestamp
123 | odelta = om.timestamp - self.base_timestamp
124 | if abs(edelta) > 0x7FFFFFF0 or abs(odelta) > 0x7FFFFFF0:
125 | self.rebase(int((em.timestamp + om.timestamp) / 2))
126 | edelta = em.timestamp - self.base_timestamp
127 | odelta = om.timestamp - self.base_timestamp
128 |
129 | STRUCT_SYNC.pack_into(self.buf, self.used,
130 | TYPE_SYNC,
131 | edelta, odelta, bytes(em), bytes(om))
132 | self.used += STRUCT_SYNC.size
133 |
134 | if self.used > 1400:
135 | self.flush()
136 |
137 | def send_split_sync(self, m):
138 | if not self.used:
139 | self.prepare_header(m.timestamp)
140 |
141 | delta = m.timestamp - self.base_timestamp
142 | if abs(delta) > 0x7FFFFFF0:
143 | self.rebase(m.timestamp)
144 | delta = 0
145 |
146 | STRUCT_SSYNC.pack_into(self.buf, self.used,
147 | TYPE_SSYNC,
148 | delta, bytes(m))
149 | self.used += STRUCT_SSYNC.size
150 |
151 | if self.used > 1400:
152 | self.flush()
153 |
154 | def flush(self):
155 | if not self.used:
156 | return
157 |
158 | try:
159 | self.sock.send(memoryview(self.buf)[0:self.used])
160 | except socket.error:
161 | pass
162 |
163 | global_stats.server_udp_bytes += self.used
164 |
165 | self.used = 0
166 | self.base_timestamp = None
167 | self.seq = (self.seq + 1) & 0xffff
168 |
169 | def close(self):
170 | self.used = 0
171 | self.sock.close()
172 |
173 | def __str__(self):
174 | return '{0}:{1}'.format(self.host, self.port)
175 |
176 |
177 | class JsonServerConnection(mlat.client.net.ReconnectingConnection):
178 | reconnect_interval = 30.0
179 | heartbeat_interval = 120.0
180 | inactivity_timeout = 60.0
181 |
182 | def __init__(self, host, port, handshake_data, offer_zlib, offer_udp, return_results):
183 | super().__init__(host, port)
184 | self.handshake_data = handshake_data
185 | self.offer_zlib = offer_zlib
186 | self.offer_udp = offer_udp
187 | self.return_results = return_results
188 | self.coordinator = None
189 | self.udp_transport = None
190 |
191 | self.reset_connection()
192 |
193 | def start(self):
194 | self.reconnect()
195 |
196 | def reset_connection(self):
197 | self.readbuf = bytearray()
198 | self.writebuf = bytearray()
199 | self.linebuf = []
200 | self.fill_writebuf = None
201 | self.handle_server_line = None
202 | self.server_heartbeat_at = None
203 | self.last_data_received = None
204 |
205 | if self.udp_transport:
206 | self.udp_transport.close()
207 | self.udp_transport = None
208 |
209 | def lost_connection(self):
210 | self.coordinator.server_disconnected()
211 |
212 | def readable(self):
213 | return self.handle_server_line is not None
214 |
215 | def writable(self):
216 | return self.connecting or self.writebuf or (self.fill_writebuf and self.linebuf)
217 |
218 | @mlat.profile.trackcpu
219 | def handle_write(self):
220 | if self.fill_writebuf:
221 | self.fill_writebuf()
222 |
223 | if self.writebuf:
224 | sent = self.send(self.writebuf)
225 | del self.writebuf[:sent]
226 | global_stats.server_tx_bytes += sent
227 | if len(self.writebuf) > 65536:
228 | raise IOError('Server write buffer overflow (too much unsent data)')
229 |
230 | def fill_uncompressed(self):
231 | if not self.linebuf:
232 | return
233 |
234 | lines = '\n'.join(self.linebuf)
235 | self.writebuf.extend(lines.encode('ascii'))
236 | self.writebuf.extend(b'\n')
237 | self.linebuf = []
238 |
239 | def fill_zlib(self):
240 | if not self.linebuf:
241 | return
242 |
243 | data = bytearray()
244 | pending = False
245 | for line in self.linebuf:
246 | data.extend(self.compressor.compress((line + '\n').encode('ascii')))
247 | pending = True
248 |
249 | if len(data) >= 32768:
250 | data.extend(self.compressor.flush(zlib.Z_SYNC_FLUSH))
251 | assert len(data) < 65540
252 | assert data[-4:] == b'\x00\x00\xff\xff'
253 | del data[-4:]
254 | self.writebuf.extend(struct.pack('!H', len(data)))
255 | self.writebuf.extend(data)
256 | pending = False
257 |
258 | if pending:
259 | data.extend(self.compressor.flush(zlib.Z_SYNC_FLUSH))
260 | assert len(data) < 65540
261 | assert data[-4:] == b'\x00\x00\xff\xff'
262 | del data[-4:]
263 | self.writebuf.extend(struct.pack('!H', len(data)))
264 | self.writebuf.extend(data)
265 |
266 | self.linebuf = []
267 |
268 | def _send_json(self, o):
269 | if DEBUG:
270 | log('Send: {0}', o)
271 | self.linebuf.append(json.dumps(o, separators=(',', ':')))
272 |
273 | #
274 | # TCP transport
275 | #
276 |
277 | def send_tcp_mlat(self, message):
278 | self.linebuf.append('{{"mlat":{{"t":{0},"m":"{1}"}}}}'.format(
279 | message.timestamp,
280 | str(message)))
281 |
282 | def send_tcp_sync(self, em, om):
283 | self.linebuf.append('{{"sync":{{"et":{0},"em":"{1}","ot":{2},"om":"{3}"}}}}'.format(
284 | em.timestamp,
285 | str(em),
286 | om.timestamp,
287 | str(om)))
288 |
289 | def send_tcp_split_sync(self, m):
290 | self.linebuf.append('{{"ssync":{{"t":{0},"m":"{1}"}}}}'.format(
291 | m.timestamp,
292 | str(m)))
293 |
294 | def send_seen(self, aclist):
295 | self._send_json({'seen': ['{0:06x}'.format(icao) for icao in aclist]})
296 |
297 | def send_lost(self, aclist):
298 | self._send_json({'lost': ['{0:06x}'.format(icao) for icao in aclist]})
299 |
300 | def send_rate_report(self, report):
301 | r2 = dict([('{0:06X}'.format(k), round(v, 2)) for k, v in report.items()])
302 | self._send_json({'rate_report': r2})
303 |
304 | def send_input_connected(self):
305 | self._send_json({'input_connected': 'connected'})
306 |
307 | def send_input_disconnected(self):
308 | self._send_json({'input_disconnected': 'disconnected'})
309 |
310 | def send_clock_reset(self, reason, frequency=None, epoch=None, mode=None):
311 | details = {
312 | 'reason': reason
313 | }
314 |
315 | if frequency is not None:
316 | details['frequency'] = frequency
317 | details['epoch'] = epoch
318 | details['mode'] = mode
319 |
320 | self._send_json({'clock_reset': details})
321 |
322 | def send_position_update(self, lat, lon, alt, altref):
323 | pass
324 |
325 | def start_connection(self):
326 | log('Connected to multilateration server at {0}:{1}, handshaking', self.host, self.port)
327 | self.state = 'handshaking'
328 | self.last_data_received = monotonic_time()
329 |
330 | compress_methods = ['none']
331 | if self.offer_zlib:
332 | compress_methods.append('zlib')
333 | compress_methods.append('zlib2')
334 |
335 | handshake_msg = {'version': 3,
336 | 'client_version': mlat.client.version.CLIENT_VERSION,
337 | 'compress': compress_methods,
338 | 'selective_traffic': True,
339 | 'heartbeat': True,
340 | 'return_results': self.return_results,
341 | 'udp_transport': 2 if self.offer_udp else False,
342 | 'return_result_format': 'ecef'}
343 | handshake_msg.update(self.handshake_data)
344 | if DEBUG:
345 | log("Handshake: {0}", handshake_msg)
346 | self.writebuf += (json.dumps(handshake_msg) + '\n').encode('ascii') # linebuf not used yet
347 | self.consume_readbuf = self.consume_readbuf_uncompressed
348 | self.handle_server_line = self.handle_handshake_response
349 |
350 | def heartbeat(self, now):
351 | super().heartbeat(now)
352 |
353 | if self.state in ('ready', 'handshaking') and (now - self.last_data_received) > self.inactivity_timeout:
354 | self.disconnect('No data (not even keepalives) received for {0:.0f} seconds'.format(
355 | self.inactivity_timeout))
356 | self.reconnect()
357 | return
358 |
359 | if self.udp_transport:
360 | self.udp_transport.flush()
361 |
362 | if self.server_heartbeat_at is not None and self.server_heartbeat_at < now:
363 | self.server_heartbeat_at = now + self.heartbeat_interval
364 | self._send_json({'heartbeat': {'client_time': round(time.time(), 3)}})
365 |
366 | def handle_read(self):
367 | try:
368 | moredata = self.recv(16384)
369 | except socket.error as e:
370 | if e.errno == errno.EAGAIN:
371 | return
372 | raise
373 |
374 | if not moredata:
375 | self.close()
376 | self.schedule_reconnect()
377 | return
378 |
379 | self.last_data_received = monotonic_time()
380 | self.readbuf += moredata
381 | global_stats.server_rx_bytes += len(moredata)
382 | self.consume_readbuf()
383 |
384 | def consume_readbuf_uncompressed(self):
385 | lines = self.readbuf.split(b'\n')
386 | self.readbuf = lines[-1]
387 | for line in lines[:-1]:
388 | try:
389 | msg = json.loads(line.decode('ascii'))
390 | except ValueError:
391 | log("json parsing problem, line: >>{line}<<", line=line)
392 | raise
393 |
394 | if DEBUG:
395 | log('Receive: {0}', msg)
396 | self.handle_server_line(msg)
397 |
398 | def consume_readbuf_zlib(self):
399 | i = 0
400 | while i + 2 < len(self.readbuf):
401 | hlen, = struct.unpack_from('!H', self.readbuf, i)
402 | end = i + 2 + hlen
403 | if end > len(self.readbuf):
404 | break
405 |
406 | packet = self.readbuf[i + 2:end] + b'\x00\x00\xff\xff'
407 | linebuf = self.decompressor.decompress(packet)
408 | lines = linebuf.split(b'\n')
409 | for line in lines[:-1]:
410 | try:
411 | msg = json.loads(line.decode('ascii'))
412 | except ValueError:
413 | log("json parsing problem, line: >>{line}<<", line=line)
414 | raise
415 |
416 | self.handle_server_line(msg)
417 |
418 | i = end
419 |
420 | del self.readbuf[:i]
421 |
422 | def handle_handshake_response(self, response):
423 | if 'reconnect_in' in response:
424 | self.reconnect_interval = response['reconnect_in']
425 |
426 | if 'deny' in response:
427 | log('Server explicitly rejected our connection, saying:')
428 | for reason in response['deny']:
429 | log(' {0}', reason)
430 | raise IOError('Server rejected our connection attempt')
431 |
432 | if 'motd' in response:
433 | log('Server says: {0}', response['motd'])
434 |
435 | compress = response.get('compress', 'none')
436 | if response['compress'] == 'none':
437 | self.fill_writebuf = self.fill_uncompressed
438 | self.consume_readbuf = self.consume_readbuf_uncompressed
439 | elif response['compress'] == 'zlib' and self.offer_zlib:
440 | self.compressor = zlib.compressobj(1)
441 | self.fill_writebuf = self.fill_zlib
442 | self.consume_readbuf = self.consume_readbuf_uncompressed
443 | elif response['compress'] == 'zlib2' and self.offer_zlib:
444 | self.compressor = zlib.compressobj(1)
445 | self.decompressor = zlib.decompressobj()
446 | self.fill_writebuf = self.fill_zlib
447 | self.consume_readbuf = self.consume_readbuf_zlib
448 | else:
449 | raise IOError('Server response asked for a compression method {0}, which we do not support'.format(
450 | response['compress']))
451 |
452 | self.server_heartbeat_at = monotonic_time() + self.heartbeat_interval
453 |
454 | if 'udp_transport' in response:
455 | host, port, key = response['udp_transport']
456 | if not host:
457 | host = self.host
458 |
459 | self.udp_transport = UdpServerConnection(host, port, key)
460 |
461 | self.send_mlat = self.udp_transport.send_mlat
462 | self.send_sync = self.udp_transport.send_sync
463 | self.send_split_sync = self.udp_transport.send_split_sync
464 | else:
465 | self.udp_transport = None
466 | self.send_mlat = self.send_tcp_mlat
467 | self.send_sync = self.send_tcp_sync
468 | self.send_split_sync = self.send_tcp_split_sync
469 |
470 | # turn off the sync method we don't want
471 | if response.get('split_sync', False):
472 | self.send_sync = None
473 | else:
474 | self.send_split_sync = None
475 |
476 | log('Handshake complete.')
477 | log(' Compression: {0}', compress)
478 | log(' UDP transport: {0}', self.udp_transport and str(self.udp_transport) or 'disabled')
479 | log(' Split sync: {0}', self.send_split_sync and 'enabled' or 'disabled')
480 |
481 | self.state = 'ready'
482 | self.handle_server_line = self.handle_connected_request
483 | self.coordinator.server_connected()
484 |
485 | # dummy rate report to indicate we'll be sending them
486 | self.send_rate_report({})
487 |
488 | def handle_connected_request(self, request):
489 | if DEBUG:
490 | log('Receive: {0}', request)
491 | if 'start_sending' in request:
492 | self.coordinator.server_start_sending([int(x, 16) for x in request['start_sending']])
493 | elif 'stop_sending' in request:
494 | self.coordinator.server_stop_sending([int(x, 16) for x in request['stop_sending']])
495 | elif 'heartbeat' in request:
496 | pass
497 | elif 'result' in request:
498 | result = request['result']
499 | ecef = result.get('ecef')
500 | if ecef is not None:
501 | # new format
502 | lat, lon, alt = mlat.geodesy.ecef2llh(ecef)
503 | alt = alt / 0.3038 # convert meters to feet
504 | ecef_cov = result.get('cov')
505 | if ecef_cov:
506 | var_est = ecef_cov[0] + ecef_cov[3] + ecef_cov[5]
507 | if var_est >= 0:
508 | error_est = math.sqrt(var_est)
509 | else:
510 | error_est = -1
511 | else:
512 | error_est = -1
513 | nstations = result['nd']
514 | callsign = None
515 | squawk = None
516 | else:
517 | lat = result['lat']
518 | lon = result['lon']
519 | alt = result['alt']
520 | error_est = result['gdop'] * 300 # make a guess
521 | nstations = result['nstations']
522 | callsign = result['callsign']
523 | squawk = result['squawk']
524 |
525 | self.coordinator.server_mlat_result(timestamp=result['@'],
526 | addr=int(result['addr'], 16),
527 | lat=lat,
528 | lon=lon,
529 | alt=alt,
530 | nsvel=None,
531 | ewvel=None,
532 | vrate=None,
533 | callsign=callsign,
534 | squawk=squawk,
535 | error_est=error_est,
536 | nstations=nstations,
537 | anon=False,
538 | modeac=False)
539 | else:
540 | log('ignoring request from server: {0}', request)
541 |
--------------------------------------------------------------------------------
/mlat/client/net.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | Common networking bits, based on asyncore
21 | """
22 |
23 | import sys
24 | import socket
25 | import asyncore
26 | from mlat.client.util import log, log_exc, monotonic_time
27 |
28 |
29 | __all__ = ('LoggingMixin', 'ReconnectingConnection')
30 |
31 |
32 | class LoggingMixin:
33 | """A mixin that redirects asyncore's logging to the client's
34 | global logging."""
35 |
36 | def log(self, message):
37 | log('{0}', message)
38 |
39 | def log_info(self, message, type='info'):
40 | log('{0}: {1}', message, type)
41 |
42 |
43 | class ReconnectingConnection(LoggingMixin, asyncore.dispatcher):
44 | """
45 | An asyncore connection that maintains a TCP connection to a particular
46 | host/port, reconnecting on connection loss.
47 | """
48 |
49 | reconnect_interval = 30.0
50 |
51 | def __init__(self, host, port):
52 | asyncore.dispatcher.__init__(self)
53 | self.host = host
54 | self.port = port
55 | self.addrlist = []
56 | self.state = 'disconnected'
57 | self.reconnect_at = None
58 |
59 | def heartbeat(self, now):
60 | if self.reconnect_at is None or self.reconnect_at > now:
61 | return
62 | if self.state == 'ready':
63 | return
64 | self.reconnect_at = None
65 | self.reconnect()
66 |
67 | def close(self, manual_close=False):
68 | try:
69 | asyncore.dispatcher.close(self)
70 | except AttributeError:
71 | # blarg, try to eat asyncore bugs
72 | pass
73 |
74 | if self.state != 'disconnected':
75 | if not manual_close:
76 | log('Lost connection to {host}:{port}', host=self.host, port=self.port)
77 |
78 | self.state = 'disconnected'
79 | self.reset_connection()
80 | self.lost_connection()
81 |
82 | if not manual_close:
83 | self.schedule_reconnect()
84 |
85 | def disconnect(self, reason):
86 | if self.state != 'disconnected':
87 | log('Disconnecting from {host}:{port}: {reason}', host=self.host, port=self.port, reason=reason)
88 | self.close(True)
89 |
90 | def writable(self):
91 | return self.connecting
92 |
93 | def schedule_reconnect(self):
94 | if self.reconnect_at is None:
95 | if len(self.addrlist) > 0:
96 | # we still have more addresses to try
97 | # nb: asyncore breaks in odd ways if you try
98 | # to reconnect immediately at this point
99 | # (pending events for the old socket go to
100 | # the new socket) so do it in 0.5s time
101 | # so the caller can clean up the old
102 | # socket and discard the events.
103 | interval = 0.5
104 | else:
105 | interval = self.reconnect_interval
106 |
107 | log('Reconnecting in {0} seconds', interval)
108 | self.reconnect_at = monotonic_time() + interval
109 |
110 | def refresh_address_list(self):
111 | self.address
112 |
113 | def reconnect(self):
114 | if self.state != 'disconnected':
115 | self.disconnect('About to reconnect')
116 |
117 | try:
118 | self.reset_connection()
119 |
120 | if len(self.addrlist) == 0:
121 | # ran out of addresses to try, resolve it again
122 | self.addrlist = socket.getaddrinfo(host=self.host,
123 | port=self.port,
124 | family=socket.AF_UNSPEC,
125 | type=socket.SOCK_STREAM,
126 | proto=0,
127 | flags=0)
128 |
129 | # try the next available address
130 | a_family, a_type, a_proto, a_canonname, a_sockaddr = self.addrlist[0]
131 | del self.addrlist[0]
132 |
133 | self.create_socket(a_family, a_type)
134 | self.connect(a_sockaddr)
135 | except socket.error as e:
136 | log('Connection to {host}:{port} failed: {ex!s}', host=self.host, port=self.port, ex=e)
137 | self.close()
138 |
139 | def handle_connect(self):
140 | self.state = 'connected'
141 | self.addrlist = [] # connect was OK, re-resolve next time
142 | self.start_connection()
143 |
144 | def handle_read(self):
145 | pass
146 |
147 | def handle_write(self):
148 | pass
149 |
150 | def handle_close(self):
151 | self.close()
152 |
153 | def handle_error(self):
154 | t, v, tb = sys.exc_info()
155 | if isinstance(v, IOError):
156 | log('Connection to {host}:{port} lost: {ex!s}',
157 | host=self.host,
158 | port=self.port,
159 | ex=v)
160 | else:
161 | log_exc('Unexpected exception on connection to {host}:{port}',
162 | host=self.host,
163 | port=self.port)
164 |
165 | self.handle_close()
166 |
167 | def reset_connection(self):
168 | pass
169 |
170 | def start_connection(self):
171 | pass
172 |
173 | def lost_connection(self):
174 | pass
175 |
--------------------------------------------------------------------------------
/mlat/client/options.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- mode: python; indent-tabs-mode: nil -*-
3 |
4 | # Part of mlat-client - an ADS-B multilateration client.
5 | # Copyright 2015, Oliver Jowett
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 | import argparse
21 | import functools
22 |
23 | import _modes
24 | from mlat.client.receiver import ReceiverConnection
25 | from mlat.client.output import OutputListener, OutputConnector
26 | from mlat.client.output import BasestationConnection, ExtBasestationConnection, BeastConnection
27 | from mlat.client.util import log
28 |
29 | _receiver_types = {
30 | # input type -> decoder mode, server clock type
31 | # the server clock type is used by the server to set
32 | # the clock jitter etc; clock frequency and
33 | # epoch are provided by the client.
34 |
35 | 'auto': (None, 'unknown'),
36 | 'dump1090': (_modes.BEAST, 'dump1090'),
37 | 'beast': (_modes.BEAST, 'beast'),
38 | 'radarcape_12mhz': (_modes.BEAST, 'radarcape_12mhz'), # compat
39 | 'radarcape_gps': (_modes.RADARCAPE, 'radarcape_gps'), # compat
40 | 'radarcape': (_modes.BEAST, 'radarcape'), # autodetects gps if present
41 | 'sbs': (_modes.SBS, 'sbs'),
42 | 'avrmlat': (_modes.AVRMLAT, 'unknown'),
43 | }
44 |
45 |
46 | def latitude(s):
47 | lat = float(s)
48 | if lat < -90 or lat > 90:
49 | raise argparse.ArgumentTypeError('Latitude %s must be in the range -90 to 90' % s)
50 | return lat
51 |
52 |
53 | def longitude(s):
54 | lon = float(s)
55 | if lon < -180 or lon > 360:
56 | raise argparse.ArgumentTypeError('Longitude %s must be in the range -180 to 360' % s)
57 | if lon > 180:
58 | lon -= 360
59 | return lon
60 |
61 |
62 | def altitude(s):
63 | if s.endswith('m'):
64 | alt = float(s[:-1])
65 | elif s.endswith('ft'):
66 | alt = float(s[:-2]) * 0.3048
67 | else:
68 | alt = float(s)
69 |
70 | # Wikipedia to the rescue!
71 | # "The lowest point on dry land is the shore of the Dead Sea [...]
72 | # 418m below sea level". Perhaps not the best spot for a receiver?
73 | # La Rinconada, Peru, pop. 30,000, is at 5100m.
74 | if alt < -420 or alt > 5100:
75 | raise argparse.ArgumentTypeError('Altitude %s must be in the range -420m to 6000m' % s)
76 | return alt
77 |
78 |
79 | def port(s):
80 | port = int(s)
81 | if port < 1 or port > 65535:
82 | raise argparse.ArgumentTypeError('Port %s must be in the range 1 to 65535' % s)
83 | return port
84 |
85 |
86 | def hostport(s):
87 | parts = s.split(':')
88 | if len(parts) != 2:
89 | raise argparse.ArgumentTypeError("{} should be in 'host:port' format".format(s))
90 | return (parts[0], int(parts[1]))
91 |
92 |
93 | def make_inputs_group(parser):
94 | inputs = parser.add_argument_group('Mode S receiver input connection')
95 | inputs.add_argument('--input-type',
96 | help="Sets the input receiver type.",
97 | choices=_receiver_types.keys(),
98 | default='dump1090')
99 | inputs.add_argument('--input-connect',
100 | help="host:port to connect to for Mode S traffic. Required.",
101 | required=True,
102 | type=hostport,
103 | default=('localhost', 30005))
104 |
105 |
106 | def clock_frequency(args):
107 | return _modes.Reader(_receiver_types[args.input_type][0]).frequency
108 |
109 |
110 | def clock_epoch(args):
111 | return _modes.Reader(_receiver_types[args.input_type][0]).epoch
112 |
113 |
114 | def clock_type(args):
115 | return _receiver_types[args.input_type][1]
116 |
117 |
118 | def connection_mode(args):
119 | return _receiver_types[args.input_type][0]
120 |
121 |
122 | def make_results_group(parser):
123 | results = parser.add_argument_group('Results output')
124 | results.add_argument('--results',
125 | help="""
126 | ,connect,host:port or ,listen,port.
127 | Protocol may be 'basestation', 'ext_basestation', or 'beast'. Can be specified multiple times.""",
128 | action='append',
129 | default=[])
130 | results.add_argument("--no-anon-results",
131 | help="Do not generate results for anonymized aircraft",
132 | action='store_false',
133 | dest='allow_anon_results',
134 | default=True)
135 | results.add_argument("--no-modeac-results",
136 | help="Do not generate results for Mode A/C tracks",
137 | action='store_false',
138 | dest='allow_modeac_results',
139 | default=True)
140 |
141 | return results
142 |
143 |
144 | def output_factory(s):
145 | parts = s.split(',')
146 | if len(parts) != 3:
147 | raise ValueError('exactly three comma-separated values are needed (see help)')
148 |
149 | ctype, cmode, addr = parts
150 |
151 | connections = {
152 | 'basestation': BasestationConnection,
153 | 'ext_basestation': ExtBasestationConnection,
154 | 'beast': BeastConnection
155 | }
156 |
157 | c = connections.get(ctype)
158 | if c is None:
159 | raise ValueError("connection type '{0}' is not supported; options are: '{1}'".format(
160 | ctype, "','".join(connections.keys())))
161 |
162 | if cmode == 'listen':
163 | return functools.partial(OutputListener, port=int(addr), connection_factory=c)
164 | elif cmode == 'connect':
165 | return functools.partial(OutputConnector, addr=hostport(addr), connection_factory=c)
166 | else:
167 | raise ValueError("connection mode '{0}' is not supported; options are: 'connect','listen'".format(cmode))
168 |
169 |
170 | def build_outputs(args):
171 | outputs = []
172 | for s in args.results:
173 | try:
174 | factory = output_factory(s)
175 | except ValueError as e:
176 | log("Warning: Ignoring bad results output option '{0}': {1}",
177 | s, str(e))
178 | continue
179 |
180 | try:
181 | output = factory()
182 | except Exception as e:
183 | log("Warning: Could not create results output '{0}': {1}",
184 | s, str(e))
185 | continue
186 |
187 | outputs.append(output)
188 |
189 | return outputs
190 |
191 |
192 | def build_receiver_connection(args):
193 | return ReceiverConnection(host=args.input_connect[0],
194 | port=args.input_connect[1],
195 | mode=connection_mode(args))
196 |
--------------------------------------------------------------------------------
/mlat/client/output.py:
--------------------------------------------------------------------------------
1 | # -*- python -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import sys
20 | import asyncore
21 | import socket
22 | import time
23 | import math
24 | import errno
25 |
26 | from mlat.client.net import LoggingMixin
27 | from mlat.client.util import log, monotonic_time
28 | from mlat.client.synthetic_es import make_altitude_only_frame, \
29 | make_position_frame_pair, make_velocity_frame, DF18, DF18ANON, DF18TRACK
30 |
31 |
32 | class OutputListener(LoggingMixin, asyncore.dispatcher):
33 | def __init__(self, port, connection_factory):
34 | asyncore.dispatcher.__init__(self)
35 | self.port = port
36 |
37 | self.a_type = socket.SOCK_STREAM
38 | try:
39 | # bind to V6 so we can accept both V4 and V6
40 | # (asyncore makes it a hassle to bind to more than
41 | # one address here)
42 | self.a_family = socket.AF_INET6
43 | self.create_socket(self.a_family, self.a_type)
44 | except socket.error:
45 | # maybe no v6 support?
46 | self.a_family = socket.AF_INET
47 | self.create_socket(self.a_family, self.a_type)
48 |
49 | try:
50 | self.set_reuse_addr()
51 | self.bind(('', port))
52 | self.listen(5)
53 | except Exception:
54 | self.close()
55 | raise
56 |
57 | self.output_channels = set()
58 | self.connection_factory = connection_factory
59 | log('Listening for {0} on port {1}', connection_factory.describe(), port)
60 |
61 | def handle_accept(self):
62 | accepted = self.accept()
63 | if not accepted:
64 | return
65 |
66 | new_socket, address = accepted
67 | log('Accepted {0} from {1}:{2}', self.connection_factory.describe(), address[0], address[1])
68 |
69 | self.output_channels.add(self.connection_factory(self, new_socket, self.a_type, self.a_family, address))
70 |
71 | def send_position(self, timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
72 | callsign, squawk, error_est, nstations, anon, modeac):
73 | for channel in list(self.output_channels):
74 | channel.send_position(timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
75 | callsign, squawk, error_est, nstations, anon, modeac)
76 |
77 | def heartbeat(self, now):
78 | for channel in list(self.output_channels):
79 | channel.heartbeat(now)
80 |
81 | def disconnect(self, reason=None):
82 | for channel in list(self.output_channels):
83 | channel.close()
84 | self.close()
85 |
86 | def connection_lost(self, child):
87 | self.output_channels.discard(child)
88 |
89 |
90 | class OutputConnector:
91 | reconnect_interval = 30.0
92 |
93 | def __init__(self, addr, connection_factory):
94 | self.addr = addr
95 | self.connection_factory = connection_factory
96 |
97 | self.output_channel = None
98 | self.next_reconnect = monotonic_time()
99 | self.addrlist = []
100 |
101 | def log(self, fmt, *args, **kwargs):
102 | log('{what} with {host}:{port}: ' + fmt,
103 | *args,
104 | what=self.describe(), host=self.addr[0], port=self.addr[1],
105 | **kwargs)
106 |
107 | def reconnect(self):
108 | if len(self.addrlist) == 0:
109 | try:
110 | self.addrlist = socket.getaddrinfo(host=self.addr[0],
111 | port=self.addr[1],
112 | family=socket.AF_UNSPEC,
113 | type=socket.SOCK_STREAM,
114 | proto=0,
115 | flags=0)
116 | except socket.error as e:
117 | self.log('{ex!s}', ex=e)
118 | self.next_reconnect = monotonic_time() + self.reconnect_interval
119 | return
120 |
121 | # try the next available address
122 | a_family, a_type, a_proto, a_canonname, a_sockaddr = self.addrlist[0]
123 | del self.addrlist[0]
124 |
125 | self.output_channel = self.connection_factory(self, None, a_family, a_type, a_sockaddr)
126 | self.output_channel.connect_now()
127 |
128 | def send_position(self, timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
129 | callsign, squawk, error_est, nstations, anon, modeac):
130 | if self.output_channel:
131 | self.output_channel.send_position(timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
132 | callsign, squawk, error_est, nstations, anon, modeac)
133 |
134 | def heartbeat(self, now):
135 | if self.output_channel:
136 | self.output_channel.heartbeat(now)
137 | elif now > self.next_reconnect:
138 | self.reconnect()
139 |
140 | def disconnect(self, reason=None):
141 | if self.output_channel:
142 | self.output_channel.close()
143 |
144 | def connection_lost(self, child):
145 | if self.output_channel is child:
146 | self.output_channel = None
147 | self.next_reconnect = monotonic_time() + self.reconnect_interval
148 |
149 |
150 | def format_time(timestamp):
151 | return time.strftime("%H:%M:%S", time.gmtime(timestamp)) + ".{0:03.0f}".format(math.modf(timestamp)[0] * 1000)
152 |
153 |
154 | def format_date(timestamp):
155 | return time.strftime("%Y/%m/%d", time.gmtime(timestamp))
156 |
157 |
158 | def csv_quote(s):
159 | if s is None:
160 | return ''
161 | if s.find('\n') == -1 and s.find('"') == -1 and s.find(',') == -1:
162 | return s
163 | else:
164 | return '"' + s.replace('"', '""') + '"'
165 |
166 |
167 | class BasicConnection(LoggingMixin, asyncore.dispatcher):
168 | def __init__(self, listener, socket, s_family, s_type, addr):
169 | super().__init__(sock=socket)
170 | self.listener = listener
171 | self.s_family = s_family
172 | self.s_type = s_type
173 | self.addr = addr
174 | self.writebuf = bytearray()
175 |
176 | def log(self, fmt, *args, **kwargs):
177 | log('{what} with {addr[0]}:{addr[1]}: ' + fmt, *args, what=self.describe(), addr=self.addr, **kwargs)
178 |
179 | def readable(self):
180 | return True
181 |
182 | def handle_connect(self):
183 | self.log('connection established')
184 |
185 | def handle_read(self):
186 | try:
187 | self.recv(1024) # discarded
188 | except socket.error as e:
189 | self.log('{ex!s}', ex=e)
190 | self.close()
191 |
192 | def writable(self):
193 | return self.connecting or self.writebuf
194 |
195 | def handle_write(self):
196 | try:
197 | sent = super().send(self.writebuf)
198 | del self.writebuf[0:sent]
199 | except socket.error as e:
200 | if e.errno == errno.EAGAIN:
201 | return
202 | self.log('{ex!s}', ex=e)
203 | self.close()
204 |
205 | def handle_close(self):
206 | if self.connected:
207 | self.log('connection lost')
208 | self.close()
209 |
210 | def close(self):
211 | try:
212 | super().close()
213 | except AttributeError:
214 | # blarg, try to eat asyncore bugs
215 | pass
216 |
217 | self.listener.connection_lost(self)
218 |
219 | def handle_error(self):
220 | t, v, tb = sys.exc_info()
221 | self.log('{ex!s}', ex=v)
222 | self.handle_close()
223 |
224 | def connect_now(self):
225 | if self.socket:
226 | return
227 |
228 | try:
229 | self.create_socket(self.s_family, self.s_type)
230 | self.connect(self.addr)
231 | except socket.error as e:
232 | self.log('{ex!s}', ex=e)
233 | self.close()
234 |
235 | def send(self, data):
236 | self.writebuf.extend(data)
237 |
238 |
239 | class BasestationConnection(BasicConnection):
240 | heartbeat_interval = 30.0
241 | template = 'MSG,3,1,1,{addrtype}{addr:06X},1,{rcv_date},{rcv_time},{now_date},{now_time},{callsign},{altitude},{speed},{heading},{lat},{lon},{vrate},{squawk},{fs},{emerg},{ident},{aog}' # noqa
242 |
243 | def __init__(self, listener, socket, s_family, s_type, addr):
244 | super().__init__(listener, socket, s_family, s_type, addr)
245 | self.next_heartbeat = monotonic_time() + self.heartbeat_interval
246 |
247 | @staticmethod
248 | def describe():
249 | return 'Basestation-format results connection'
250 |
251 | def heartbeat(self, now):
252 | if now > self.next_heartbeat:
253 | self.next_heartbeat = now + self.heartbeat_interval
254 | try:
255 | self.send('\n'.encode('ascii'))
256 | except socket.error:
257 | self.handle_error()
258 |
259 | def send_position(self, timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
260 | callsign, squawk, error_est, nstations, anon, modeac):
261 | if not self.connected:
262 | return
263 |
264 | now = time.time()
265 | if timestamp is None:
266 | timestamp = now
267 |
268 | if nsvel is not None and ewvel is not None:
269 | speed = math.sqrt(nsvel ** 2 + ewvel ** 2)
270 | heading = math.degrees(math.atan2(ewvel, nsvel))
271 | if heading < 0:
272 | heading += 360
273 | else:
274 | speed = None
275 | heading = None
276 |
277 | if modeac:
278 | addrtype = '@'
279 | elif anon:
280 | addrtype = '~'
281 | else:
282 | addrtype = ''
283 |
284 | line = self.template.format(addr=addr,
285 | addrtype=addrtype,
286 | rcv_date=format_date(timestamp),
287 | rcv_time=format_time(timestamp),
288 | now_date=format_date(now),
289 | now_time=format_time(now),
290 | callsign=csv_quote(callsign) if callsign else '',
291 | altitude=int(alt),
292 | speed=int(speed) if (speed is not None) else '',
293 | heading=int(heading) if (heading is not None) else '',
294 | lat=round(lat, 4),
295 | lon=round(lon, 4),
296 | vrate=int(vrate) if (vrate is not None) else '',
297 | squawk=csv_quote(squawk) if (squawk is not None) else '',
298 | fs='',
299 | emerg='',
300 | ident='',
301 | aog='',
302 | error_est=round(error_est, 0) if (error_est is not None) else '',
303 | nstations=nstations if (nstations is not None) else '')
304 |
305 | self.send((line + '\n').encode('ascii'))
306 | self.next_heartbeat = monotonic_time() + self.heartbeat_interval
307 |
308 |
309 | class ExtBasestationConnection(BasestationConnection):
310 | template = 'MLAT,3,1,1,{addrtype}{addr:06X},1,{rcv_date},{rcv_time},{now_date},{now_time},{callsign},{altitude},{speed},{heading},{lat},{lon},{vrate},{squawk},{fs},{emerg},{ident},{aog},{nstations},,{error_est}' # noqa
311 |
312 | @staticmethod
313 | def describe():
314 | return 'Extended Basestation-format results connection'
315 |
316 |
317 | class BeastConnection(BasicConnection):
318 | heartbeat_interval = 30.0
319 |
320 | @staticmethod
321 | def describe():
322 | return 'Beast-format results connection'
323 |
324 | def __init__(self, listener, socket, s_family, s_type, addr):
325 | super().__init__(listener, socket, s_family, s_type, addr)
326 | self.writebuf = bytearray()
327 | self.last_write = monotonic_time()
328 |
329 | def heartbeat(self, now):
330 | if (now - self.last_write) > 60.0:
331 | # write a keepalive frame
332 | self.send(b'\x1A1\x00\x00\x00\x00\x00\x00\x00\x00\x00')
333 | self.last_write = now
334 |
335 | def send_frame(self, frame):
336 | """Send a 14-byte message in the Beast binary format, using the magic mlat timestamp"""
337 |
338 | # format:
339 | # 1A '3' long frame follows
340 | # FF 00 'MLAT' 6-byte timestamp, this is the magic MLAT timestamp
341 | # 00 signal level
342 | # ... 14 bytes of frame data, with 1A bytes doubled
343 |
344 | self.writebuf.extend(b'\x1A3\xFF\x00MLAT\x00')
345 | if b'\x1a' not in frame:
346 | self.writebuf.extend(frame)
347 | else:
348 | for b in frame:
349 | if b == 0x1A:
350 | self.writebuf.append(b)
351 | self.writebuf.append(b)
352 |
353 | self.last_write = monotonic_time()
354 |
355 | def send_position(self, timestamp, addr, lat, lon, alt, nsvel, ewvel, vrate,
356 | callsign, squawk, error_est, nstations, anon, modeac):
357 | if not self.connected:
358 | return
359 |
360 | if modeac:
361 | df = DF18TRACK
362 | elif anon:
363 | df = DF18ANON
364 | else:
365 | df = DF18
366 |
367 | if lat is None or lon is None:
368 | if alt is not None:
369 | self.send_frame(make_altitude_only_frame(addr, alt, df=df))
370 | else:
371 | even, odd = make_position_frame_pair(addr, lat, lon, alt, df=df)
372 | self.send_frame(even)
373 | self.send_frame(odd)
374 |
375 | if nsvel is not None or ewvel is not None or vrate is not None:
376 | self.send_frame(make_velocity_frame(addr, nsvel, ewvel, vrate, df=df))
377 |
--------------------------------------------------------------------------------
/mlat/client/receiver.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | Handles receiving Mode S messages from receivers using various formats.
21 | """
22 |
23 | import socket
24 | import errno
25 |
26 | import _modes
27 | import mlat.profile
28 | from mlat.client.stats import global_stats
29 | from mlat.client.net import ReconnectingConnection
30 | from mlat.client.util import log, monotonic_time
31 |
32 |
33 | class ReceiverConnection(ReconnectingConnection):
34 | inactivity_timeout = 150.0
35 |
36 | def __init__(self, host, port, mode):
37 | ReconnectingConnection.__init__(self, host, port)
38 | self.coordinator = None
39 | self.last_data_received = None
40 | self.mode = mode
41 |
42 | # set up filters
43 |
44 | # this set gets put into specific_filter in
45 | # multiple places, so we can just add an address
46 | # to this set when we want mlat data.
47 | self.interested_mlat = set()
48 |
49 | self.default_filter = [False] * 32
50 | self.specific_filter = [None] * 32
51 |
52 | # specific filters for mlat
53 | for df in (0, 4, 5, 11, 16, 20, 21):
54 | self.specific_filter[df] = self.interested_mlat
55 |
56 | # we want all DF17 messages so we can report position rates
57 | # and distinguish ADS-B from Mode-S-only aircraft
58 | self.default_filter[17] = True
59 |
60 | self.modeac_filter = set()
61 |
62 | self.reset_connection()
63 |
64 | def detect(self, data):
65 | n, detected_mode = detect_data_format(data)
66 | if detected_mode is not None:
67 | log("Detected {mode} format input".format(mode=detected_mode))
68 | if detected_mode == _modes.AVR:
69 | log("Input format is AVR with no timestamps. "
70 | "This format does not contain enough information for multilateration. "
71 | "Please enable mlat timestamps on your receiver.")
72 | self.close()
73 | return (0, (), False)
74 |
75 | self.reader.mode = detected_mode
76 | self.feed = self.reader.feed
77 |
78 | # synthesize a mode-change event before the real messages
79 | mode_change = (mode_change_event(self.reader), )
80 |
81 | try:
82 | m, messages, pending_error = self.feed(data[n:])
83 | except ValueError:
84 | # return just the mode change and keep the error pending
85 | return (n, mode_change, True)
86 |
87 | # put the mode change on the front of the message list
88 | return (n + m, mode_change + messages, pending_error)
89 | else:
90 | if len(data) > 512:
91 | raise ValueError('Unable to autodetect input message format')
92 | return (0, (), False)
93 |
94 | def reset_connection(self):
95 | self.residual = None
96 | self.reader = _modes.Reader(self.mode)
97 | if self.mode is None:
98 | self.feed = self.detect
99 | else:
100 | self.feed = self.reader.feed
101 | # configure filter, seen-tracking
102 | self.reader.seen = set()
103 | self.reader.default_filter = self.default_filter
104 | self.reader.specific_filter = self.specific_filter
105 | self.reader.modeac_filter = self.modeac_filter
106 |
107 | def start_connection(self):
108 | log('Input connected to {0}:{1}', self.host, self.port)
109 | self.last_data_received = monotonic_time()
110 | self.state = 'connected'
111 | self.coordinator.input_connected()
112 |
113 | # synthesize a mode change immediately if we are not autodetecting
114 | if self.reader.mode is not None:
115 | self.coordinator.input_received_messages((mode_change_event(self.reader),))
116 |
117 | self.send_settings_message()
118 |
119 | def send_settings_message(self):
120 | # if we are connected to something that is Beast-like (or autodetecting), send a beast settings message
121 | if self.state != 'connected':
122 | return
123 |
124 | if self.reader.mode not in (None, _modes.BEAST, _modes.RADARCAPE, _modes.RADARCAPE_EMULATED):
125 | return
126 |
127 | if not self.modeac_filter:
128 | # Binary format, no filters, CRC checks enabled, mode A/C disabled
129 | settings_message = b'\x1a1C\x1a1d\x1a1f\x1a1j'
130 | else:
131 | # Binary format, no filters, CRC checks enabled, mode A/C enabled
132 | settings_message = b'\x1a1C\x1a1d\x1a1f\x1a1J'
133 |
134 | self.send(settings_message)
135 |
136 | def lost_connection(self):
137 | self.coordinator.input_disconnected()
138 |
139 | def heartbeat(self, now):
140 | ReconnectingConnection.heartbeat(self, now)
141 |
142 | if self.state == 'connected' and (now - self.last_data_received) > self.inactivity_timeout:
143 | self.disconnect('No data (not even keepalives) received for {0:.0f} seconds'.format(
144 | self.inactivity_timeout))
145 | self.reconnect()
146 |
147 | def recent_aircraft(self):
148 | """Return the set of aircraft seen from the receiver since the
149 | last call to recent_aircraft(). This includes aircraft where no
150 | messages were forwarded due to filtering."""
151 | recent = set(self.reader.seen)
152 | self.reader.seen.clear()
153 | return recent
154 |
155 | def update_filter(self, wanted_mlat):
156 | """Update the receiver filters so we receive mlat-relevant messages
157 | (basically, anything that's not DF17) for the given addresses only."""
158 | # do this in place, because self.interested_mlat is referenced
159 | # from the filters installed on the reader; updating the set in
160 | # place automatically updates all the DF-specific filters.
161 | self.interested_mlat.clear()
162 | self.interested_mlat.update(wanted_mlat)
163 |
164 | def update_modeac_filter(self, wanted_modeac):
165 | """Update the receiver filters so that we receive mode A/C messages
166 | for the given Mode A codes"""
167 |
168 | changed = (self.modeac_filter and not wanted_modeac) or (not self.modeac_filter and wanted_modeac)
169 | self.modeac_filter.clear()
170 | self.modeac_filter.update(wanted_modeac)
171 | if changed:
172 | self.send_settings_message()
173 |
174 | @mlat.profile.trackcpu
175 | def handle_read(self):
176 | try:
177 | moredata = self.recv(16384)
178 | except socket.error as e:
179 | if e.errno == errno.EAGAIN:
180 | return
181 | raise
182 |
183 | if not moredata:
184 | self.close()
185 | return
186 |
187 | global_stats.receiver_rx_bytes += len(moredata)
188 |
189 | if self.residual:
190 | moredata = self.residual + moredata
191 |
192 | self.last_data_received = monotonic_time()
193 |
194 | try:
195 | consumed, messages, pending_error = self.feed(moredata)
196 | except ValueError as e:
197 | log("Parsing receiver data failed: {e}", e=str(e))
198 | self.close()
199 | return
200 |
201 | if consumed < len(moredata):
202 | self.residual = moredata[consumed:]
203 | if len(self.residual) > 5120:
204 | raise RuntimeError('parser broken - buffer not being consumed')
205 | else:
206 | self.residual = None
207 |
208 | global_stats.receiver_rx_messages += self.reader.received_messages
209 | global_stats.receiver_rx_filtered += self.reader.suppressed_messages
210 | self.reader.received_messages = self.reader.suppressed_messages = 0
211 |
212 | if messages:
213 | self.coordinator.input_received_messages(messages)
214 |
215 | if pending_error:
216 | # call it again to get the exception
217 | # now that we've handled all the messages
218 | try:
219 | if self.residual is None:
220 | self.feed(b'')
221 | else:
222 | self.feed(self.residual)
223 | except ValueError as e:
224 | log("Parsing receiver data failed: {e}", e=str(e))
225 | self.close()
226 | return
227 |
228 |
229 | def mode_change_event(reader):
230 | return _modes.EventMessage(_modes.DF_EVENT_MODE_CHANGE, 0, {
231 | "mode": reader.mode,
232 | "frequency": reader.frequency,
233 | "epoch": reader.epoch})
234 |
235 |
236 | def detect_data_format(data):
237 | """Try to work out what sort of data format this is.
238 |
239 | Returns (offset, mode) where offset is the byte offset
240 | to start at and mode is the decoder mode to use,
241 | or None if detection failed."""
242 |
243 | for i in range(len(data)-4):
244 | mode = None
245 |
246 | if data[i] != b'\x1a' and data[i+1:i+3] in (b'\x1a1', b'\x1a2', b'\x1a3', b'\x1a4'):
247 | mode = _modes.BEAST
248 | offset = 1
249 |
250 | elif data[i:i+4] == b'\x10\0x03\x10\0x02':
251 | mode = _modes.SBS
252 | offset = 2
253 |
254 | else:
255 | if data[i:i+3] in (b';\n\r', b';\r\n'):
256 | avr_prefix = 3
257 | elif data[i:i+2] in (b';\n', b';\r'):
258 | avr_prefix = 2
259 | else:
260 | avr_prefix = None
261 |
262 | if avr_prefix:
263 | firstbyte = data[i + avr_prefix]
264 | if firstbyte in (ord('@'), ord('%'), ord('<')):
265 | mode = _modes.AVRMLAT
266 | offset = avr_prefix
267 | elif firstbyte in (ord('*'), ord('.')):
268 | mode = _modes.AVR
269 | offset = avr_prefix
270 |
271 | if mode:
272 | reader = _modes.Reader(mode)
273 | # don't actually want any data, just parse it
274 | reader.want_events = False
275 | reader.default_filter = [False] * 32
276 | try:
277 | n, _, pending_error = reader.feed(data[i + offset:])
278 | if n > 0 and not pending_error:
279 | # consumed some data without problems
280 | return (i + offset, mode)
281 | except ValueError:
282 | # parse error, ignore it
283 | pass
284 |
285 | return (0, None)
286 |
--------------------------------------------------------------------------------
/mlat/client/stats.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | Global stats gathering.
21 | """
22 |
23 | from mlat.client.util import monotonic_time, log
24 |
25 |
26 | class Stats:
27 | def __init__(self):
28 | self.reset()
29 |
30 | def reset(self, now=None):
31 | if now is None:
32 | now = monotonic_time()
33 | self.start = now
34 | self.server_tx_bytes = 0
35 | self.server_rx_bytes = 0
36 | self.server_udp_bytes = 0
37 | self.receiver_rx_bytes = 0
38 | self.receiver_rx_messages = 0
39 | self.receiver_rx_filtered = 0
40 | self.mlat_positions = 0
41 |
42 | def log_and_reset(self):
43 | now = monotonic_time()
44 | elapsed = now - self.start
45 |
46 | processed = self.receiver_rx_messages - self.receiver_rx_filtered
47 | log('Receiver: {0:6.1f} msg/s received {1:6.1f} msg/s processed ({2:.0f}%)',
48 | self.receiver_rx_messages / elapsed,
49 | processed / elapsed,
50 | 0 if self.receiver_rx_messages == 0 else 100.0 * processed / self.receiver_rx_messages)
51 | log('Server: {0:6.1f} kB/s from server {1:4.1f}kB/s TCP to server {2:6.1f}kB/s UDP to server',
52 | self.server_rx_bytes / elapsed / 1000.0,
53 | self.server_tx_bytes / elapsed / 1000.0,
54 | self.server_udp_bytes / elapsed / 1000.0)
55 | if self.mlat_positions:
56 | log('Results: {0:3.1f} positions/minute',
57 | self.mlat_positions / elapsed * 60.0)
58 | self.reset(now)
59 |
60 |
61 | global_stats = Stats()
62 |
--------------------------------------------------------------------------------
/mlat/client/synthetic_es.py:
--------------------------------------------------------------------------------
1 | # -*- python -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import math
20 | import _modes
21 | import bisect
22 |
23 | # It would be nice to merge this into a proper all-singing
24 | # all-dancing Mode S module that combined _modes, this code,
25 | # and the server side Mode S decoder. A task for another day..
26 |
27 | __all__ = ('make_altitude_only_frame', 'make_position_frame_pair', 'make_velocity_frame', 'DF17', 'DF18')
28 |
29 | # types of frame we can build
30 | DF17 = 'DF17'
31 | DF18 = 'DF18'
32 | DF18ANON = 'DF18ANON'
33 | DF18TRACK = 'DF18TRACK'
34 |
35 | # lookup table for CPR_NL
36 | nl_table = (
37 | (10.47047130, 59),
38 | (14.82817437, 58),
39 | (18.18626357, 57),
40 | (21.02939493, 56),
41 | (23.54504487, 55),
42 | (25.82924707, 54),
43 | (27.93898710, 53),
44 | (29.91135686, 52),
45 | (31.77209708, 51),
46 | (33.53993436, 50),
47 | (35.22899598, 49),
48 | (36.85025108, 48),
49 | (38.41241892, 47),
50 | (39.92256684, 46),
51 | (41.38651832, 45),
52 | (42.80914012, 44),
53 | (44.19454951, 43),
54 | (45.54626723, 42),
55 | (46.86733252, 41),
56 | (48.16039128, 40),
57 | (49.42776439, 39),
58 | (50.67150166, 38),
59 | (51.89342469, 37),
60 | (53.09516153, 36),
61 | (54.27817472, 35),
62 | (55.44378444, 34),
63 | (56.59318756, 33),
64 | (57.72747354, 32),
65 | (58.84763776, 31),
66 | (59.95459277, 30),
67 | (61.04917774, 29),
68 | (62.13216659, 28),
69 | (63.20427479, 27),
70 | (64.26616523, 26),
71 | (65.31845310, 25),
72 | (66.36171008, 24),
73 | (67.39646774, 23),
74 | (68.42322022, 22),
75 | (69.44242631, 21),
76 | (70.45451075, 20),
77 | (71.45986473, 19),
78 | (72.45884545, 18),
79 | (73.45177442, 17),
80 | (74.43893416, 16),
81 | (75.42056257, 15),
82 | (76.39684391, 14),
83 | (77.36789461, 13),
84 | (78.33374083, 12),
85 | (79.29428225, 11),
86 | (80.24923213, 10),
87 |
88 | (81.19801349, 9),
89 | (82.13956981, 8),
90 | (83.07199445, 7),
91 | (83.99173563, 6),
92 | (84.89166191, 5),
93 | (85.75541621, 4),
94 | (86.53536998, 3),
95 | (87.00000000, 2),
96 | (90.00000000, 1)
97 | )
98 |
99 | nl_lats = [x[0] for x in nl_table]
100 | nl_vals = [x[1] for x in nl_table]
101 |
102 |
103 | def CPR_NL(lat):
104 | """The NL function referenced in the CPR calculations: the number of longitude zones at a given latitude"""
105 | if lat < 0:
106 | lat = -lat
107 |
108 | nl = nl_vals[bisect.bisect_left(nl_lats, lat)]
109 | return nl
110 |
111 |
112 | def CPR_N(lat, odd):
113 | """The N function referenced in the CPR calculations: the number of longitude zones at a given latitude / oddness"""
114 | nl = CPR_NL(lat) - (odd and 1 or 0)
115 | if nl < 1:
116 | nl = 1
117 | return nl
118 |
119 |
120 | def cpr_encode(lat, lon, odd):
121 | """Encode an airborne position using a CPR encoding with the given odd flag value"""
122 |
123 | NbPow = 2**17
124 | Dlat = 360.0 / (odd and 59 or 60)
125 | YZ = int(math.floor(NbPow * (lat % Dlat) / Dlat + 0.5))
126 |
127 | Rlat = Dlat * (1.0 * YZ / NbPow + math.floor(lat / Dlat))
128 | Dlon = (360.0 / CPR_N(Rlat, odd))
129 | XZ = int(math.floor(NbPow * (lon % Dlon) / Dlon + 0.5))
130 |
131 | return (YZ & 0x1FFFF), (XZ & 0x1FFFF)
132 |
133 |
134 | def encode_altitude(ft):
135 | """Encode an altitude in feet using the representation expected in DF17 messages"""
136 | if ft is None:
137 | return 0
138 |
139 | i = int((ft + 1012.5) / 25)
140 | if i < 0:
141 | i = 0
142 | elif i > 0x7ff:
143 | i = 0x7ff
144 |
145 | # insert Q=1 in bit 4
146 | return ((i & 0x7F0) << 1) | 0x010 | (i & 0x00F)
147 |
148 |
149 | def encode_velocity(kts, supersonic):
150 | """Encode a groundspeed in kts using the representation expected in DF17 messages"""
151 | if kts is None:
152 | return 0
153 |
154 | if kts < 0:
155 | signbit = 0x400
156 | kts = 0 - kts
157 | else:
158 | signbit = 0
159 |
160 | if supersonic:
161 | kts /= 4
162 |
163 | kts = int(kts + 1.5)
164 | if kts > 1023:
165 | return 1023 | signbit
166 | else:
167 | return kts | signbit
168 |
169 |
170 | def encode_vrate(vr):
171 | """Encode a vertical rate in fpm using the representation expected in DF17 messages"""
172 | if vr is None:
173 | return 0
174 |
175 | if vr < 0:
176 | signbit = 0x200
177 | vr = 0 - vr
178 | else:
179 | signbit = 0
180 |
181 | vr = int(vr / 64 + 1.5)
182 | if vr > 511:
183 | return 511 | signbit
184 | else:
185 | return vr | signbit
186 |
187 |
188 | def make_altitude_only_frame(addr, lat, lon, alt, df=DF18):
189 | """Create an altitude-only DF17 frame"""
190 | # ME type 0: airborne position, horizontal position unavailable
191 | return make_position_frame(0, addr, 0, 0, encode_altitude(alt), False, df)
192 |
193 |
194 | def make_position_frame_pair(addr, lat, lon, alt, df=DF18):
195 | """Create a pair of DF17 frames - one odd, one even - for the given position"""
196 | ealt = encode_altitude(alt)
197 | even_lat, even_lon = cpr_encode(lat, lon, False)
198 | odd_lat, odd_lon = cpr_encode(lat, lon, True)
199 |
200 | # ME type 18: airborne position, baro alt, NUCp=0
201 | eframe = make_position_frame(18, addr, even_lat, even_lon, ealt, False, df)
202 | oframe = make_position_frame(18, addr, odd_lat, odd_lon, ealt, True, df)
203 |
204 | return eframe, oframe
205 |
206 |
207 | def make_position_frame(metype, addr, elat, elon, ealt, oddflag, df):
208 | """Create single DF17/DF18 position frame"""
209 |
210 | frame = bytearray(14)
211 |
212 | if df is DF17:
213 | # DF=17, CA=6 (ES, Level 2 or above transponder and ability
214 | # to set CA code 7 and either airborne or on the ground)
215 | frame[0] = (17 << 3) | (6)
216 | imf = 0
217 | elif df is DF18:
218 | # DF=18, CF=2, IMF=0 (ES/NT, fine TIS-B message with 24-bit address)
219 | frame[0] = (18 << 3) | (2)
220 | imf = 0
221 | elif df is DF18ANON:
222 | # DF=18, CF=5, IMF=0 (ES/NT, fine TIS-B message with anonymous 24-bit address)
223 | frame[0] = (18 << 3) | (5)
224 | imf = 0
225 | elif df is DF18TRACK:
226 | # DF=18, CF=2, IMF=1 (ES/NT, fine TIS-B message with track file number)
227 | frame[0] = (18 << 3) | (2)
228 | imf = 1
229 | else:
230 | raise ValueError('df must be DF17 or DF18 or DF18ANON or DF18TRACK')
231 |
232 | frame[1] = (addr >> 16) & 255 # AA
233 | frame[2] = (addr >> 8) & 255 # AA
234 | frame[3] = addr & 255 # AA
235 | frame[4] = (metype << 3) # ME type, status 0
236 | frame[4] |= imf # SAF (DF17) / IMF (DF 18)
237 | frame[5] = (ealt >> 4) & 255 # Altitude (MSB)
238 | frame[6] = (ealt & 15) << 4 # Altitude (LSB)
239 | if oddflag:
240 | frame[6] |= 4 # CPR format
241 | frame[6] |= (elat >> 15) & 3 # CPR latitude (top bits)
242 | frame[7] = (elat >> 7) & 255 # CPR latitude (middle bits)
243 | frame[8] = (elat & 127) << 1 # CPR latitude (low bits)
244 | frame[8] |= (elon >> 16) & 1 # CPR longitude (high bit)
245 | frame[9] = (elon >> 8) & 255 # CPR longitude (middle bits)
246 | frame[10] = elon & 255 # CPR longitude (low bits)
247 |
248 | # CRC
249 | c = _modes.crc(frame[0:11])
250 | frame[11] = (c >> 16) & 255
251 | frame[12] = (c >> 8) & 255
252 | frame[13] = c & 255
253 |
254 | return frame
255 |
256 |
257 | def make_velocity_frame(addr, nsvel, ewvel, vrate, df=DF18):
258 | """Create a DF17/DF18 airborne velocity frame"""
259 |
260 | supersonic = (nsvel is not None and abs(nsvel) > 1000) or (ewvel is not None and abs(ewvel) > 1000)
261 |
262 | e_ns = encode_velocity(nsvel, supersonic)
263 | e_ew = encode_velocity(ewvel, supersonic)
264 | e_vr = encode_vrate(vrate)
265 |
266 | frame = bytearray(14)
267 |
268 | if df is DF17:
269 | # DF=17, CA=6 (ES, Level 2 or above transponder and ability
270 | # to set CA code 7 and either airborne or on the ground)
271 | frame[0] = (17 << 3) | (6)
272 | imf = 0
273 | elif df is DF18:
274 | # DF=18, CF=2, IMF=0 (ES/NT, fine TIS-B message with 24-bit address)
275 | frame[0] = (18 << 3) | (2)
276 | imf = 0
277 | elif df is DF18ANON:
278 | # DF=18, CF=5, IMF=1 (ES/NT, fine TIS-B message with anonymous 24-bit address)
279 | frame[0] = (18 << 3) | (5)
280 | imf = 0
281 | elif df is DF18TRACK:
282 | # DF=18, CF=2, IMF=1 (ES/NT, fine TIS-B message with track file number)
283 | frame[0] = (18 << 3) | (2)
284 | imf = 1
285 | else:
286 | raise ValueError('df must be DF17 or DF18 or DF18ANON or DF18TRACK')
287 |
288 | frame[1] = (addr >> 16) & 255 # AA
289 | frame[2] = (addr >> 8) & 255 # AA
290 | frame[3] = addr & 255 # AA
291 | frame[4] = (19 << 3) # ES type 19, airborne velocity
292 | if supersonic:
293 | frame[4] |= 2 # subtype 2, ground speed, supersonic
294 | else:
295 | frame[4] |= 1 # subtype 1, ground speed, subsonic
296 |
297 | frame[5] = (imf << 7) # IMF, NACp 0
298 | frame[5] |= (e_ew >> 8) & 7 # E/W velocity sign and top bits
299 | frame[6] = (e_ew & 255) # E/W velocity low bits
300 | frame[7] = (e_ns >> 3) & 255 # N/S velocity top bits
301 | frame[8] = (e_ns & 7) << 5 # N/S velocity low bits
302 | frame[8] |= 16 # vertical rate source = baro
303 | frame[8] |= (e_vr >> 6) & 15 # vertical rate top bits
304 | frame[9] = (e_vr & 63) << 2 # vertical rate low bits
305 | frame[10] = 0 # GNSS/Baro alt offset, no data
306 |
307 | # CRC
308 | c = _modes.crc(frame[0:11])
309 | frame[11] = (c >> 16) & 255
310 | frame[12] = (c >> 8) & 255
311 | frame[13] = c & 255
312 |
313 | return frame
314 |
--------------------------------------------------------------------------------
/mlat/client/util.py:
--------------------------------------------------------------------------------
1 | # -*- python -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import sys
20 | import time
21 | import traceback
22 |
23 |
24 | __all__ = ('log', 'log_exc', 'monotonic_time')
25 |
26 |
27 | suppress_log_timestamps = False
28 |
29 |
30 | def log(msg, *args, **kwargs):
31 | if suppress_log_timestamps:
32 | print(msg.format(*args, **kwargs), file=sys.stderr)
33 | else:
34 | print(time.ctime(), msg.format(*args, **kwargs), file=sys.stderr)
35 | sys.stderr.flush()
36 |
37 |
38 | def log_exc(msg, *args, **kwargs):
39 | if suppress_log_timestamps:
40 | print(msg.format(*args, **kwargs), file=sys.stderr)
41 | else:
42 | print(time.ctime(), msg.format(*args, **kwargs), file=sys.stderr)
43 | traceback.print_exc(file=sys.stderr)
44 | sys.stderr.flush()
45 |
46 |
47 | _adjust = 0
48 | _last = 0
49 |
50 |
51 | def monotonic_time():
52 | """Emulates time.monotonic() if not available."""
53 | global _adjust, _last
54 |
55 | now = time.time()
56 | if now < _last:
57 | # system clock went backwards, add in a
58 | # fudge factor so our monotonic clock
59 | # does not.
60 | _adjust = _adjust + (_last - now)
61 |
62 | _last = now
63 | return now + _adjust
64 |
65 |
66 | try:
67 | # try to use the 3.3+ version when available
68 | from time import monotonic as monotonic_time # noqa
69 | except ImportError:
70 | pass
71 |
--------------------------------------------------------------------------------
/mlat/client/version.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """Just a version constant!"""
20 |
21 | CLIENT_VERSION = "0.2.13"
22 |
--------------------------------------------------------------------------------
/mlat/constants.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-server: a Mode S multilateration server
4 | # Copyright (C) 2015 Oliver Jowett
5 |
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 |
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU Affero General Public License for more details.
15 |
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | Useful constants for unit conversion.
21 | """
22 |
23 | import math
24 |
25 | # signal propagation speed in metres per second
26 | Cair = 299792458 / 1.0003
27 |
28 | # degrees to radians
29 | DTOR = math.pi / 180.0
30 | # radians to degrees
31 | RTOD = 180.0 / math.pi
32 |
33 | # feet to metres
34 | FTOM = 0.3048
35 | # metres to feet
36 | MTOF = 1.0/FTOM
37 |
38 | # m/s to knots
39 | MS_TO_KTS = 1.9438
40 |
41 | # m/s to fpm
42 | MS_TO_FPM = MTOF * 60
43 |
--------------------------------------------------------------------------------
/mlat/geodesy.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-server: a Mode S multilateration server
4 | # Copyright (C) 2015 Oliver Jowett
5 |
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 |
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU Affero General Public License for more details.
15 |
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | """
20 | Utility functions to convert between coordinate systems and calculate distances.
21 | """
22 |
23 | import math
24 | from .constants import DTOR, RTOD
25 |
26 | # WGS84 ellipsoid Earth parameters
27 | WGS84_A = 6378137.0
28 | WGS84_F = 1.0/298.257223563
29 | WGS84_B = WGS84_A * (1 - WGS84_F)
30 | WGS84_ECC_SQ = 1 - WGS84_B * WGS84_B / (WGS84_A * WGS84_A)
31 | WGS84_ECC = math.sqrt(WGS84_ECC_SQ)
32 |
33 | # Average radius for a spherical Earth
34 | SPHERICAL_R = 6371e3
35 |
36 | # Some derived values
37 | _wgs84_ep = math.sqrt((WGS84_A**2 - WGS84_B**2) / WGS84_B**2)
38 | _wgs84_ep2_b = _wgs84_ep**2 * WGS84_B
39 | _wgs84_e2_a = WGS84_ECC_SQ * WGS84_A
40 |
41 |
42 | def llh2ecef(llh):
43 | """Converts from WGS84 lat/lon/height to ellipsoid-earth ECEF"""
44 |
45 | lat = llh[0] * DTOR
46 | lng = llh[1] * DTOR
47 | alt = llh[2]
48 |
49 | slat = math.sin(lat)
50 | slng = math.sin(lng)
51 | clat = math.cos(lat)
52 | clng = math.cos(lng)
53 |
54 | d = math.sqrt(1 - (slat * slat * WGS84_ECC_SQ))
55 | rn = WGS84_A / d
56 |
57 | x = (rn + alt) * clat * clng
58 | y = (rn + alt) * clat * slng
59 | z = (rn * (1 - WGS84_ECC_SQ) + alt) * slat
60 |
61 | return (x, y, z)
62 |
63 |
64 | def ecef2llh(ecef):
65 | "Converts from ECEF to WGS84 lat/lon/height"
66 |
67 | x, y, z = ecef
68 |
69 | lon = math.atan2(y, x)
70 |
71 | p = math.sqrt(x**2 + y**2)
72 | th = math.atan2(WGS84_A * z, WGS84_B * p)
73 | lat = math.atan2(z + _wgs84_ep2_b * math.sin(th)**3,
74 | p - _wgs84_e2_a * math.cos(th)**3)
75 |
76 | N = WGS84_A / math.sqrt(1 - WGS84_ECC_SQ * math.sin(lat)**2)
77 | alt = p / math.cos(lat) - N
78 |
79 | return (lat*RTOD, lon*RTOD, alt)
80 |
81 |
82 | def greatcircle(p0, p1):
83 | """Returns a great-circle distance in metres between two LLH points,
84 | _assuming spherical earth_ and _ignoring altitude_. Don't use this if you
85 | need a distance accurate to better than 1%."""
86 |
87 | lat0 = p0[0] * DTOR
88 | lon0 = p0[1] * DTOR
89 | lat1 = p1[0] * DTOR
90 | lon1 = p1[1] * DTOR
91 | return SPHERICAL_R * math.acos(
92 | math.sin(lat0) * math.sin(lat1) +
93 | math.cos(lat0) * math.cos(lat1) * math.cos(abs(lon0 - lon1)))
94 |
95 |
96 | # direct implementation here turns out to be _much_ faster (10-20x) compared to
97 | # scipy.spatial.distance.euclidean or numpy-based approaches
98 | def ecef_distance(p0, p1):
99 | """Returns the straight-line distance in metres between two ECEF points."""
100 | return math.sqrt((p0[0] - p1[0])**2 + (p0[1] - p1[1])**2 + (p0[2] - p1[2])**2)
101 |
--------------------------------------------------------------------------------
/mlat/profile.py:
--------------------------------------------------------------------------------
1 | # -*- mode: python; indent-tabs-mode: nil -*-
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import os
20 |
21 | # NB: This requires Python 3.3 when MLAT_CPU_PROFILE is set.
22 |
23 |
24 | if not int(os.environ.get('MLAT_CPU_PROFILE', '0')):
25 | def trackcpu(f, **kwargs):
26 | return f
27 |
28 | def dump_cpu_profiles():
29 | pass
30 | else:
31 | import sys
32 | import time
33 | import operator
34 | import functools
35 |
36 | _cpu_tracking = []
37 | print('CPU profiling enabled', file=sys.stderr)
38 |
39 | def trackcpu(f, name=None, **kwargs):
40 | if name is None:
41 | name = f.__module__ + '.' + f.__qualname__
42 |
43 | print('Profiling:', name, file=sys.stderr)
44 | tracking = [name, 0, 0.0]
45 | _cpu_tracking.append(tracking)
46 |
47 | @functools.wraps(f)
48 | def cpu_measurement_wrapper(*args, **kwargs):
49 | start = time.clock_gettime(time.CLOCK_THREAD_CPUTIME_ID)
50 | try:
51 | return f(*args, **kwargs)
52 | finally:
53 | end = time.clock_gettime(time.CLOCK_THREAD_CPUTIME_ID)
54 | tracking[1] += 1
55 | tracking[2] += (end - start)
56 |
57 | return cpu_measurement_wrapper
58 |
59 | def dump_cpu_profiles():
60 | print('{rank:4s} {name:60s} {count:6s} {total:8s} {each:8s}'.format(
61 | rank='#',
62 | name='Function',
63 | count='Calls',
64 | total='Total(s)',
65 | each='Each(us)'), file=sys.stderr)
66 |
67 | rank = 1
68 | for name, count, total in sorted(_cpu_tracking, key=operator.itemgetter(2), reverse=True):
69 | if count == 0:
70 | break
71 |
72 | print('{rank:4d} {name:60s} {count:6d} {total:8.3f} {each:8.0f}'.format(
73 | rank=rank,
74 | name=name,
75 | count=count,
76 | total=total,
77 | each=total * 1e6 / count), file=sys.stderr)
78 | rank += 1
79 |
80 | sys.stderr.flush()
81 |
--------------------------------------------------------------------------------
/modes_crc.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Part of mlat-client - an ADS-B multilateration client.
3 | * Copyright 2015, Oliver Jowett
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #include "_modes.h"
20 |
21 |
22 | /********** CRC ****************/
23 |
24 | /* Generator polynomial for the Mode S CRC */
25 | #define MODES_GENERATOR_POLY 0xfff409U
26 |
27 | /* CRC values for all single-byte messages; used to speed up CRC calculation. */
28 | static uint32_t crc_table[256];
29 |
30 | int modescrc_module_init(PyObject *m)
31 | {
32 | int i;
33 |
34 | for (i = 0; i < 256; ++i) {
35 | uint32_t c = i << 16;
36 | int j;
37 | for (j = 0; j < 8; ++j) {
38 | if (c & 0x800000)
39 | c = (c<<1) ^ MODES_GENERATOR_POLY;
40 | else
41 | c = (c<<1);
42 | }
43 |
44 | crc_table[i] = c & 0x00ffffff;
45 | }
46 |
47 | return 0;
48 | }
49 |
50 | void modescrc_module_free(PyObject *m)
51 | {
52 | }
53 |
54 | uint32_t modescrc_buffer_crc(uint8_t *buf, Py_ssize_t len)
55 | {
56 | uint32_t rem;
57 | Py_ssize_t i;
58 | for (rem = 0, i = len; i > 0; --i) {
59 | rem = ((rem & 0x00ffff) << 8) ^ crc_table[*buf++ ^ ((rem & 0xff0000) >> 16)];
60 | }
61 |
62 | return rem;
63 | }
64 |
65 | PyObject *modescrc_crc(PyObject *self, PyObject *args)
66 | {
67 | Py_buffer buffer;
68 | PyObject *rv = NULL;
69 |
70 | if (!PyArg_ParseTuple(args, "s*", &buffer))
71 | return NULL;
72 |
73 | if (buffer.itemsize != 1) {
74 | PyErr_SetString(PyExc_ValueError, "buffer itemsize is not 1");
75 | goto out;
76 | }
77 |
78 | if (!PyBuffer_IsContiguous(&buffer, 'C')) {
79 | PyErr_SetString(PyExc_ValueError, "buffer is not contiguous");
80 | goto out;
81 | }
82 |
83 | rv = PyLong_FromLong(modescrc_buffer_crc(buffer.buf, buffer.len));
84 |
85 | out:
86 | PyBuffer_Release(&buffer);
87 | return rv;
88 | }
89 |
--------------------------------------------------------------------------------
/modes_message.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Part of mlat-client - an ADS-B multilateration client.
3 | * Copyright 2015, Oliver Jowett
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #include "_modes.h"
20 |
21 | /* methods / type behaviour */
22 | static PyObject *modesmessage_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
23 | static void modesmessage_dealloc(modesmessage *self);
24 | static int modesmessage_init(modesmessage *self, PyObject *args, PyObject *kwds);
25 | static int modesmessage_bf_getbuffer(PyObject *self, Py_buffer *view, int flags);
26 | static Py_ssize_t modesmessage_sq_length(modesmessage *self);
27 | static PyObject *modesmessage_sq_item(modesmessage *self, Py_ssize_t i);
28 | static long modesmessage_hash(PyObject *self);
29 | static PyObject *modesmessage_richcompare(PyObject *self, PyObject *other, int op);
30 | static PyObject *modesmessage_repr(PyObject *self);
31 | static PyObject *modesmessage_str(PyObject *self);
32 |
33 | /* internal helpers */
34 | static PyObject *decode_ac13(unsigned ac13);
35 | static uint32_t crc_residual(uint8_t *message, int len);
36 | static int decode(modesmessage *self);
37 |
38 | /* modesmessage fields */
39 | /* todo: these can probably all be read/write */
40 | static PyMemberDef modesmessageMembers[] = {
41 | { "timestamp", T_ULONGLONG, offsetof(modesmessage, timestamp), 0, "12MHz timestamp" }, /* read/write */
42 | { "signal", T_UINT, offsetof(modesmessage, signal), READONLY, "signal level" },
43 | { "df", T_UINT, offsetof(modesmessage, df), READONLY, "downlink format or a special DF_* value" },
44 | { "nuc", T_UINT, offsetof(modesmessage, nuc), READONLY, "NUCp value" },
45 | { "even_cpr", T_BOOL, offsetof(modesmessage, even_cpr), READONLY, "CPR even-format flag" },
46 | { "odd_cpr", T_BOOL, offsetof(modesmessage, odd_cpr), READONLY, "CPR odd-format flag" },
47 | { "valid", T_BOOL, offsetof(modesmessage, valid), READONLY, "Does the message look OK?" },
48 | { "crc_residual", T_OBJECT, offsetof(modesmessage, crc), READONLY, "CRC residual" },
49 | { "address", T_OBJECT, offsetof(modesmessage, address), READONLY, "ICAO address" },
50 | { "altitude", T_OBJECT, offsetof(modesmessage, altitude), READONLY, "altitude" },
51 | { "eventdata", T_OBJECT, offsetof(modesmessage, eventdata), READONLY, "event data dictionary for special event messages" },
52 | { NULL, 0, 0, 0, NULL }
53 | };
54 |
55 | /* modesmessage buffer protocol */
56 | static PyBufferProcs modesmessageBufferProcs = {
57 | modesmessage_bf_getbuffer, /* bf_getbuffer */
58 | NULL /* bf_releasebuffer */
59 | };
60 |
61 | /* modesmessage sequence protocol */
62 | static PySequenceMethods modesmessageSequenceMethods = {
63 | (lenfunc)modesmessage_sq_length, /* sq_length */
64 | 0, /* sq_concat */
65 | 0, /* sq_repeat */
66 | (ssizeargfunc)modesmessage_sq_item, /* sq_item */
67 | 0, /* sq_ass_item */
68 | 0, /* sq_contains */
69 | 0, /* sq_inplace_concat */
70 | 0, /* sq_inplace_repeat */
71 | };
72 |
73 | /* modesmessage type object */
74 | static PyTypeObject modesmessageType = {
75 | PyVarObject_HEAD_INIT(NULL, 0)
76 | "_modes.Message", /* tp_name */
77 | sizeof(modesmessage), /* tp_basicsize */
78 | 0, /* tp_itemsize */
79 | (destructor)modesmessage_dealloc, /* tp_dealloc */
80 | 0, /* tp_print */
81 | 0, /* tp_getattr */
82 | 0, /* tp_setattr */
83 | 0, /* tp_reserved */
84 | (reprfunc)modesmessage_repr, /* tp_repr */
85 | 0, /* tp_as_number */
86 | &modesmessageSequenceMethods, /* tp_as_sequence */
87 | 0, /* tp_as_mapping */
88 | (hashfunc)modesmessage_hash, /* tp_hash */
89 | 0, /* tp_call */
90 | (reprfunc)modesmessage_str, /* tp_str */
91 | PyObject_GenericGetAttr, /* tp_getattro */
92 | PyObject_GenericSetAttr, /* tp_setattro */
93 | &modesmessageBufferProcs, /* tp_as_buffer */
94 | Py_TPFLAGS_DEFAULT||Py_TPFLAGS_BASETYPE, /* tp_flags */
95 | "A ModeS message.", /* tp_doc */
96 | 0, /* tp_traverse */
97 | 0, /* tp_clear */
98 | modesmessage_richcompare, /* tp_richcompare */
99 | 0, /* tp_weaklistoffset */
100 | 0, /* tp_iter */
101 | 0, /* tp_iternext */
102 | 0, /* tp_methods */
103 | modesmessageMembers, /* tp_members */
104 | 0, /* tp_getset */
105 | 0, /* tp_base */
106 | 0, /* tp_dict */
107 | 0, /* tp_descr_get */
108 | 0, /* tp_descr_set */
109 | 0, /* tp_dictoffset */
110 | (initproc)modesmessage_init, /* tp_init */
111 | 0, /* tp_alloc */
112 | modesmessage_new, /* tp_new */
113 | };
114 |
115 | /*
116 | * module setup
117 | */
118 | int modesmessage_module_init(PyObject *m)
119 | {
120 | if (PyType_Ready(&modesmessageType) < 0)
121 | return -1;
122 |
123 | Py_INCREF(&modesmessageType);
124 | if (PyModule_AddObject(m, "Message", (PyObject *)&modesmessageType) < 0) {
125 | Py_DECREF(&modesmessageType);
126 | return -1;
127 | }
128 |
129 | /* Add DF_* constants */
130 | if (PyModule_AddIntMacro(m, DF_MODEAC) < 0)
131 | return -1;
132 |
133 | if (PyModule_AddIntMacro(m, DF_EVENT_TIMESTAMP_JUMP) < 0)
134 | return -1;
135 |
136 | if (PyModule_AddIntMacro(m, DF_EVENT_MODE_CHANGE) < 0)
137 | return -1;
138 |
139 | if (PyModule_AddIntMacro(m, DF_EVENT_EPOCH_ROLLOVER) < 0)
140 | return -1;
141 |
142 | if (PyModule_AddIntMacro(m, DF_EVENT_RADARCAPE_STATUS) < 0)
143 | return -1;
144 |
145 | if (PyModule_AddIntMacro(m, DF_EVENT_RADARCAPE_POSITION) < 0)
146 | return -1;
147 |
148 | return 0;
149 | }
150 |
151 | void modesmessage_module_free(PyObject *m)
152 | {
153 | }
154 |
155 | static PyObject *modesmessage_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
156 | {
157 | modesmessage *self;
158 |
159 | self = (modesmessage *)type->tp_alloc(type, 0);
160 | if (!self)
161 | return NULL;
162 |
163 | /* minimal init */
164 | self->timestamp = 0;
165 | self->signal = 0;
166 | self->df = 0;
167 | self->nuc = 0;
168 | self->even_cpr = self->odd_cpr = 0;
169 | self->valid = 0;
170 | self->crc = NULL;
171 | self->address = NULL;
172 | self->altitude = NULL;
173 | self->data = NULL;
174 | self->datalen = 0;
175 | self->eventdata = NULL;
176 |
177 | return (PyObject *)self;
178 | }
179 |
180 | static void modesmessage_dealloc(modesmessage *self)
181 | {
182 | Py_XDECREF(self->crc);
183 | Py_XDECREF(self->address);
184 | Py_XDECREF(self->altitude);
185 | Py_XDECREF(self->eventdata);
186 | free(self->data);
187 | Py_TYPE(self)->tp_free((PyObject*)self);
188 | }
189 |
190 | /* internal entry point to build a new message from a buffer */
191 | PyObject *modesmessage_from_buffer(unsigned long long timestamp, unsigned signal, uint8_t *data, int datalen)
192 | {
193 | modesmessage *message;
194 | uint8_t *copydata;
195 |
196 | if (! (message = (modesmessage*)modesmessage_new(&modesmessageType, NULL, NULL)))
197 | goto err;
198 |
199 | /* minimal init so deallocation works */
200 | message->data = NULL;
201 |
202 | copydata = malloc(datalen);
203 | if (!copydata) {
204 | PyErr_NoMemory();
205 | goto err;
206 | }
207 | memcpy(copydata, data, datalen);
208 |
209 | message->timestamp = timestamp;
210 | message->signal = signal;
211 | message->data = copydata;
212 | message->datalen = datalen;
213 |
214 | if (decode(message) < 0)
215 | goto err;
216 |
217 | return (PyObject*)message;
218 |
219 | err:
220 | Py_XDECREF(message);
221 | return NULL;
222 | }
223 |
224 | /* internal entry point to build a new event message
225 | * steals a reference from eventdata
226 | */
227 | PyObject *modesmessage_new_eventmessage(int type, unsigned long long timestamp, PyObject *eventdata)
228 | {
229 | modesmessage *message;
230 |
231 | if (! (message = (modesmessage*)modesmessage_new(&modesmessageType, NULL, NULL)))
232 | return NULL;
233 |
234 | message->df = type;
235 | message->timestamp = timestamp;
236 | message->eventdata = eventdata;
237 | return (PyObject *)message;
238 | }
239 |
240 | /* external entry point to build a new event message from python i.e. _modes.EventMessage(...) */
241 | PyObject *modesmessage_eventmessage(PyObject *self, PyObject *args, PyObject *kwds)
242 | {
243 | static char *kwlist[] = { "type", "timestamp", "eventdata", NULL };
244 | int type;
245 | unsigned long long timestamp;
246 | PyObject *eventdata = NULL;
247 | PyObject *rv = NULL;
248 |
249 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "iKO", kwlist, &type, ×tamp, &eventdata))
250 | return NULL;
251 |
252 | Py_INCREF(eventdata);
253 | if (! (rv = modesmessage_new_eventmessage(type, timestamp, eventdata))) {
254 | Py_DECREF(eventdata);
255 | return NULL;
256 | }
257 |
258 | return rv;
259 | }
260 |
261 | /* external entry point to build a new message from python (i.e. _modes.Message(...)) */
262 | static int modesmessage_init(modesmessage *self, PyObject *args, PyObject *kwds)
263 | {
264 | static char *kwlist[] = { "data", "timestamp", "signal", NULL };
265 | Py_buffer data;
266 | int rv = -1;
267 |
268 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "s*|KI", kwlist, &data, &self->timestamp, &self->signal))
269 | return -1;
270 |
271 | if (data.itemsize != 1) {
272 | PyErr_SetString(PyExc_ValueError, "buffer itemsize is not 1");
273 | goto out;
274 | }
275 |
276 | if (!PyBuffer_IsContiguous(&data, 'C')) {
277 | PyErr_SetString(PyExc_ValueError, "buffer is not contiguous");
278 | goto out;
279 | }
280 |
281 | self->datalen = 0;
282 | free(self->data);
283 |
284 | self->data = malloc(data.len);
285 | if (!self->data) {
286 | PyErr_NoMemory();
287 | goto out;
288 | }
289 |
290 | memcpy(self->data, data.buf, data.len);
291 | self->datalen = data.len;
292 |
293 | rv = decode(self);
294 |
295 | out:
296 | PyBuffer_Release(&data);
297 | return rv;
298 | }
299 |
300 | static PyObject *decode_ac13(unsigned ac13)
301 | {
302 | int h, f, a;
303 |
304 | if (ac13 == 0)
305 | Py_RETURN_NONE;
306 |
307 | if (ac13 & 0x0040) /* M bit */
308 | Py_RETURN_NONE;
309 |
310 | if (ac13 & 0x0010) { /* Q bit */
311 | int n = ((ac13 & 0x1f80) >> 2) | ((ac13 & 0x0020) >> 1) | (ac13 & 0x000f);
312 | return PyLong_FromLong(n * 25 - 1000);
313 | }
314 |
315 | /* convert from Gillham code */
316 | if (! ((ac13 & 0x1500))) {
317 | /* illegal gillham code */
318 | Py_RETURN_NONE;
319 | }
320 |
321 | h = 0;
322 | if (ac13 & 0x1000) h ^= 7; /* C1 */
323 | if (ac13 & 0x0400) h ^= 3; /* C2 */
324 | if (ac13 & 0x0100) h ^= 1; /* C4 */
325 |
326 | if (h & 5)
327 | h ^= 5;
328 |
329 | if (h > 5)
330 | Py_RETURN_NONE; /* illegal */
331 |
332 | f = 0;
333 | if (ac13 & 0x0010) f ^= 0x1ff; /* D1 */
334 | if (ac13 & 0x0004) f ^= 0x0ff; /* D2 */
335 | if (ac13 & 0x0001) f ^= 0x07f; /* D4 */
336 | if (ac13 & 0x0800) f ^= 0x03f; /* A1 */
337 | if (ac13 & 0x0200) f ^= 0x01f; /* A2 */
338 | if (ac13 & 0x0080) f ^= 0x00f; /* A4 */
339 | if (ac13 & 0x0020) f ^= 0x007; /* B1 */
340 | if (ac13 & 0x0008) f ^= 0x003; /* B2 */
341 | if (ac13 & 0x0002) f ^= 0x001; /* B4 */
342 |
343 | if (f & 1)
344 | h = (6 - h);
345 |
346 | a = 500 * f + 100 * h - 1300;
347 | if (a < -1200)
348 | Py_RETURN_NONE; /* illegal */
349 |
350 | return PyLong_FromLong(a);
351 | }
352 |
353 | static PyObject *decode_ac12(unsigned ac12)
354 | {
355 | return decode_ac13(((ac12 & 0x0fc0) << 1) | (ac12 & 0x003f));
356 | }
357 |
358 | static uint32_t crc_residual(uint8_t *message, int len)
359 | {
360 | uint32_t crc;
361 |
362 | if (len < 3)
363 | return 0;
364 |
365 | crc = modescrc_buffer_crc(message, len - 3);
366 | crc = crc ^ (message[len-3] << 16);
367 | crc = crc ^ (message[len-2] << 8);
368 | crc = crc ^ (message[len-1]);
369 | return crc;
370 | }
371 |
372 | static int decode(modesmessage *self)
373 | {
374 | uint32_t crc;
375 |
376 | /* clear state */
377 | self->valid = 0;
378 | self->nuc = 0;
379 | self->odd_cpr = self->even_cpr = 0;
380 | Py_CLEAR(self->crc);
381 | Py_CLEAR(self->address);
382 | Py_CLEAR(self->altitude);
383 |
384 | if (self->datalen == 2) {
385 | self->df = DF_MODEAC;
386 | self->address = PyLong_FromLong((self->data[0] << 8) | self->data[1]);
387 | self->valid = 1;
388 | return 0;
389 | }
390 |
391 | self->df = (self->data[0] >> 3) & 31;
392 |
393 | if ((self->df < 16 && self->datalen != 7) || (self->df >= 16 && self->datalen != 14)) {
394 | /* wrong length, no further processing */
395 | return 0;
396 | }
397 |
398 | if (self->df != 0 && self->df != 4 && self->df != 5 && self->df != 11 &&
399 | self->df != 16 && self->df != 17 && self->df != 18 && self->df != 20 && self->df != 21) {
400 | /* we do not know how to handle this message type, no further processing */
401 | return 0;
402 | }
403 |
404 | crc = crc_residual(self->data, self->datalen);
405 | if (!(self->crc = PyLong_FromLong(crc)))
406 | return -1;
407 |
408 | switch (self->df) {
409 | case 0:
410 | case 4:
411 | case 16:
412 | case 20:
413 | self->address = (Py_INCREF(self->crc), self->crc);
414 | if (! (self->altitude = decode_ac13((self->data[2] & 0x1f) << 8 | (self->data[3]))))
415 | return -1;
416 | self->valid = 1;
417 | break;
418 |
419 | case 5:
420 | case 21:
421 | case 24:
422 | self->address = (Py_INCREF(self->crc), self->crc);
423 | self->valid = 1;
424 | break;
425 |
426 | case 11:
427 | self->valid = ((crc & ~0x7f) == 0);
428 | if (self->valid) {
429 | if (! (self->address = PyLong_FromLong( (self->data[1] << 16) | (self->data[2] << 8) | (self->data[3]) )))
430 | return -1;
431 | }
432 | break;
433 |
434 | case 17:
435 | case 18:
436 | self->valid = (crc == 0);
437 | if (self->valid) {
438 | unsigned metype;
439 |
440 | if (! (self->address = PyLong_FromLong( (self->data[1] << 16) | (self->data[2] << 8) | (self->data[3]) )))
441 | return -1;
442 |
443 | metype = self->data[4] >> 3;
444 | if ((metype >= 9 && metype <= 18) || (metype >= 20 && metype < 22)) {
445 | if (metype == 22)
446 | self->nuc = 0;
447 | else if (metype <= 18)
448 | self->nuc = 18 - metype;
449 | else
450 | self->nuc = 29 - metype;
451 |
452 | if (self->data[6] & 0x04)
453 | self->odd_cpr = 1;
454 | else
455 | self->even_cpr = 1;
456 |
457 | if (! (self->altitude = decode_ac12((self->data[5] << 4) | ((self->data[6] & 0xF0) >> 4))))
458 | return -1;
459 | }
460 | }
461 | break;
462 |
463 | default:
464 | break;
465 | }
466 |
467 | return 0;
468 | }
469 |
470 | static int modesmessage_bf_getbuffer(PyObject *self, Py_buffer *view, int flags)
471 | {
472 | return PyBuffer_FillInfo(view, self, ((modesmessage*)self)->data, ((modesmessage*)self)->datalen, 1, flags);
473 | }
474 |
475 | static Py_ssize_t modesmessage_sq_length(modesmessage *self)
476 | {
477 | return self->datalen;
478 | }
479 |
480 | static PyObject *modesmessage_sq_item(modesmessage *self, Py_ssize_t i)
481 | {
482 | if (i < 0 || i >= self->datalen) {
483 | PyErr_SetString(PyExc_IndexError, "byte index out of range");
484 | return NULL;
485 | }
486 |
487 | return PyLong_FromLong(self->data[i]);
488 | }
489 |
490 | static long modesmessage_hash(PyObject *self)
491 | {
492 | modesmessage *msg = (modesmessage*)self;
493 | uint32_t hash = 0;
494 | int i;
495 |
496 | /* Jenkins one-at-a-time hash */
497 | for (i = 0; i < 4 && i < msg->datalen; ++i) {
498 | hash += msg->data[i] & 0xff;
499 | hash += hash << 10;
500 | hash ^= hash >> 6;
501 | }
502 |
503 | hash += (hash << 3);
504 | hash ^= (hash >> 11);
505 | hash += (hash << 15);
506 |
507 | return (long)hash;
508 | }
509 |
510 | static PyObject *modesmessage_richcompare(PyObject *self, PyObject *other, int op)
511 | {
512 | PyObject *result = NULL;
513 |
514 | if (! PyObject_TypeCheck(self, &modesmessageType) ||
515 | ! PyObject_TypeCheck(other, &modesmessageType)) {
516 | result = Py_NotImplemented;
517 | } else {
518 | modesmessage *message1 = (modesmessage*)self;
519 | modesmessage *message2 = (modesmessage*)other;
520 | int c = 0;
521 |
522 | switch (op) {
523 | case Py_EQ:
524 | c = (message1->datalen == message2->datalen) && (memcmp(message1->data, message2->data, message1->datalen) == 0);
525 | break;
526 | case Py_NE:
527 | c = (message1->datalen != message2->datalen) || (memcmp(message1->data, message2->data, message1->datalen) != 0);
528 | break;
529 | case Py_LT:
530 | c = (message1->datalen < message2->datalen) ||
531 | (message1->datalen == message2->datalen && memcmp(message1->data, message2->data, message1->datalen) < 0);
532 | break;
533 | case Py_LE:
534 | c = (message1->datalen < message2->datalen) ||
535 | (message1->datalen == message2->datalen && memcmp(message1->data, message2->data, message1->datalen) <= 0);
536 | break;
537 | case Py_GT:
538 | c = (message1->datalen > message2->datalen) ||
539 | (message1->datalen == message2->datalen && memcmp(message1->data, message2->data, message1->datalen) > 0);
540 | break;
541 | case Py_GE:
542 | c = (message1->datalen > message2->datalen) ||
543 | (message1->datalen == message2->datalen && memcmp(message1->data, message2->data, message1->datalen) >= 0);
544 | break;
545 | default:
546 | result = Py_NotImplemented;
547 | break;
548 | }
549 |
550 | if (!result)
551 | result = (c ? Py_True : Py_False);
552 | }
553 |
554 | Py_INCREF(result);
555 | return result;
556 | }
557 |
558 | static const char *df_event_name(int df)
559 | {
560 | switch (df) {
561 | case DF_EVENT_TIMESTAMP_JUMP:
562 | return "DF_EVENT_TIMESTAMP_JUMP";
563 | case DF_EVENT_MODE_CHANGE:
564 | return "DF_EVENT_MODE_CHANGE";
565 | case DF_EVENT_EPOCH_ROLLOVER:
566 | return "DF_EVENT_EPOCH_ROLLOVER";
567 | case DF_EVENT_RADARCAPE_STATUS:
568 | return "DF_EVENT_RADARCAPE_STATUS";
569 | default:
570 | return NULL;
571 | }
572 | }
573 |
574 | static char hexdigit[16] = "0123456789abcdef";
575 | static PyObject *modesmessage_repr(PyObject *self)
576 | {
577 | modesmessage *message = (modesmessage *)self;
578 | if (message->data) {
579 | char buf[256];
580 | char *p = buf;
581 | int i;
582 |
583 | for (i = 0; i < message->datalen; ++i) {
584 | *p++ = '\\';
585 | *p++ = 'x';
586 | *p++ = hexdigit[(message->data[i] >> 4) & 15];
587 | *p++ = hexdigit[message->data[i] & 15];
588 | }
589 | *p++ = 0;
590 |
591 | return PyUnicode_FromFormat("_modes.Message(b'%s',%llu,%u)", buf, (unsigned long long)message->timestamp, (unsigned)message->signal);
592 | } else {
593 | const char *eventname = df_event_name(message->df);
594 | if (eventname) {
595 | return PyUnicode_FromFormat("_modes.EventMessage(_modes.%s,%llu,%R)",
596 | df_event_name(message->df),
597 | (unsigned long long)message->timestamp,
598 | message->eventdata);
599 | } else {
600 | return PyUnicode_FromFormat("_modes.EventMessage(%d,%llu,%R)",
601 | message->df,
602 | (unsigned long long)message->timestamp,
603 | message->eventdata);
604 | }
605 | }
606 | }
607 |
608 | static PyObject *modesmessage_str(PyObject *self)
609 | {
610 | modesmessage *message = (modesmessage *)self;
611 | if (message->data) {
612 | char buf[256];
613 | char *p = buf;
614 | int i;
615 |
616 | for (i = 0; i < message->datalen; ++i) {
617 | *p++ = hexdigit[(message->data[i] >> 4) & 15];
618 | *p++ = hexdigit[message->data[i] & 15];
619 | }
620 | *p++ = 0;
621 |
622 | return PyUnicode_FromString(buf);
623 | } else {
624 | const char *eventname = df_event_name(message->df);
625 | if (eventname) {
626 | return PyUnicode_FromFormat("%s@%llu:%R",
627 | eventname,
628 | (unsigned long long)message->timestamp,
629 | message->eventdata);
630 | } else {
631 | return PyUnicode_FromFormat("DF%d@%llu:%R",
632 | message->df,
633 | (unsigned long long)message->timestamp,
634 | message->eventdata);
635 | }
636 | }
637 | }
638 |
639 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=40.8.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "MlatClient"
7 | authors = [
8 | {name = "Oliver Jowett", email = "oliver.jowett@flightaware.com"},
9 | ]
10 | description = "Multilateration client package"
11 | dependencies = []
12 | # entry-points is only dynamic to make setuptools complain less;
13 | # the setup.py values are identical to the project.scripts values below
14 | dynamic = ["version", "entry-points"]
15 |
16 | [project.scripts]
17 | mlat-client = "mlat.client.cli:main"
18 | fa-mlat-client = "flightaware.client.cli:main"
19 |
20 | [tool.setuptools]
21 | packages = ['mlat', 'mlat.client', 'flightaware', 'flightaware.client']
22 |
23 | [tool.setuptools.dynamic]
24 | version = { attr = "mlat.client.version.CLIENT_VERSION" }
25 |
--------------------------------------------------------------------------------
/run-flake8.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | D=`dirname $0`
3 | flake8 --exclude=.git,__pycache__,build,debian,tools mlat-client fa-mlat-client $D
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Part of mlat-client - an ADS-B multilateration client.
4 | # Copyright 2015, Oliver Jowett
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | from setuptools import setup, Extension
20 |
21 | CLIENT_VERSION = "unknown"
22 | exec(open('mlat/client/version.py').read())
23 |
24 | setup(name='MlatClient',
25 | description='Multilateration client package',
26 | version=CLIENT_VERSION,
27 | author='Oliver Jowett',
28 | author_email='oliver.jowett@flightaware.com',
29 | packages=['mlat', 'mlat.client', 'flightaware', 'flightaware.client'],
30 | ext_modules=[
31 | Extension('_modes',
32 | include_dirs=["."],
33 | sources=['_modes.c', 'modes_reader.c', 'modes_message.c', 'modes_crc.c'])],
34 | entry_points={
35 | 'console_scripts': [
36 | 'mlat-client = mlat.client.cli:main',
37 | 'fa-mlat-client = flightaware.client.cli:main'
38 | ]}
39 | )
40 |
--------------------------------------------------------------------------------
/tools/compare-message-timing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # build/install the _modes module:
4 | # $ python3 ./setup.py install --user
5 | #
6 | # run the script, passing hostnames for 2 or more receivers:
7 | # $ ./compare-message-timing.py host1 host2
8 | #
9 | # output should start after around 20 seconds;
10 | # the output format is TSV with a pair of columns for each
11 | # receiver: time (seconds) and offset from the first receiver time
12 | # (nanoseconds)
13 |
14 | import sys
15 | import traceback
16 | import asyncio
17 | import concurrent.futures
18 |
19 | import _modes
20 |
21 | class Receiver(object):
22 | def __init__(self, *, loop, id, host, port, correlator):
23 | self.loop = loop
24 | self.id = id
25 | self.host = host
26 | self.port = port
27 |
28 | self.parser = _modes.Reader(_modes.BEAST)
29 | self.parser.default_filter = [False] * 32
30 | self.parser.default_filter[17] = True
31 |
32 | self.frequency = None
33 | self.task = asyncio.async(self.handle_connection())
34 | self.correlator = correlator
35 |
36 | def __str__(self):
37 | return 'client #{0} ({1}:{2})'.format(self.id, self.host, self.port)
38 |
39 | @asyncio.coroutine
40 | def handle_connection(self):
41 | try:
42 | reader, writer = yield from asyncio.open_connection(self.host, self.port)
43 | print(self, 'connected', file=sys.stderr)
44 |
45 | # Binary format, no filters, CRC checks enabled, mode A/C disabled
46 | writer.write(b'\x1a1C\x1a1d\x1a1f\x1a1j')
47 |
48 | self.loop.call_later(5.0, self.set_default_freq)
49 |
50 | data = b''
51 | while True:
52 | moredata = yield from reader.read(4096)
53 | if len(moredata) == 0:
54 | break
55 |
56 | data += moredata
57 | consumed, messages, pending_error = self.parser.feed(data)
58 | data = data[consumed:]
59 | self.handle_messages(messages)
60 |
61 | if pending_error:
62 | self.parser.feed(self.data)
63 |
64 | print(self, 'connection closed', file=sys.stderr)
65 | writer.close()
66 | except concurrent.futures.CancelledError:
67 | pass
68 | except Exception:
69 | print(self, 'unexpected exception', file=sys.stderr)
70 | traceback.print_exc(file=sys.stderr)
71 |
72 | def set_default_freq(self):
73 | if self.frequency is None:
74 | print(self, 'assuming 12MHz clock frequency', file=sys.stderr)
75 | self.frequency = 12e6
76 |
77 | def handle_messages(self, messages):
78 | for message in messages:
79 | if message.df == _modes.DF_EVENT_MODE_CHANGE:
80 | self.frequency = message.eventdata['frequency']
81 | print(self, 'clock frequency changed to {0:.0f}MHz'.format(self.frequency/1e6), file=sys.stderr)
82 | elif message.df == 17 and (message.even_cpr or message.odd_cpr) and self.frequency is not None:
83 | self.correlator(self.id, self.frequency, message)
84 |
85 |
86 | class Correlator(object):
87 | def __init__(self, loop):
88 | self.loop = loop
89 | self.pending = {}
90 | self.clients = set()
91 | self.sorted_clients = []
92 | self.base_times = None
93 |
94 | def add_client(self, client_id):
95 | self.clients.add(client_id)
96 | self.sorted_clients = sorted(self.clients)
97 |
98 | def correlate(self, client_id, frequency, message):
99 | if message in self.pending:
100 | self.pending[message].append((client_id, frequency, message))
101 | else:
102 | self.pending[message] = [ (client_id, frequency, message) ]
103 | self.loop.call_later(10.0, self.resolve, message)
104 |
105 | def resolve(self, message):
106 | copies = self.pending.pop(message)
107 | client_times = {}
108 | for client_id, frequency, message_copy in copies:
109 | if client_id in client_times:
110 | # occurs multiple times, ambiguous, skip it
111 | return
112 | client_times[client_id] = float(message_copy.timestamp) / frequency
113 |
114 | if set(client_times.keys()) == self.clients:
115 | # all clients saw this message
116 |
117 | if self.base_times is None:
118 | # first matching message, record baseline timestamps
119 | self.base_times = client_times
120 |
121 | line = ''
122 | ref_time = client_times[self.sorted_clients[0]] - self.base_times[self.sorted_clients[0]]
123 | for client_id in self.sorted_clients:
124 | this_time = client_times[client_id] - self.base_times[client_id]
125 | offset = this_time - ref_time
126 | line += '{t:.9f}\t{o:.0f}\t'.format(t = this_time, o = offset * 1e9)
127 | print(line[:-1])
128 | sys.stdout.flush()
129 |
130 |
131 | if __name__ == '__main__':
132 | if len(sys.argv) < 3:
133 | print('usage: {0} host1 host2 ...'.format(sys.argv[0]))
134 | sys.exit(1)
135 |
136 | loop = asyncio.get_event_loop()
137 | correlator = Correlator(loop)
138 |
139 | clients = []
140 | for i in range(1, len(sys.argv)):
141 | client_id = str(i)
142 | clients.append(Receiver(loop = loop, id = client_id, host = sys.argv[i], port = 30005, correlator = correlator.correlate))
143 | correlator.add_client(client_id)
144 |
145 | tasks = [client.task for client in clients]
146 | try:
147 | done, pending = loop.run_until_complete(asyncio.wait(tasks, return_when = asyncio.FIRST_COMPLETED))
148 | except KeyboardInterrupt:
149 | pending = tasks
150 |
151 | for task in pending:
152 | task.cancel()
153 | loop.run_until_complete(asyncio.gather(*pending))
154 |
155 | loop.close()
156 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=120
3 |
--------------------------------------------------------------------------------