diff --git a/LICENSE b/LICENSE index d6a9326..437605d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,340 +1,24 @@ -GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - +Copyright 2015, Oliver Jowett +Copyright (c) 2019, FlightAware LLC. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..00be569 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +CC=gcc +CFLAGS+=-Wall -Werror -O2 -g -Ilibs + +CXX=g++ +CXXFLAGS+=-std=c++11 -Wall -Werror -O2 -g -Ilibs + +LIBS=-lboost_system -lboost_program_options -lboost_regex -lSoapySDR -lpthread + +all: dump978 faup978 + +dump978: dump978_main.o socket_output.o message_dispatch.o fec.o libs/fec/init_rs_char.o libs/fec/decode_rs_char.o sample_source.o soapy_source.o convert.o demodulator.o uat_message.o + $(CXX) $(CXXFLAGS) $(LDFLAGS) $^ -o $@ $(LIBS) + +faup978: faup978_main.o socket_input.o uat_message.o track.o + $(CXX) $(CXXFLAGS) $(LDFLAGS) $^ -o $@ $(LIBS) + +clean: + rm -f *.o fec/*.o dump978 faup978 diff --git a/common.h b/common.h new file mode 100644 index 0000000..f12d8b8 --- /dev/null +++ b/common.h @@ -0,0 +1,24 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef UAT_COMMON_H +#define UAT_COMMON_H + +#include +#include +#include + +namespace uat { + typedef std::vector Bytes; + typedef std::vector PhaseBuffer; + + inline static double RoundN(double value, unsigned dp) { + const double scale = std::pow(10, dp); + return std::round(value * scale) / scale; + } +}; + +#endif diff --git a/convert.cc b/convert.cc new file mode 100644 index 0000000..aa2118d --- /dev/null +++ b/convert.cc @@ -0,0 +1,158 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "convert.h" + +#include +#include + +// TODO: This probably needs PhaseBuffer to use a POD-vector-like thing (not +// std::vector) to run at an acceptable speed. Profile it and check. + +namespace dump978 { + static inline std::uint16_t scaled_atan2(double y, double x) { + double ang = std::atan2(y, x) + M_PI; // atan2 returns [-pi..pi], normalize to [0..2*pi] + double scaled_ang = std::round(32768 * ang / M_PI); + return scaled_ang < 0 ? 0 : scaled_ang > 65535 ? 65535 : (std::uint16_t)scaled_ang; + } + + SampleConverter::Pointer SampleConverter::Create(SampleFormat format) { + switch (format) { + case SampleFormat::CU8: + return Pointer(new CU8Converter()); + case SampleFormat::CS8: + return Pointer(new CS8Converter()); + case SampleFormat::CS16H: + return Pointer(new CS16HConverter()); + case SampleFormat::CF32H: + return Pointer(new CF32HConverter()); + default: + throw std::runtime_error("format not implemented yet"); + } + } + + CU8Converter::CU8Converter() { + cu8_alias u; + + unsigned i,q; + for (i = 0; i < 256; ++i) { + double d_i = (i - 127.5); + for (q = 0; q < 256; ++q) { + double d_q = (q - 127.5); + u.iq[0] = i; + u.iq[1] = q; + lookup_[u.iq16] = scaled_atan2(d_q, d_i); + } + } + } + + void CU8Converter::Convert(const uat::Bytes &in, uat::PhaseBuffer &out) { + const cu8_alias *in_iq = reinterpret_cast(in.data()); + + // unroll the loop + const auto n = in.size() / 2; + const auto n8 = n / 8; + const auto n7 = n & 7; + + out.reserve(out.size() + n); + for (unsigned i = 0; i < n8; ++i, in_iq += 8) { + out.push_back(lookup_[in_iq[0].iq16]); + out.push_back(lookup_[in_iq[1].iq16]); + out.push_back(lookup_[in_iq[2].iq16]); + out.push_back(lookup_[in_iq[3].iq16]); + out.push_back(lookup_[in_iq[4].iq16]); + out.push_back(lookup_[in_iq[5].iq16]); + out.push_back(lookup_[in_iq[6].iq16]); + out.push_back(lookup_[in_iq[7].iq16]); + } + for (unsigned i = 0; i < n7; ++i, ++in_iq) { + out.push_back(lookup_[in_iq[0].iq16]); + } + } + + CS8Converter::CS8Converter() { + cs8_alias u; + + int i,q; + for (i = -128; i <= 127; ++i) { + for (q = -128; q <= 127; ++q) { + u.iq[0] = i; + u.iq[1] = q; + lookup_[u.iq16] = scaled_atan2(q, i); + } + } + } + + void CS8Converter::Convert(const uat::Bytes &in, uat::PhaseBuffer &out) { + const cs8_alias *in_iq = reinterpret_cast(in.data()); + + // unroll the loop + const auto n = in.size() / 2; + const auto n8 = n / 8; + const auto n7 = n & 7; + + out.reserve(out.size() + n); + for (unsigned i = 0; i < n8; ++i, in_iq += 8) { + out.push_back(lookup_[in_iq[0].iq16]); + out.push_back(lookup_[in_iq[1].iq16]); + out.push_back(lookup_[in_iq[2].iq16]); + out.push_back(lookup_[in_iq[3].iq16]); + out.push_back(lookup_[in_iq[4].iq16]); + out.push_back(lookup_[in_iq[5].iq16]); + out.push_back(lookup_[in_iq[6].iq16]); + out.push_back(lookup_[in_iq[7].iq16]); + } + for (unsigned i = 0; i < n7; ++i, ++in_iq) { + out.push_back(lookup_[in_iq[0].iq16]); + } + } + + void CS16HConverter::Convert(const uat::Bytes &in, uat::PhaseBuffer &out) { + const std::int16_t *in_iq = reinterpret_cast(in.data()); + + // unroll the loop + const auto n = in.size() / 4; + const auto n8 = n / 8; + const auto n7 = n & 7; + + out.reserve(out.size() + n); + for (unsigned i = 0; i < n8; ++i, in_iq += 8) { + out.push_back(scaled_atan2(in_iq[1], in_iq[0])); + out.push_back(scaled_atan2(in_iq[3], in_iq[2])); + out.push_back(scaled_atan2(in_iq[5], in_iq[4])); + out.push_back(scaled_atan2(in_iq[7], in_iq[6])); + out.push_back(scaled_atan2(in_iq[9], in_iq[8])); + out.push_back(scaled_atan2(in_iq[11], in_iq[10])); + out.push_back(scaled_atan2(in_iq[13], in_iq[12])); + out.push_back(scaled_atan2(in_iq[15], in_iq[14])); + } + for (unsigned i = 0; i < n7; ++i, ++in_iq) { + out.push_back(scaled_atan2(in_iq[1], in_iq[0])); + } + } + + void CF32HConverter::Convert(const uat::Bytes &in, uat::PhaseBuffer &out) { + const double *in_iq = reinterpret_cast(in.data()); + + // unroll the loop + const auto n = in.size() / 8; + const auto n8 = n / 8; + const auto n7 = n & 7; + + out.reserve(out.size() + n); + for (unsigned i = 0; i < n8; ++i, in_iq += 8) { + out.push_back(scaled_atan2(in_iq[1], in_iq[0])); + out.push_back(scaled_atan2(in_iq[3], in_iq[2])); + out.push_back(scaled_atan2(in_iq[5], in_iq[4])); + out.push_back(scaled_atan2(in_iq[7], in_iq[6])); + out.push_back(scaled_atan2(in_iq[9], in_iq[8])); + out.push_back(scaled_atan2(in_iq[11], in_iq[10])); + out.push_back(scaled_atan2(in_iq[13], in_iq[12])); + out.push_back(scaled_atan2(in_iq[15], in_iq[14])); + } + for (unsigned i = 0; i < n7; ++i, ++in_iq) { + out.push_back(scaled_atan2(in_iq[15], in_iq[14])); + } + } +} diff --git a/convert.h b/convert.h new file mode 100644 index 0000000..5714563 --- /dev/null +++ b/convert.h @@ -0,0 +1,91 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef DUMP978_CONVERT_H +#define DUMP978_CONVERT_H + +#include + +#include "common.h" + +namespace dump978 { + // Describes a sample data layout: + // CU8 - interleaved I/Q data, 8 bit unsigned integers + // CS8 - interleaved I/Q data, 8 bit signed integers + // CS16H - interleaved I/Q data, 16 bit signed integers, host byte order + // CF32H - interleaved I/Q data, 32 bit signed floats, host byte order + enum class SampleFormat { CU8, CS8, CS16H, CF32H, UNKNOWN }; + + // Return the number of bytes for 1 sample in the given format + inline static unsigned BytesPerSample(SampleFormat f) { + switch (f) { + case SampleFormat::CU8: return 2; + case SampleFormat::CS8: return 2; + case SampleFormat::CS16H: return 4; + case SampleFormat::CF32H: return 8; + default: return 0; + } + } + + // Base class for all sample converters. + // Use SampleConverter::Create to build converters. + class SampleConverter { + public: + typedef std::shared_ptr Pointer; + + virtual ~SampleConverter() {}; + + // Read samples from `in` and append one phase value per sample to `out`. + // The input buffer should contain an integral number of samples (trailing + // partial samples are ignored, not buffered) + virtual void Convert(const uat::Bytes &in, uat::PhaseBuffer &out) = 0; + + // Return a new SampleConverter that converts from the given format + static Pointer Create(SampleFormat format); + }; + + class CU8Converter : public SampleConverter { + public: + CU8Converter(); + + void Convert(const uat::Bytes &in, uat::PhaseBuffer &out) override; + + private: + union cu8_alias { + std::uint8_t iq[2]; + std::uint16_t iq16; + }; + + std::array lookup_; + }; + + class CS8Converter : public SampleConverter { + public: + CS8Converter(); + + void Convert(const uat::Bytes &in, uat::PhaseBuffer &out) override; + + private: + union cs8_alias { + std::int8_t iq[2]; + std::uint16_t iq16; + }; + + std::array lookup_; + }; + + class CS16HConverter : public SampleConverter { + public: + void Convert(const uat::Bytes &in, uat::PhaseBuffer &out) override; + }; + + class CF32HConverter : public SampleConverter { + public: + void Convert(const uat::Bytes &in, uat::PhaseBuffer &out) override; + }; +}; + +#endif diff --git a/demodulator.cc b/demodulator.cc new file mode 100644 index 0000000..57abe32 --- /dev/null +++ b/demodulator.cc @@ -0,0 +1,344 @@ +// Copyright 2015, Oliver Jowett +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "demodulator.h" + +#include +#include +#include + +using namespace uat; + +namespace dump978 { + SingleThreadReceiver::SingleThreadReceiver(SampleFormat format) + : converter_(SampleConverter::Create(format)), + demodulator_(new TwoMegDemodulator()) + {} + + // Handle samples in 'buffer' by: + // converting them to a phase buffer + // demodulating the phase buffer + // dispatching any demodulated messages + // preserving the end of the phase buffer for reuse in the next call + void SingleThreadReceiver::HandleSamples(std::uint64_t timestamp, const Bytes &buffer) { + assert(converter_); + converter_->Convert(buffer, phase_); // appends to phase_ + + auto messages = demodulator_->Demodulate(timestamp, phase_); // FIXME: not correct because of the preamble + if (messages && !messages->empty()) { + DispatchMessages(messages); + } + + // preserve the tail of the phase buffer for next time + const auto tail_size = demodulator_->NumTrailingSamples(); + if (phase_.size() > tail_size) { + std::copy(phase_.end() - tail_size, phase_.end(), phase_.begin()); + phase_.resize(tail_size); + } + } + + static inline std::int16_t PhaseDifference(std::uint16_t from, std::uint16_t to) { + int32_t difference = to - from; // lies in the range -65535 .. +65535 + if (difference >= 32768) // +32768..+65535 + return difference - 65536; // -> -32768..-1: always in range + else if (difference < -32768) // -65535..-32769 + return difference + 65536; // -> +1..32767: always in range + else + return difference; + } + + static inline bool SyncWordMatch(std::uint64_t word, std::uint64_t expected) { + std::uint64_t diff; + + if (word == expected) + return 1; + + diff = word ^ expected; // guaranteed nonzero + + // This is a bit-twiddling popcount + // hack, tweaked as we only care about + // "=N" set bits for fixed N - + // so we can bail out early after seeing N + // set bits. + // + // It relies on starting with a nonzero value + // with zero or more trailing clear bits + // after the last set bit: + // + // 010101010101010000 + // ^ + // Subtracting one, will flip the + // bits starting at the last set bit: + // + // 010101010101001111 + // ^ + // then we can use that as a bitwise-and + // mask to clear the lowest set bit: + // + // 010101010101000000 + // ^ + // And repeat until the value is zero + // or we have seen too many set bits. + + // >= 1 bit + diff &= (diff-1); // clear lowest set bit + if (!diff) + return 1; // 1 bit error + + // >= 2 bits + diff &= (diff-1); // clear lowest set bit + if (!diff) + return 1; // 2 bits error + + // >= 3 bits + diff &= (diff-1); // clear lowest set bit + if (!diff) + return 1; // 3 bits error + + // >= 4 bits + diff &= (diff-1); // clear lowest set bit + if (!diff) + return 1; // 4 bits error + + // > 4 bits in error, give up + return 0; + } + + // check that there is a valid sync word starting at 'start' + // that matches the sync word 'pattern'. Return a pair: + // first element is true if the sync word looks OK; second + // element has the dphi threshold to use for bit slicing + static inline std::pair CheckSyncWord(const PhaseBuffer &buffer, unsigned start, std::uint64_t pattern) { + const unsigned MAX_SYNC_ERRORS = 4; + + std::int32_t dphi_zero_total = 0; + int zero_bits = 0; + std::int32_t dphi_one_total = 0; + int one_bits = 0; + + // find mean dphi for zero and one bits; + // take the mean of the two as our central value + + for (unsigned i = 0; i < SYNC_BITS; ++i) { + auto dphi = PhaseDifference(buffer[start + i * 2], buffer[start + i * 2 + 1]); + if (pattern & (1UL << (35-i))) { + ++one_bits; + dphi_one_total += dphi; + } else { + ++zero_bits; + dphi_zero_total += dphi; + } + } + + dphi_zero_total /= zero_bits; + dphi_one_total /= one_bits; + + std::int16_t center = (dphi_one_total + dphi_zero_total) / 2; + + // recheck sync word using our center value + unsigned error_bits = 0; + for (unsigned i = 0; i < SYNC_BITS; ++i) { + auto dphi = PhaseDifference(buffer[start + i * 2], buffer[start + i * 2 + 1]); + + if (pattern & (1UL << (35-i))) { + if (dphi < center) + ++error_bits; + } else { + if (dphi > center) + ++error_bits; + } + } + + return std::make_pair<>((error_bits <= MAX_SYNC_ERRORS), center); + } + + // demodulate 'bytes' bytes from samples at 'start' using 'center' as the bit slicing threshold + static inline Bytes DemodBits(const PhaseBuffer &buffer, unsigned start, unsigned bytes, std::int16_t center) + { + Bytes result; + result.reserve(bytes); + + if (start + bytes * 8 * 2 >= buffer.size()) { + throw std::runtime_error("would overrun source buffer"); + } + + auto *phase = buffer.data() + start; + for (unsigned i = 0; i < bytes; ++i) { + std::uint8_t b = 0; + if (PhaseDifference(phase[0], phase[1]) > center) b |= 0x80; + if (PhaseDifference(phase[2], phase[3]) > center) b |= 0x40; + if (PhaseDifference(phase[4], phase[5]) > center) b |= 0x20; + if (PhaseDifference(phase[6], phase[7]) > center) b |= 0x10; + if (PhaseDifference(phase[8], phase[9]) > center) b |= 0x08; + if (PhaseDifference(phase[10], phase[11]) > center) b |= 0x04; + if (PhaseDifference(phase[12], phase[13]) > center) b |= 0x02; + if (PhaseDifference(phase[14], phase[15]) > center) b |= 0x01; + result.push_back(b); + phase += 16; + } + + return result; + } + + unsigned TwoMegDemodulator::NumTrailingSamples() { + return (SYNC_BITS + UPLINK_BITS) * 2; + } + + // Try to demodulate messages from `buffer` and return a list of messages. + // Messages that start near the end of `buffer` may not be demodulated + // (less than (SYNC_BITS + UPLINK_BITS)*2 before the end of the buffer) + SharedMessageVector TwoMegDemodulator::Demodulate(std::uint64_t timestamp, const PhaseBuffer &buffer) { + // We expect samples at twice the UAT bitrate. + // We look at phase difference between pairs of adjacent samples, i.e. + // sample 1 - sample 0 -> sync0 + // sample 2 - sample 1 -> sync1 + // sample 3 - sample 2 -> sync0 + // sample 4 - sample 3 -> sync1 + // ... + // + // We accumulate bits into two buffers, sync0 and sync1. + // Then we compare those buffers to the expected 36-bit sync word that + // should be at the start of each UAT frame. When (if) we find it, + // that tells us which sample to start decoding from. + + // Stop when we run out of remaining samples for a max-sized frame. + // Arrange for our caller to pass the trailing data back to us next time; + // ensure we don't consume any partial sync word we might be part-way + // through. This means we don't need to maintain state between calls. + + SharedMessageVector messages = std::make_shared(); + + const auto trailing_samples = (SYNC_BITS + UPLINK_BITS) * 2 - 2; + if (buffer.size() <= trailing_samples) { + return messages; + } + const auto limit = buffer.size() - trailing_samples; + + unsigned sync_bits = 0; + std::uint64_t sync0 = 0, sync1 = 0; + const std::uint64_t SYNC_MASK = ((((std::uint64_t)1)< 0 ? 1 : 0)) & SYNC_MASK; + sync1 = ((sync1 << 1) | (d1 > 0 ? 1 : 0)) & SYNC_MASK; + + if (++sync_bits < SYNC_BITS) + continue; // haven't fully populated sync0/1 yet + + // see if we have (the start of) a valid sync word + // when we find a match, try to demodulate both with that match + // and with the next position, and pick the one with fewer + // errors. + if (SyncWordMatch(sync0, DOWNLINK_SYNC_WORD)) { + auto start = i - SYNC_BITS * 2 + 2; + auto start_timestamp = timestamp + start * 1000 / 2083333; + auto message = DemodBest(buffer, start, true /* downlink */, start_timestamp); + if (message) { + i = start + message.BitLength() * 2; + sync_bits = 0; + messages->emplace_back(std::move(message)); + continue; + } + } + + if (SyncWordMatch(sync1, DOWNLINK_SYNC_WORD)) { + auto start = i - SYNC_BITS * 2 + 3; + auto start_timestamp = timestamp + start * 1000 / 2083333; + auto message = DemodBest(buffer, start, true /* downlink */, start_timestamp); + if (message) { + i = start + message.BitLength() * 2; + sync_bits = 0; + messages->emplace_back(std::move(message)); + continue; + } + } + + if (SyncWordMatch(sync0, UPLINK_SYNC_WORD)) { + auto start = i - SYNC_BITS * 2 + 2; + auto start_timestamp = timestamp + start * 1000 / 2083333; + auto message = DemodBest(buffer, start, false /* !downlink */, start_timestamp); + if (message) { + i = start + message.BitLength() * 2; + sync_bits = 0; + messages->emplace_back(std::move(message)); + continue; + } + } + + if (SyncWordMatch(sync1, UPLINK_SYNC_WORD)) { + auto start = i - SYNC_BITS * 2 + 3; + auto start_timestamp = timestamp + start * 1000 / 2083333; + auto message = DemodBest(buffer, start, false /* !downlink */, start_timestamp); + if (message) { + i = start + message.BitLength() * 2; + sync_bits = 0; + messages->emplace_back(std::move(message)); + continue; + } + } + } + + return messages; + } + + RawMessage TwoMegDemodulator::DemodBest(const PhaseBuffer &buffer, unsigned start, bool downlink, std::uint64_t timestamp) { + auto message0 = downlink ? DemodOneDownlink(buffer, start, timestamp) : DemodOneUplink(buffer, start, timestamp); + auto message1 = downlink ? DemodOneDownlink(buffer, start + 1, timestamp) : DemodOneUplink(buffer, start + 1, timestamp); + + unsigned errors0 = (message0 ? message0.Errors() : 9999); + unsigned errors1 = (message1 ? message1.Errors() : 9999); + + if (errors0 <= errors1) + return message0; // should be move-eligible + else + return message1; // should be move-eligible + } + + RawMessage TwoMegDemodulator::DemodOneDownlink(const PhaseBuffer &buffer, unsigned start, std::uint64_t timestamp) { + auto sync = CheckSyncWord(buffer, start, DOWNLINK_SYNC_WORD); + if (!sync.first) { + // Sync word had errors + return RawMessage(); + } + + Bytes raw = DemodBits(buffer, start + SYNC_BITS*2, DOWNLINK_LONG_BYTES, sync.second); + + bool success; + uat::Bytes corrected; + unsigned errors; + std::tie(success, corrected, errors) = fec_.CorrectDownlink(raw); + if (!success) { + // Error correction failed + return RawMessage(); + } + + return RawMessage(std::move(corrected), timestamp, errors, 0); + } + + RawMessage TwoMegDemodulator::DemodOneUplink(const PhaseBuffer &buffer, unsigned start, std::uint64_t timestamp) { + auto sync = CheckSyncWord(buffer, start, UPLINK_SYNC_WORD); + if (!sync.first) { + // Sync word had errors + return RawMessage(); + } + + Bytes raw = DemodBits(buffer, start + SYNC_BITS*2, UPLINK_BYTES, sync.second); + + bool success; + uat::Bytes corrected; + unsigned errors; + std::tie(success, corrected, errors) = fec_.CorrectUplink(raw); + + if (!success) { + // Error correction failed + return RawMessage(); + } + + return RawMessage(std::move(corrected), timestamp, errors, 0); + } +} diff --git a/demodulator.h b/demodulator.h new file mode 100644 index 0000000..418fd01 --- /dev/null +++ b/demodulator.h @@ -0,0 +1,62 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef DUMP978_DEMODULATOR_H +#define DUMP978_DEMODULATOR_H + +#include +#include + +#include "fec.h" +#include "common.h" +#include "uat_message.h" +#include "convert.h" +#include "message_source.h" + +namespace dump978 { + class Demodulator { + public: + virtual ~Demodulator() {} + virtual uat::SharedMessageVector Demodulate(std::uint64_t timestamp, const uat::PhaseBuffer &buffer) = 0; + + virtual unsigned NumTrailingSamples() = 0; + + protected: + uat::FEC fec_; + }; + + class TwoMegDemodulator : public Demodulator { + public: + uat::SharedMessageVector Demodulate(std::uint64_t timestamp, const uat::PhaseBuffer &buffer) override; + unsigned NumTrailingSamples() override; + + private: + uat::RawMessage DemodBest(const uat::PhaseBuffer &buffer, unsigned start, bool downlink, std::uint64_t timestamp); + uat::RawMessage DemodOneDownlink(const uat::PhaseBuffer &buffer, unsigned start, std::uint64_t timestamp); + uat::RawMessage DemodOneUplink(const uat::PhaseBuffer &buffer, unsigned start, std::uint64_t timestamp); + }; + + class Receiver : public uat::MessageSource { + public: + virtual void HandleSamples(std::uint64_t timestamp, const uat::Bytes &buffer) = 0; + }; + + class SingleThreadReceiver : public Receiver { + public: + SingleThreadReceiver(SampleFormat format); + + void HandleSamples(std::uint64_t timestamp, const uat::Bytes &buffer) override; + + private: + SampleConverter::Pointer converter_; + uat::PhaseBuffer phase_; + std::unique_ptr demodulator_; + }; + +}; + +#endif + diff --git a/dump978_main.cc b/dump978_main.cc new file mode 100644 index 0000000..a410f1e --- /dev/null +++ b/dump978_main.cc @@ -0,0 +1,224 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include +#include +#include +#include +#include + +#include +#include + +#include "socket_output.h" +#include "message_dispatch.h" +#include "sample_source.h" +#include "soapy_source.h" +#include "convert.h" +#include "demodulator.h" + +using namespace uat; +using namespace dump978; + +namespace po = boost::program_options; +using boost::asio::ip::tcp; + +struct listen_option { + std::string host; + std::string port; +}; + +struct format_option { + SampleFormat format; +}; + +// Specializations of validate for --xxx-port +void validate(boost::any& v, + const std::vector& values, + listen_option* target_type, int) +{ + po::validators::check_first_occurrence(v); + const std::string &s = po::validators::get_single_string(values); + + static const boost::regex r("(?:([^:]+):)?(\\d+)"); + boost::smatch match; + if (boost::regex_match(s, match, r)) { + listen_option o; + o.host = match[1]; + o.port = match[2]; + v = boost::any(o); + } else { + throw po::validation_error(po::validation_error::invalid_option_value); + } +} + +// Specializations of validate for --format +void validate(boost::any& v, + const std::vector& values, + format_option* target_type, int) +{ + po::validators::check_first_occurrence(v); + const std::string &s = po::validators::get_single_string(values); + + static std::map formats { + { "CU8", SampleFormat::CU8 }, + { "CS8", SampleFormat::CS8 }, + { "CS16H", SampleFormat::CS16H }, + { "CF32H", SampleFormat::CF32H } + }; + + auto entry = formats.find(s); + if (entry == formats.end()) + throw po::validation_error(po::validation_error::invalid_option_value); + + v = boost::any(format_option { entry->second }); +} + +#define EXIT_NO_RESTART (64) + +static int realmain(int argc, char **argv) +{ + boost::asio::io_service io_service; + + po::options_description desc("Allowed options"); + desc.add_options() + ("help", "produce help message") + ("raw-stdout", "write raw messages to stdout") + ("json-stdout", "write decoded json to stdout") + ("format", po::value()->default_value({ SampleFormat::CU8 }, "CU8"), "set sample format") + ("stdin", "read sample data from stdin") + ("file", po::value(), "read sample data from a file") + ("file-throttle", "throttle file input to realtime") + ("sdr", po::value(), "read sample data from named SDR device") + ("raw-port", po::value< std::vector >(), "listen for connections on [host:]port and provide raw messages") + ("json-port", po::value< std::vector >(), "listen for connections on [host:]port and provide decoded json"); + + po::variables_map opts; + + try { + po::store(po::parse_command_line(argc, argv, desc), opts); + po::notify(opts); + } catch (boost::program_options::error &err) { + std::cerr << err.what() << std::endl; + std::cerr << desc << std::endl; + return EXIT_NO_RESTART; + } + + if (opts.count("help")) { + std::cerr << desc << std::endl; + return EXIT_NO_RESTART; + } + + MessageDispatch dispatch; + SampleSource::Pointer source; + + tcp::resolver resolver(io_service); + + auto format = opts["format"].as().format; + + if (opts.count("stdin") + opts.count("file") + opts.count("sdr") != 1) { + std::cerr << "Exactly one of --stdin, --file, or --sdr must be used" << std::endl; + return EXIT_NO_RESTART; + } + + if (opts.count("stdin")) { + source = StdinSampleSource::Create(io_service, format); + } else if (opts.count("file")) { + boost::filesystem::path path(opts["file"].as()); + source = FileSampleSource::Create(io_service, path, format, opts.count("file-throttle") > 0); + } else if (opts.count("sdr")) { + auto device = opts["sdr"].as(); + source = SoapySampleSource::Create(format, device); + } else { + assert("impossible case" && false); + } + + auto create_output_port = [&](std::string option, SocketListener::ConnectionFactory factory) -> bool { + if (!opts.count(option)) { + return true; + } + + bool ok = true; + for (auto l : opts[option].as< std::vector >()) { + tcp::resolver::query query(l.host, l.port, tcp::resolver::query::passive); + boost::system::error_code ec; + + bool success = false; + tcp::resolver::iterator end; + for (auto i = resolver.resolve(query, ec); i != end; ++i) { + const auto &endpoint = i->endpoint(); + + try { + auto listener = SocketListener::Create(io_service, endpoint, dispatch, factory); + listener->Start(); + std::cerr << option << ": listening for connections on " << endpoint << std::endl; + success = true; + } catch (boost::system::system_error &err) { + std::cerr << option << ": could not listen on " << endpoint << ": " << err.what() << std::endl; + ec = err.code(); + } + } + + if (!success) { + std::cerr << option << ": no available listening addresses" << std::endl; + ok = false; + } + } + + return ok; + }; + + auto raw_ok = create_output_port("raw-port", &RawOutput::Create); + auto json_ok = create_output_port("json-port", &JsonOutput::Create); + if (!raw_ok || !json_ok) { + return 1; + } + + if (opts.count("raw-stdout")) { + dispatch.AddClient([](SharedMessageVector messages) { + for (const auto &message : *messages) { + std::cout << message << std::endl; + } + }); + } + + if (opts.count("json-stdout")) { + dispatch.AddClient([](SharedMessageVector messages) { + for (const auto &message : *messages) { + if (message.Type() == MessageType::DOWNLINK_SHORT || message.Type() == MessageType::DOWNLINK_LONG) { + std::cout << AdsbMessage(message).ToJson() << std::endl; + } + } + }); + } + + auto receiver = std::make_shared(format); + receiver->SetConsumer(std::bind(&MessageDispatch::Dispatch, &dispatch, std::placeholders::_1)); + + source->SetConsumer([&io_service,receiver](std::uint64_t timestamp, const Bytes &buffer, const boost::system::error_code &ec) { + if (ec) { + std::cerr << "sample source reports error: " << ec.message() << std::endl; + io_service.stop(); + } else { + receiver->HandleSamples(timestamp, buffer); + } + }); + + source->Start(); + + io_service.run(); + + source->Stop(); + return 0; +} + +int main(int argc, char **argv) +{ + try { + return realmain(argc, argv); + } catch (...) { + std::cerr << "Uncaught exception: " << boost::current_exception_diagnostic_information() << std::endl; + return 2; + } +} diff --git a/faup978_main.cc b/faup978_main.cc new file mode 100644 index 0000000..99fb6f4 --- /dev/null +++ b/faup978_main.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include +#include +#include + +#include +#include + +#include "socket_input.h" +#include "message_source.h" +#include "uat_message.h" +#include "track.h" + +using namespace uat; + +namespace po = boost::program_options; +using boost::asio::ip::tcp; + +struct connect_option { + std::string host; + std::string port; +}; + +// Specializations of validate for --connect +void validate(boost::any& v, + const std::vector& values, + connect_option* target_type, int) +{ + po::validators::check_first_occurrence(v); + const std::string &s = po::validators::get_single_string(values); + + static const boost::regex r("(?:([^:]+):)?(\\d+)"); + boost::smatch match; + if (boost::regex_match(s, match, r)) { + v = boost::any(connect_option { match[1], match[2] }); + } else { + throw po::validation_error(po::validation_error::invalid_option_value); + } +} + +#define EXIT_NO_RESTART (64) + +static int realmain(int argc, char **argv) +{ + boost::asio::io_service io_service; + + po::options_description desc("Allowed options"); + desc.add_options() + ("help", "produce help message") + ("connect", po::value(), "connect to host:port for raw UAT data"); + + po::variables_map opts; + + try { + po::store(po::parse_command_line(argc, argv, desc), opts); + po::notify(opts); + } catch (boost::program_options::error &err) { + std::cerr << err.what() << std::endl; + std::cerr << desc << std::endl; + return EXIT_NO_RESTART; + } + + if (opts.count("help")) { + std::cerr << desc << std::endl; + return EXIT_NO_RESTART; + } + + if (!opts.count("connect")) { + std::cerr << "--connect option is required" << std::endl; + return EXIT_NO_RESTART; + } + + auto connect = opts["connect"].as(); + auto input = RawInput::Create(io_service, connect.host, connect.port); + auto tracker = Tracker::Create(io_service); + + input->SetConsumer([tracker](SharedMessageVector messages) { + /* + for (const auto &message : *messages) { + std::cout << message << std::endl; + if (message.Type() == MessageType::DOWNLINK_SHORT || message.Type() == MessageType::DOWNLINK_LONG) { + std::cout << AdsbMessage(message).ToJson() << std::endl; + } + } + */ + tracker->HandleMessages(messages); + std::cout << "tracking " << tracker->Aircraft().size() << " aircraft" << std::endl; + }); + + tracker->Start(); + input->Start(); + + io_service.run(); + + input->Stop(); + tracker->Stop(); + return 0; +} + +int main(int argc, char **argv) +{ +#if 0 + try { + return realmain(argc, argv); + } catch (...) { + std::cerr << "Uncaught exception: " << boost::current_exception_diagnostic_information() << std::endl; + return 2; + } +#else + return realmain(argc, argv); +#endif +} diff --git a/fec.cc b/fec.cc new file mode 100644 index 0000000..5e1f70c --- /dev/null +++ b/fec.cc @@ -0,0 +1,97 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "fec.h" +#include "uat_protocol.h" + +extern "C" { +#include "fec/rs.h" +} + +uat::FEC::FEC(void) +{ + rs_downlink_short_ = ::init_rs_char(8, /* gfpoly */ DOWNLINK_POLY, /* fcr */ 120, /* prim */ 1, /* nroots */ 12, /* pad */ 225); + rs_downlink_long_ = ::init_rs_char(8, /* gfpoly */ DOWNLINK_POLY, /* fcr */ 120, /* prim */ 1, /* nroots */ 14, /* pad */ 207); + rs_uplink_ = ::init_rs_char(8, /* gfpoly */ UPLINK_POLY, /* fcr */ 120, /* prim */ 1, /* nroots */ 20, /* pad */ 163); +} + +uat::FEC::~FEC(void) +{ + ::free_rs_char(rs_downlink_short_); + ::free_rs_char(rs_downlink_long_); + ::free_rs_char(rs_uplink_); +} + +std::tuple uat::FEC::CorrectDownlink(const Bytes &raw) +{ + using R = std::tuple; + + if (raw.size() != DOWNLINK_LONG_BYTES) { + return R { false, {}, 0 }; + } + + // Try decoding as a Long UAT. + Bytes corrected; + std::copy(raw.begin(), raw.end(), std::back_inserter(corrected)); + + int n_corrected = ::decode_rs_char(rs_downlink_long_, corrected.data(), NULL, 0); + if (n_corrected >= 0 && n_corrected <= 7 && (corrected[0]>>3) != 0) { + // Valid long frame. + corrected.resize(DOWNLINK_LONG_DATA_BYTES); + return R { true, std::move(corrected), n_corrected }; + } + + // Retry as Basic UAT + // We rely on decode_rs_char not modifying the data if there were + // uncorrectable errors in the previous step. + n_corrected = ::decode_rs_char(rs_downlink_short_, corrected.data(), NULL, 0); + if (n_corrected >= 0 && n_corrected <= 6 && (corrected[0]>>3) == 0) { + // Valid short frame + corrected.resize(DOWNLINK_SHORT_DATA_BYTES); + return R { true, std::move(corrected), n_corrected }; + } + + // Failed. + return R { false, {}, 0 }; +} + +std::tuple uat::FEC::CorrectUplink(const Bytes &raw) +{ + using R = std::tuple; + + if (raw.size() != UPLINK_BYTES) { + return R { false, {}, 0 }; + } + + // uplink messages consist of 6 blocks, interleaved; each block consists of a data section + // then an ECC section; we need to deinterleave, check/correct the data, then join the blocks + // removing the ECC sections. + unsigned total_errors = 0; + Bytes corrected; + Bytes blockdata; + + corrected.reserve(UPLINK_DATA_BYTES); + blockdata.resize(UPLINK_BLOCK_BYTES); + + for (unsigned block = 0; block < UPLINK_BLOCKS_PER_FRAME; ++block) { + // deinterleave + for (unsigned i = 0; i < UPLINK_BLOCK_BYTES; ++i) { + blockdata[i] = raw[i * UPLINK_BLOCKS_PER_FRAME + block]; + } + + // error-correct + int n_corrected = ::decode_rs_char(rs_uplink_, blockdata.data(), NULL, 0); + if (n_corrected < 0 || n_corrected > 10) { + // Failed + return R { false, {}, 0 }; + } + + total_errors += n_corrected; + + // copy the data into the right place + std::copy(blockdata.begin(), blockdata.begin() + UPLINK_BLOCK_DATA_BYTES, std::back_inserter(corrected)); + } + + return R { true, std::move(corrected), total_errors }; +} diff --git a/fec.h b/fec.h new file mode 100644 index 0000000..2279739 --- /dev/null +++ b/fec.h @@ -0,0 +1,48 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef UAT_FEC_H +#define UAT_FEC_H + +#include + +#include "common.h" + +namespace uat { + // Deinterleaving and error-correction of UAT messages. + // This delegates to the "fec" library (in fec/) for the actual Reed-Solomon + // error-correction work. + class FEC { + public: + FEC(); + ~FEC(); + + // Given DOWNLINK_LONG_BYTES of demodulated data, returns a tuple of: + // bool - true if the message is good, false if it was uncorrectable. + // Bytes - a buffer containing the corrected data with FEC bits removed; + // this will be either DOWNLINK_SHORT_DATA_BYTES or + // DOWNLINK_LONG_DATA_BYTES in size depending on the detected + // message type. Empty if the message was uncorrectable. + // unsigned - the number of errors corrected. 0 if the message was uncorrectable + std::tuple CorrectDownlink(const Bytes &raw); + + // Given UPLINK_BYTES of demodulated data, returns a tuple of: + // bool - true if the message is good, false if it was uncorrectable. + // Bytes - a buffer containing the deinterleaved, corrected data with + // FEC bits removed; this will be exactly UPLINK_DATA_BYTES + // in size. Empty if the message was uncorrectable. + // unsigned - the number of errors corrected. 0 if the message was uncorrectable + std::tuple CorrectUplink(const Bytes &raw); + + private: + void *rs_uplink_; + void *rs_downlink_short_; + void *rs_downlink_long_; + }; +}; + +#endif + diff --git a/legacy/LICENSE b/legacy/LICENSE new file mode 100644 index 0000000..d6a9326 --- /dev/null +++ b/legacy/LICENSE @@ -0,0 +1,340 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/message_dispatch.cc b/message_dispatch.cc new file mode 100644 index 0000000..b6f72cd --- /dev/null +++ b/message_dispatch.cc @@ -0,0 +1,89 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "message_dispatch.h" + +#include +#include + +namespace uat { + MessageDispatch::MessageDispatch() + : next_handle_(0), + busy_(0) + {} + + MessageDispatch::Handle MessageDispatch::AddClient(MessageHandler handler) { + std::unique_lock lock(mutex_); + + Handle h = next_handle_++; + clients_[h] = { handler, false }; + return h; + } + + void MessageDispatch::RemoveClient(Handle h) { + std::unique_lock lock(mutex_); + + auto i = clients_.find(h); + if (i == clients_.end()) + return; + + i->second.deleted = true; + PurgeDeadClients(); + } + + template + class BusyCounter { + public: + BusyCounter(T& var) + : var_(var), owned_(true) + { + ++var_; + } + + BusyCounter(const T&) = delete; + BusyCounter& operator=(const T&) = delete; + + ~BusyCounter() { + release(); + } + + void release() { + if (owned_) { + --var_; + owned_ = false; + } + } + private: + T& var_; + bool owned_; + }; + + void MessageDispatch::Dispatch(SharedMessageVector messages) { + std::unique_lock lock(mutex_); + + BusyCounter counter(busy_); + for (auto i = clients_.begin(); i != clients_.end(); ++i) { + Client &c = i->second; + if (!c.deleted) + c.handler(messages); + } + counter.release(); + + PurgeDeadClients(); + } + + void MessageDispatch::PurgeDeadClients() { + // caller must lock! + if (busy_) + return; + + for (auto i = clients_.begin(); i != clients_.end(); ) { + Client &c = i->second; + if (c.deleted) + clients_.erase(i++); + else + ++i; + } + } +} diff --git a/message_dispatch.h b/message_dispatch.h new file mode 100644 index 0000000..a66e150 --- /dev/null +++ b/message_dispatch.h @@ -0,0 +1,49 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef MESSAGE_DISPATCH_H +#define MESSAGE_DISPATCH_H + +#include +#include +#include +#include + +#include "uat_message.h" + +namespace uat { + class MessageDispatch { + public: + typedef unsigned Handle; + typedef std::function MessageHandler; + + MessageDispatch(); + MessageDispatch(const MessageDispatch &) = delete; + MessageDispatch &operator=(const MessageDispatch &) = delete; + + Handle AddClient(MessageHandler handler); + void RemoveClient(Handle client); + + void Dispatch(SharedMessageVector messages); + + protected: + void PurgeDeadClients(); + + private: + std::recursive_mutex mutex_; + Handle next_handle_; + unsigned busy_; + + struct Client { + MessageHandler handler; + bool deleted; + }; + + std::map clients_; + }; +}; + +#endif diff --git a/message_source.h b/message_source.h new file mode 100644 index 0000000..13d026e --- /dev/null +++ b/message_source.h @@ -0,0 +1,35 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef DUMP978_MESSAGE_SOURCE_H +#define DUMP978_MESSAGE_SOURCE_H + +#include "uat_message.h" + +namespace uat { + class MessageSource { + public: + typedef std::function Consumer; + + virtual ~MessageSource() {} + + void SetConsumer(Consumer consumer) { + consumer_ = consumer; + } + + protected: + void DispatchMessages(uat::SharedMessageVector messages) { + if (consumer_) { + consumer_(messages); + } + } + + private: + Consumer consumer_; + }; +}; + +#endif diff --git a/sample_source.cc b/sample_source.cc new file mode 100644 index 0000000..35f6c19 --- /dev/null +++ b/sample_source.cc @@ -0,0 +1,139 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "sample_source.h" + +#include +#include + +namespace dump978 { + void FileSampleSource::Start() { + stream_.open(path_.native()); + if (!stream_.good()) { + auto ec = boost::system::error_code(errno, boost::system::system_category()); + stream_.close(); + DispatchError(ec); + return; + } + + next_block_ = std::chrono::steady_clock::now(); + timestamp_ = 1; // always use synthetic timestamps for file sources + + auto self = std::static_pointer_cast(shared_from_this()); + service_.post(std::bind(&FileSampleSource::ReadBlock, self, boost::system::error_code())); + } + + void FileSampleSource::Stop() { + timer_.cancel(); + if (stream_.is_open()) { + stream_.close(); + DispatchError(boost::asio::error::eof); + } + } + + void FileSampleSource::ReadBlock(const boost::system::error_code &ec) { + if (ec) { + if (ec == boost::asio::error::operation_aborted) { + return; + } + + stream_.close(); + DispatchError(ec); + return; + } + + if (!stream_.is_open()) { + return; + } + + block_.resize(block_.capacity()); + stream_.read(reinterpret_cast(block_.data()), block_.size()); + + if (stream_.bad()) { + auto ec = boost::system::error_code(errno, boost::system::system_category()); + stream_.close(); + DispatchError(ec); + return; + } + + block_.resize(stream_.gcount() - (stream_.gcount() % alignment_)); + if (!block_.empty()) { + DispatchBuffer(timestamp_, block_); + timestamp_ += (block_.size() * 1000ULL / bytes_per_second_); + } + + if (stream_.eof()) { + stream_.close(); + DispatchError(boost::asio::error::eof); + return; + } + + auto self = std::static_pointer_cast(shared_from_this()); + if (throttle_) { + auto delay = std::chrono::nanoseconds(1000000000ULL * block_.size() / bytes_per_second_); + next_block_ += delay; + timer_.expires_at(next_block_); + timer_.async_wait(std::bind(&FileSampleSource::ReadBlock, self, std::placeholders::_1)); + } else { + service_.post(std::bind(&FileSampleSource::ReadBlock, self, boost::system::error_code())); + } + } + + // + // + // + + void StdinSampleSource::Start() { + stream_.assign(::dup(STDIN_FILENO)); + ScheduleRead(); + } + + void StdinSampleSource::Stop() { + if (stream_.is_open()) { + stream_.close(); + DispatchError(boost::asio::error::eof); + } + } + + void StdinSampleSource::ScheduleRead() { + if (!stream_.is_open()) { + return; + } + + auto self = shared_from_this(); + stream_.async_read_some(boost::asio::buffer(block_.data() + used_, block_.size() - used_), + [this,self] (const boost::system::error_code& ec, std::size_t bytes_transferred) { + if (ec) { + if (ec == boost::asio::error::operation_aborted) { + return; + } + + stream_.close(); + DispatchError(ec); + return; + } + + used_ += bytes_transferred; + + // work out a starting timestamp + static auto unix_epoch = std::chrono::system_clock::from_time_t(0); + auto end_of_block = std::chrono::system_clock::now(); + auto start_of_block = end_of_block - (std::chrono::milliseconds(1000) * bytes_transferred / samples_per_second_ / alignment_); + std::uint64_t timestamp = + std::chrono::duration_cast(start_of_block - unix_epoch).count(); + + // fixme, don't copy! + auto trailing_bytes = used_ % alignment_; + auto leading_bytes = used_ - trailing_bytes; + + uat::Bytes buffer; + buffer.resize(leading_bytes); + std::copy(block_.begin(), block_.begin() + leading_bytes, buffer.begin()); + std::copy(block_.begin() + leading_bytes, block_.end(), block_.begin()); + used_ = trailing_bytes; + DispatchBuffer(timestamp, buffer); + ScheduleRead(); + }); + } +}; diff --git a/sample_source.h b/sample_source.h new file mode 100644 index 0000000..85317fe --- /dev/null +++ b/sample_source.h @@ -0,0 +1,128 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef DUMP978_SAMPLE_SOURCE_H +#define DUMP978_SAMPLE_SOURCE_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.h" +#include "convert.h" + +namespace dump978 { + class SampleSource : public std::enable_shared_from_this { + public: + typedef std::shared_ptr Pointer; + typedef std::function Consumer; + + virtual ~SampleSource() {} + + virtual void Start() = 0; + virtual void Stop() = 0; + + void SetConsumer(Consumer consumer) { + consumer_ = consumer; + } + + protected: + SampleSource() {} + + void DispatchBuffer(std::uint64_t timestamp, const uat::Bytes &buffer) { + if (consumer_) { + consumer_(timestamp, buffer, boost::system::error_code()); + } + } + + void DispatchError(const boost::system::error_code &ec) { + if (consumer_) { + consumer_(0, uat::Bytes(), ec); + } + } + + private: + Consumer consumer_; + }; + + class FileSampleSource : public SampleSource { + public: + static SampleSource::Pointer Create(boost::asio::io_service &service, + const boost::filesystem::path &path, + SampleFormat format, + bool throttle, + std::size_t samples_per_second = 2083333, + std::size_t samples_per_block = 524288) { + return Pointer(new FileSampleSource(service, path, format, throttle, samples_per_second, samples_per_block)); + } + + void Start() override; + void Stop() override; + + private: + FileSampleSource(boost::asio::io_service &service, + const boost::filesystem::path &path, + SampleFormat format, + bool throttle, + std::size_t samples_per_second, + std::size_t samples_per_block) + : service_(service), path_(path), alignment_(BytesPerSample(format)), throttle_(throttle), bytes_per_second_(samples_per_second * alignment_), timer_(service) + { + block_.reserve(samples_per_block * alignment_); + } + + void ReadBlock(const boost::system::error_code &ec); + + boost::asio::io_service &service_; + boost::filesystem::path path_; + unsigned alignment_; + bool throttle_; + std::size_t bytes_per_second_; + + std::ifstream stream_; + boost::asio::steady_timer timer_; + std::chrono::steady_clock::time_point next_block_; + uat::Bytes block_; + std::uint64_t timestamp_; + }; + + class StdinSampleSource : public SampleSource { + public: + static SampleSource::Pointer Create(boost::asio::io_service &service, + SampleFormat format, + std::size_t samples_per_second = 2083333, + std::size_t samples_per_block = 524288) { + return Pointer(new StdinSampleSource(service, format, samples_per_second, samples_per_block)); + } + + void Start() override; + void Stop() override; + + private: + StdinSampleSource(boost::asio::io_service &service, SampleFormat format, std::size_t samples_per_second, std::size_t samples_per_block) + : service_(service), alignment_(BytesPerSample(format)), samples_per_second_(samples_per_second), stream_(service), used_(0) + { + block_.resize(samples_per_block * alignment_); + } + + void ScheduleRead(); + + boost::asio::io_service &service_; + unsigned alignment_; + std::size_t samples_per_second_; + boost::asio::posix::stream_descriptor stream_; + uat::Bytes block_; + std::size_t used_; + }; +}; + +#endif diff --git a/soapy_source.cc b/soapy_source.cc new file mode 100644 index 0000000..ab9453c --- /dev/null +++ b/soapy_source.cc @@ -0,0 +1,151 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "soapy_source.h" + +#include + +#include +#include + +namespace dump978 { + std::atomic_bool SoapySampleSource::log_handler_registered_(false); + + static void SoapyLogger(const SoapySDRLogLevel logLevel, const char *message) + { + static std::map levels = { + { SOAPY_SDR_FATAL, "FATAL" }, + { SOAPY_SDR_CRITICAL, "CRITICAL" }, + { SOAPY_SDR_ERROR, "ERROR" }, + { SOAPY_SDR_WARNING, "WARNING" }, + { SOAPY_SDR_NOTICE, "NOTICE" }, + { SOAPY_SDR_INFO, "INFO" }, + { SOAPY_SDR_DEBUG, "DEBUG" }, + { SOAPY_SDR_TRACE, "TRACE" }, + { SOAPY_SDR_SSI, "SSI" } + }; + + std::string level; + auto i = levels.find(logLevel); + if (i == levels.end()) + level = "UNKNOWN"; + else + level = i->second; + + std::cerr << "SoapySDR: " << level << ": " << message << std::endl; + } + + SoapySampleSource::SoapySampleSource(SampleFormat format, const std::string &device_name) + : format_(format), device_name_(device_name), halt_(false) + { + if (!log_handler_registered_.exchange(true)) { + SoapySDR::registerLogHandler(SoapyLogger); + } + } + + SoapySampleSource::~SoapySampleSource() + { + Stop(); + } + + void SoapySampleSource::Start() + { + device_ = { SoapySDR::Device::make(device_name_), &SoapySDR::Device::unmake }; + if (!device_) { + throw std::runtime_error("no suitable device found"); + } + + // hacky mchackerson + device_->setSampleRate(SOAPY_SDR_RX, 0, 2083333.0); + device_->setFrequency(SOAPY_SDR_RX, 0, 978000000); + device_->setGain(SOAPY_SDR_RX, 0, 50.0); + device_->setBandwidth(SOAPY_SDR_RX, 0, 3.0e6); + + std::string soapy_format; + switch (format_) { + case SampleFormat::CU8: soapy_format = "CU8"; break; + case SampleFormat::CS8: soapy_format = "CS8"; break; + case SampleFormat::CS16H: soapy_format = "CS16"; break; + case SampleFormat::CF32H: soapy_format = "CF32"; break; + default: + throw std::runtime_error("unsupported sample format"); + } + + std::vector channels = { 0 }; + + stream_ = { device_->setupStream(SOAPY_SDR_RX, soapy_format, channels), + std::bind(&SoapySDR::Device::closeStream, device_, std::placeholders::_1) }; + if (!stream_) { + throw std::runtime_error("failed to construct stream"); + } + + device_->activateStream(stream_.get()); + + halt_ = false; + rx_thread_.reset(new std::thread(&SoapySampleSource::Run, this)); + } + + void SoapySampleSource::Stop() + { + if (stream_) { + // rtlsdr needs the rx thread to drain data before this returns.. + device_->deactivateStream(stream_.get()); + } + if (rx_thread_) { + halt_ = true; + rx_thread_->join(); + rx_thread_.reset(); + } + if (stream_) { + stream_.reset(); + } + if (device_) { + device_.reset(); + } + } + + void SoapySampleSource::Run() + { + const auto bytes_per_element = BytesPerSample(format_); + const auto elements = std::max(65536, device_->getStreamMTU(stream_.get())); + + uat::Bytes block; + block.reserve(elements * bytes_per_element); + + while (!halt_) { + void *buffs[1] = { block.data() }; + int flags = 0; + long long time_ns; + + block.resize(elements * bytes_per_element); + auto elements_read = device_->readStream(stream_.get(), + buffs, + elements, + flags, + time_ns, + /* timeout, microseconds */ 1000000); + if (halt_) { + break; + } + + if (elements_read < 0) { + std::cerr << "SoapySDR reports error: " << SoapySDR::errToStr(elements_read) << std::endl; + auto ec = boost::system::error_code(0, boost::system::generic_category()); + DispatchError(ec); + break; + } + + block.resize(elements_read * bytes_per_element); + + // work out a starting timestamp + static auto unix_epoch = std::chrono::system_clock::from_time_t(0); + auto end_of_block = std::chrono::system_clock::now(); + auto start_of_block = end_of_block - (std::chrono::milliseconds(1000) * elements / 2083333); + std::uint64_t timestamp = + std::chrono::duration_cast(start_of_block - unix_epoch).count(); + + DispatchBuffer(timestamp, block); + } + } +}; diff --git a/soapy_source.h b/soapy_source.h new file mode 100644 index 0000000..c14f890 --- /dev/null +++ b/soapy_source.h @@ -0,0 +1,48 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef DUMP978_SOAPY_SOURCE_H +#define DUMP978_SOAPY_SOURCE_H + +#include +#include +#include + +#include + +#include "sample_source.h" + +namespace dump978 { + class SoapySampleSource : public SampleSource { + public: + static SampleSource::Pointer Create(SampleFormat format, + const std::string &device_name) { + return Pointer(new SoapySampleSource(format, device_name)); + } + + virtual ~SoapySampleSource(); + + void Start() override; + void Stop() override; + + private: + SoapySampleSource(SampleFormat format, const std::string &device_name); + + void Run(); + + SampleFormat format_; + std::string device_name_; + + std::shared_ptr device_; + std::shared_ptr stream_; + std::unique_ptr rx_thread_; + bool halt_; + + static std::atomic_bool log_handler_registered_; + }; +}; + +#endif diff --git a/socket_input.cc b/socket_input.cc new file mode 100644 index 0000000..7684373 --- /dev/null +++ b/socket_input.cc @@ -0,0 +1,241 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "socket_input.h" + +using namespace uat; +using boost::asio::ip::tcp; + +#include + +RawInput::RawInput(boost::asio::io_service &service, + const std::string &host, + const std::string &port_or_service) + : service_(service), + host_(host), + port_or_service_(port_or_service), + resolver_(service), + socket_(service), + used_(0) +{ + readbuf_.resize(8192); +} + +void RawInput::Start() +{ + auto self(shared_from_this()); + + tcp::resolver::query query(host_, port_or_service_); + resolver_.async_resolve(query, [this,self] (const boost::system::error_code &ec, + tcp::resolver::iterator it) { + if (!ec) { + next_endpoint_ = it; + TryNextEndpoint(boost::asio::error::make_error_code(boost::asio::error::host_not_found)); + } else if (ec == boost::asio::error::operation_aborted) { + return; + } else { + HandleError(ec); + return; + } + }); +} + +void RawInput::Stop() +{ + socket_.close(); +} + +void RawInput::TryNextEndpoint(const boost::system::error_code &last_error) +{ + if (next_endpoint_ == tcp::resolver::iterator()) { + // No more addresses to try + HandleError(last_error); + return; + } + + tcp::endpoint endpoint = *next_endpoint_++; + auto self(shared_from_this()); + socket_.async_connect(endpoint, + [this,self,endpoint] (const boost::system::error_code &ec) { + if (!ec) { + std::cerr << "Connected to " << endpoint << std::endl; + ScheduleRead(); + } else if (ec == boost::asio::error::operation_aborted) { + return; + } else { + std::cerr << "connection to " << endpoint << " failed: " << ec.message() << std::endl; + socket_.close(); + TryNextEndpoint(ec); + } + }); +} + +void RawInput::ScheduleRead() +{ + auto self(shared_from_this()); + + if (used_ >= readbuf_.size()) { + HandleError(boost::asio::error::make_error_code(boost::asio::error::no_buffer_space)); + return; + } + + socket_.async_read_some(boost::asio::buffer(readbuf_.data() + used_, readbuf_.size() - used_), + [this,self] (const boost::system::error_code &ec, std::size_t len) { + if (ec) { + HandleError(ec); + return; + } + + used_ += len; + ParseBuffer(); + ScheduleRead(); + }); +} + +void RawInput::HandleError(const boost::system::error_code &ec) +{ + if (ec == boost::asio::error::operation_aborted) { + return; + } + // I .. guess we should do something here, huh? + std::cerr << ec.message() << std::endl; + socket_.close(); +} + +void RawInput::ParseBuffer() +{ + SharedMessageVector messages; + + auto sol = readbuf_.begin(); + auto end = readbuf_.begin() + used_; + while (sol < end) { + auto eol = std::find(sol, end, '\n'); + if (eol == end) + break; + + std::string line(sol, eol); + auto result = ParseLine(line); + if (result) { + if (!messages) { + messages = std::make_shared(); + } + messages->emplace_back(std::move(*result)); + } else { + std::cerr << "warning: failed to parse input line: " << line << std::endl; + } + sol = eol + 1; + } + + if (sol != readbuf_.begin()) { + std::copy(sol, end, readbuf_.begin()); + used_ = std::distance(sol, end); + } + if (messages) { + DispatchMessages(messages); + } +} + +static inline int hexvalue(char c) +{ + switch (c) { + case '0': return 0; + case '1': return 1; + case '2': return 2; + case '3': return 3; + case '4': return 4; + case '5': return 5; + case '6': return 6; + case '7': return 7; + case '8': return 8; + case '9': return 9; + case 'a': case 'A': return 10; + case 'b': case 'B': return 11; + case 'c': case 'C': return 12; + case 'd': case 'D': return 13; + case 'e': case 'E': return 14; + case 'f': case 'F': return 15; + default: return -1; + } +} + +boost::optional RawInput::ParseLine(const std::string &line) +{ + if (line.size() < 2) { + // too short + return boost::none; + } + + if (line[0] != '-' && line[0] != '+') { + // badly formatted + return boost::none; + } + + auto eod = line.find(';', 1); + if (eod == std::string::npos) { + // missing semicolon + return boost::none; + } + + auto hexlength = eod - 1; + if (hexlength % 2 != 0) { + // wrong number of data characters + return boost::none; + } + + // parse hex digits + uat::Bytes payload; + payload.reserve(hexlength / 2); + + for (decltype(eod) i = 1; i < eod; i += 2) { + auto h1 = hexvalue(line[i]); + auto h2 = hexvalue(line[i+1]); + if (h1 < 0 || h2 < 0) { + // bad hex value + return boost::none; + } + payload.push_back((std::uint8_t)((h1 << 4) | h2)); + } + + // parse key-value pairs + + unsigned rs = 0; + double rssi = 0; + std::uint64_t t = 0; + + for (auto i = eod + 1; i < line.size(); ) { + auto equals = line.find('=', i); + auto semicolon = line.find(';', i); + if (equals == std::string::npos || semicolon == std::string::npos || semicolon < equals) { + // no more valid data + break; + } + + auto key = line.substr(i, equals - i); + auto value = line.substr(equals + 1, semicolon - equals - 1); + + if (key == "rs") { + try { + rs = std::stoi(value, nullptr, 10); + } catch (const std::exception &e) { + rs = 0; + } + } else if (key == "rssi") { + try { + rssi = std::stod(value); + } catch (const std::exception &e) { + rssi = 0; + } + } else if (key == "t") { + try { + t = (std::uint64_t) (std::stod(value) * 1000); + } catch (const std::exception &e) { + t = 0; + } + } + + i = semicolon + 1; + } + + return uat::RawMessage(std::move(payload), t, rs, rssi); +} diff --git a/socket_input.h b/socket_input.h new file mode 100644 index 0000000..750e719 --- /dev/null +++ b/socket_input.h @@ -0,0 +1,58 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef SOCKET_INPUT_H +#define SOCKET_INPUT_H + +#include +#include + +#include +#include + +#include "message_source.h" + +namespace uat { + class RawInput : public MessageSource, public std::enable_shared_from_this { + public: + typedef std::shared_ptr Pointer; + + static Pointer Create(boost::asio::io_service &service, + const std::string &host, + const std::string &port_or_service) { + return Pointer(new RawInput(service, host, port_or_service)); + } + + void Start(); + void Stop(); + + private: + RawInput(boost::asio::io_service &service, + const std::string &host, + const std::string &port_or_service); + + void TryNextEndpoint(const boost::system::error_code &last_error); + void ScheduleRead(); + void ParseBuffer(); + boost::optional ParseLine(const std::string &line); + void HandleError(const boost::system::error_code &ec); + + boost::asio::io_service &service_; + std::string host_; + std::string port_or_service_; + + boost::asio::ip::tcp::resolver resolver_; + boost::asio::ip::tcp::socket socket_; + boost::asio::ip::tcp::resolver::iterator next_endpoint_; + + std::vector readbuf_; + std::size_t used_; + }; +}; + +#endif + + diff --git a/socket_output.cc b/socket_output.cc new file mode 100644 index 0000000..1cc67d0 --- /dev/null +++ b/socket_output.cc @@ -0,0 +1,186 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include +#include + +#include +#include + +#include "socket_output.h" +#include "message_dispatch.h" + +namespace asio = boost::asio; +using boost::asio::ip::tcp; + +using namespace uat; + +namespace dump978 { + SocketOutput::SocketOutput(asio::io_service &service, + tcp::socket &&socket) + : service_(service), + strand_(service), + socket_(std::move(socket)), + peer_(socket_.remote_endpoint()), + flush_pending_(false) + { + } + + void SocketOutput::Start() + { + ReadAndDiscard(); + } + + void SocketOutput::ReadAndDiscard() + { + auto self(shared_from_this()); + auto buf = std::make_shared(512); + socket_.async_read_some(asio::buffer(*buf), + strand_.wrap([this,self,buf] (const boost::system::error_code &ec, std::size_t len) { + if (ec) { + HandleError(ec); + } else { + ReadAndDiscard(); + } + })); + } + + void SocketOutput::Write(SharedMessageVector messages) + { + auto self(shared_from_this()); + strand_.dispatch([this,self,messages]() { + if (IsOpen()) { + InternalWrite(messages); + Flush(); + } + }); + } + + void SocketOutput::Flush() + { + if (flush_pending_) + return; + + auto writebuf = std::make_shared(outbuf_.str()); + if (writebuf->empty()) + return; + + flush_pending_ = true; + outbuf_.str(std::string()); + + auto self(shared_from_this()); + async_write(socket_, boost::asio::buffer(*writebuf), + strand_.wrap([this,self,writebuf] (const boost::system::error_code &ec, size_t len) { + flush_pending_ = false; + if (ec) { + HandleError(ec); + return; + } + + Flush(); // maybe some more data arrived + })); + } + + void SocketOutput::HandleError(const boost::system::error_code &ec) + { + if (ec == boost::asio::error::eof) { + std::cerr << peer_ << ": connection closed" << std::endl; + } else if (ec != boost::asio::error::operation_aborted) { + std::cerr << peer_ << ": connection error: " << ec.message() << std::endl; + } + + Close(); + } + + void SocketOutput::Close() + { + socket_.close(); + if (close_notifier_) { + close_notifier_(); + } + } + + ////////////// + + void RawOutput::InternalWrite(SharedMessageVector messages) + { + for (const auto &message : *messages) { + Buf() << message << '\n'; + } + } + + ////////////// + + void JsonOutput::InternalWrite(SharedMessageVector messages) + { + for (const auto &message : *messages) { + if (message.Type() == MessageType::DOWNLINK_SHORT || message.Type() == MessageType::DOWNLINK_LONG) { + Buf() << AdsbMessage(message).ToJson() << '\n'; + } + } + } + + ////////////// + + SocketListener::SocketListener(asio::io_service &service, + const tcp::endpoint &endpoint, + MessageDispatch &dispatch, + ConnectionFactory factory) + : service_(service), + acceptor_(service), + endpoint_(endpoint), + socket_(service), + dispatch_(dispatch), + factory_(factory) + { + } + + void SocketListener::Start() + { + acceptor_.open(endpoint_.protocol()); + acceptor_.set_option(asio::socket_base::reuse_address(true)); + acceptor_.set_option(tcp::acceptor::reuse_address(true)); + + // We are v6 aware and bind separately to v4 and v6 addresses + if (endpoint_.protocol() == tcp::v6()) + acceptor_.set_option(asio::ip::v6_only(true)); + + acceptor_.bind(endpoint_); + acceptor_.listen(); + Accept(); + } + + void SocketListener::Close() + { + acceptor_.cancel(); + socket_.close(); + } + + void SocketListener::Accept() + { + auto self(shared_from_this()); + + acceptor_.async_accept(socket_, + peer_, + [this,self] (const boost::system::error_code &ec) { + if (!ec) { + std::cerr << endpoint_ << ": accepted a connection from " << peer_ << std::endl; + auto new_output = factory_(service_, std::move(socket_)); + if (new_output) { + auto handle = dispatch_.AddClient(std::bind(&SocketOutput::Write, new_output, std::placeholders::_1)); + new_output->SetCloseNotifier([this,self,handle] { + dispatch_.RemoveClient(handle); + }); + new_output->Start(); + } + } else { + if (ec == boost::system::errc::operation_canceled) + return; + std::cerr << endpoint_ << ": accept error: " << ec.message() << std::endl; + } + + Accept(); + }); + } +}; diff --git a/socket_output.h b/socket_output.h new file mode 100644 index 0000000..290224d --- /dev/null +++ b/socket_output.h @@ -0,0 +1,131 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef SOCKET_OUTPUT_H +#define SOCKET_OUTPUT_H + +#include +#include + +#include +#include +#include + +#include "uat_message.h" +#include "message_dispatch.h" + +namespace dump978 { + class SocketOutput : public std::enable_shared_from_this { + public: + typedef std::shared_ptr Pointer; + + virtual void Start(); + void Write(uat::SharedMessageVector messages); + virtual void Close(); + + void SetCloseNotifier(std::function notifier) { + close_notifier_ = notifier; + } + + bool IsOpen() const { + return socket_.is_open(); + } + + protected: + SocketOutput(boost::asio::io_service &service_, + boost::asio::ip::tcp::socket &&socket_); + std::ostringstream& Buf() { return outbuf_; } + + virtual void InternalWrite(uat::SharedMessageVector messages) = 0; + + private: + void HandleError(const boost::system::error_code &ec); + void Flush(); + void ReadAndDiscard(); + + boost::asio::io_service &service_; + boost::asio::io_service::strand strand_; + boost::asio::ip::tcp::socket socket_; + boost::asio::ip::tcp::endpoint peer_; + + std::ostringstream outbuf_; + bool flush_pending_; + + std::function close_notifier_; + }; + + class RawOutput : public SocketOutput { + public: + // factory method, this class must always be constructed via make_shared + static Pointer Create(boost::asio::io_service &service, + boost::asio::ip::tcp::socket &&socket) + { + return Pointer(new RawOutput(service, std::move(socket))); + } + + protected: + void InternalWrite(uat::SharedMessageVector messages) override; + + private: + RawOutput(boost::asio::io_service &service_, boost::asio::ip::tcp::socket &&socket_) + : SocketOutput(service_, std::move(socket_)) + {} + }; + + class JsonOutput : public SocketOutput { + public: + // factory method, this class must always be constructed via make_shared + static Pointer Create(boost::asio::io_service &service, + boost::asio::ip::tcp::socket &&socket) + { + return Pointer(new JsonOutput(service, std::move(socket))); + } + + protected: + void InternalWrite(uat::SharedMessageVector messages) override; + + private: + JsonOutput(boost::asio::io_service &service_, boost::asio::ip::tcp::socket &&socket_) + : SocketOutput(service_, std::move(socket_)) + {} + }; + + class SocketListener : public std::enable_shared_from_this { + public: + typedef std::shared_ptr Pointer; + typedef std::function ConnectionFactory; + + // factory method, this class must always be constructed via make_shared + static Pointer Create(boost::asio::io_service &service, + const boost::asio::ip::tcp::endpoint &endpoint, + uat::MessageDispatch &dispatch, + ConnectionFactory factory) + { + return Pointer(new SocketListener(service, endpoint, dispatch, factory)); + } + + void Start(); + void Close(); + + private: + SocketListener(boost::asio::io_service &service, + const boost::asio::ip::tcp::endpoint &endpoint, + uat::MessageDispatch &dispatch, + ConnectionFactory factory); + + void Accept(); + + boost::asio::io_service &service_; + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ip::tcp::endpoint endpoint_; + boost::asio::ip::tcp::socket socket_; + boost::asio::ip::tcp::endpoint peer_; + uat::MessageDispatch &dispatch_; + ConnectionFactory factory_; + }; +}; + +#endif diff --git a/track.cc b/track.cc new file mode 100644 index 0000000..b56dc79 --- /dev/null +++ b/track.cc @@ -0,0 +1,122 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "track.h" + +using namespace uat; + +#include +#include + +void AircraftState::UpdateFromMessage(std::uint64_t at, const uat::AdsbMessage &message) +{ +#define UPDATE(x) do { if (message.x) { x.MaybeUpdate(at, *message.x); } } while(0) + + UPDATE(position); // latitude, longitude + UPDATE(pressure_altitude); + UPDATE(geometric_altitude); + UPDATE(nic); + UPDATE(airground_state); + UPDATE(north_velocity); + UPDATE(east_velocity); + UPDATE(vv_src); + UPDATE(vertical_velocity); + UPDATE(ground_speed); + UPDATE(magnetic_heading); + UPDATE(true_heading); + UPDATE(true_track); + UPDATE(aircraft_size); // length, width + UPDATE(gps_lateral_offset); + UPDATE(gps_longitudinal_offset); + UPDATE(gps_position_offset_applied); + UPDATE(utc_coupled); + + UPDATE(emitter_category); + UPDATE(callsign); + UPDATE(flightplan_id); // aka Mode 3/A squawk + UPDATE(emergency); + UPDATE(mops_version); + UPDATE(sil); + UPDATE(transmit_mso); + UPDATE(sda); + UPDATE(nac_p); + UPDATE(nac_v); + UPDATE(nic_baro); + UPDATE(capability_codes); + UPDATE(operational_modes); + UPDATE(sil_supplement); + UPDATE(gva); + UPDATE(single_antenna); + UPDATE(nic_supplement); + + UPDATE(selected_altitude_type); + UPDATE(selected_altitude); + UPDATE(barometric_pressure_setting); + UPDATE(selected_heading); + UPDATE(mode_indicators); + + last_message_time = std::max(last_message_time, at); + +#undef UPDATE +} + +void Tracker::Start() +{ + PurgeOld(); // starts timer +} + +void Tracker::Stop() +{ + timer_.cancel(); +} + +void Tracker::PurgeOld() +{ + static auto unix_epoch = std::chrono::system_clock::from_time_t(0); + auto expires = std::chrono::system_clock::now() - timeout_; + std::uint64_t expires_timestamp = std::chrono::duration_cast(expires - unix_epoch).count(); + + std::cerr << "starting to expiry across " << aircraft_.size() << " aircraft, expiry time " << expires_timestamp << std::endl; + for (auto i = aircraft_.begin(); i != aircraft_.end(); ) { + if (i->second.last_message_time < expires_timestamp) { + std::cerr << "expire " << std::hex << std::setfill('0') << std::setw(6) << i->second.address << std::dec << std::setfill(' ') << " with last time " << i->second.last_message_time << std::endl; + i = aircraft_.erase(i); + } else { + ++i; + } + } + std::cerr << "done expiring, now have " << aircraft_.size() << " aircraft" << std::endl; + + auto self(shared_from_this()); + timer_.expires_from_now(timeout_ / 4); + timer_.async_wait(strand_.wrap([this,self](const boost::system::error_code &ec) { + if (!ec) { + PurgeOld(); + } + })); +} + +void Tracker::HandleMessages(SharedMessageVector messages) +{ + auto self(shared_from_this()); + strand_.dispatch([this,self,messages]() { + for (const auto &message : *messages) { + if (message.Type() == MessageType::DOWNLINK_SHORT || message.Type() == MessageType::DOWNLINK_LONG) { + HandleMessage(message.ReceivedAt(), AdsbMessage(message)); + } + } + }); +} + +void Tracker::HandleMessage(std::uint64_t at, const uat::AdsbMessage &message) +{ + AddressKey key { message.address_qualifier, message.address }; + auto i = aircraft_.find(key); + if (i == aircraft_.end()) { + std::cerr << "new aircraft: " << (int)message.address_qualifier << "/" << std::hex << std::setfill('0') << std::setw(6) << message.address << std::dec << std::setfill(' ') << std::endl; + aircraft_[key] = { message.address_qualifier, message.address }; + } + + aircraft_[key].UpdateFromMessage(at, message); +} diff --git a/track.h b/track.h new file mode 100644 index 0000000..861ede1 --- /dev/null +++ b/track.h @@ -0,0 +1,161 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef FAUP978_TRACK_H +#define FAUP978_TRACK_H + +#include +#include + +#include +#include +#include + +#include "uat_message.h" + +namespace uat { + template + class AgedField { + public: + AgedField() : v_() {} + AgedField(const T& v) : v_(v) {} + + operator bool() const { + return (updated_ != 0); + } + + std::uint64_t Changed() const { + return changed_; + } + + std::uint64_t Updated() const { + return updated_; + } + + std::uint64_t ChangeAge(std::uint64_t at) { + if (at < changed_) { + return 0; + } else { + return at - changed_; + } + } + + std::uint64_t UpdateAge(std::uint64_t at) { + if (at < updated_) { + return 0; + } else { + return at - updated_; + } + } + + bool MaybeUpdate(std::uint64_t at, const T& v) { + if (at > updated_) { + updated_ = at; + if (v != v_) { + changed_ = at; + } + v_ = v; + return true; + } else { + return false; + } + } + + T Value() { + return v_; + } + + private: + T v_; + std::uint64_t updated_ = 0; + std::uint64_t changed_ = 0; + }; + + struct AircraftState { + AddressQualifier address_qualifier; + AdsbAddress address; + std::uint64_t last_message_time; + + AgedField> position; // latitude, longitude + AgedField pressure_altitude; + AgedField geometric_altitude; + AgedField nic; + AgedField airground_state; + AgedField north_velocity; + AgedField east_velocity; + AgedField vv_src; + AgedField vertical_velocity; + AgedField ground_speed; + AgedField magnetic_heading; + AgedField true_heading; + AgedField true_track; + AgedField> aircraft_size; // length, width + AgedField gps_lateral_offset; + AgedField gps_longitudinal_offset; + AgedField gps_position_offset_applied; + AgedField utc_coupled; + + AgedField emitter_category; + AgedField callsign; + AgedField flightplan_id; // aka Mode 3/A squawk + AgedField emergency; + AgedField mops_version; + AgedField sil; + AgedField transmit_mso; + AgedField sda; + AgedField nac_p; + AgedField nac_v; + AgedField nic_baro; + AgedField capability_codes; + AgedField operational_modes; + AgedField sil_supplement; + AgedField gva; + AgedField single_antenna; + AgedField nic_supplement; + + AgedField selected_altitude_type; + AgedField selected_altitude; + AgedField barometric_pressure_setting; + AgedField selected_heading; + AgedField mode_indicators; + + void UpdateFromMessage(std::uint64_t at, const uat::AdsbMessage &message); + }; + + typedef std::pair AddressKey; + + class Tracker : public std::enable_shared_from_this { + public: + typedef std::shared_ptr Pointer; + typedef std::map MapType; + + static Pointer Create(boost::asio::io_service &service, std::chrono::seconds timeout = std::chrono::seconds(300)) { + return Pointer(new Tracker(service, timeout)); + } + + void Start(); + void Stop(); + void HandleMessages(SharedMessageVector messages); + + const MapType &Aircraft() const { return aircraft_; } + + private: + Tracker(boost::asio::io_service &service, std::chrono::seconds timeout) + : service_(service), strand_(service), timer_(service), timeout_(timeout) + {} + + void PurgeOld(); + void HandleMessage(std::uint64_t at, const uat::AdsbMessage &message); + + boost::asio::io_service &service_; + boost::asio::io_service::strand strand_; + boost::asio::steady_timer timer_; + std::chrono::seconds timeout_; + MapType aircraft_; + }; +}; + +#endif diff --git a/uat_message.cc b/uat_message.cc new file mode 100644 index 0000000..09b2036 --- /dev/null +++ b/uat_message.cc @@ -0,0 +1,511 @@ +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#include "uat_message.h" + +#include +#include +#include +#include + +#include + +namespace uat { + // + // streaming raw messages + // + + std::ostream &operator<<(std::ostream &os, const RawMessage &message) { + boost::io::ios_flags_saver ifs(os); + + switch (message.Type()) { + case MessageType::DOWNLINK_SHORT: + case MessageType::DOWNLINK_LONG: + os << '-'; + break; + case MessageType::UPLINK: + os << '+'; + break; + default: + throw std::logic_error("unexpected message type"); + } + + os << std::setfill('0'); + for (auto b : message.Payload()) { + os << std::hex << std::setw(2) << (int)b; + } + + os << ";"; + if (message.Errors() > 0) { + os << "rs=" << std::dec << std::setw(0) << message.Errors() << ';'; + } + if (message.Rssi() != 0) { + os << "rssi=" << std::dec << std::setprecision(1) << message.Rssi() << ';'; + } + if (message.ReceivedAt() != 0) { + os << "t=" << std::dec << std::setw(0) << (message.ReceivedAt() / 1000) << '.' + << std::setfill('0') << std::setw(3) << (message.ReceivedAt() % 1000) << ';'; + } + return os; + } + + // + // decoding messages + // + + AdsbMessage::AdsbMessage(const RawMessage &raw) { + if (raw.Type() != MessageType::DOWNLINK_SHORT && raw.Type() != MessageType::DOWNLINK_LONG) { + throw std::logic_error("can't parse this sort of message as a downlink ADS-B message"); + } + + // HDR + payload_type = raw.Bits(1,1, 1,5); + address_qualifier = static_cast(raw.Bits(1,6, 1,8)); + address = raw.Bits(2,1, 4,8); + + // Optional parts of the message + // DO-282B Table 2-10 "Composition of the ADS-B Payload" + switch (payload_type) { + case 0: + DecodeSV(raw); + break; + case 1: + DecodeSV(raw); + DecodeMS(raw); + DecodeAUXSV(raw); + break; + case 2: + DecodeSV(raw); + DecodeAUXSV(raw); + break; + case 3: + DecodeSV(raw); + DecodeMS(raw); + DecodeTS(raw, 30); + break; + case 4: + DecodeSV(raw); + DecodeTS(raw, 30); + break; + case 5: + DecodeSV(raw); + DecodeAUXSV(raw); + break; + case 6: + DecodeSV(raw); + DecodeTS(raw, 25); + DecodeAUXSV(raw); + break; + case 7: + case 8: + case 9: + case 10: + DecodeSV(raw); + break; + + default: + // 11..31, HDR only + break; + } + } + + void AdsbMessage::DecodeSV(const RawMessage &raw) { + auto raw_lat = raw.Bits(5,1, 7,7); + auto raw_lon = raw.Bits(7,8, 10,7); + + auto raw_alt = raw.Bits(11,1, 12,4); + if (raw_alt != 0) { + auto altitude = (raw_alt - 41) * 25; + if (raw.Bit(10,8)) { // 2.2.4.5.2.2 "ALTITUDE TYPE" field + geometric_altitude = altitude; + } else { + pressure_altitude = altitude; + } + } + + nic = raw.Bits(12,5, 12,8); + + if (raw_lat != 0 || raw_lon != 0 || *nic != 0) { + // NB: north and south pole encode identically. We return north pole in this case + auto lat = raw_lat * 360.0 / 16777216.0; + if (lat > 90) + lat -= 180; + + auto lon = raw_lon * 360.0 / 16777216.0; + if (lon > 180) + lon -= 360; + + position = std::make_pair<>(RoundN(lat, 5), RoundN(lon, 5)); + } + + airground_state = static_cast(raw.Bits(13,1, 13,2)); + + // bit 13,3 reserved + + switch (*airground_state) { + case AirGroundState::AIRBORNE_SUBSONIC: + case AirGroundState::AIRBORNE_SUPERSONIC: { + int supersonic = (*airground_state == AirGroundState::AIRBORNE_SUPERSONIC ? 4 : 1); + int ns_sign = raw.Bit(13,4) ? -1 : 1; + auto raw_ns = raw.Bits(13,5, 14,6); + if (raw_ns != 0) + north_velocity = supersonic * ns_sign * (raw_ns - 1); + + int ew_sign = raw.Bit(14,7) ? -1 : 1; + auto raw_ew = raw.Bits(14,8, 16,1); + if (raw_ew != 0) + east_velocity = supersonic * ew_sign * (raw_ew - 1); + + // derive groundspeed, true track from north/east velocity for convenience + if (north_velocity && east_velocity) { // nb: testing for presence, not non-zero value + ground_speed = RoundN(std::sqrt(1.0 * (*north_velocity) * (*north_velocity) + 1.0 * (*east_velocity) * (*east_velocity)), 1); + true_track = RoundN(std::atan2(*east_velocity, *north_velocity) * 180.0 / M_PI, 1); + } + + vv_src = static_cast(raw.Bits(16,2, 16,2)); + int vv_sign = raw.Bit(16,3) ? -1 : 1; + auto raw_vv = raw.Bits(16,4, 17,4); + if (raw_vv != 0) + vertical_velocity = vv_sign * (raw_vv - 1) * 64; + + break; + } + + case AirGroundState::ON_GROUND: { + // 13,4 reserved + auto raw_gs = raw.Bits(13,5, 14,6); + if (raw_gs != 0) + ground_speed = (raw_gs - 1); + + auto tah_type = raw.Bits(14,7, 14,8); + auto angle = RoundN(raw.Bits(15,1, 16,1) * 360.0 / 512.0, 1); + switch (tah_type) { // 2.2.4.5.2.6.4 / Table 2-28 "Track Angle/Heading Type" + case 0: // data unavailable + break; + case 1: // true track + true_track = angle; + break; + case 2: // magnetic heading + magnetic_heading = angle; + break; + case 3: // true heading + true_heading = angle; + break; + } + + auto raw_av_size = raw.Bits(16,2, 16,5); + if (raw_av_size != 0) { + // DO-282B Table 2-35 + static std::array,16> aircraft_sizes = { { + { 0, 0 }, // no data + { 15, 23 }, + { 25, 28.5 }, + { 25, 34 }, + { 35, 33 }, + { 35, 38 }, + { 45, 39.5 }, + { 45, 45 }, + { 55, 45 }, + { 55, 52 }, + { 65, 59.5 }, + { 65, 67 }, + { 75, 72.5 }, + { 75, 80 }, + { 85, 80 }, + { 85, 90 } + } }; + + aircraft_size = aircraft_sizes[raw_av_size]; + } + + if (raw.Bit(16,7)) { + // Longitudinal GPS offset + auto raw_gps_long = raw.Bits(16,8, 17,4); + if (raw_gps_long != 0) { + if (raw_gps_long == 1) { + gps_position_offset_applied = true; + } else { + gps_position_offset_applied = false; + gps_longitudinal_offset = (raw_gps_long - 1) * 2; + } + } + } else { + // Lateral GPS offset + // We adopt the convention that left is negative + auto raw_gps_lat = raw.Bits(16,8, 17,2); + if (raw_gps_lat != 0) { + if (raw_gps_lat <= 3) { + gps_lateral_offset = raw_gps_lat * -2; + } else { + gps_lateral_offset = (raw_gps_lat - 4) * 2; + } + } + } + + break; + } + + default: + // nothing; + break; + } + + switch (address_qualifier) { + case AddressQualifier::ADSB_ICAO: + case AddressQualifier::ADSB_OTHER: + case AddressQualifier::VEHICLE: + case AddressQualifier::FIXED_BEACON: + utc_coupled = raw.Bit(17,5); + uplink_feedback = raw.Bits(17,6, 17,8); + break; + + case AddressQualifier::TISB_ICAO: + case AddressQualifier::TISB_OTHER: + case AddressQualifier::ADSR_OTHER: + tisb_site_id = raw.Bits(17,5, 17,8); + break; + + default: + // nothing + break; + } + } + + void AdsbMessage::DecodeTS(const RawMessage &raw, unsigned startbyte) { + // TS starts at byte 30 (§2.2.4.5.6) in payload type 3 or 4; + // or at byte 25 (§2.2.4.5.7) in payload type 6; + // the starting offset to use is passed by the caller + + selected_altitude_type = static_cast(raw.Bits(startbyte+0,1, startbyte+0,1)); + auto raw_altitude = raw.Bits(startbyte+0,2, startbyte+1,4); + if (raw_altitude != 0) { + selected_altitude = (raw_altitude - 1) * 32; + } + + auto raw_bps = raw.Bits(startbyte+1,5, startbyte+2,5); + if (raw_bps != 0) + barometric_pressure_setting = 800 + (raw_bps - 1) * 0.8; + + if (raw.Bit(startbyte+2,6)) { + int heading_sign = raw.Bit(startbyte+2,7) ? -1 : 1; + auto heading = RoundN(raw.Bits(startbyte+2,8, startbyte+3,7) * 180.0 / 256.0, 1); + selected_heading = heading_sign * heading; + } + + if (raw.Bit(startbyte+3,8)) { + mode_indicators.emplace(); + mode_indicators->autopilot = raw.Bit(startbyte+4,1); + mode_indicators->vnav = raw.Bit(startbyte+4,2); + mode_indicators->altitude_hold = raw.Bit(startbyte+4,3); + mode_indicators->approach = raw.Bit(startbyte+4,4); + mode_indicators->lnav = raw.Bit(startbyte+4,5); + } + + // 34,6 .. 34,8 reserved + } + + void AdsbMessage::DecodeMS(const RawMessage &raw) { + static const char *base40_alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ *??"; + auto raw1 = raw.Bits(18,1, 19,8); + auto raw2 = raw.Bits(20,1, 21,8); + auto raw3 = raw.Bits(22,1, 23,8); + + emitter_category = (raw1 / 1600) % 40; + + std::string raw_callsign; + raw_callsign.reserve(8); + raw_callsign.push_back(base40_alphabet[(raw1 / 40) % 40]); + raw_callsign.push_back(base40_alphabet[raw1 % 40]); + raw_callsign.push_back(base40_alphabet[(raw2 / 1600) % 40]); + raw_callsign.push_back(base40_alphabet[(raw2 / 40) % 40]); + raw_callsign.push_back(base40_alphabet[raw2 % 40]); + raw_callsign.push_back(base40_alphabet[(raw3 / 1600) % 40]); + raw_callsign.push_back(base40_alphabet[(raw3 / 40) % 40]); + raw_callsign.push_back(base40_alphabet[raw3 % 40]); + + // trim trailing spaces and code 37 + while (!raw_callsign.empty() && (raw_callsign.back() == ' ' || raw_callsign.back() == '*')) { + raw_callsign.pop_back(); + } + + if (!raw_callsign.empty()) { + if (raw.Bit(27,7)) { // CSID field, 1 = callsign, 0 = flightplan ID (aka squawk) + callsign.emplace(std::move(raw_callsign)); + } else { + flightplan_id.emplace(std::move(raw_callsign)); + } + } + + emergency = static_cast(raw.Bits(24,1, 24,3)); + mops_version = raw.Bits(24,4, 24,6); + sil = raw.Bits(24,7, 24,8); + transmit_mso = raw.Bits(25,1, 25,6); + sda = raw.Bits(25,7, 25,8); + nac_p = raw.Bits(26,1, 26,4); + nac_v = raw.Bits(26,5, 26,7); + nic_baro = raw.Bits(26,8, 26,8); + + capability_codes.emplace(); + capability_codes->uat_in = raw.Bit(27,1); + capability_codes->es_in = raw.Bit(27,2); + capability_codes->tcas_operational = raw.Bit(27,3); + + operational_modes.emplace(); + operational_modes->tcas_ra_active = raw.Bit(27,4); + operational_modes->ident_active = raw.Bit(27,5); + operational_modes->atc_services = raw.Bit(27,6); + + sil_supplement = static_cast(raw.Bits(27,8, 27,8)); + gva = raw.Bits(28,1, 28,2); + single_antenna = raw.Bit(28,3); + nic_supplement = raw.Bit(28,4); + // 28,5 .. 29,8 reserved + + } + + void AdsbMessage::DecodeAUXSV(const RawMessage &raw) { + auto raw_alt = raw.Bits(30,1, 31,4); + if (raw_alt != 0) { + auto altitude = (raw_alt - 41) * 25; + if (raw.Bit(10,8)) { // 2.2.4.5.2.2 "ALTITUDE TYPE" field (in SV, which is always present when AUXSV is present) + pressure_altitude = altitude; + } else { + geometric_altitude = altitude; + } + } + } + + // + // converting decoded messages to json + // + + NLOHMANN_JSON_SERIALIZE_ENUM( AddressQualifier, { + { AddressQualifier::ADSB_ICAO, "adsb_icao" }, + { AddressQualifier::ADSB_OTHER, "adsb_other" }, + { AddressQualifier::TISB_ICAO, "tisb_icao" }, + { AddressQualifier::TISB_OTHER, "tisb_other" }, + { AddressQualifier::VEHICLE, "vehicle" }, + { AddressQualifier::ADSB_ICAO, "fixed_beacon" }, + { AddressQualifier::ADSB_ICAO, "adsr_other" }, + { AddressQualifier::RESERVED_7, "reserved_7" } + } ); + + NLOHMANN_JSON_SERIALIZE_ENUM( AirGroundState, { + { AirGroundState::AIRBORNE_SUBSONIC, "airborne" }, + { AirGroundState::AIRBORNE_SUPERSONIC, "supersonic" }, + { AirGroundState::ON_GROUND, "ground" }, + { AirGroundState::RESERVED, "reserved" } + } ); + + NLOHMANN_JSON_SERIALIZE_ENUM( VerticalVelocitySource, { + { VerticalVelocitySource::GEOMETRIC, "geometric" }, + { VerticalVelocitySource::BAROMETRIC, "barometric" } + } ); + + NLOHMANN_JSON_SERIALIZE_ENUM( EmergencyPriorityStatus, { + { EmergencyPriorityStatus::NONE, "none" }, + { EmergencyPriorityStatus::GENERAL, "general" }, + { EmergencyPriorityStatus::MEDICAL, "medical" }, + { EmergencyPriorityStatus::NORDO, "nordo" }, + { EmergencyPriorityStatus::UNLAWFUL, "unlawful" }, + { EmergencyPriorityStatus::DOWNED, "downed" }, + { EmergencyPriorityStatus::RESERVED_7, "reserved_7" } + } ); + + NLOHMANN_JSON_SERIALIZE_ENUM( SILSupplement, { + { SILSupplement::PER_HOUR, "per_hour" }, + { SILSupplement::PER_SAMPLE, "per_sample" } + } ); + + NLOHMANN_JSON_SERIALIZE_ENUM( SelectedAltitudeType, { + { SelectedAltitudeType::MCP_FCU, "mcp_fcu" }, + { SelectedAltitudeType::FMS, "fms" } + } ); + + nlohmann::json AdsbMessage::ToJson() const { + nlohmann::json o; + + o["address_qualifier"] = address_qualifier; + + std::ostringstream os; + os << std::hex << std::setfill('0') << std::setw(6) << address; + o["address"] = os.str(); + +#define EMIT(x) do { if (x) { o[#x] = *x; } } while(0) + + if (position) + o["position"] = { {"lat", position->first}, {"lon", position->second} }; + + EMIT(pressure_altitude); + EMIT(geometric_altitude); + EMIT(nic); + EMIT(airground_state); + EMIT(north_velocity); + EMIT(east_velocity); + EMIT(vv_src); + EMIT(vertical_velocity); + EMIT(ground_speed); + EMIT(magnetic_heading); + EMIT(true_heading); + EMIT(true_track); + + if (aircraft_size) + o["aircraft_size"] = { {"length", aircraft_size->first}, {"width", aircraft_size->second} }; + + EMIT(gps_lateral_offset); + EMIT(gps_longitudinal_offset); + EMIT(gps_position_offset_applied); + EMIT(utc_coupled); + EMIT(uplink_feedback); + EMIT(tisb_site_id); + + EMIT(emitter_category); + EMIT(callsign); + EMIT(flightplan_id); + EMIT(emergency); + EMIT(mops_version); + EMIT(sil); + EMIT(transmit_mso); + EMIT(sda); + EMIT(nac_p); + EMIT(nac_v); + EMIT(nic_baro); + + if (capability_codes) { + auto &cc = o["capability_codes"] = nlohmann::json::object(); + cc["uat_in"] = capability_codes->uat_in; + cc["es_in"] = capability_codes->es_in; + cc["tcas_operational"] = capability_codes->tcas_operational; + } + + if (operational_modes) { + auto &om = o["operational_modes"] = nlohmann::json::object(); + om["tcas_ra_active"] = operational_modes->tcas_ra_active; + om["ident_active"] = operational_modes->ident_active; + om["atc_services"] = operational_modes->atc_services; + } + + EMIT(sil_supplement); + EMIT(gva); + EMIT(single_antenna); + EMIT(nic_supplement); + EMIT(selected_altitude_type); + EMIT(selected_altitude); + EMIT(barometric_pressure_setting); + EMIT(selected_heading); + + if (mode_indicators) { + auto &mi = o["mode_indicators"] = nlohmann::json::object(); + mi["autopilot"] = mode_indicators->autopilot; + mi["vnav"] = mode_indicators->vnav; + mi["altitude_hold"] = mode_indicators->altitude_hold; + mi["approach"] = mode_indicators->approach; + mi["lnav"] = mode_indicators->lnav; + } + +#undef EMIT + + return o; + } +}; diff --git a/uat_message.h b/uat_message.h new file mode 100644 index 0000000..465e715 --- /dev/null +++ b/uat_message.h @@ -0,0 +1,377 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef UAT_MESSAGE_H +#define UAT_MESSAGE_H + +#include +#include + +#include + +#include + +#include "common.h" +#include "uat_protocol.h" + +namespace uat { + class RawMessage { + public: + RawMessage() + : type_(MessageType::INVALID), received_at_(0), errors_(0), rssi_(0) + {} + + RawMessage(const Bytes &payload, + std::uint64_t received_at, + unsigned errors, + float rssi) + : payload_(payload), + received_at_(received_at), + errors_(errors), + rssi_(rssi) + { + switch (payload_.size()) { + case DOWNLINK_SHORT_DATA_BYTES: + type_ = MessageType::DOWNLINK_SHORT; + break; + case DOWNLINK_LONG_DATA_BYTES: + type_ = MessageType::DOWNLINK_LONG; + break; + case UPLINK_DATA_BYTES: + type_ = MessageType::UPLINK; + break; + default: + type_ = MessageType::INVALID; + break; + } + } + + RawMessage(Bytes &&payload, + std::uint64_t received_at, + unsigned errors, + float rssi) + : payload_(std::move(payload)), + received_at_(received_at), + errors_(errors), + rssi_(rssi) + { + switch (payload_.size()) { + case DOWNLINK_SHORT_DATA_BYTES: + type_ = MessageType::DOWNLINK_SHORT; + break; + case DOWNLINK_LONG_DATA_BYTES: + type_ = MessageType::DOWNLINK_LONG; + break; + case UPLINK_DATA_BYTES: + type_ = MessageType::UPLINK; + break; + default: + type_ = MessageType::INVALID; + break; + } + } + + MessageType Type() const { + return type_; + } + + Bytes &Payload() { + return payload_; + } + + const Bytes &Payload() const { + return payload_; + } + + std::uint64_t ReceivedAt() const { + return received_at_; + } + + unsigned Errors() const { + return errors_; + } + + float Rssi() const { + return rssi_; + } + + // Number of raw bits in the message, excluding the sync bits + unsigned BitLength() const { + switch (type_) { + case MessageType::DOWNLINK_SHORT: return DOWNLINK_SHORT_BITS; + case MessageType::DOWNLINK_LONG: return DOWNLINK_LONG_BITS; + case MessageType::UPLINK: return UPLINK_BITS; + default: return 0; + } + } + + operator bool() const { + return (type_ != MessageType::INVALID); + } + + + __attribute__((always_inline)) bool Bit(unsigned byte, unsigned bit) const { + assert(byte >= 1); + assert(bit >= 1); + assert(bit <= 8); + + const unsigned bi = (byte-1) * 8 + bit - 1; + const unsigned by = bi >> 3; + const unsigned mask = 1 << (7 - (bi & 7)); + + return (payload_.at(by) & mask) != 0; + } + + __attribute__((always_inline)) std::uint32_t Bits(unsigned first_byte, unsigned first_bit, unsigned last_byte, unsigned last_bit) const { + assert(first_byte >= 1); + assert(first_bit >= 1); + assert(first_bit <= 8); + assert(last_byte >= 1); + assert(last_bit >= 1); + assert(last_bit <= 8); + + const unsigned fbi = (first_byte-1) * 8 + first_bit - 1; + const unsigned lbi = (last_byte-1) * 8 + last_bit - 1; + assert(fbi <= lbi); + + const unsigned nbi = (lbi - fbi + 1); + assert(nbi > 0); + assert(nbi <= 32); + + const unsigned fby = fbi >> 3; + const unsigned lby = lbi >> 3; + const unsigned nby = (lby - fby) + 1; + + const unsigned shift = 7 - (lbi & 7); + const unsigned topmask = 0xFF >> (fbi & 7); + + assert(nby > 0); + assert(nby <= 5); + + if (payload_.size() < fby + nby) + throw std::out_of_range("bit range exceeds available data"); + + if (nby == 5) { + return + ((payload_[fby] & topmask) << (32 - shift)) | + (payload_[fby + 1] << (24 - shift)) | + (payload_[fby + 2] << (16 - shift)) | + (payload_[fby + 3] << (8 - shift)) | + (payload_[fby + 4] >> shift); + } else if (nby == 4) { + return + ((payload_[fby] & topmask) << (24 - shift)) | + (payload_[fby + 1] << (16 - shift)) | + (payload_[fby + 2] << (8 - shift)) | + (payload_[fby + 3] >> shift); + } else if (nby == 3) { + return + ((payload_[fby] & topmask) << (16 - shift)) | + (payload_[fby + 1] << (8 - shift)) | + (payload_[fby + 2] >> shift); + } else if (nby == 2) { + return + ((payload_[fby] & topmask) << (8 - shift)) | + (payload_[fby + 1] >> shift); + } else if (nby == 1) { + return + (payload_[fby] & topmask) >> shift; + } else { + return 0; + } + } + + private: + MessageType type_; + Bytes payload_; + std::uint64_t received_at_; + unsigned errors_; + float rssi_; + }; + + std::ostream &operator<<(std::ostream& os, const RawMessage &message); + + typedef std::vector MessageVector; + typedef std::shared_ptr SharedMessageVector; + + // 2.2.4.5.1.2 "ADDRESS QUALIFIER" field + enum class AddressQualifier : unsigned char { + ADSB_ICAO = 0, + ADSB_OTHER = 1, + TISB_ICAO = 2, + TISB_OTHER = 3, + VEHICLE = 4, + FIXED_BEACON = 5, + ADSR_OTHER = 6, + RESERVED_7 = 7 + }; + + // 2.2.4.5.2.5 "A/G STATE" field + enum class AirGroundState : unsigned char { + AIRBORNE_SUBSONIC = 0, + AIRBORNE_SUPERSONIC = 1, + ON_GROUND = 2, + RESERVED = 3 + }; + + // 2.2.4.5.2.7.1.1 "VV Src" subfield + enum class VerticalVelocitySource : unsigned char { + GEOMETRIC = 0, + BAROMETRIC = 1 + }; + + // 2.2.4.5.4.4 "EMERGENCY/PRIORITY STATUS" field + enum class EmergencyPriorityStatus : unsigned char { + NONE = 0, + GENERAL = 1, + MEDICAL = 2, + MINFUEL = 3, + NORDO = 4, + UNLAWFUL = 5, + DOWNED = 6, + RESERVED_7 = 7 + }; + + // 2.2.4.5.4.16 SIL Supplement Flag + enum class SILSupplement : unsigned char { + PER_HOUR = 0, + PER_SAMPLE = 1 + }; + + // 2.2.4.5.4.12 "CAPABILITY CODES" field + struct CapabilityCodes { + bool uat_in : 1; + bool es_in : 1; + bool tcas_operational : 1; + + bool operator==(const CapabilityCodes& o) const { + return (uat_in == o.uat_in && + es_in == o.es_in && + tcas_operational == o.tcas_operational); + } + + bool operator!=(const CapabilityCodes& o) const { + return !(*this == o); + } + }; + + // 2.2.4.5.4.13 "OPERATIONAL MODES" field + struct OperationalModes { + bool tcas_ra_active : 1; + bool ident_active : 1; + bool atc_services : 1; + + bool operator==(const OperationalModes& o) const { + return (tcas_ra_active == o.tcas_ra_active && + ident_active == o.ident_active && + atc_services == o.atc_services); + } + + bool operator!=(const OperationalModes& o) const { + return !(*this == o); + } + }; + + // 2.2.4.5.6.1 "Selected Altitude Type (SAT)" field + enum class SelectedAltitudeType : unsigned char { + MCP_FCU = 0, + FMS = 1 + }; + + // 2.2.4.5.6.5 - 2.2.4.5.6.10 Mode Bits / Mode Indicators + struct ModeIndicators { + bool autopilot : 1; + bool vnav : 1; + bool altitude_hold : 1; + bool approach : 1; + bool lnav : 1; + + bool operator==(const ModeIndicators& o) const { + return (autopilot == o.autopilot && + vnav == o.vnav && + altitude_hold == o.altitude_hold && + approach == o.approach && + lnav == o.lnav); + } + + bool operator!=(const ModeIndicators& o) const { + return !(*this == o); + } + }; + + typedef std::uint32_t AdsbAddress; + + struct AdsbMessage { + AdsbMessage(const RawMessage &raw); + + // 2.2.4.5 HEADER Element + unsigned payload_type; + AddressQualifier address_qualifier; + AdsbAddress address; + + // 2.2.4.5.2 STATE VECTOR Element (ADS-B) + // 2.2.4.5.3 STATE VECTOR Element (TIS-B/ADS-B) + boost::optional> position; // latitude, longitude + boost::optional pressure_altitude; + boost::optional geometric_altitude; + boost::optional nic; + boost::optional airground_state; + boost::optional north_velocity; + boost::optional east_velocity; + boost::optional vv_src; + boost::optional vertical_velocity; + boost::optional ground_speed; + boost::optional magnetic_heading; + boost::optional true_heading; + boost::optional true_track; + boost::optional> aircraft_size; // length, width + boost::optional gps_lateral_offset; + boost::optional gps_longitudinal_offset; + boost::optional gps_position_offset_applied; + boost::optional utc_coupled; // ADS-B + boost::optional uplink_feedback; // ADS-B + boost::optional tisb_site_id; // TIS-B/ADS-R + + // 2.2.4.5.4 MODE STATUS element + boost::optional emitter_category; + boost::optional callsign; + boost::optional flightplan_id; // aka Mode 3/A squawk + boost::optional emergency; + boost::optional mops_version; + boost::optional sil; + boost::optional transmit_mso; + boost::optional sda; + boost::optional nac_p; + boost::optional nac_v; + boost::optional nic_baro; + boost::optional capability_codes; + boost::optional operational_modes; + boost::optional sil_supplement; + boost::optional gva; + boost::optional single_antenna; + boost::optional nic_supplement; + + // 2.2.4.5.5 AUXILIARY STATE VECTOR element + // (included above) + + // 2.2.4.5.6 TARGET STATE element + boost::optional selected_altitude_type; + boost::optional selected_altitude; + boost::optional barometric_pressure_setting; + boost::optional selected_heading; + boost::optional mode_indicators; + + nlohmann::json ToJson() const; + + private: + void DecodeSV(const RawMessage &raw); + void DecodeTS(const RawMessage &raw, unsigned startbyte); + void DecodeMS(const RawMessage &raw); + void DecodeAUXSV(const RawMessage &raw); + }; +} + +#endif diff --git a/uat_protocol.h b/uat_protocol.h new file mode 100644 index 0000000..80c4b19 --- /dev/null +++ b/uat_protocol.h @@ -0,0 +1,47 @@ +// -*- c++ -*- + +// Copyright (c) 2019, FlightAware LLC. +// All rights reserved. +// Licensed under the 2-clause BSD license; see the LICENSE file + +#ifndef UAT_PROTOCOL_H +#define UAT_PROTOCOL_H + +namespace uat { + enum class MessageType { + DOWNLINK_SHORT, + DOWNLINK_LONG, + UPLINK, + INVALID + }; + + const unsigned SYNC_BITS = 36; + const std::uint64_t DOWNLINK_SYNC_WORD = 0xEACDDA4E2UL; + const std::uint64_t UPLINK_SYNC_WORD = 0x153225B1DUL; + + const unsigned DOWNLINK_SHORT_DATA_BITS = 144; + const unsigned DOWNLINK_SHORT_DATA_BYTES = DOWNLINK_SHORT_DATA_BITS / 8; + const unsigned DOWNLINK_SHORT_BITS = DOWNLINK_SHORT_DATA_BITS + 96; + const unsigned DOWNLINK_SHORT_BYTES = DOWNLINK_SHORT_BITS / 8; + + const unsigned DOWNLINK_LONG_DATA_BITS = 272; + const unsigned DOWNLINK_LONG_DATA_BYTES = DOWNLINK_LONG_DATA_BITS / 8; + const unsigned DOWNLINK_LONG_BITS = 272 + 112; + const unsigned DOWNLINK_LONG_BYTES = DOWNLINK_LONG_BITS / 8; + + const unsigned UPLINK_BLOCK_DATA_BITS = 576; + const unsigned UPLINK_BLOCK_DATA_BYTES = UPLINK_BLOCK_DATA_BITS / 8; + const unsigned UPLINK_BLOCK_BITS = UPLINK_BLOCK_DATA_BITS + 160; + const unsigned UPLINK_BLOCK_BYTES = UPLINK_BLOCK_BITS / 8; + + const unsigned UPLINK_BLOCKS_PER_FRAME = 6; + const unsigned UPLINK_DATA_BITS = UPLINK_BLOCK_DATA_BITS * UPLINK_BLOCKS_PER_FRAME; + const unsigned UPLINK_DATA_BYTES = UPLINK_DATA_BITS / 8; + const unsigned UPLINK_BITS = UPLINK_BLOCK_BITS * UPLINK_BLOCKS_PER_FRAME; + const unsigned UPLINK_BYTES = UPLINK_BITS / 8; + + const unsigned UPLINK_POLY = 0x187; + const unsigned DOWNLINK_POLY = 0x187; +}; + +#endif