├── .gitignore ├── LICENSE ├── Octave.png ├── README.md ├── Test.md ├── cpp ├── add_turbo.cc └── remove_turbo.cc ├── gnuradio ├── README.md └── correlation_test.grc ├── links.md └── matlab ├── create_gnuradio_zc.m ├── find_zc.m ├── normalized_xcorr.m └── updated_scripts ├── calculate_channel.m ├── create_zc.m ├── extract_bursts_from_file.m ├── extract_ofdm_symbol_samples.m ├── find_sto_cp.m ├── find_zc_indices_by_file.m ├── generate_scrambler_seq.m ├── get_bytes_per_sample.m ├── get_cyclic_prefix_lengths.m ├── get_data_carrier_indices.m ├── get_fft_size.m ├── get_sample_count_of_file.m ├── is_octave.m ├── must_be_member.m ├── normalized_xcorr_fast.m ├── process_file.m ├── quantize_qpsk.m ├── read_complex.m ├── transmit ├── calculate_crc.m ├── create_burst.m ├── create_frame_bytes.m ├── get_seconds_since_epoch.m ├── to_bytes.m └── to_qpsk.m └── unused_scripts ├── apply_freq_offset_time_domain.m ├── convert_complex_file_type.m ├── create_zc_seq.m ├── cyclic_prefix_sto_test.m ├── est_integer_freq_offset.m ├── ifo_test.m ├── normalize.m ├── plot_fft.m ├── plot_symbol_boundaries.m ├── read_complex_floats.m ├── test_integer_freq_offset.m ├── write_complex.m └── write_complex_floats.m /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.asv 2 | **/*.fc32 3 | **/octave-workspace 4 | collects/ 5 | **/images/*.png 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 David Protzman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Octave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proto17/dji_droneid/7c6dddad563724df93132e91b89e74650182ea1e/Octave.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DJI DroneID RF Analysis 2 | 3 | *THIS IS A WORK IN PROGRESS!!!* 4 | 5 | Also, the credit for this work is not mine alone. I have several people who are helping me through this process. Without their hints about how the signal is structured I would not have gotten very far in this short a time. So thank you very much to those who have assisted me to this point!!!! 6 | 7 | To that point, huge thanks to the following people / groups: 8 | - Chair for System Security @ Ruhr-Universität Bochum / CISPA Helmholtz Center for Information Security 9 | 10 | ![Title Picture](Octave.png?raw=true) 11 | 12 | 13 | This project aims to demodulate DJI DroneID frames and eventually be able to craft arbitrary DroneID frames that can be sent with an SDR 14 | 15 | The `.m` files in this project *should* work with Octave 5.2.0 and MATLAB. If using Octave, make sure to install the `signal` package 16 | 17 | The IQ file used in this example will not be made available publicly as it likely contains GPS information about where the drone was when the recording was taken. The drone used in testing is the DJI Mini 2 with no modifications. Recordings were taken with an Ettus B205-mini at a sampling rate of 30.72 MSPS. The signal of interest is in 2.4 GHz and will show up every 600 ms or so. It will be 10 MHz wide (15.36 MHz with guard carriers). 18 | 19 | # How to Process Samples 20 | You will need to record DroneID bursts with an SDR, save the samples as 32-bit floating point IQ data, and then edit the `matlab/updated_scripts/process_file.m` script to read your file. If there is a major frequency offset (say you recorded 7 MHz off center) you will need to specify that in the script. 21 | 22 | List of tasks: 23 | - Identify ZC sequence (done) 24 | - Detect ZC sequence (done) 25 | - Coarse frequency offset detection/correction (done) 26 | - Fine frequency offset detection/correction (skipped) 27 | - Phase correction (done) 28 | - Equalization (done) 29 | - Symbol extraction (done) 30 | - Descrambling (done) 31 | - Turbo Product Code removal (done) 32 | - Deframing (done) 33 | - Profit! 34 | 35 | ## Identify ZC Sequence 36 | This was completed by brute forcing through all of the possible ZC sequence root indexes for 601 output samples. I was at a complete standstill until it was suggested that I try all possible combos, and it turns out that it wasn't all that difficult. 37 | 38 | The tricky bit was that you need to generate 1 plus the number of data carriers (always 600) worth of ZC sequence, zero the center element, map the generated samples onto the center of an FFT (number of bins is based on sample rate), and compute the IFFT. The resulting time domain samples can be cross correlated against a recording of the same sample rate to find the sequence. 39 | 40 | The root indices are 41 | - 600 for the first ZC sequence 42 | - 147 for the second ZC sequence 43 | 44 | 45 | ## Detect ZC Sequence 46 | This is being done using a normalized cross correlation with a golden reference. The first ZC sequence is generated in MATLAB and that copy is used to find the ZC sequence in the collect. 47 | 48 | ## Coarse CFO Detection/Adjustment 49 | Currently using the cyclic prefix of the first OFDM symbol to detect and correct for coarse frequency offset. 50 | 51 | I don't think this will work should the CFO be very large. I suspect anything over 1 FFT bin (15 KHz) is going to fail to demodulate 52 | 53 | ## Fine CFO Detection/Adjustment 54 | It seems that the coarse offset estimate using the cyclic prefix is doing just fine and does not need help from a fine offset adjustment. 55 | 56 | ## Phase Correction 57 | This ended up being a little more difficult than expected. If the time that the first sample is plucked from the airwaves does not happen at exactly the right time, then there will be a fractional time offset. This fractional time offset will show up in the frequency domain as a walking phase offset. Meaning that each OFDM carrier has an accumulating phase offset left to right. If the first carrier might have an offset of 0.1 radians, the next 0.2 radians, then 0.3 radians, etc. Since there are no pilots, the phase offset estimate has to come from the ZC sequences. But, using the first or second ZC sequences to adjust phase for each OFDM symbol works fine for the OFDM symbols directly to the left and right (in time) of the used ZC sequence, but not so well for the others. Each other OFDM symbol will have a constellation that is rotating more the further from the ZC sequence it's located. 58 | 59 | To deal with this, the channel for each ZC sequence is calculated, and the phase difference between those channels is calculated. This difference divided by 2 is the walking phase offset. That walking phase offset is then used to adjust each of the other OFDM symbols to get all of the constallations locked properly. 60 | 61 | ## Symbol Extraction 62 | This is super simple and just requires being time and frequency aligned with knowledge of the cyclic prefixes 63 | 64 | ## Descrambling 65 | Thanks to some kind souls this has been figured out. In the case of the 9 OFDM symbol drones, the first OFDM symbol gets zero'ed out by the scrambler, and then the scrambler starts over for the remaining 8 OFDM symbols. 66 | 67 | ## Turbo Product Code Removal 68 | There is a C++ application in the repository to handle this 69 | 70 | ## Deframing 71 | This is known, and a deframer will be added at a later date 72 | 73 | # Known Values 74 | ## Frequencies 75 | Look in 2.4/5.8 GHz for the Drone ID frames 76 | Some frequencies I have noted thus far: 77 | - 2.4595 GHz 78 | - 2.4445 GHz 79 | - 2.4295 GHz 80 | - 2.4145 GHz 81 | - 2.3995 GHz 82 | - 5.7565 GHz 83 | - 5.7765 GHz 84 | - 5.7965 GHz 85 | 86 | There might be others, but that's just what I've seen 87 | 88 | ## Burst Duration/Interval 89 | The Drone ID bursts happen ~ every 600 milliseconds 90 | The frequency varies and I've heard that there is a pattern to it, but cannot validate with my SDR as the bandwidth isn't high enough 91 | Each burst is 9 OFDM symbols with two symbols using a long cyclic prefix and the others using the short sequence (see below for edge case) 92 | 93 | ## OFDM Structure 94 | As mentioned above, there are 9 OFDM symbols (see below for edge case) 95 | The 4th and 6th symbols (1-based) appear to be Zadoff-Chu (ZC) sequences (these are used in LTE and I know for a fact they are present in the uplink signal for Ocusync). The parameters for the ZC sequences are not known. I have first hand knowledge that the sequences are almost certainly not following the standard. 96 | The remaining OFDM symbols carry just a QPSK. If there are pilots they are either QPSK pilots or a 45 degree offset BPSK. As pointed out by https://github.com/tmbinc/random/tree/master/dji/ocusync2 the DC carrier appears to always be sitting around 45 degrees with a much smaller amplitude than the data carriers. Not totally sure what's going on there. 97 | 98 | ### 8 OFDM Symbol Edge Case 99 | It turns out that some drones don't send the first OFDM symbol. Instead they skip it and end up with their OFDM symbol 1 being the 9 OFDM symbol cases 2nd OFDM symbol. 100 | 101 | Since there's no change to the order of the symbols, and the 9 OFDM symbol case's first OFDM symbol being useless, the scripts in this repo treat all bursts as having 9 OFDM symbols and just don't do anything with the first OFDM symbol. 102 | 103 | # Current Questions 104 | 105 | ## Cross Correlation 106 | MATLAB can do a normalized cross correlation using `xcorr(X, Y, 0, 'normalized')` but it's *crazy* slow. So I wrote my own function [normalized_xcorr.m](matlab/updated_scripts/normalized_xcorr_fast.m) that is ~ 8x faster than `xcorr` but over 100x slower than the `filter` function that was being used. If anyone has a better idea on how to do a truly normalized (0.0 - 1.0) cross correlation please let me know. 107 | 108 | ## Burst Extraction 109 | Right now the normalized cross correlation mentioned above is used to find the bursts using [process_file.m](matlab/updated_scripts/process_file.m). Since the cross correlation is so slow, a file that contains tens of millions of samples takes a long time in MATLAB and an even longer time in Octave. The old method of using the `filter` function was blazing fast, but there was no way to know what the correlation thresholds needed to be without making multiple passes through the file. Energy detection would probably work, but that falls apart in low SNR conditions. A normalized autocorrelation would likely be as slow as the cross correlation, which probably rules out autocorrelating for the ZC sequence. I'd love to hear some ideas to help speed this process up. 110 | -------------------------------------------------------------------------------- /Test.md: -------------------------------------------------------------------------------- 1 | # DJI DroneID RF Demodulator (Non-WiFi) 2 | 3 | *This repository is a work in progress!!!* 4 | 5 | ## Thanks 6 | 7 | The credit for this code is not mine alone. I have had several people help me through this process. Without their hints about certain parts of the signal I would have hit some very serious roadblocks. So, thank you very much to those that have assisted me in this work!!! 8 | 9 | To that point, huge thanks to the following people (just the folks that are okay with public thanks) 10 | - Chair for System Security @ Ruhr-Universität Bochum / CISPA Helmholtz Center for Information Security 11 | 12 | ## Drone Compatibility 13 | 14 | This repo has only really been tested against my DJI Mini 2. It's known that there are at least two flavors of DroneID and mine appears to be in the minority of types. That being said, the code here is written to support both types that I know of. 15 | 16 | It's very important to remember that DroneID is present on (AFAIK) all modern DJI drones, but this repo is **ONLY FOR NON-WIFI DRONES** If you are after a WiFi DroneID detector/demodualtor, check out [Kismet](https://www.kismetwireless.net/development/droneid/). 17 | 18 | ## Required Software 19 | 20 | ### MATLAB 21 | The most up to date code in this repo will always be the MATLAB scripts. This is the code that is used to generate and test the GNU Radio modules. To prevent people needing to spend ~ $100/yr on a MATLAB license, the MATLAB scripts all *should* work with Octave (at least 5.2.0). It should be noted that Octave will be a **LOT** slower than MATLAB. In order to do this there have been some functions that MATLAB natively supports that have had to be written by hand to support both tools. 22 | 23 | If you have MATLAB, please use at least R2022a. 24 | 25 | ### GNU Radio 26 | There are GNU Radio modules, but at the time of writing (Oct 31, 2022) they are not very good. There is a re-write effort going on that aims to have all of the same features as the MATLAB. I would not expect that update to be complete for another 2-3 months as it's a lot of work to test and make performant. 27 | 28 | You might notice that there several GNU Radio branches. My apologies as there have been multiple iterations of the GNU Radio module and I did a bad job of orgnaizing everything :( Here's the breakdown: 29 | - `gr-droneid`: Outdated, probably best to not use 30 | - `gr-droneid-3.8`: Probably best to not use 31 | - `gr-droneid-3.10`: Probably best to not use 32 | - `gr-droneid-update`: Use this if you have GNU Radio 3.8 33 | - `gr-droneid-update-3.10`: Use this if you have GNU Radio 3.10 34 | - `gr-droneid-rewrite`: This is the ongoing re-write 35 | 36 | ### LTE Rate Matching 37 | There are two libraries required to build the applications that remove/add the LTE rate matching: 38 | - [turbofec](https://github.com/ttsou/turbofec) 39 | - [CRCpp](https://github.com/d-bahr/CRCpp) 40 | 41 | `turbofec` must be fully built and installed. `CRCpp` doesn't need to be installed, you just need to the `CRC.h` header file. 42 | 43 | At the time of writing (31 Oct 2022) only Linux is supported for the LTE rate matching logic. [Cygwin](https://www.cygwin.com/) might work, but no promises. 44 | 45 | See the files in [cpp](cpp/) for instructions on how to build 46 | 47 | ## Wiki 48 | *WORK IN PROGRESS* 49 | Check out the [Wiki](../../wiki/) for more details about how to use this repo. 50 | -------------------------------------------------------------------------------- /cpp/add_turbo.cc: -------------------------------------------------------------------------------- 1 | // To build this you must have installed turbofec [1] and downloaded the CRC.h file from CRCpp [2] 2 | // [1] https://github.com/ttsou/turbofec 3 | // [2] https://github.com/d-bahr/CRCpp/blob/master/inc/CRC.h 4 | 5 | // Assuming that CRC.h is in the current directory and you installed libturbofec with defaults, 6 | // build with the following: 7 | // g++ -Wall add_turbo.cc -o add_turbo -I. -I/usr/local/include -L/usr/local/lib -lturbofec 8 | 9 | // This line is required before the include of CRC.h so that it actually loads the CRC_24_LTEA definition 10 | #define CRCPP_INCLUDE_ESOTERIC_CRC_DEFINITIONS 11 | 12 | #include // strlen 13 | #include // std::vector<> 14 | 15 | #include // *int*_t 16 | #include // fprintf, fopen, feof, fseek, ftell 17 | #include // exit 18 | 19 | // The turbofec includes must be imported as extern C otherwise the linker will fail 20 | #ifdef __cplusplus 21 | extern "C" { 22 | #endif 23 | 24 | #include 25 | #include 26 | 27 | #ifdef __cplusplus 28 | } 29 | #endif 30 | 31 | // From https://github.com/d-bahr/CRCpp/blob/master/inc/CRC.h 32 | #include 33 | 34 | // Number of bytes that the user must provide 35 | // TODO(22April2022): This needs to be flexible in the future. For now it's hard coded to support the normal DroneID frame 36 | static const int required_bytes = 91; 37 | 38 | void usage(const char * const program) { 39 | fprintf(stderr, "\n"); 40 | fprintf(stderr, "Usage: %s \n", program); 41 | fprintf(stderr, "\n"); 42 | fprintf(stderr, " => Path to file containing raw bytes to Turbo encode and rate match\n"); 43 | fprintf(stderr, " !!Must be %d bytes!!\n", required_bytes); 44 | fprintf(stderr, " => Where to write out the Turbo encoded and rate matched bits\n"); 45 | fprintf(stderr, "\n"); 46 | exit(1); 47 | } 48 | 49 | // These values always exist at the end of the frame. Not sure what they are used for, but they don't change and 50 | // are required for the Turbo code application. The bytes change between power cycles. Might just be garbage data 51 | // from the RAM of the drone. Zeroing out instead of using 'valid' data as it's unclear if the data I have can 52 | // be called 'valid'. Might also contain identifying information O.o 53 | // If demodulation doesn't work with DJI devices then this might be the issue 54 | const std::vector tail_bytes(82, 0); 55 | 56 | int main(int argc, const char ** argv) { 57 | // Make sure all required parameters were provided 58 | if (argc != 3) { 59 | fprintf(stderr, "[ERROR] Invalid number of arguments\n"); 60 | usage(argv[0]); 61 | } 62 | 63 | // Open the input file 64 | FILE * handle = fopen(argv[1], "rb"); 65 | if (! handle) { 66 | fprintf(stderr, "[ERROR] File '%s' does not exist or is not readable\n", argv[1]); 67 | usage(argv[0]); 68 | } 69 | 70 | // Calculate the number of bytes in the file 71 | fseek(handle, 0, SEEK_END); 72 | const auto num_bytes = ftell(handle); 73 | fseek(handle, 0, SEEK_SET); 74 | 75 | // Sanity check that all bytes are there 76 | if (num_bytes != required_bytes) { 77 | fprintf(stderr, "[ERROR] Invalid number of bytes. Expected %d, got %ld\n", required_bytes, num_bytes); 78 | usage(argv[0]); 79 | } 80 | 81 | // Read the bytes into a vector 82 | std::vector payload(required_bytes); 83 | const auto total_read = fread(&payload[0], sizeof(uint8_t), required_bytes, handle); 84 | if (total_read != required_bytes) { 85 | fprintf(stderr, "[ERROR] Failed to read all bytes. Expected %d, read %ld\n", required_bytes, total_read); 86 | usage(argv[0]); 87 | } 88 | 89 | // Print the bytes that were read from the file as a sanity check 90 | fprintf(stdout, "Payload: 0x"); 91 | for (const auto & val : payload) { 92 | fprintf(stdout, "%02x", val); 93 | } 94 | fprintf(stdout, "\n"); 95 | 96 | // Add on the additional payload bytes that are not a part of the DroneID protocol 97 | payload.insert(payload.end(), tail_bytes.begin(), tail_bytes.end()); 98 | fprintf(stdout, "Full payload: 0x"); 99 | for (const auto & val : payload) { 100 | fprintf(stdout, "%02x", val); 101 | } 102 | fprintf(stdout, "\n"); 103 | 104 | // Make sure that all bytes are accounted for prior to calculating the CRC 105 | const auto pre_crc_byte_count = 173; 106 | if (payload.size() != pre_crc_byte_count) { 107 | fprintf(stderr, "[ERROR] Number of total payload bytes is not correct. Expected %u, got %ld\n", 108 | pre_crc_byte_count, payload.size()); 109 | exit(1); 110 | } 111 | 112 | // Calculate the CRC and add it to the payload vector 113 | const uint32_t calculated_crc = CRC::Calculate(&payload[0], payload.size(), CRC::CRC_24_LTEA()); 114 | 115 | fprintf(stdout, "CRC: %06x\n", calculated_crc); 116 | 117 | payload.push_back((calculated_crc >> 16) & 0xff); 118 | payload.push_back((calculated_crc >> 8) & 0xff); 119 | payload.push_back((calculated_crc >> 0) & 0xff); 120 | 121 | // Specify the number of bits and bytes that are expected in the steps below. The bit count has an additional 122 | // four bits for the Turbo coder tail 123 | const auto total_payload_bytes = pre_crc_byte_count + 3; 124 | const auto turbo_encoder_bit_count = (total_payload_bytes * 8) + 4; 125 | 126 | // Allocate buffers for the Turbo decoder. After rate matching there will be 7200 bits 127 | std::vector d1(turbo_encoder_bit_count, 0); 128 | std::vector d2(turbo_encoder_bit_count, 0); 129 | std::vector d3(turbo_encoder_bit_count, 0); 130 | std::vector unpacked_bits; 131 | std::vector rate_matcher_output(7200, 0); 132 | 133 | // Create the required structure to run the Turbo decoder 134 | struct lte_turbo_code turbo_encoder { 135 | .n = 2, 136 | .k = 4, 137 | .len = total_payload_bytes * 8, 138 | .rgen = 11, 139 | .gen = 13, 140 | }; 141 | 142 | // Unpack the bytes into bits 143 | for (const auto & val : payload) { 144 | for (int idx = 0; idx < 8; idx++) { 145 | unpacked_bits.push_back((val >> (7-idx)) & 0x1); 146 | } 147 | } 148 | 149 | // Using old school casting because for some reason lte_turbo_encode takes a different set of `d` types 150 | // than the lte_rate_matcher_io struct :( 151 | lte_turbo_encode(&turbo_encoder, &unpacked_bits[0], (uint8_t*)&d1[0], (uint8_t*)&d2[0], (uint8_t*)&d3[0]); 152 | 153 | // Create the struct that controls the rate matcher 154 | struct lte_rate_matcher_io rate_matcher_io = { 155 | .D = turbo_encoder_bit_count, 156 | .E = static_cast(rate_matcher_output.size()), 157 | .d = {&d1[0], &d2[0], &d3[0]}, 158 | .e = &rate_matcher_output[0] 159 | }; 160 | 161 | struct lte_rate_matcher* rate_matcher = lte_rate_matcher_alloc(); 162 | 163 | lte_rate_match_fw(rate_matcher, &rate_matcher_io, 0); 164 | 165 | // Write out the 7200 bits to the user specified file. Each bit will be written as a 0x01 or 0x00. 166 | FILE * output_handle = fopen(argv[2], "wb"); 167 | if (! output_handle) { 168 | fprintf(stderr, "[ERROR] Could not open output file '%s' for writing\n", argv[2]); 169 | exit(1); 170 | } 171 | 172 | const auto written = 173 | fwrite(&rate_matcher_output[0], sizeof(rate_matcher_output[0]), rate_matcher_output.size(), output_handle); 174 | 175 | // Sanity check that all bytes were written 176 | if (written != rate_matcher_output.size()) { 177 | fprintf(stderr, "[ERROR] Did not write all data. Expected %ld elements, got %lu\n", rate_matcher_output.size(), written); 178 | exit(1); 179 | } 180 | 181 | // Free LTE related pointers 182 | lte_rate_matcher_free(rate_matcher); 183 | } 184 | -------------------------------------------------------------------------------- /cpp/remove_turbo.cc: -------------------------------------------------------------------------------- 1 | // To build this you must have installed turbofec [1] and downloaded the CRC.h file from CRCpp [2] 2 | // [1] https://github.com/ttsou/turbofec 3 | // [2] https://github.com/d-bahr/CRCpp/blob/master/inc/CRC.h 4 | 5 | // Assuming that CRC.h is in the current directory and you installed libturbofec with defaults, 6 | // build with the following: 7 | // g++ -Wall remove_turbo.cc -o remove_turbo -I. -I/usr/local/include -L/usr/local/lib -lturbofec 8 | 9 | // This line is required before the include of CRC.h so that it actually loads the CRC_24_LTEA definition 10 | #define CRCPP_INCLUDE_ESOTERIC_CRC_DEFINITIONS 11 | 12 | #include // std::vector<> 13 | 14 | #include // *int*_t 15 | #include // fprintf, fopen, feof, fseek, ftell 16 | #include // exit 17 | 18 | // The turbofec includes must be imported as extern C otherwise the linker will fail 19 | #ifdef __cplusplus 20 | extern "C" { 21 | #endif 22 | 23 | #include 24 | #include 25 | 26 | #ifdef __cplusplus 27 | } 28 | #endif 29 | 30 | // From https://github.com/d-bahr/CRCpp/blob/master/inc/CRC.h 31 | #include 32 | 33 | void usage(const char * program) { 34 | fprintf(stderr, "\n"); 35 | fprintf(stderr, "Usage: %s \n", program); 36 | fprintf(stderr, "\n"); 37 | fprintf(stderr, " => Path to file containing 7200 ASCII '1' and '0' values\n"); 38 | fprintf(stderr, "\n"); 39 | fprintf(stderr, " This program will remove the Turbo coding and rate matching found in DJI DroneID signals\n"); 40 | fprintf(stderr, " and output the decoded frame in hex to the terminal\n"); 41 | fprintf(stderr, "\n"); 42 | fprintf(stderr, " It has only been tested against the DJI Mini 2\n"); 43 | fprintf(stderr, "\n\n"); 44 | 45 | exit(1); 46 | } 47 | 48 | int main(int argc, const char ** argv) { 49 | const size_t input_file_bit_count = 7200; 50 | 51 | if (argc != 2) { 52 | usage(argv[0]); 53 | } 54 | 55 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 56 | // Open and validate input file 57 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 58 | FILE * input_handle = fopen(argv[1], "r"); 59 | if (! input_handle) { 60 | fprintf(stderr, "[ERROR] Failed to open input file '%s'\n", argv[1]); 61 | usage(argv[0]); 62 | } 63 | 64 | // Get the number of bytes present in the file 65 | fseek(input_handle, 0, SEEK_END); 66 | const size_t byte_count = ftell(input_handle); 67 | fseek(input_handle, 0, SEEK_SET); 68 | 69 | // Make sure that the number of bytes in the file is correct 70 | if (byte_count != input_file_bit_count) { 71 | fprintf(stderr, "[ERROR] Invalid number of bytes in input file '%s'. Expected %ld, got %ld\n", 72 | argv[1], input_file_bit_count, byte_count); 73 | usage(argv[0]); 74 | } 75 | 76 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 77 | // Read bits from input file 78 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 79 | std::vector input_file_bits(input_file_bit_count); 80 | size_t bytes_read = fread(&input_file_bits[0], sizeof(char), input_file_bit_count, input_handle); 81 | 82 | if (bytes_read != input_file_bit_count) { 83 | fprintf(stderr, "[ERROR] Expected to read %ld bytes, but got %ld\n", input_file_bit_count, bytes_read); 84 | usage(argv[0]); 85 | } 86 | 87 | fclose(input_handle); 88 | 89 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 90 | // Validate and convert bits from input file 91 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 92 | 93 | // Load bearing constants. Do not change!! Just playing with the values, most will work up to, and including, 63 94 | // Going past 63 seems to result in a failure to decode. 95 | int8_t bit_value_lut[2] = {-63, 63}; 96 | 97 | std::vector turbo_decoder_input(input_file_bit_count); 98 | 99 | for (size_t idx = 0; idx < input_file_bit_count; idx++) { 100 | const int8_t value = (int8_t)input_file_bits[idx]; 101 | 102 | // Don't let values other than 0 or 1 past as these will walk past the edge of the LUT 103 | if (value != 1 && value != 0) { 104 | fprintf(stderr, "Invalid bit value '%02x' at offset %ld. Must be 0x1 or 0x0\n", value, idx); 105 | usage(argv[0]); 106 | } 107 | 108 | // Map the 1/0 value to the correct magic number from the lookup table 109 | turbo_decoder_input[idx] = bit_value_lut[value]; 110 | } 111 | 112 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 113 | // Setup and run the Turbo decoder 114 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 115 | const int turbo_iterations = 4; 116 | const int turbo_decoder_bit_count = 1412; // Number of bits that the Turbo decoder will take in 117 | const int expected_payload_bytes = 176; // Number of bytes that the Turbo decoder will output 118 | const int expected_payload_bits = expected_payload_bytes * 8; 119 | 120 | // Allocate buffers for the Turbo decoder 121 | std::vector d1(turbo_decoder_bit_count); 122 | std::vector d2(turbo_decoder_bit_count); 123 | std::vector d3(turbo_decoder_bit_count); 124 | std::vector decoded_bytes(expected_payload_bytes); 125 | 126 | // Create the required structures to run the Turbo decoder 127 | struct lte_rate_matcher * rate_matcher = lte_rate_matcher_alloc(); 128 | struct tdecoder * turbo_decoder = alloc_tdec(); 129 | struct lte_rate_matcher_io rate_matcher_io = { 130 | .D = turbo_decoder_bit_count, 131 | .E = input_file_bit_count, 132 | .d = {&d1[0], &d2[0], &d3[0]}, 133 | .e = &turbo_decoder_input[0] 134 | }; 135 | 136 | // Setup the rate matching logic 137 | lte_rate_match_rv(rate_matcher, &rate_matcher_io, 0); 138 | 139 | // Run the turbo decoder (will do rate matching as well) 140 | const int decode_status = lte_turbo_decode(turbo_decoder, expected_payload_bits, turbo_iterations, 141 | &decoded_bytes[0], &d1[0], &d2[0], &d3[0]); 142 | 143 | if (decode_status != 0) { 144 | fprintf(stderr, "[ERROR] Failed to remove Turbo coder. Exit code %d\n", decode_status); 145 | usage(argv[0]); 146 | } 147 | 148 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 149 | // Validate the CRC24 at the end of the Turbo decoded data 150 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 151 | const uint32_t calculated_crc = CRC::Calculate(&decoded_bytes[0], decoded_bytes.size(), CRC::CRC_24_LTEA()); 152 | 153 | // Since the received CRC bytes were included in the calculation above, the output should be all zeros 154 | if (calculated_crc != 0) { 155 | fprintf(stderr, "[ERROR] CRC did not zero out. Got %06x after calculation\n", calculated_crc); 156 | return 1; 157 | } 158 | 159 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 160 | // Print the decoded frame in hex 161 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 162 | const uint32_t payload_crc_byte_len = 2; // There is a 16-bit CRC at the end of the payload 163 | const uint32_t additional_payload_bytes = 1; // There is a single byte of 0x00 just before the 16-bit CRC that 164 | // doesn't appear to be a part of the length field (first byte) 165 | const uint32_t payload_length = decoded_bytes[0] + payload_crc_byte_len + additional_payload_bytes; 166 | 167 | // Print out the frame in hex 168 | for (uint32_t idx = 0; idx < decoded_bytes.size(); idx++) { 169 | fprintf(stdout, "%02x", decoded_bytes[idx]); 170 | } 171 | fprintf(stdout, "\n"); 172 | 173 | free_tdec(turbo_decoder); 174 | lte_rate_matcher_free(rate_matcher); 175 | 176 | return 0; 177 | } 178 | -------------------------------------------------------------------------------- /gnuradio/README.md: -------------------------------------------------------------------------------- 1 | For the actual GNU Radio OOT module please use `gr-droneid-update-3.10` for GNU Radio 3.10 or `gr-droneid-update` for GNU Radio 3.8 2 | -------------------------------------------------------------------------------- /gnuradio/correlation_test.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: main 4 | category: '[GRC Hier Blocks]' 5 | cmake_opt: '' 6 | comment: '' 7 | copyright: '' 8 | description: '' 9 | gen_cmake: 'On' 10 | gen_linking: dynamic 11 | generate_options: qt_gui 12 | hier_block_src_path: '.:' 13 | id: correlator_test 14 | max_nouts: '0' 15 | output_language: python 16 | placement: (0,0) 17 | qt_qss_theme: '' 18 | realtime_scheduling: '' 19 | run: 'True' 20 | run_command: '{python} -u {filename}' 21 | run_options: prompt 22 | sizing_mode: fixed 23 | thread_safe_setters: '' 24 | title: Not titled yet 25 | window_size: '' 26 | states: 27 | bus_sink: false 28 | bus_source: false 29 | bus_structure: null 30 | coordinate: [8, 8] 31 | rotation: 0 32 | state: enabled 33 | 34 | blocks: 35 | - name: channel 36 | id: variable_qtgui_chooser 37 | parameters: 38 | comment: '' 39 | gui_hint: '' 40 | label: Drone ID Channel 41 | label0: Ch 1 42 | label1: Ch 2 43 | label2: Ch 3 44 | label3: Ch 4 45 | label4: Ch 5 46 | labels: '[]' 47 | num_opts: '5' 48 | option1: 2.4145e9 49 | option2: 2.4295e9 50 | option3: 2.4445e9 51 | option4: 2.4595e9 52 | options: '[0, 1, 2]' 53 | orient: Qt.QVBoxLayout 54 | type: real 55 | value: 2.3995e9 56 | widget: combo_box 57 | states: 58 | bus_sink: false 59 | bus_source: false 60 | bus_structure: null 61 | coordinate: [79, 651] 62 | rotation: 0 63 | state: disabled 64 | - name: correlation_mag_scaler 65 | id: variable_qtgui_range 66 | parameters: 67 | comment: '' 68 | gui_hint: '' 69 | label: Correlation Scaler 70 | min_len: '200' 71 | orient: Qt.Horizontal 72 | rangeType: float 73 | start: '0' 74 | step: '0.01' 75 | stop: '10' 76 | value: '1' 77 | widget: counter_slider 78 | states: 79 | bus_sink: false 80 | bus_source: false 81 | bus_structure: null 82 | coordinate: [1778, 31] 83 | rotation: 0 84 | state: enabled 85 | - name: decimation 86 | id: variable 87 | parameters: 88 | comment: '' 89 | value: '2' 90 | states: 91 | bus_sink: false 92 | bus_source: false 93 | bus_structure: null 94 | coordinate: [962, 46] 95 | rotation: 0 96 | state: true 97 | - name: delay 98 | id: variable_qtgui_range 99 | parameters: 100 | comment: '' 101 | gui_hint: '' 102 | label: Delay 103 | min_len: '200' 104 | orient: Qt.Horizontal 105 | rangeType: int 106 | start: '0' 107 | step: '1' 108 | stop: '100000' 109 | value: int(fft_size * 4) + long_cp_len + (short_cp_len * 3) 110 | widget: counter_slider 111 | states: 112 | bus_sink: false 113 | bus_source: false 114 | bus_structure: null 115 | coordinate: [1591, 41] 116 | rotation: 0 117 | state: enabled 118 | - name: fft_size 119 | id: variable 120 | parameters: 121 | comment: '' 122 | value: samp_rate / decimation / 15e3 123 | states: 124 | bus_sink: false 125 | bus_source: false 126 | bus_structure: null 127 | coordinate: [1076, 44] 128 | rotation: 0 129 | state: true 130 | - name: filter_taps 131 | id: variable_low_pass_filter_taps 132 | parameters: 133 | beta: '6.76' 134 | comment: '' 135 | cutoff_freq: 5e6 136 | gain: '1.0' 137 | samp_rate: samp_rate 138 | width: 750e3 139 | win: firdes.WIN_HAMMING 140 | states: 141 | bus_sink: false 142 | bus_source: false 143 | bus_structure: null 144 | coordinate: [623, 15] 145 | rotation: 0 146 | state: true 147 | - name: freq 148 | id: variable_qtgui_range 149 | parameters: 150 | comment: '' 151 | gui_hint: '' 152 | label: Freq 153 | min_len: '200' 154 | orient: Qt.Horizontal 155 | rangeType: float 156 | start: 60e6 157 | step: .5e6 158 | stop: 6e9 159 | value: '2429500000' 160 | widget: counter_slider 161 | states: 162 | bus_sink: false 163 | bus_source: false 164 | bus_structure: null 165 | coordinate: [456, 10] 166 | rotation: 0 167 | state: enabled 168 | - name: gain 169 | id: variable_qtgui_range 170 | parameters: 171 | comment: '' 172 | gui_hint: '' 173 | label: Gain 174 | min_len: '200' 175 | orient: Qt.Horizontal 176 | rangeType: float 177 | start: '0' 178 | step: '0.05' 179 | stop: '1' 180 | value: '.35' 181 | widget: counter_slider 182 | states: 183 | bus_sink: false 184 | bus_source: false 185 | bus_structure: null 186 | coordinate: [321, 10] 187 | rotation: 0 188 | state: true 189 | - name: long_cp_len 190 | id: variable 191 | parameters: 192 | comment: '' 193 | value: round(0.0000052 * samp_rate / decimation) 194 | states: 195 | bus_sink: false 196 | bus_source: false 197 | bus_structure: null 198 | coordinate: [1199, 48] 199 | rotation: 0 200 | state: true 201 | - name: offset 202 | id: variable_qtgui_range 203 | parameters: 204 | comment: '' 205 | gui_hint: '' 206 | label: Offset 207 | min_len: '200' 208 | orient: Qt.Horizontal 209 | rangeType: float 210 | start: -samp_rate/2 211 | step: .5e6 212 | stop: samp_rate/2 213 | value: 7.5e6 214 | widget: counter_slider 215 | states: 216 | bus_sink: false 217 | bus_source: false 218 | bus_structure: null 219 | coordinate: [795, 15] 220 | rotation: 0 221 | state: enabled 222 | - name: samp_rate 223 | id: variable_qtgui_range 224 | parameters: 225 | comment: '' 226 | gui_hint: '' 227 | label: Sample Rate 228 | min_len: '200' 229 | orient: Qt.Horizontal 230 | rangeType: float 231 | start: 10e6 232 | step: 1e6 233 | stop: 56e6 234 | value: 15.36e6*2 235 | widget: counter_slider 236 | states: 237 | bus_sink: false 238 | bus_source: false 239 | bus_structure: null 240 | coordinate: [187, 11] 241 | rotation: 0 242 | state: true 243 | - name: short_cp_len 244 | id: variable 245 | parameters: 246 | comment: '' 247 | value: round(0.00000469 * samp_rate / decimation) 248 | states: 249 | bus_sink: false 250 | bus_source: false 251 | bus_structure: null 252 | coordinate: [1324, 51] 253 | rotation: 0 254 | state: true 255 | - name: time_domain_mag_offset 256 | id: variable_qtgui_range 257 | parameters: 258 | comment: '' 259 | gui_hint: '' 260 | label: Time Domain Mag^2 Offset 261 | min_len: '200' 262 | orient: Qt.Horizontal 263 | rangeType: float 264 | start: '-100' 265 | step: '0.1' 266 | stop: '100' 267 | value: '1' 268 | widget: counter_slider 269 | states: 270 | bus_sink: false 271 | bus_source: false 272 | bus_structure: null 273 | coordinate: [2128, 31] 274 | rotation: 0 275 | state: enabled 276 | - name: time_domain_mag_scaler 277 | id: variable_qtgui_range 278 | parameters: 279 | comment: '' 280 | gui_hint: '' 281 | label: Time Domain Mag^2 Scaler 282 | min_len: '200' 283 | orient: Qt.Horizontal 284 | rangeType: float 285 | start: '0' 286 | step: '0.01' 287 | stop: '100' 288 | value: '1' 289 | widget: counter_slider 290 | states: 291 | bus_sink: false 292 | bus_source: false 293 | bus_structure: null 294 | coordinate: [1946, 30] 295 | rotation: 0 296 | state: enabled 297 | - name: analog_sig_source_x_0 298 | id: analog_sig_source_x 299 | parameters: 300 | affinity: '' 301 | alias: '' 302 | amp: '1' 303 | comment: '' 304 | freq: offset 305 | maxoutbuf: '0' 306 | minoutbuf: '0' 307 | offset: '0' 308 | phase: '0' 309 | samp_rate: samp_rate 310 | type: complex 311 | waveform: analog.GR_COS_WAVE 312 | states: 313 | bus_sink: false 314 | bus_source: false 315 | bus_structure: null 316 | coordinate: [386, 282] 317 | rotation: 0 318 | state: disabled 319 | - name: blocks_add_const_vxx_0 320 | id: blocks_add_const_vxx 321 | parameters: 322 | affinity: '' 323 | alias: '' 324 | comment: '' 325 | const: time_domain_mag_offset 326 | maxoutbuf: '0' 327 | minoutbuf: '0' 328 | type: float 329 | vlen: '1' 330 | states: 331 | bus_sink: false 332 | bus_source: false 333 | bus_structure: null 334 | coordinate: [1707, 479] 335 | rotation: 0 336 | state: true 337 | - name: blocks_complex_to_mag_squared_0 338 | id: blocks_complex_to_mag_squared 339 | parameters: 340 | affinity: '' 341 | alias: '' 342 | comment: '' 343 | maxoutbuf: '0' 344 | minoutbuf: '0' 345 | vlen: '1' 346 | states: 347 | bus_sink: false 348 | bus_source: false 349 | bus_structure: null 350 | coordinate: [1263, 382] 351 | rotation: 0 352 | state: true 353 | - name: blocks_complex_to_mag_squared_0_1 354 | id: blocks_complex_to_mag_squared 355 | parameters: 356 | affinity: '' 357 | alias: '' 358 | comment: '' 359 | maxoutbuf: '0' 360 | minoutbuf: '0' 361 | vlen: '1' 362 | states: 363 | bus_sink: false 364 | bus_source: false 365 | bus_structure: null 366 | coordinate: [1267, 482] 367 | rotation: 0 368 | state: true 369 | - name: blocks_delay_0 370 | id: blocks_delay 371 | parameters: 372 | affinity: '' 373 | alias: '' 374 | comment: '' 375 | delay: delay 376 | maxoutbuf: '0' 377 | minoutbuf: '0' 378 | num_ports: '1' 379 | type: complex 380 | vlen: '1' 381 | states: 382 | bus_sink: false 383 | bus_source: false 384 | bus_structure: null 385 | coordinate: [986, 479] 386 | rotation: 0 387 | state: true 388 | - name: blocks_file_source_0 389 | id: blocks_file_source 390 | parameters: 391 | affinity: '' 392 | alias: '' 393 | begin_tag: pmt.PMT_NIL 394 | comment: '' 395 | file: /opt/dji/collects/2437MHz_30.72MSPS.fc32 396 | length: '0' 397 | maxoutbuf: '0' 398 | minoutbuf: '0' 399 | offset: '0' 400 | repeat: 'True' 401 | type: complex 402 | vlen: '1' 403 | states: 404 | bus_sink: false 405 | bus_source: false 406 | bus_structure: null 407 | coordinate: [108, 176] 408 | rotation: 0 409 | state: disabled 410 | - name: blocks_multiply_const_vxx_0 411 | id: blocks_multiply_const_vxx 412 | parameters: 413 | affinity: '' 414 | alias: '' 415 | comment: '' 416 | const: correlation_mag_scaler 417 | maxoutbuf: '0' 418 | minoutbuf: '0' 419 | type: float 420 | vlen: '1' 421 | states: 422 | bus_sink: false 423 | bus_source: false 424 | bus_structure: null 425 | coordinate: [1463, 378] 426 | rotation: 0 427 | state: true 428 | - name: blocks_multiply_const_vxx_0_0 429 | id: blocks_multiply_const_vxx 430 | parameters: 431 | affinity: '' 432 | alias: '' 433 | comment: '' 434 | const: time_domain_mag_scaler 435 | maxoutbuf: '0' 436 | minoutbuf: '0' 437 | type: float 438 | vlen: '1' 439 | states: 440 | bus_sink: false 441 | bus_source: false 442 | bus_structure: null 443 | coordinate: [1501, 479] 444 | rotation: 0 445 | state: true 446 | - name: blocks_multiply_xx_0 447 | id: blocks_multiply_xx 448 | parameters: 449 | affinity: '' 450 | alias: '' 451 | comment: '' 452 | maxoutbuf: '0' 453 | minoutbuf: '0' 454 | num_inputs: '2' 455 | type: complex 456 | vlen: '1' 457 | states: 458 | bus_sink: false 459 | bus_source: false 460 | bus_structure: null 461 | coordinate: [650, 200] 462 | rotation: 0 463 | state: disabled 464 | - name: blocks_throttle_0 465 | id: blocks_throttle 466 | parameters: 467 | affinity: '' 468 | alias: '' 469 | comment: '' 470 | ignoretag: 'True' 471 | maxoutbuf: '0' 472 | minoutbuf: '0' 473 | samples_per_second: samp_rate 474 | type: complex 475 | vlen: '1' 476 | states: 477 | bus_sink: false 478 | bus_source: false 479 | bus_structure: null 480 | coordinate: [410, 195] 481 | rotation: 0 482 | state: disabled 483 | - name: fft_filter_xxx_0 484 | id: fft_filter_xxx 485 | parameters: 486 | affinity: '' 487 | alias: '' 488 | comment: '' 489 | decim: decimation 490 | maxoutbuf: '0' 491 | minoutbuf: '0' 492 | nthreads: '1' 493 | samp_delay: '0' 494 | taps: filter_taps 495 | type: ccc 496 | states: 497 | bus_sink: false 498 | bus_source: false 499 | bus_structure: null 500 | coordinate: [759, 331] 501 | rotation: 0 502 | state: enabled 503 | - name: fft_filter_xxx_1 504 | id: fft_filter_xxx 505 | parameters: 506 | affinity: '' 507 | alias: corr_filt 508 | comment: '' 509 | decim: '1' 510 | maxoutbuf: '0' 511 | minoutbuf: '0' 512 | nthreads: '1' 513 | samp_delay: '0' 514 | taps: '[(0.01763858+-0.01759975j),(-0.00388397+0.00275807j),(-0.01390060+0.01484881j),(0.01325312+-0.01048136j),(0.00881741+-0.01230320j),(-0.01657599+0.01394610j),(0.00161920+0.00512291j),(0.01819849+-0.01780315j),(-0.00950583+0.00041899j),(-0.01221672+0.01630932j),(0.01823595+-0.00857353j),(0.00533939+-0.01474219j),(-0.02011417+0.01306319j),(0.00639906+0.00831797j),(0.02000275+-0.01817355j),(-0.01474438+-0.00291138j),(-0.01210344+0.01823531j),(0.02334750+-0.00556697j),(0.00363457+-0.01806044j),(-0.02446152+0.01110847j),(0.00940491+0.01289557j),(0.02340473+-0.01767729j),(-0.01850256+-0.00814895j),(-0.01440082+0.01972270j),(0.02760724+-0.00017418j),(0.00503745+-0.02159205j),(-0.02893355+0.00642888j),(0.00885422+0.01855276j),(0.02818713+-0.01433409j),(-0.01864631+-0.01548892j),(-0.01951055+0.01854131j),(0.02866813+0.00843348j),(0.01069245+-0.02297966j),(-0.03114444+-0.00256164j),(0.00276783+0.02297415j),(0.03212885+-0.00570236j),(-0.01230563+-0.02287292j),(-0.02560602+0.01133507j),(0.02275246+0.01870841j),(0.01942053+-0.01794711j),(-0.02643795+-0.01511006j),(-0.00859650+0.02102139j),(0.02977802+0.00855868j),(0.00147073+-0.02443041j),(-0.02658694+-0.00363057j),(0.00740788+0.02425279j),(0.02469227+-0.00308371j),(-0.01058027+-0.02457072j),(-0.01857745+0.00735873j),(0.01486942+0.02190278j),(0.01626261+-0.01270162j),(-0.01395859+-0.02031444j),(-0.01162699+0.01534499j),(0.01565579+0.01652380j),(0.01186154+-0.01889297j),(-0.01392288+-0.01435918j),(-0.00971853+0.01995592j),(0.01646395+0.01057821j),(0.01160634+-0.02209695j),(-0.01652383+-0.00867963j),(-0.00971391+0.02215184j),(0.02103875+0.00544428j),(0.01053460+-0.02349902j),(-0.02236658+-0.00407928j),(-0.00661891+0.02309553j),(0.02698776+0.00146709j),(0.00525385+-0.02407342j),(-0.02705686+-0.00060978j),(0.00032044+0.02349869j),(0.02941959+-0.00142764j),(-0.00215588+-0.02427621j),(-0.02676047+0.00179504j),(0.00683333+0.02362328j),(0.02671798+-0.00321841j),(-0.00648172+-0.02425929j),(-0.02253918+0.00300450j),(0.00823215+0.02358706j),(0.02228678+-0.00369147j),(-0.00482181+-0.02415246j),(-0.01913822+0.00277589j),(0.00410942+0.02353859j),(0.02085658+-0.00263555j),(0.00075302+-0.02409316j),(-0.01995562+0.00097740j),(-0.00168818+0.02354974j),(0.02357755+-0.00002383j),(0.00578003+-0.02402643j),(-0.02361741+-0.00231087j),(-0.00535112+0.02338798j),(0.02696291+0.00397094j),(0.00811686+-0.02351577j),(-0.02547945+-0.00684159j),(-0.00688369+0.02241780j),(0.02642383+0.00903485j),(0.00973954+-0.02176375j),(-0.02213446+-0.01222224j),(-0.00947159+0.01974421j),(0.02047262+0.01466057j),(0.01392311+-0.01786227j),(-0.01418783+-0.01777473j),(-0.01532227+0.01454308j),(0.01140998+0.01993551j),(0.02093667+-0.01117578j),(-0.00482455+-0.02229874j),(-0.02241830+0.00647639j),(0.00235197+0.02334998j),(0.02674007+-0.00177560j),(0.00370173+-0.02400651j),(-0.02549785+-0.00386427j),(-0.00570067+0.02293933j),(0.02597781+0.00912466j),(0.01151729+-0.02093759j),(-0.02028489+-0.01456751j),(-0.01340883+0.01700720j),(0.01639734+0.01889219j),(0.01895844+-0.01198404j),(-0.00706996+-0.02234203j),(-0.01995268+0.00539147j),(0.00085395+0.02376333j),(0.02355149+0.00172946j),(0.00915068+-0.02329168j),(-0.02127081+-0.00923974j),(-0.01423996+0.02021346j),(0.02036066+0.01590018j),(0.02139663+-0.01490856j),(-0.01281531+-0.02110870j),(-0.02215768+0.00746762j),(0.00676604+0.02375680j),(0.02391116+0.00118478j),(0.00467929+-0.02328666j),(-0.01872316+-0.01001141j),(-0.01225830+0.01936907j),(0.01465063+0.01765912j),(0.02201916+-0.01218921j),(-0.00455091+-0.02259578j),(-0.02442482+0.00280149j),(-0.00265858+0.02384916j),(0.02601652+0.00754102j),(0.01330255+-0.02049234j),(-0.01858727+-0.01654419j),(-0.01791104+0.01314484j),(0.01066931+0.02263594j),(0.02274282+-0.00251843j),(0.00362849+-0.02365189j),(-0.01901272+-0.00867479j),(-0.01372881+0.01952101j),(0.01445752+0.01844779j),(0.02432328+-0.01009927j),(-0.00247187+-0.02341052j),(-0.02508303+-0.00177899j),(-0.00684102+0.02267245j),(0.02257626+0.01392442j),(0.01816000+-0.01503782j),(-0.00984830+-0.02183143j),(-0.02076847+0.00327629j),(-0.00250744+0.02381882j),(0.02053474+0.01034473j),(0.01787515+-0.01764356j),(-0.00990281+-0.02027066j),(-0.02400552+0.00603408j),(-0.00092193+0.02408622j),(0.02543539+0.00861474j),(0.01525644+-0.01861755j),(-0.01416012+-0.01968669j),(-0.02063245+0.00674855j),(0.00087798+0.02414493j),(0.02135157+0.00877755j),(0.01633513+-0.01819014j),(-0.00963691+-0.02018542j),(-0.02354786+0.00530972j),(-0.00327377+0.02399031j),(0.02411823+0.01099273j),(0.01857358+-0.01626456j),(-0.01059841+-0.02167872j),(-0.02223257+0.00175569j),(-0.00448061+0.02320601j),(0.01843306+0.01480060j),(0.02059717+-0.01235152j),(-0.00170716+-0.02320245j),(-0.02295945+-0.00399729j),(-0.01333077+0.02065185j),(0.01645931+0.01951942j),(0.02505838+-0.00582326j),(0.00214315+-0.02345175j),(-0.01999106+-0.01141066j),(-0.01612916+0.01515928j),(0.00645063+0.02338843j),(0.02340384+0.00335090j),(0.01455622+-0.02031075j),(-0.01291026+-0.01889784j),(-0.02419076+0.00573076j),(-0.00297354+0.02389695j),(0.02208851+0.01395236j),(0.02000855+-0.01210758j),(-0.00280117+-0.02338124j),(-0.02024105+-0.00674766j),(-0.01533629+0.01816326j),(0.00866513+0.02241716j),(0.02585863+0.00127843j),(0.01291895+-0.02084043j),(-0.01513621+-0.01869518j),(-0.02274968+0.00513630j),(-0.00348731+0.02360889j),(0.01829592+0.01603804j),(0.02167107+-0.00890890j),(0.00376932+-0.02344191j),(-0.01929970+-0.01165016j),(-0.02093116+0.01336495j),(0.00355852+0.02429016j),(0.02449575+0.00939079j),(0.01789285+-0.01510384j),(-0.00521708+-0.02288262j),(-0.02067840+-0.00580343j),(-0.01567718+0.01786577j),(0.00678157+0.02322719j),(0.02526638+0.00484779j),(0.01703504+-0.01802432j),(-0.00944185+-0.02175193j),(-0.02315968+-0.00263679j),(-0.01201339+0.01964799j),(0.00974227+0.02249249j),(0.02337539+0.00322025j),(0.01631611+-0.01869630j),(-0.00804951+-0.02146841j),(-0.02446534+-0.00250510j),(-0.01364863+0.01942189j),(0.01082476+0.02288534j),(0.02338089+0.00467850j),(0.01575323+-0.01745047j),(-0.00486588+-0.02228901j),(-0.02234394+-0.00540305j),(-0.01799248+0.01708638j),(0.00630297+0.02404197j),(0.02421169+0.00909650j),(0.01869056+-0.01371096j),(-0.00120294+-0.02329403j),(-0.01868606+-0.01101386j),(-0.02085532+0.01176855j),(-0.00277376+0.02442844j),(0.02043702+0.01572335j),(0.02426044+-0.00642247j),(0.00627326+-0.02229726j),(-0.01414759+-0.01799391j),(-0.02178119+0.00253727j),(-0.01211465+0.02135323j),(0.01004899+0.02230633j),(0.02564758+0.00475285j),(0.01782657+-0.01625512j),(-0.00461814+-0.02296748j),(-0.02016322+-0.00988523j),(-0.01935510+0.01191721j),(-0.00379767+0.02436674j),(0.01743215+0.01737833j),(0.02572626+-0.00317823j),(0.01138314+-0.02058087j),(-0.01091075+-0.02088622j),(-0.02180484+-0.00400836j),(-0.01659586+0.01652604j),(0.00098250+0.02457448j),(0.02062412+0.01401417j),(0.02470496+-0.00681130j),(0.00803330+-0.02182141j),(-0.01287401+-0.01961780j),(-0.02189717+-0.00203868j),(-0.01614244+0.01754722j),(0.00170233+0.02453998j),(0.02112234+0.01391701j),(0.02457703+-0.00635587j),(0.00866988+-0.02141619j),(-0.01106682+-0.02041379j),(-0.02146480+-0.00424042j),(-0.01851272+0.01541975j),(-0.00164649+0.02466325j),(0.01894224+0.01713733j),(0.02514208+-0.00175954j),(0.01307692+-0.01886615j),(-0.00517634+-0.02247708j),(-0.01936213+-0.01029778j),(-0.02241350+0.00928574j),(-0.00890280+0.02312654j),(0.01256998+0.02212150j),(0.02398657+0.00708364j),(0.01952637+-0.01201364j),(0.00524744+-0.02275797j),(-0.01271070+-0.01836548j),(-0.02419436+-0.00180839j),(-0.01820126+0.01634620j),(0.00055342+0.02472033j),(0.01685399+0.01822036j),(0.02330613+0.00103395j),(0.01810987+-0.01621768j),(0.00084468+-0.02311348j),(-0.01802187+-0.01564036j),(-0.02354118+0.00160323j),(-0.01460085+0.01832832j),(0.00074512+0.02475890j),(0.01693226+0.01731257j),(0.02546714+0.00044119j),(0.01773777+-0.01615047j),(-0.00043574+-0.02309909j),(-0.01559617+-0.01662575j),(-0.02235234+-0.00049757j),(-0.01867132+0.01630125j),(-0.00287727+0.02463726j),(0.01586464+0.02012312j),(0.02394619+0.00532412j),(0.01962936+-0.01177913j),(0.00759392+-0.02217363j),(-0.00942869+-0.02055810j),(-0.02291287+-0.00792285j),(-0.02224326+0.00915674j),(-0.00968967+0.02198525j),(0.00545216+0.02418903j),(0.01891447+0.01483349j),(0.02519027+-0.00132473j),(0.01746154+-0.01634824j),(0.00064014+-0.02301200j),(-0.01380391+-0.01823975j),(-0.02187679+-0.00445444j),(-0.02171414+0.01176517j),(-0.00973646+0.02289019j),(0.00817926+0.02389323j),(0.02015668+0.01445096j),(0.02308617+-0.00103487j),(0.01827180+-0.01557942j),(0.00441737+-0.02280573j),(-0.01288797+-0.01967344j),(-0.02229476+-0.00767090j),(-0.02140005+0.00794310j),(-0.01377200+0.02050982j),(0.00042672+0.02483528j),(0.01657016+0.01926092j),(0.02416496+0.00620496j),(0.02099713+-0.00892942j),(0.01196101+-0.02003610j),(-0.00187408+-0.02278609j),(-0.01707514+-0.01623877j),(-0.02401979+-0.00306393j),(-0.02011840+0.01158394j),(-0.01076914+0.02211560j),(0.00239261+0.02465612j),(0.01711242+0.01840249j),(0.02455795+0.00577834j),(0.02129721+-0.00853951j),(0.01254094+-0.01937935j),(0.00024369+-0.02296327j),(-0.01443504+-0.01816794j),(-0.02341480+-0.00678260j),(-0.02211734+0.00719352j),(-0.01498718+0.01897999j),(-0.00404817+0.02467978j),(0.01060410+0.02252930j),(0.02173449+0.01335831j),(0.02352400+0.00023656j),(0.01919379+-0.01257813j),(0.01054860+-0.02106155j),(-0.00346251+-0.02266944j),(-0.01673086+-0.01702626j),(-0.02236934+-0.00596229j),(-0.02210619+0.00710477j),(-0.01724887+0.01827684j),(-0.00542239+0.02434544j),(0.00886740+0.02365639j),(0.01815935+0.01652154j),(0.02272124+0.00507236j),(0.02323474+-0.00743489j),(0.01585477+-0.01757680j),(0.00290380+-0.02266571j),(-0.00838596+-0.02143520j),(-0.01725668+-0.01433876j),(-0.02397609+-0.00333608j),(-0.02334963+0.00869879j),(-0.01507077+0.01871835j),(-0.00520559+0.02426454j),(0.00526349+0.02407601j),(0.01686169+0.01832575j),(0.02368686+0.00848698j),(0.02285306+-0.00305227j),(0.01857744+-0.01357728j),(0.01120531+-0.02071451j),(-0.00103141+-0.02294526j),(-0.01274272+-0.01987383j),(-0.01929372+-0.01227800j),(-0.02310373+-0.00189834j),(-0.02352449+0.00900949j),(-0.01678265+0.01817462j),(-0.00635545+0.02375155j),(0.00284804+0.02468764j),(0.01242594+0.02091316j),(0.02138224+0.01327730j),(0.02412815+0.00332766j),(0.02149192+-0.00699342j),(0.01731976+-0.01574398j),(0.00946710+-0.02136599j),(-0.00220353+-0.02292562j),(-0.01170079+-0.02023332j),(-0.01784611+-0.01386406j),(-0.02327040+-0.00501172j),(-0.02471278+0.00477633j),(-0.01970955+0.01386125j),(-0.01285911+0.02077040j),(-0.00568049+0.02445257j),(0.00450889+0.02442333j),(0.01437037+0.02077892j),(0.01950391+0.01415874j),(0.02252471+0.00563749j),(0.02420351+-0.00348431j),(0.02045640+-0.01189218j),(0.01289326+-0.01841831j),(0.00603886+-0.02220266j),(-0.00195073+-0.02281651j),(-0.01187089+-0.02027311j),(-0.01860115+-0.01498356j),(-0.02151643+-0.00770434j),(-0.02405629+0.00058277j),(-0.02378548+0.00883518j),(-0.01827840+0.01606664j),(-0.01184778+0.02144390j),(-0.00591867+0.02439938j),(0.00270819+0.02468346j),(0.01156613+0.02234318j),(0.01659914+0.01770591j),(0.02048249+0.01134561j),(0.02426971+0.00398235j),(0.02372380+-0.00361383j),(0.01969741+-0.01067837j),(0.01619623+-0.01653253j),(0.01099186+-0.02066803j),(0.00249666+-0.02276534j),(-0.00471727+-0.02269407j),(-0.00991670+-0.02053572j),(-0.01648641+-0.01656101j),(-0.02193473+-0.01116509j),(-0.02310976+-0.00483625j),(-0.02344047+0.00187335j),(-0.02386140+0.00842109j),(-0.02047137+0.01432334j),(-0.01482280+0.01916178j),(-0.01065511+0.02261725j),(-0.00530402+0.02450742j),(0.00249282+0.02476861j),(0.00831091+0.02343803j),(0.01222397+0.02066798j),(0.01766374+0.01670301j),(0.02191821+0.01183085j),(0.02242897+0.00637507j),(0.02290666+0.00068771j),(0.02374963+-0.00489894j),(0.02108920+-0.01009364j),(0.01685052+-0.01463347j),(0.01425188+-0.01830644j),(0.01014470+-0.02097801j),(0.00359763+-0.02256942j),(-0.00111850+-0.02304756j),(-0.00475025+-0.02244752j),(-0.01056683+-0.02085841j),(-0.01552328+-0.01839054j),(-0.01753544+-0.01518538j),(-0.02039941+-0.01141883j),(-0.02387978+-0.00726502j),(-0.02413950+-0.00288884j),(-0.02340602+0.00153564j),(-0.02432402+0.00584651j),(-0.02333232+0.00991650j),(-0.01986311+0.01363157j),(-0.01798616+0.01688845j),(-0.01631785+0.01962228j),(-0.01180451+0.02179605j),(-0.00775218+0.02337789j),(-0.00570679+0.02436075j),(-0.00179884+0.02476964j),(0.00318093+0.02463208j),(0.00556684+0.02397991j),(0.00792766+0.02287066j),(0.01237445+0.02136691j),(0.01506705+0.01951924j),(0.01586260+0.01738945j),(0.01860229+0.01504935j),(0.02131307+0.01255311j),(0.02120088+0.00994925j),(0.02174744+0.00729828j),(0.02389086+0.00464873j),(0.02368491+0.00203057j),(0.02237166+-0.00051844j),(0.02328711+-0.00296093j),(0.02345981+-0.00528187j),(0.02123050+-0.00746818j),(0.02053854+-0.00949707j),(0.02099309+-0.01136173j),(0.01894061+-0.01306795j),(0.01681194+-0.01460908j),(0.01701462+-0.01598154j),(0.01588229+-0.01720018j),(0.01304182+-0.01827322j),(0.01241037+-0.01919918j),(0.01229429+-0.01999246j),(0.00973112+-0.02067153j),(0.00804623+-0.02123886j),(0.00843785+-0.02170265j),(0.00696066+-0.02208452j),(0.00456523+-0.02239297j),(0.00470046+-0.02262955j),(0.00456995+-0.02281159j),(0.00224442+-0.02295344j),(0.00156845+-0.02305363j),(0.00240400+-0.02312028j),(0.00098607+-0.02317014j),(-0.00051374+-0.02320353j),(0.00050009+-0.02321878j),(0.00045776+-0.02322941j),(-0.00131008+-0.02324041j),(-0.00087251+-0.02324385j),(0.00032002+-0.02324492j),(-0.00088102+-0.02325229j),(-0.00132710+-0.02325730j),(0.00043224+-0.02325473j),(0.00046606+-0.02325254j),(-0.00055627+-0.02324573j),(0.00093504+-0.02322077j),(0.00234447+-0.02317935j),(0.00150042+-0.02312113j),(0.00216789+-0.02302937j),(0.00448493+-0.02289594j),(0.00460695+-0.02272233j),(0.00446323+-0.02249417j),(0.00685018+-0.02219413j),(0.00831890+-0.02182068j),(0.00791880+-0.02136529j),(0.00959522+-0.02080637j),(0.01214993+-0.02013569j),(0.01225755+-0.01935081j),(0.01288054+-0.01843323j),(0.01571257+-0.01736857j),(0.01683646+-0.01615831j),(0.01662535+-0.01479422j),(0.01874559+-0.01326144j),(0.02078965+-0.01156357j),(0.02032669+-0.00970726j),(0.02101025+-0.00768671j),(0.02323116+-0.00550873j),(0.02305008+-0.00319611j),(0.02212625+-0.00076193j),(0.02343113+0.00177878j),(0.02362872+0.00438864j),(0.02147695+0.00702990j),(0.02092206+0.00967260j),(0.02102591+0.01226820j),(0.01830682+0.01475618j),(0.01555882+0.01708805j),(0.01475498+0.01920960j),(0.01205410+0.02104906j),(0.00759903+0.02254460j),(0.00522996+0.02364565j),(0.00283580+0.02428964j),(-0.00215220+0.02441904j),(-0.00606837+0.02400199j),(-0.00812197+0.02301099j),(-0.01218249+0.02142103j),(-0.01670401+0.01923914j),(-0.01838048+0.01649720j),(-0.02026558+0.01323224j),(-0.02374292+0.00950911j),(-0.02474275+0.00543106j),(-0.02383284+0.00111215j),(-0.02457441+-0.00332035j),(-0.02432276+-0.00770454j),(-0.02085044+-0.01186634j),(-0.01799451+-0.01564086j),(-0.01599037+-0.01885398j),(-0.01104192+-0.02132979j),(-0.00523332+-0.02292682j),(-0.00160954+-0.02353476j),(0.00309865+-0.02306451j),(0.00963778+-0.02148096j),(0.01373706+-0.01881724j),(0.01632781+-0.01515210j),(0.02055862+-0.01062007j),(0.02321120+-0.00543317j),(0.02236040+0.00014572j),(0.02187490+0.00582533j),(0.02135635+0.01127338j),(0.01709411+0.01613784j),(0.01164660+0.02009512j),(0.00772582+0.02285751j),(0.00190003+0.02418045j),(-0.00590449+0.02391164j),(-0.01126323+0.02201387j),(-0.01543856+0.01855082j),(-0.02109475+0.01370484j),(-0.02449236+0.00779506j),(-0.02407900+0.00123981j),(-0.02375583+-0.00547727j),(-0.02258831+-0.01181357j),(-0.01714748+-0.01721692j),(-0.01058524+-0.02119903j),(-0.00539325+-0.02336477j),(0.00181327+-0.02344339j),(0.01030108+-0.02135341j),(0.01549809+-0.01722522j),(0.01899193+-0.01137834j),(0.02301101+-0.00432105j),(0.02354963+0.00326790j),(0.01975516+0.01062396j),(0.01586459+0.01697709j),(0.01082437+0.02160722j),(0.00195926+0.02394037j),(-0.00667475+0.02364921j),(-0.01261097+0.02068667j),(-0.01904868+0.01530237j),(-0.02456282+0.00806391j),(-0.02484066+-0.00019547j),(-0.02230780+-0.00848953j),(-0.01939949+-0.01577567j),(-0.01267617+-0.02107210j),(-0.00276292+-0.02362235j),(0.00521979+-0.02301533j),(0.01206734+-0.01923778j),(0.01962366+-0.01271841j),(0.02336398+-0.00431727j),(0.02167843+0.00479782j),(0.01865091+0.01331240j),(0.01351068+0.01992594j),(0.00364254+0.02356375j),(-0.00655347+0.02358642j),(-0.01373868+0.01989770j),(-0.02059567+0.01298204j),(-0.02560544+0.00389065j),(-0.02416955+-0.00590384j),(-0.01875171+-0.01476260j),(-0.01261282+-0.02113823j),(-0.00312195+-0.02383687j),(0.00854232+-0.02228354j),(0.01638866+-0.01666781j),(0.02055453+-0.00792348j),(0.02318451+0.00239139j),(0.02043239+0.01233487j),(0.01146992+0.01996460j),(0.00188587+0.02373299j),(-0.00732373+0.02279084j),(-0.01775700+0.01720788j),(-0.02450487+0.00803676j),(-0.02409011+-0.00287702j),(-0.02028606+-0.01326259j),(-0.01374098+-0.02086429j),(-0.00203556+-0.02394156j),(0.01019532+-0.02171662j),(0.01756163+-0.01458515j),(0.02183148+-0.00406587j),(0.02265956+0.00746769j),(0.01582869+0.01730082j),(0.00422483+0.02304547j),(-0.00624986+0.02322842j),(-0.01612061+0.01767670j),(-0.02440502+0.00765165j),(-0.02503698+-0.00438867j),(-0.01832302+-0.01539677j),(-0.00945772+-0.02249858j),(0.00182667+-0.02373443j),(0.01477230+-0.01865082j),(0.02214697+-0.00851416j),(0.02162822+0.00398788j),(0.01706112+0.01543188j),(0.00776400+0.02256160j),(-0.00653093+0.02324556j),(-0.01836250+0.01717191j),(-0.02322486+0.00599483j),(-0.02349301+-0.00707719j),(-0.01785950+-0.01814609j),(-0.00459606+-0.02379415j),(0.00941016+-0.02219110j),(0.01805052+-0.01371247j),(0.02237594+-0.00090253j),(0.02058168+0.01221451j),(0.00944658+0.02138083j),(-0.00521035+0.02352668j),(-0.01615397+0.01782232j),(-0.02328871+0.00603130j),(-0.02459070+-0.00794931j),(-0.01561542+-0.01933910j),(-0.00094113+-0.02413884j),(0.01135172+-0.02055928j),(0.02010364+-0.00972375j),(0.02336008+0.00458982j),(0.01591029+0.01720974j),(0.00118626+0.02345919j),(-0.01197966+0.02091453j),(-0.02133304+0.01037878j),(-0.02523851+-0.00427314j),(-0.01829790+-0.01745198j),(-0.00310083+-0.02400326j),(0.01073033+-0.02125717j),(0.01976254+-0.01015436j),(0.02292653+0.00497621j),(0.01532793+0.01802839j),(-0.00081927+0.02359902j),(-0.01502170+0.01926989j),(-0.02265340+0.00669953j),(-0.02355173+-0.00891805j),(-0.01414851+-0.02092413j),(0.00315332+-0.02405991j),(0.01700428+-0.01683705j),(0.02181522+-0.00229589j),(0.01888235+0.01318659j),(0.00690161+0.02262555j),(-0.01101739+0.02161926j),(-0.02299831+0.01049104j),(-0.02316414+-0.00573173j),(-0.01509439+-0.01952015j),(-0.00065343+-0.02429546j),(0.01616494+-0.01763472j),(0.02389068+-0.00261417j),(0.01761194+0.01354112j),(0.00414674+0.02289380j),(-0.01099794+0.02068721j),(-0.02355431+0.00785592j),(-0.02422667+-0.00922639j),(-0.01074518+-0.02186431j),(0.00627478+-0.02348246j),(0.01830763+-0.01309054j),(0.02262192+0.00401019j),(0.01453787+0.01880671j),(-0.00420648+0.02331843j),(-0.02000293+0.01498004j),(-0.02368630+-0.00182110j),(-0.01693241+-0.01795157j),(-0.00177424+-0.02442712j),(0.01639709+-0.01748068j),(0.02412431+-0.00089115j),(0.01558734+0.01597816j),(-0.00060185+0.02342245j),(-0.01594981+0.01698990j),(-0.02489208+0.00026289j),(-0.01937465+-0.01698258j),(-0.00050994+-0.02445753j),(0.01675346+-0.01756350j),(0.02194798+-0.00031359j),(0.01549416+0.01687115j),(-0.00080805+0.02336950j),(-0.01956431+0.01499380j),(-0.02555894+-0.00316231j),(-0.01407676+-0.01972087j),(0.00387995+-0.02411477j),(0.01815750+-0.01337181j),(0.02261638+0.00572415j),(0.01119851+0.02076075j),(-0.01027548+0.02176458j),(-0.02378735+0.00792261j),(-0.02073710+-0.01166201j),(-0.00655238+-0.02384237j),(0.01169986+-0.02023245j),(0.02376405+-0.00312680j),(0.01756330+0.01576917j),(-0.00302628+0.02329423j),(-0.01989332+0.01404993j),(-0.02284616+-0.00561099j),(-0.01244888+-0.02178506j),(0.00728716+-0.02278810j),(0.02319372+-0.00772838j),(0.01973848+0.01254395j),(0.00031797+0.02316643j),(-0.01752725+0.01617323j),(-0.02328236+-0.00341306j),(-0.01425954+-0.02099251j),(0.00664748+-0.02319640j),(0.02331891+-0.00818653j),(0.01923788+0.01263876j),(-0.00040386+0.02319894j),(-0.01798231+0.01515042j),(-0.02319131+-0.00538400j),(-0.01229720+-0.02226184j),(0.00999677+-0.02195641j),(0.02434003+-0.00455364j),(0.01604609+0.01600310j),(-0.00518349+0.02299175j),(-0.02074064+0.01054250j),(-0.02154841+-0.01125960j),(-0.00600294+-0.02434147j),(0.01644221+-0.01762867j),(0.02426372+0.00337979j),(0.00866567+0.02093382j),(-0.01349737+0.01998132j),(-0.02316326+0.00116600j),(-0.01552895+-0.01936448j),(0.00489266+-0.02366708j),(0.02288064+-0.00779149j),(0.01905808+0.01435519j),(-0.00415179+0.02306118j),(-0.02223238+0.01040225j),(-0.02006210+-0.01237916j),(-0.00257792+-0.02465827j),(0.01731670+-0.01507409j),(0.02283900+0.00773453j),(0.00493151+0.02268122j),(-0.01936267+0.01572689j),(-0.02371281+-0.00676123j),(-0.00623337+-0.02364582j),(0.01438717+-0.01880586j),(0.02201631+0.00332458j),(0.00946171+0.02153294j),(-0.01501010+0.01807106j),(-0.02582517+-0.00385431j),(-0.00940766+-0.02281595j),(0.01495970+-0.02004212j),(0.02202077+0.00187621j),(0.00838948+0.02115027j),(-0.01336429+0.01830765j),(-0.02450863+-0.00397521j),(-0.01078882+-0.02308837j),(0.01569011+-0.01935874j),(0.02392356+0.00351545j),(0.00544088+0.02189697j),(-0.01701567+0.01653773j),(-0.02201465+-0.00712924j),(-0.00655104+-0.02420616j),(0.01656124+-0.01642505j),(0.02316654+0.00807195j),(0.00223175+0.02297375j),(-0.02225544+0.01205102j),(-0.02062143+-0.01296157j),(0.00245018+-0.02475074j),(0.02035457+-0.01021511j),(0.01698212+0.01473450j),(-0.00479837+0.02230807j),(-0.02405795+0.00383826j),(-0.01644163+-0.01999041j),(0.01161641+-0.02213279j),(0.02455904+-0.00001102j),(0.00736853+0.02113068j),(-0.01662985+0.01687980j),(-0.02153153+-0.00802806j),(-0.00408852+-0.02465853j),(0.01872438+-0.01338172j),(0.02080758+0.01268143j),(-0.00425119+0.02262927j),(-0.02546509+0.00446639j),(-0.01418121+-0.02015886j),(0.01328869+-0.02156838j),(0.02213978+0.00209671j),(0.00519009+0.02213774j),(-0.01738613+0.01391213j),(-0.02124442+-0.01265423j),(0.00089345+-0.02469168j),(0.02381239+-0.00705952j),(0.01521708+0.01828689j),(-0.01456920+0.01942309j),(-0.02419403+-0.00522223j),(-0.00293785+-0.02442353j),(0.01937043+-0.01356869j),(0.01721030+0.01358739j),(-0.00569934+0.02199681j),(-0.02344721+0.00055053j),(-0.01180893+-0.02287978j),(0.01736723+-0.01746149j),(0.02291610+0.00979998j),(-0.00447164+0.02280180j),(-0.02474143+0.00412548j),(-0.01082613+-0.02136534j),(0.01515031+-0.01936571j),(0.02017119+0.00760639j),(-0.00029791+0.02297822j),(-0.02180382+0.00558633j),(-0.01532691+-0.02084437j),(0.01409426+-0.01977065j),(0.02427788+0.00746627j),(-0.00207474+0.02294241j),(-0.02515358+0.00489499j),(-0.01104608+-0.02140500j),(0.01673671+-0.01877310j),(0.01940118+0.00922002j),(-0.00363608+0.02269900j),(-0.02189215+0.00216139j),(-0.01096697+-0.02294136j),(0.01704638+-0.01614275j),(0.02146772+0.01282455j),(-0.00794443+0.02157766j),(-0.02618127+-0.00286864j),(-0.00356489+-0.02449500j),(0.02323552+-0.01117854j),(0.01337505+0.01737378j),(-0.01480595+0.01845229j),(-0.02008447+-0.00973817j),(0.00256215+-0.02470990j),(0.02168193+-0.00357103j),(0.00961393+0.02158880j),(-0.01896089+0.01210319j),(-0.01963154+-0.01758030j),(0.01226390+-0.02152289j),(0.02498351+0.00651609j),(-0.00368589+0.02282988j),(-0.02544639+0.00178790j),(-0.00556671+-0.02360365j),(0.02100917+-0.01319132j),(0.01364648+0.01666282j),(-0.01325657+0.01837860j),(-0.01971550+-0.01099600j),(0.00369291+-0.02426533j),(0.02293077+0.00021205j),(0.00579169+0.02279006j),(-0.02312596+0.00650691j),(-0.01377748+-0.02206335j),(0.02044061+-0.01585712j),(0.01941082+0.01495775j),(-0.01518360+0.01927720j),(-0.02220819+-0.01016980j),(0.00821958+-0.02421550j),(0.02262671+0.00081191j),(-0.00006447+0.02285208j),(-0.02086471+0.00448657j),(-0.00797556+-0.02324057j),(0.01805930+-0.01287617j),(0.01550469+0.01800651j),(-0.01429496+0.01612799j),(-0.02116446+-0.01544021j),(0.01064431+-0.02180374j),(0.02511146+0.00826508j),(-0.00656036+0.02208635j),(-0.02635085+-0.00471061j),(0.00285545+-0.02484618j),(0.02590055+-0.00260856j),(0.00151923+0.02252375j),(-0.02324422+0.00565692j),(-0.00564362+-0.02311142j),(0.02012448+-0.01198163j),(0.01060470+0.01913652j),(-0.01612061+0.01375097j),(-0.01497921+-0.01855992j),(0.01313874+-0.01864052j),(0.01969528+0.01388930j),(-0.01024188+0.01897994j),(-0.02289766+-0.01297947j),(0.00898346+-0.02251392j),(0.02567490+0.00829177j),(-0.00762545+0.02168184j),(-0.02621401+-0.00757041j),(0.00738953+-0.02423742j),(0.02624283+0.00325649j),(-0.00606391+0.02268076j),(-0.02432289+-0.00301084j),(0.00508189+-0.02471912j),(0.02288677+-0.00070921j),(-0.00237157+0.02287168j),(-0.02063159+0.00030671j),(0.00008449+-0.02475648j),(0.02019551+-0.00329145j),(0.00345584+0.02289011j),(-0.01978428+0.00213488j),(-0.00546033+-0.02478600j),(0.02165582+-0.00431751j),(0.00760878+0.02296855j),(-0.02315494+0.00239354j),(-0.00708985+-0.02486267j),(0.02611751+-0.00381419j),(0.00624053+0.02303512j),(-0.02734557+0.00121452j),(-0.00273325+-0.02484907j),(0.02884997+-0.00199281j),(-0.00024142+0.02294122j),(-0.02761093+-0.00115952j),(0.00470759+-0.02461541j),(0.02644933+0.00093286j),(-0.00714949+0.02256909j),(-0.02288929+-0.00459791j),(0.01001978+-0.02400982j),(0.02053184+0.00494132j),(-0.01021289+0.02165675j),(-0.01701486+-0.00916683j),(0.01112326+-0.02257625j),(0.01598886+0.01010683j),(-0.01018562+0.01949248j),(-0.01438195+-0.01481467j),(0.01141051+-0.01934048j),(0.01521281+0.01608428j),(-0.01206190+0.01491348j),(-0.01438542+-0.02073793j),(0.01584389+-0.01311707j),(0.01445882+0.02149538j),(-0.01897992+0.00695941j),(-0.01097459+-0.02496196j),(0.02430611+-0.00346686j),(0.00702990+0.02387776j),(-0.02695673+-0.00399747j),(0.00110914+-0.02478917j),(0.02942465+0.00820808j),(-0.00894163+0.02067896j),(-0.02677483+-0.01544431j),(0.01909190+-0.01827317j),(0.02243210+0.01839055j),(-0.02591810+0.01102543j),(-0.01260941+-0.02317432j),(0.03183338+-0.00599552j),(0.00248067+0.02268924j),(-0.03142327+-0.00283829j),(0.01042196+-0.02324803j),(0.02840599+0.00817339j),(-0.01976433+0.01828951j),(-0.01889172+-0.01573242j),(0.02795010+-0.01456927j),(0.00862557+0.01832590j),(-0.02915380+0.00621035j),(0.00482560+-0.02180224j),(0.02740380+-0.00037603j),(-0.01459584+0.01952921j),(-0.01868915+-0.00833408j),(0.02322657+-0.01785406j),(0.00923519+0.01272718j),(-0.02462280+0.01094846j),(0.00348175+-0.01821206j),(0.02320314+-0.00571021j),(-0.01223934+0.01810048j),(-0.01487181+-0.00303782j),(0.01988380+-0.01829157j),(0.00628858+0.00820836j),(-0.02021617+0.01296199j),(0.00524589+-0.01483497j),(0.01815093+-0.00865789j),(-0.01229325+0.01623340j),(-0.00957386+0.00035149j),(0.01813896+-0.01786222j),(0.00156817+0.00507228j),(-0.01661852+0.01390390j),(0.00878338+-0.01233696j),(0.01322760+-0.01050668j),(-0.01391762+0.01483193j),(-0.00389248+0.00274962j)]' 515 | type: ccc 516 | states: 517 | bus_sink: false 518 | bus_source: false 519 | bus_structure: null 520 | coordinate: [974, 333] 521 | rotation: 0 522 | state: enabled 523 | - name: fosphor_glfw_sink_c_0 524 | id: fosphor_glfw_sink_c 525 | parameters: 526 | affinity: '' 527 | alias: '' 528 | comment: '' 529 | freq_center: '0' 530 | freq_span: samp_rate 531 | wintype: firdes.WIN_BLACKMAN_hARRIS 532 | states: 533 | bus_sink: false 534 | bus_source: false 535 | bus_structure: null 536 | coordinate: [976, 248] 537 | rotation: 0 538 | state: enabled 539 | - name: qtgui_time_sink_x_1 540 | id: qtgui_time_sink_x 541 | parameters: 542 | affinity: '' 543 | alias: '' 544 | alpha1: '1.0' 545 | alpha10: '1.0' 546 | alpha2: '1.0' 547 | alpha3: '1.0' 548 | alpha4: '1.0' 549 | alpha5: '1.0' 550 | alpha6: '1.0' 551 | alpha7: '1.0' 552 | alpha8: '1.0' 553 | alpha9: '1.0' 554 | autoscale: 'False' 555 | axislabels: 'True' 556 | color1: blue 557 | color10: dark blue 558 | color2: red 559 | color3: green 560 | color4: black 561 | color5: cyan 562 | color6: magenta 563 | color7: yellow 564 | color8: dark red 565 | color9: dark green 566 | comment: '' 567 | ctrlpanel: 'False' 568 | entags: 'False' 569 | grid: 'False' 570 | gui_hint: '' 571 | label1: Signal 1 572 | label10: Signal 10 573 | label2: Signal 2 574 | label3: Signal 3 575 | label4: Signal 4 576 | label5: Signal 5 577 | label6: Signal 6 578 | label7: Signal 7 579 | label8: Signal 8 580 | label9: Signal 9 581 | legend: 'True' 582 | marker1: '-1' 583 | marker10: '-1' 584 | marker2: '-1' 585 | marker3: '-1' 586 | marker4: '-1' 587 | marker5: '-1' 588 | marker6: '-1' 589 | marker7: '-1' 590 | marker8: '-1' 591 | marker9: '-1' 592 | name: '""' 593 | nconnections: '2' 594 | size: '10240' 595 | srate: samp_rate 596 | stemplot: 'False' 597 | style1: '1' 598 | style10: '1' 599 | style2: '1' 600 | style3: '1' 601 | style4: '1' 602 | style5: '1' 603 | style6: '1' 604 | style7: '1' 605 | style8: '1' 606 | style9: '1' 607 | tr_chan: '0' 608 | tr_delay: '0' 609 | tr_level: '.7' 610 | tr_mode: qtgui.TRIG_MODE_NORM 611 | tr_slope: qtgui.TRIG_SLOPE_POS 612 | tr_tag: '""' 613 | type: float 614 | update_time: '0.10' 615 | width1: '1' 616 | width10: '1' 617 | width2: '1' 618 | width3: '1' 619 | width4: '1' 620 | width5: '1' 621 | width6: '1' 622 | width7: '1' 623 | width8: '1' 624 | width9: '1' 625 | ylabel: Amplitude 626 | ymax: '0.005' 627 | ymin: '-0.005' 628 | yunit: '""' 629 | states: 630 | bus_sink: false 631 | bus_source: false 632 | bus_structure: null 633 | coordinate: [1893, 378] 634 | rotation: 0 635 | state: enabled 636 | - name: uhd_usrp_source_0 637 | id: uhd_usrp_source 638 | parameters: 639 | affinity: '' 640 | alias: '' 641 | ant0: RX2 642 | ant1: RX2 643 | ant10: RX2 644 | ant11: RX2 645 | ant12: RX2 646 | ant13: RX2 647 | ant14: RX2 648 | ant15: RX2 649 | ant16: RX2 650 | ant17: RX2 651 | ant18: RX2 652 | ant19: RX2 653 | ant2: RX2 654 | ant20: RX2 655 | ant21: RX2 656 | ant22: RX2 657 | ant23: RX2 658 | ant24: RX2 659 | ant25: RX2 660 | ant26: RX2 661 | ant27: RX2 662 | ant28: RX2 663 | ant29: RX2 664 | ant3: RX2 665 | ant30: RX2 666 | ant31: RX2 667 | ant4: RX2 668 | ant5: RX2 669 | ant6: RX2 670 | ant7: RX2 671 | ant8: RX2 672 | ant9: RX2 673 | bw0: 11e6 674 | bw1: '0' 675 | bw10: '0' 676 | bw11: '0' 677 | bw12: '0' 678 | bw13: '0' 679 | bw14: '0' 680 | bw15: '0' 681 | bw16: '0' 682 | bw17: '0' 683 | bw18: '0' 684 | bw19: '0' 685 | bw2: '0' 686 | bw20: '0' 687 | bw21: '0' 688 | bw22: '0' 689 | bw23: '0' 690 | bw24: '0' 691 | bw25: '0' 692 | bw26: '0' 693 | bw27: '0' 694 | bw28: '0' 695 | bw29: '0' 696 | bw3: '0' 697 | bw30: '0' 698 | bw31: '0' 699 | bw4: '0' 700 | bw5: '0' 701 | bw6: '0' 702 | bw7: '0' 703 | bw8: '0' 704 | bw9: '0' 705 | center_freq0: uhd.tune_request_t(freq, 6e6) 706 | center_freq1: '0' 707 | center_freq10: '0' 708 | center_freq11: '0' 709 | center_freq12: '0' 710 | center_freq13: '0' 711 | center_freq14: '0' 712 | center_freq15: '0' 713 | center_freq16: '0' 714 | center_freq17: '0' 715 | center_freq18: '0' 716 | center_freq19: '0' 717 | center_freq2: '0' 718 | center_freq20: '0' 719 | center_freq21: '0' 720 | center_freq22: '0' 721 | center_freq23: '0' 722 | center_freq24: '0' 723 | center_freq25: '0' 724 | center_freq26: '0' 725 | center_freq27: '0' 726 | center_freq28: '0' 727 | center_freq29: '0' 728 | center_freq3: '0' 729 | center_freq30: '0' 730 | center_freq31: '0' 731 | center_freq4: '0' 732 | center_freq5: '0' 733 | center_freq6: '0' 734 | center_freq7: '0' 735 | center_freq8: '0' 736 | center_freq9: '0' 737 | clock_rate: 0e0 738 | clock_source0: '' 739 | clock_source1: '' 740 | clock_source2: '' 741 | clock_source3: '' 742 | clock_source4: '' 743 | clock_source5: '' 744 | clock_source6: '' 745 | clock_source7: '' 746 | comment: '' 747 | dc_offs_enb0: '""' 748 | dc_offs_enb1: '""' 749 | dc_offs_enb10: '""' 750 | dc_offs_enb11: '""' 751 | dc_offs_enb12: '""' 752 | dc_offs_enb13: '""' 753 | dc_offs_enb14: '""' 754 | dc_offs_enb15: '""' 755 | dc_offs_enb16: '""' 756 | dc_offs_enb17: '""' 757 | dc_offs_enb18: '""' 758 | dc_offs_enb19: '""' 759 | dc_offs_enb2: '""' 760 | dc_offs_enb20: '""' 761 | dc_offs_enb21: '""' 762 | dc_offs_enb22: '""' 763 | dc_offs_enb23: '""' 764 | dc_offs_enb24: '""' 765 | dc_offs_enb25: '""' 766 | dc_offs_enb26: '""' 767 | dc_offs_enb27: '""' 768 | dc_offs_enb28: '""' 769 | dc_offs_enb29: '""' 770 | dc_offs_enb3: '""' 771 | dc_offs_enb30: '""' 772 | dc_offs_enb31: '""' 773 | dc_offs_enb4: '""' 774 | dc_offs_enb5: '""' 775 | dc_offs_enb6: '""' 776 | dc_offs_enb7: '""' 777 | dc_offs_enb8: '""' 778 | dc_offs_enb9: '""' 779 | dev_addr: '""' 780 | dev_args: '""' 781 | gain0: gain 782 | gain1: '0' 783 | gain10: '0' 784 | gain11: '0' 785 | gain12: '0' 786 | gain13: '0' 787 | gain14: '0' 788 | gain15: '0' 789 | gain16: '0' 790 | gain17: '0' 791 | gain18: '0' 792 | gain19: '0' 793 | gain2: '0' 794 | gain20: '0' 795 | gain21: '0' 796 | gain22: '0' 797 | gain23: '0' 798 | gain24: '0' 799 | gain25: '0' 800 | gain26: '0' 801 | gain27: '0' 802 | gain28: '0' 803 | gain29: '0' 804 | gain3: '0' 805 | gain30: '0' 806 | gain31: '0' 807 | gain4: '0' 808 | gain5: '0' 809 | gain6: '0' 810 | gain7: '0' 811 | gain8: '0' 812 | gain9: '0' 813 | iq_imbal_enb0: '""' 814 | iq_imbal_enb1: '""' 815 | iq_imbal_enb10: '""' 816 | iq_imbal_enb11: '""' 817 | iq_imbal_enb12: '""' 818 | iq_imbal_enb13: '""' 819 | iq_imbal_enb14: '""' 820 | iq_imbal_enb15: '""' 821 | iq_imbal_enb16: '""' 822 | iq_imbal_enb17: '""' 823 | iq_imbal_enb18: '""' 824 | iq_imbal_enb19: '""' 825 | iq_imbal_enb2: '""' 826 | iq_imbal_enb20: '""' 827 | iq_imbal_enb21: '""' 828 | iq_imbal_enb22: '""' 829 | iq_imbal_enb23: '""' 830 | iq_imbal_enb24: '""' 831 | iq_imbal_enb25: '""' 832 | iq_imbal_enb26: '""' 833 | iq_imbal_enb27: '""' 834 | iq_imbal_enb28: '""' 835 | iq_imbal_enb29: '""' 836 | iq_imbal_enb3: '""' 837 | iq_imbal_enb30: '""' 838 | iq_imbal_enb31: '""' 839 | iq_imbal_enb4: '""' 840 | iq_imbal_enb5: '""' 841 | iq_imbal_enb6: '""' 842 | iq_imbal_enb7: '""' 843 | iq_imbal_enb8: '""' 844 | iq_imbal_enb9: '""' 845 | lo_export0: 'False' 846 | lo_export1: 'False' 847 | lo_export10: 'False' 848 | lo_export11: 'False' 849 | lo_export12: 'False' 850 | lo_export13: 'False' 851 | lo_export14: 'False' 852 | lo_export15: 'False' 853 | lo_export16: 'False' 854 | lo_export17: 'False' 855 | lo_export18: 'False' 856 | lo_export19: 'False' 857 | lo_export2: 'False' 858 | lo_export20: 'False' 859 | lo_export21: 'False' 860 | lo_export22: 'False' 861 | lo_export23: 'False' 862 | lo_export24: 'False' 863 | lo_export25: 'False' 864 | lo_export26: 'False' 865 | lo_export27: 'False' 866 | lo_export28: 'False' 867 | lo_export29: 'False' 868 | lo_export3: 'False' 869 | lo_export30: 'False' 870 | lo_export31: 'False' 871 | lo_export4: 'False' 872 | lo_export5: 'False' 873 | lo_export6: 'False' 874 | lo_export7: 'False' 875 | lo_export8: 'False' 876 | lo_export9: 'False' 877 | lo_source0: internal 878 | lo_source1: internal 879 | lo_source10: internal 880 | lo_source11: internal 881 | lo_source12: internal 882 | lo_source13: internal 883 | lo_source14: internal 884 | lo_source15: internal 885 | lo_source16: internal 886 | lo_source17: internal 887 | lo_source18: internal 888 | lo_source19: internal 889 | lo_source2: internal 890 | lo_source20: internal 891 | lo_source21: internal 892 | lo_source22: internal 893 | lo_source23: internal 894 | lo_source24: internal 895 | lo_source25: internal 896 | lo_source26: internal 897 | lo_source27: internal 898 | lo_source28: internal 899 | lo_source29: internal 900 | lo_source3: internal 901 | lo_source30: internal 902 | lo_source31: internal 903 | lo_source4: internal 904 | lo_source5: internal 905 | lo_source6: internal 906 | lo_source7: internal 907 | lo_source8: internal 908 | lo_source9: internal 909 | maxoutbuf: '0' 910 | minoutbuf: '0' 911 | nchan: '1' 912 | norm_gain0: 'True' 913 | norm_gain1: 'False' 914 | norm_gain10: 'False' 915 | norm_gain11: 'False' 916 | norm_gain12: 'False' 917 | norm_gain13: 'False' 918 | norm_gain14: 'False' 919 | norm_gain15: 'False' 920 | norm_gain16: 'False' 921 | norm_gain17: 'False' 922 | norm_gain18: 'False' 923 | norm_gain19: 'False' 924 | norm_gain2: 'False' 925 | norm_gain20: 'False' 926 | norm_gain21: 'False' 927 | norm_gain22: 'False' 928 | norm_gain23: 'False' 929 | norm_gain24: 'False' 930 | norm_gain25: 'False' 931 | norm_gain26: 'False' 932 | norm_gain27: 'False' 933 | norm_gain28: 'False' 934 | norm_gain29: 'False' 935 | norm_gain3: 'False' 936 | norm_gain30: 'False' 937 | norm_gain31: 'False' 938 | norm_gain4: 'False' 939 | norm_gain5: 'False' 940 | norm_gain6: 'False' 941 | norm_gain7: 'False' 942 | norm_gain8: 'False' 943 | norm_gain9: 'False' 944 | num_mboards: '1' 945 | otw: '' 946 | rx_agc0: Default 947 | rx_agc1: Default 948 | rx_agc10: Default 949 | rx_agc11: Default 950 | rx_agc12: Default 951 | rx_agc13: Default 952 | rx_agc14: Default 953 | rx_agc15: Default 954 | rx_agc16: Default 955 | rx_agc17: Default 956 | rx_agc18: Default 957 | rx_agc19: Default 958 | rx_agc2: Default 959 | rx_agc20: Default 960 | rx_agc21: Default 961 | rx_agc22: Default 962 | rx_agc23: Default 963 | rx_agc24: Default 964 | rx_agc25: Default 965 | rx_agc26: Default 966 | rx_agc27: Default 967 | rx_agc28: Default 968 | rx_agc29: Default 969 | rx_agc3: Default 970 | rx_agc30: Default 971 | rx_agc31: Default 972 | rx_agc4: Default 973 | rx_agc5: Default 974 | rx_agc6: Default 975 | rx_agc7: Default 976 | rx_agc8: Default 977 | rx_agc9: Default 978 | samp_rate: samp_rate 979 | sd_spec0: '' 980 | sd_spec1: '' 981 | sd_spec2: '' 982 | sd_spec3: '' 983 | sd_spec4: '' 984 | sd_spec5: '' 985 | sd_spec6: '' 986 | sd_spec7: '' 987 | show_lo_controls: 'False' 988 | stream_args: '' 989 | stream_chans: '[]' 990 | sync: sync 991 | time_source0: '' 992 | time_source1: '' 993 | time_source2: '' 994 | time_source3: '' 995 | time_source4: '' 996 | time_source5: '' 997 | time_source6: '' 998 | time_source7: '' 999 | type: fc32 1000 | states: 1001 | bus_sink: false 1002 | bus_source: false 1003 | bus_structure: null 1004 | coordinate: [375, 418] 1005 | rotation: 0 1006 | state: enabled 1007 | 1008 | connections: 1009 | - [analog_sig_source_x_0, '0', blocks_multiply_xx_0, '1'] 1010 | - [blocks_add_const_vxx_0, '0', qtgui_time_sink_x_1, '1'] 1011 | - [blocks_complex_to_mag_squared_0, '0', blocks_multiply_const_vxx_0, '0'] 1012 | - [blocks_complex_to_mag_squared_0_1, '0', blocks_multiply_const_vxx_0_0, '0'] 1013 | - [blocks_delay_0, '0', blocks_complex_to_mag_squared_0_1, '0'] 1014 | - [blocks_file_source_0, '0', blocks_throttle_0, '0'] 1015 | - [blocks_multiply_const_vxx_0, '0', qtgui_time_sink_x_1, '0'] 1016 | - [blocks_multiply_const_vxx_0_0, '0', blocks_add_const_vxx_0, '0'] 1017 | - [blocks_multiply_xx_0, '0', fft_filter_xxx_0, '0'] 1018 | - [blocks_throttle_0, '0', blocks_multiply_xx_0, '0'] 1019 | - [fft_filter_xxx_0, '0', blocks_delay_0, '0'] 1020 | - [fft_filter_xxx_0, '0', fft_filter_xxx_1, '0'] 1021 | - [fft_filter_xxx_0, '0', fosphor_glfw_sink_c_0, '0'] 1022 | - [fft_filter_xxx_1, '0', blocks_complex_to_mag_squared_0, '0'] 1023 | - [uhd_usrp_source_0, '0', fft_filter_xxx_0, '0'] 1024 | 1025 | metadata: 1026 | file_format: 1 1027 | -------------------------------------------------------------------------------- /links.md: -------------------------------------------------------------------------------- 1 | This page contains a handfull of links that have helped me understand what's going on 2 | 3 | https://www.slideshare.net/ChiehChun/lte-training-course-128947387 4 | 5 | https://www.sharetechnote.com/html/Handbook_LTE_PhyParameter_DL_FDD.html 6 | 7 | https://www.sharetechnote.com/html/BasicProcedure_LTE_TimeSync.html 8 | 9 | https://twitter.com/DiGMi1/status/1463076874926673927 10 | 11 | https://github.com/tmbinc/random/tree/master/dji/ocusync2 12 | 13 | https://www.sharetechnote.com/html/Handbook_LTE_PseudoRandomSequence.html 14 | -------------------------------------------------------------------------------- /matlab/create_gnuradio_zc.m: -------------------------------------------------------------------------------- 1 | function [] = create_gnuradio_zc(fft_size, symbol_number) 2 | zc = conj(create_zc(fft_size, symbol_number)); 3 | 4 | fprintf('['); 5 | for idx=1:length(zc) 6 | fprintf('(%0.4f+%0.4fj)', real(zc(idx)), imag(zc(idx))); 7 | if (idx ~= length(zc)) 8 | fprintf(',') 9 | end 10 | end 11 | fprintf(']\n'); 12 | end 13 | 14 | -------------------------------------------------------------------------------- /matlab/find_zc.m: -------------------------------------------------------------------------------- 1 | % Exploits the fact that the first ZC sequence is symmetrical to find where 2 | % it starts. 3 | % 4 | % The basic idea is to read in FFT size blocks at each sample offset, split 5 | % the block in half, reverse the second half, and run a normalized cross 6 | % correlation. The reversed version of the second half should exactly 7 | % match the first half. And thanks to the CAZAC (constant amplitude, zero 8 | % autocorrelation) feature of the ZC sequence, there should be one very 9 | % large peak 10 | % The returned scores are the result of each cross correlation at each 11 | % offset and are complex values. To get the normalized score, take the 12 | % absolute value squared. 13 | % 14 | % NOTE: The offset with the highest value is the start of the ZC sequence, 15 | % and NOT the start of the cyclic prefix for that OFDM symbol!!!! 16 | function [scores] = find_zc(samples, sample_rate) 17 | fft_size = sample_rate / 15e3; 18 | short_cp_len = round(0.00000469 * sample_rate); 19 | 20 | % Buffer to store the xcorr scores. Since a full `fft_size` number of 21 | % samples is needed at each start index, don't seek all the way to the 22 | % last sample 23 | scores = zeros((length(samples) - fft_size - short_cp_len), 1); 24 | 25 | % Walk through each start offset 26 | for start_offset=1:length(scores) 27 | % Skip in by one short cyclic prefix and extract `fft_size` samples 28 | window = samples(start_offset+short_cp_len:start_offset+short_cp_len+fft_size-1); 29 | 30 | % The first window is just the first half of the OFDM symbol 31 | window_one = window(1:(fft_size/2)); 32 | % The second window is the second half of the OFDM symbol 33 | % *reversed* 34 | window_two = fliplr(window((fft_size/2) + 1:end)); 35 | 36 | % Run a normalized cross correlation with no lag (just do one 37 | % xcorr) 38 | scores(start_offset) = normalized_xcorr(window_one, window_two); 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /matlab/normalized_xcorr.m: -------------------------------------------------------------------------------- 1 | %% Computes the normalized cross correlation of two vectors 2 | % Used https://www.researchgate.net/post/How-can-one-calculate-normalized-cross-correlation-between-two-arrays 3 | % as the reference for this implementation 4 | function [score] = normalized_xcorr(window_one, window_two) 5 | assert(length(window_one) == length(window_two), "Windows must be equal length"); 6 | assert(isrow(window_one) || iscolumn(window_one), "Windows must be row/column vectors"); 7 | assert(isrow(window_two) || iscolumn(window_two), "Windows must be row/column vectors"); 8 | 9 | % Make both windows zero mean 10 | window_one = window_one - mean(window_one); 11 | window_two = window_two - mean(window_two); 12 | 13 | % Cross correlate and get the average 14 | xcorr_value = sum(window_one .* conj(window_two)) / length(window_one); 15 | 16 | % Final step in normalization 17 | score = xcorr_value / sqrt(var(window_one) * var(window_two)); 18 | end 19 | 20 | -------------------------------------------------------------------------------- /matlab/updated_scripts/calculate_channel.m: -------------------------------------------------------------------------------- 1 | % Calculate channel taps based on the 4th or 6th OFDM symbol (both are ZC sequences) 2 | % 3 | % There is almost certainly a better way to do this, but this gets the job done 4 | % 5 | % @param zc_seq Frequency domain ZC sequence from OFDM symbol number 4 or 6 (all FFT bins, no cyclic prefix) 6 | % @param sample_rate Sample rate (in Hz) of `zc_seq` 7 | % @param symbol_idx Which symbol (must be 4 or 6) is in the `zc_seq` vector 8 | % @return taps Result of dividing `zc_seq` into a golden reference copy of the selected ZC sequence 9 | function [taps] = calculate_channel(zc_seq, sample_rate, symbol_idx) 10 | assert(symbol_idx == 4 || symbol_idx == 6, "Symbol index must be 4 or 6"); 11 | fft_size = get_fft_size(sample_rate); 12 | 13 | % The golden reference needs to be in the frequency domain 14 | gold_seq = fftshift(fft(reshape(create_zc(fft_size, symbol_idx), size(zc_seq)))); 15 | 16 | % figure(400); 17 | % subplot(1, 2, 1); 18 | % plot(abs(gold_seq).^2) 19 | % subplot(1, 2, 2); 20 | % plot(abs(zc_seq).^2) 21 | 22 | taps = gold_seq ./ zc_seq; 23 | end -------------------------------------------------------------------------------- /matlab/updated_scripts/create_zc.m: -------------------------------------------------------------------------------- 1 | % Creates a ZC sequence that is mapped onto an OFDM symbol and FFT'd 2 | % 3 | % The results of this function can be used to cross correlate for the specified OFDM 4 | % symbol (numbers 4 or 6). There is no cyclic prefix added by this function! 5 | % 6 | % 601 samples are created by the ZC, but the middle sample is zeroed out. This is done 7 | % so that the DC carrier of the FFT is not populated 8 | % 9 | % @param fft_size Size of the OFDM FFT window (must be power of 2) 10 | % @param symbol_index Which ZC sequence symbol should be created (must be 4 or 6) 11 | % 12 | function [samples] = create_zc(fft_size, symbol_index) 13 | % Validate inputs 14 | assert(symbol_index == 4 || symbol_index == 6, "Invalid symbol index (must be 4 or 6)"); 15 | assert(log2(fft_size) == round(log2(fft_size)), "Invalid FFT size. Must be power of 2"); 16 | 17 | % Pick the correct root for the ZC sequence 18 | if (symbol_index == 4) 19 | root = 600; 20 | else 21 | root = 147; 22 | end 23 | 24 | % Would use MATLAB's zadoffChuSeq function, but Octave doesn't have that 25 | % The logic below was tested against the MATLAB function 26 | zc = reshape(exp(-1j * pi * root * (0:600) .* (1:601) / 601), [], 1); 27 | 28 | % Remove the middle value (this would be DC in the FFT) 29 | zc(301) = []; 30 | 31 | % Create a buffer to hold the freq domain carriers 32 | samples_freq = zeros(fft_size, 1); 33 | 34 | % Get which FFT bins should be used for data carriers 35 | data_carrier_indices = get_data_carrier_indices(fft_size * 15e3); 36 | 37 | % Assign just the data carrier bins (left to right) the ZC sequence values 38 | samples_freq(data_carrier_indices) = zc; 39 | 40 | % Convert to time domain making sure to flip the spectrum left to right first 41 | samples = ifft(fftshift(samples_freq)); 42 | end 43 | -------------------------------------------------------------------------------- /matlab/updated_scripts/extract_bursts_from_file.m: -------------------------------------------------------------------------------- 1 | % Searches through the specified file for the first ZC sequence, and extracts the full bursts (in time) 2 | % 3 | % It's very important that the `frequency_offset` be correct such that when applied, the signal is centered at DC (0 Hz) 4 | % Otherwise the start sample estimate from correlating for the ZC sequence will be off in time as well as the 5 | % correlation score being lower 6 | % 7 | % Function does accept the following varargs inputs: 8 | % 9 | % - SampleType: MATLAB numeric type that the samples in the provided file are stored as (ex: 'single', 'int16', etc) 10 | % Defaults to 'single' 11 | % - CorrelationFigNum: Figure number to use for plotting the results of the cross correlation in find_zc_indices_by_file 12 | % Defaults to -1 which does not show the figure. Valid values are -1, or > 0 13 | % 14 | % @param input_path File containing complex 32-bit floating point samples (interleaved I,Q,I,Q,...) 15 | % @param sample_rate Sample rate that the file was recorded at. Must be an integer multiple of 15.36 MSPS (the minimum 16 | % sample rate for the DroneID downlink) 17 | % @param frequency_offset How far off from DC the signal is in the recording (set to 0 for no frequency adjustment) 18 | % @param correlation_threshold Score on a scale from 0.0 to 1.0 where 1.0 is a perfect match with the ZC sequence. This 19 | % will determine how closely the recorded ZC sequence must match in order to be extracted 20 | % as a burst. Usually anywhere from 0.2 to 0.9 are usable values. 21 | % @param chunk_size How many samples to process at one time. This depends on how much RAM your system has. This value 22 | % should likely be set > 1e6 but < 20e6. But you do you. 23 | % @param padding How many additional samples before and after the burst to extract. Must be >= 0 24 | % @return bursts A matrix where each row contains one burst 25 | function [bursts] = extract_bursts_from_file(input_path, sample_rate, frequency_offset, correlation_threshold,... 26 | chunk_size, padding, varargin) 27 | 28 | assert(isstring(input_path) || ischar(input_path), "Input path must be a string or char array"); 29 | assert(isnumeric(sample_rate), "Sample rate must be numeric"); 30 | assert(sample_rate > 0, "Sample rate must be > 0") 31 | assert(isnumeric(frequency_offset), "Frequency offset must be numeric"); 32 | assert(isnumeric(correlation_threshold), "Correlation threshold must be numeric"); 33 | assert(correlation_threshold >= 0.0, "Correlation threshold must be >= 0.0"); 34 | assert(isnumeric(chunk_size), "Chunk size must be numeric"); 35 | assert(chunk_size > 0, "Chunk size must be > 0"); 36 | assert(isnumeric(padding), "Padding must be numeric"); 37 | assert(padding >= 0, "Padding must be >= 0"); 38 | assert(mod(length(varargin), 2) == 0, "Varargs length must be a multiple of 2"); 39 | 40 | if (correlation_threshold > 1.0) 41 | warning("Correlation threshold is greater than 1.0. This is likely going to cause the correlation to fail!"); 42 | end 43 | 44 | % Default the type of each I and Q value to 32-bit floating point 45 | sample_type = 'single'; 46 | correlation_fig_num = -1; 47 | 48 | % Process the varargs inputs if they exist 49 | for idx=1:2:length(varargin) 50 | key = varargin{idx}; 51 | val = varargin{idx+1}; 52 | 53 | switch (key) 54 | case 'SampleType' 55 | sample_type = val; 56 | case 'CorrelationFigNum' 57 | correlation_fig_num = val; 58 | otherwise 59 | error('Invalid varargs key "%s"', key); 60 | end 61 | end 62 | 63 | assert(isstring(sample_type) || ischar(sample_type), "SampleType must be a string or char array"); 64 | assert(isnumeric(correlation_fig_num), "CorrelationFigNum must be numeric"); 65 | assert(correlation_fig_num == -1 || correlation_fig_num > 0, "CorrelationFigNum must be -1 or > 0"); 66 | 67 | % Get the number of complex IQ samples in the input file 68 | num_samples = get_sample_count_of_file(input_path, sample_type); 69 | 70 | fft_size = get_fft_size(sample_rate); % Number of samples per OFDM symbol (minus cyclic prefix) 71 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate); 72 | 73 | % Pre-calculate the frequency offset as a complex value 74 | freq_offset_constant = 1j * pi * 2 * (frequency_offset / sample_rate); 75 | 76 | % The first ZC sequence is the 4th symbol, and the `find_zc_indices_by_file` function will (assuming no major 77 | % frequency offset) return the sample index of the first sample of the 5th OFDM symbol cyclic prefix. So, back the 78 | % index off by the number of samples in the first 4 OFDM symbols and their cyclic prefixes 79 | zc_seq_offset = (fft_size * 3) + long_cp_len + (short_cp_len * 3); 80 | 81 | % Find all instances of the first ZC sequence 82 | indices = find_zc_indices_by_file(input_path, sample_rate, frequency_offset, correlation_threshold, chunk_size, ... 83 | 'SampleType', sample_type, 'CorrelationFigNum', correlation_fig_num); 84 | 85 | % In the DJI Mini 2 there are 9 OFDM symbols: 2 long cyclic prefixes, 7 short. This isn't the case on all drones. 86 | % For some drones there are just 8 OFDM symbols. It looks like those drones just don't send the first OFDM symbol 87 | % that's present on the Mini 2. That symbol XOR's out to all zeros anyway, so it's not important. So, to keep 88 | % things consistent, the logic below will always extract out 9 OFDM symbols worth of samples. In later steps the 89 | % first OFDM symbol isn't used for anything. 90 | burst_sample_count = (padding * 2) + (long_cp_len * 2) + (short_cp_len * 7) + (fft_size * 9); 91 | 92 | % Pre-calculate the frequency offset adjustment vector as this will be constant for all bursts 93 | freq_offset_vec = reshape(exp(freq_offset_constant * [1:burst_sample_count]), [], 1); 94 | 95 | % It's not known right away if the first and last bursts are going to be clipped because there aren't enough 96 | % samples. So, as filthy as it is, use concatenation to build up a list of starting indices that will definitely 97 | % have all samples present in the input file 98 | valid_burst_indices = []; 99 | 100 | % Walk through the vector of ZC sequence indices 101 | for idx=1:length(indices) 102 | start_index = indices(idx); 103 | 104 | % Calculate when the burst will start and end 105 | actual_start_index = start_index - padding - zc_seq_offset; 106 | actual_end_index = actual_start_index + burst_sample_count; 107 | 108 | % Ensure that all samples related to this burst are present in the recording 109 | if (actual_start_index < 1) 110 | warning("Skipping burst at offset %d as the beginning of the burst has been clipped", start_index); 111 | continue 112 | end 113 | 114 | % Make sure that the burst is fully contained in this file and doesn't end after this file ends 115 | if (actual_end_index > num_samples) 116 | warning("Skipping burst at offset %d as the ending of the burst will be clipped", start_index); 117 | continue 118 | end 119 | 120 | % Again, concatenation is filthy, but necessary here since the actual number of bursts is unknown 121 | valid_burst_indices = [valid_burst_indices actual_start_index]; 122 | end 123 | 124 | % Now that the true number of bursts is known, create a buffer to hold everything 125 | bursts = zeros(length(valid_burst_indices), burst_sample_count); 126 | 127 | for idx=1:length(valid_burst_indices) 128 | % Read in the current burst. The starting index was calculated above. Force cast to a double just in case the 129 | % samples were not read in as such. Otherwise processing that happens later on could fail since much of 130 | % MATLAB's DSP functions require doubles 131 | burst = double(read_complex(input_path, valid_burst_indices(idx), burst_sample_count, sample_type)); 132 | 133 | % Adjust for the user-specified frequency offset that is present in the recording and save those samples off 134 | bursts(idx,:) = burst .* freq_offset_vec; 135 | end 136 | 137 | end 138 | 139 | -------------------------------------------------------------------------------- /matlab/updated_scripts/extract_ofdm_symbol_samples.m: -------------------------------------------------------------------------------- 1 | % Given time domain samples, extracts out each of the OFDM symbols minus their cyclic prefixes for both time and 2 | % frequency domains. First sample must be the exact start of the burst! 3 | % 4 | % @param samples Time domain samples as a row/column vector 5 | function [time_domain, freq_domain] = extract_ofdm_symbol_samples(samples, sample_rate) 6 | assert(isrow(samples) || iscolumn(samples), "Samples must be a row or column vector"); 7 | 8 | fft_size = get_fft_size(sample_rate); 9 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate); 10 | 11 | % List of cyclic prefix lengths for each OFDM symbol 12 | cp_lengths = [ 13 | long_cp_len,... 14 | short_cp_len,... 15 | short_cp_len,... 16 | short_cp_len,... 17 | short_cp_len,... 18 | short_cp_len,... 19 | short_cp_len,... 20 | short_cp_len,... 21 | long_cp_len... 22 | ]; 23 | 24 | freq_domain = zeros(length(cp_lengths), fft_size); 25 | time_domain = zeros(length(cp_lengths), fft_size); 26 | 27 | sample_offset = 1; 28 | for idx=1:length(cp_lengths) 29 | % Skip the cyclic prefix 30 | symbol = samples(sample_offset:sample_offset + fft_size + cp_lengths(idx) - 1); 31 | symbol = symbol(cp_lengths(idx) + 1:end); 32 | 33 | % Extract the time domain samples for this OFDM symbol 34 | time_domain(idx,:) = symbol; 35 | 36 | % Convert the time domain samples into frequency domain 37 | freq_domain(idx,:) = fftshift(fft(time_domain(idx,:))); 38 | 39 | sample_offset = sample_offset + fft_size + cp_lengths(idx); 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /matlab/updated_scripts/find_sto_cp.m: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | % Find the start time offset based on the cyclic prefix 3 | % 4 | % This method is immune to the issues that plague the ZC sequences (frequency offset causes a time shift in the 5 | % correlation results) 6 | % 7 | % It's best to provide this function an upsampled copy of the burst to help fix any fractional time offset that might be 8 | % present 9 | % 10 | % @param samples Complex IQ samples that make up the full burst 11 | % @param sample_rate Sample rate (in Hz) of the provided samples 12 | % @return start_offset Sample index that the burst starts at (first sample of the first cyclic prefix) 13 | function [start_offset] = find_sto_cp(samples, sample_rate) 14 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate); 15 | fft_size = get_fft_size(sample_rate); 16 | cyclic_prefix_length_schedule = [... 17 | long_cp_len, ... 18 | short_cp_len, ... 19 | short_cp_len, ... 20 | short_cp_len, ... 21 | short_cp_len, ... 22 | short_cp_len, ... 23 | short_cp_len, ... 24 | short_cp_len, ... 25 | long_cp_len]; 26 | num_ofdm_symbols = length(cyclic_prefix_length_schedule); 27 | 28 | full_burst_len = sum(cyclic_prefix_length_schedule) + (fft_size * num_ofdm_symbols); 29 | num_tests = length(samples) - full_burst_len; 30 | scores_cp_sto = zeros(1, num_tests); 31 | 32 | for idx=1:num_tests 33 | offset = idx; 34 | scores = zeros(1, num_ofdm_symbols); 35 | 36 | % Extract and correlate the samples that each cyclic prefix is expected 37 | % to be at 38 | for cp_idx=1:num_ofdm_symbols 39 | cp_len = cyclic_prefix_length_schedule(cp_idx); 40 | 41 | % Extract the full OFDM symbol including cyclic prefix 42 | window = samples(offset:offset + fft_size + cp_len - 1); 43 | 44 | % Extract the cyclic prefix and the final samples of the symbol 45 | left = window(1:cp_len); 46 | right = window(end - cp_len + 1:end); 47 | 48 | % Correlate the two windows 49 | scores(cp_idx) = abs(xcorr(left, right, 0)); 50 | 51 | % Move the sample pointer forward by the full symbol size 52 | offset = offset + cp_len + fft_size; 53 | end 54 | 55 | % In the real DroneID the first OFDM symbol needs to be ignored since 56 | % it isn't always present. So, just average the correlation scores of 57 | % all but the first element 58 | scores_cp_sto(idx) = sum(scores(2:end)) / (length(scores) - 1); 59 | 60 | end 61 | 62 | % Find the index of the highest score 63 | [~, start_offset] = max(scores_cp_sto); 64 | end 65 | 66 | -------------------------------------------------------------------------------- /matlab/updated_scripts/find_zc_indices_by_file.m: -------------------------------------------------------------------------------- 1 | % Find all instances of the first ZC sequence in the provided file. Does not have to read the entire file in at once 2 | % 3 | % Uses a normalized cross correlator to search for the first ZC sequence in DroneID. 4 | % 5 | % Varags: 6 | % - SampleType: MATLAB numeric type that the samples in the provided file are stored as (ex: 'single', 'int16', etc) 7 | % Defaults to 'single' 8 | % - CorrelationFigNum: Figure number to use when plotting the correlation results. Defaults to not showing the 9 | % correlation results at all. Must be > 0 to show the plot, or -1 to disable 10 | % 11 | % @param file_path Path to the complex IQ file to be processed 12 | % @param sample_rate Sample rate that the input file was recorded at 13 | % @param frequency_offset Amount of offset (in Hz, positive or negative) that the recording needs to be shifted by 14 | % before processing the samples. This is useful when the recording was taken at an offset to 15 | % remove the DC spike 16 | % @param correlation_threshold Minimum correlation magnitude required to classify a score as containing a valid ZC 17 | % sequence. Should be between 0.0 and 1.0. Frequency offset (not accounted for by the 18 | % `frequency_offset` parameter) and low SNR can cause this value to need to be set lower 19 | % than normal. A good starting value is 0.5 20 | % @param chunk_size Number of complex IQ samples to read into memory at a time. Set this value as high as you can 21 | % without running out of memory. The larger this buffer the faster the file will be processed 22 | % @param varargin (see above) 23 | % @return zc_indices A row vector containing the samples offsets where correlation scores were seen at or above the 24 | % specified threshold 25 | function [zc_indices] = find_zc_indices_by_file(file_path, sample_rate, frequency_offset, correlation_threshold, ... 26 | chunk_size, varargin) 27 | 28 | assert(isstring(file_path) || ischar(file_path), "Input file path must be a string or char array"); 29 | assert(isnumeric(sample_rate), "Sample rate must be numeric"); 30 | assert(sample_rate > 0, "Sample rate must be > 0") 31 | assert(isnumeric(frequency_offset), "Frequency offset must be numeric"); 32 | assert(isnumeric(correlation_threshold), "Correlation threshold must be numeric"); 33 | assert(isnumeric(chunk_size), "Chunk size must be numeric"); 34 | assert(chunk_size > 0, "Chunk size must be > 0"); 35 | assert(mod(length(varargin), 2) == 0, "Varargs length must be a multiple of 2"); 36 | 37 | sample_type = 'single'; 38 | correlation_fig_num = -1; 39 | 40 | for idx=1:2:length(varargin) 41 | key = varargin{idx}; 42 | val = varargin{idx+1}; 43 | 44 | switch (key) 45 | case 'SampleType' 46 | sample_type = val; 47 | case 'CorrelationFigNum' 48 | correlation_fig_num = val; 49 | otherwise 50 | error('Invalid varargs key "%s"', key); 51 | end 52 | end 53 | 54 | assert(ischar(sample_type) || isstring(sample_type), "SampleType must be a string or char array"); 55 | assert(isnumeric(correlation_fig_num), "CorrelationFigNum must be numeric"); 56 | assert(correlation_fig_num == -1 || correlation_fig_num > 0, "CorrelationFigNum must be -1, or > 0"); 57 | 58 | %% LTE parameters 59 | fft_size = get_fft_size(sample_rate); 60 | 61 | % Pre-calculate the frequency offset rotation 62 | freq_offset_constant = 1j * pi * 2 * (frequency_offset / sample_rate); 63 | 64 | % The correlator will be searching for the first ZC sequence (which resides in the 4th/3rd OFDM symbol depending on 65 | % which drone model is in use.) No need to conjugate here as that will be done in the correlator 66 | correlator_taps = create_zc(fft_size, 4); 67 | 68 | % Figure out how many samples there are in the file 69 | total_samples = get_sample_count_of_file(file_path, sample_type); 70 | fprintf('There are %d samples in "%s"\n', total_samples, file_path); 71 | 72 | % Open the IQ recording 73 | file_handle = fopen(file_path, 'r'); 74 | 75 | % Really large array to store the cross correlation results from *all* samples 76 | zc_scores = zeros(total_samples - length(correlator_taps), 1); 77 | 78 | % Default to no leftover samples for the first iteration 79 | leftover_samples = []; 80 | 81 | sample_offset = 0; 82 | zc_scores_ptr = 1; 83 | while (~ feof(file_handle)) 84 | %% Read in the next buffer 85 | % The `fread` command will return interleaved real, imag values, so pack those into complex samples making sure 86 | % that the resulting complex values are double precision (this is to prevent functions from complaining later) 87 | real_values = double(fread(file_handle, chunk_size * 2, sample_type)); 88 | 89 | % Don't continue processing if there aren't enough samples remaining keeping in mind that there might be some 90 | % samples leftover from the last iteration of the loop 91 | if ((length(real_values) / 2) + leftover_samples <= length(correlator_taps)) 92 | break; 93 | end 94 | 95 | % Convert from a vector of reals (I,Q,I,Q,I,Q,...) to complex 96 | samples = real_values(1:2:end) + 1j * real_values(2:2:end); 97 | 98 | %% Frequency shift the input 99 | % This is somewhat optional, but the correlation scores will go down fast if the offset is > 1 MHz 100 | rotation_vector = exp(freq_offset_constant * (sample_offset:(sample_offset+length(samples)-1))); 101 | samples = samples .* reshape(rotation_vector, [], 1); 102 | 103 | %% Handle unprocessed samples from last iteration 104 | 105 | % The correlation function cannot process all samples as it looks forward in time. So, after each iteration of 106 | % this loop some samples don't get processed, and those samples are added to the `leftover_samples` vector. 107 | % Those samples need to be added to the beginning of the vector of samples that were read from the file. 108 | % Additionally, the leftover samples have already been frequency corrected, so they must be ignored when 109 | % applying frequency correction (done above). 110 | % TODO(30April2022): This isn't ideal as resizing the sample vector each time is not good for performance 111 | samples = [leftover_samples; samples]; 112 | 113 | % The end of the current sample vector will be the new leftover samples for the next iteration 114 | leftover_samples = samples(end - length(correlator_taps) + 1:end); 115 | 116 | %% Correlate for the ZC sequence 117 | % Run a normalized cross correlation 118 | correlation_values = normalized_xcorr_fast(samples, correlator_taps); 119 | 120 | % Not all samples in the `samples` vector were processed by the correlator, so only update the samples that were 121 | % processed by using the `zc_scores_ptr` which points to where in the `zc_scores` the next values should go. 122 | zc_scores(zc_scores_ptr:zc_scores_ptr+length(correlation_values)-1) = correlation_values; 123 | zc_scores_ptr = zc_scores_ptr + length(correlation_values); 124 | 125 | % Move the sample counter forward so that the frequency offset adjustment logic works properly 126 | sample_offset = sample_offset + length(samples); 127 | end 128 | 129 | % Get the floating normalized correlation results 130 | abs_scores = abs(zc_scores).^2; 131 | 132 | % Plot if requested 133 | if (correlation_fig_num > 0) 134 | figure(correlation_fig_num); 135 | plot(abs_scores); 136 | title('Correlation Scores (normalized)') 137 | end 138 | 139 | % Find all places where the correlation result meets the specified threshold 140 | % This is going to find duplicates because there are very likely going to be two points right next to each other that 141 | % meet the required threshold. This will be dealt with later 142 | passing_scores = find(abs_scores > correlation_threshold); 143 | 144 | % Look through each element of the `passing_scores` vector (which is just indicies where the correlation threshold was 145 | % met) and pick just the highest value `search_window` elements around (`search_window/2` to the left and right) of each 146 | % value. The goal here is to only end up with the best score for the starting point of each burst instead of having 147 | % multiple starting points for each burst. 148 | true_peaks = []; 149 | search_window = 10; 150 | 151 | for idx = 1:length(passing_scores) 152 | % Calculate how far to the left and right to look for the highest peak 153 | left_idx = passing_scores(idx) - (search_window / 2); 154 | right_idx = left_idx + search_window - 1; 155 | 156 | % Move to the next index if there aren't enough samples to find the max peak 157 | if (left_idx < 1 || right_idx > length(abs_scores)) 158 | warning("Had to abandon searching for burst '%d' as it was too close to the end/beginning of the window", idx); 159 | continue 160 | end 161 | 162 | % Get the correlation scores for the samples around the current point 163 | window = abs_scores(left_idx:right_idx); 164 | 165 | % Find the index of the peak in the window and use that value as the actual peak 166 | [~, index] = max(window); 167 | true_peaks = [true_peaks, left_idx + index]; 168 | end 169 | 170 | % There are going to be duplicates in the vector, so just take the unique elements. What's left should just be the 171 | % actual starting indices for each ZC sequence. 172 | zc_indices = unique(true_peaks); 173 | end 174 | -------------------------------------------------------------------------------- /matlab/updated_scripts/generate_scrambler_seq.m: -------------------------------------------------------------------------------- 1 | % Generates a scrambler sequence for an arbitrary number of bits given an initial state value 2 | % 3 | % The initial value should always be the 31 bit sequence 0x12345678 *bit reversed* 4 | % 5 | % @param num_bits Number of bits to generate 6 | % @param x2_init Initial value of the second LFSR. Must be a row vector and only contain 0/1 values 7 | % @return bits Row vector of `num_bits` 1/0 values 8 | function [bits] = generate_scrambler_seq(num_bits, x2_init) 9 | assert(isrow(x2_init), "X2 initial value must be a row vector"); 10 | assert(length(x2_init) == 31, "The X2 initial value must be 31 bits"); 11 | must_be_member(x2_init, [0, 1]); 12 | 13 | % https://www.sharetechnote.com/html/Handbook_LTE_PseudoRandomSequence.html 14 | % https://edadocs.software.keysight.com/pages/viewpage.action?pageId=6076479 15 | % Initial condition of the polynomia x1(). This is fixed value as described in 36.211 7.2 16 | x1_init = [1 0 0 0 0 0 0 0 0 0 ... 17 | 0 0 0 0 0 0 0 0 0 0 ... 18 | 0 0 0 0 0 0 0 0 0 0 ... 19 | 0]; 20 | 21 | 22 | % Mpn is the length of the final sequence c() 23 | Mpn = num_bits; 24 | 25 | % Nc as defined in 36.211 7.2 26 | Nc = 1600; 27 | 28 | % Create a vector(array) for x1() and x2() all initialized with 0 29 | x1 = zeros(1,Nc + Mpn + 31); 30 | x2 = zeros(1,Nc + Mpn + 31); 31 | 32 | % Create a vector(array) for c() all initialized with 0 33 | c = zeros(1,Mpn); 34 | 35 | % Initialize x1() and x2() 36 | x1(1:31) = x1_init; 37 | x2(1:31) = x2_init; 38 | 39 | % generate the m-sequence : x1() 40 | for n = 1 : (Mpn+Nc) 41 | x1(n+31) = mod(x1(n+3) + x1(n), 2); 42 | end 43 | 44 | 45 | % generate the m-sequence : x2() 46 | for n = 1 : (Mpn+Nc) 47 | x2(n+31) = mod(x2(n+3) + x2(n+2) + x2(n+1) + x2(n),2); 48 | end 49 | 50 | % generate the resulting sequence (Gold Sequence) : c() 51 | for n = 1 : Mpn 52 | c(n)= mod(x1(n+Nc) + x2(n+Nc),2); 53 | end 54 | 55 | bits = c; 56 | end 57 | -------------------------------------------------------------------------------- /matlab/updated_scripts/get_bytes_per_sample.m: -------------------------------------------------------------------------------- 1 | % Given a sample type (eg. 'single', 'int16', 'int8', etc) get how many bytes each I and Q value takes up 2 | % 3 | % @param sample_type MATLAB data type (ec. 'single', 'double', 'int16', etc) as a char array 4 | % @return bytes Number of bytes required to represent the specified type 5 | function [bytes] = get_bytes_per_sample(sample_type) 6 | assert(ischar(sample_type) || isstring(sample_type), "Sample type must be a string or char array"); 7 | 8 | % Create an instance of the requested type 9 | try 10 | sample_type_ex = cast(1, sample_type); 11 | catch 12 | error('Failed to create value for type "%s"', sample_type); 13 | end 14 | 15 | % Get information about the variable created above 16 | sample_type_info = whos('sample_type_ex'); 17 | 18 | bytes = sample_type_info.bytes; 19 | end 20 | 21 | -------------------------------------------------------------------------------- /matlab/updated_scripts/get_cyclic_prefix_lengths.m: -------------------------------------------------------------------------------- 1 | % Get the size of the long and short cyclic prefixes based on the sample rate 2 | % 3 | % @param sample_rate Sample rate (in Hz) 4 | % @return long_cp_len Long cyclic prefix length (in samples) 5 | % @return short_cp_len Short cyclic prefix length (in samples) 6 | function [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate) 7 | long_cp_len = round(1/192000 * sample_rate); 8 | short_cp_len = round(0.0000046875 * sample_rate); 9 | end 10 | 11 | -------------------------------------------------------------------------------- /matlab/updated_scripts/get_data_carrier_indices.m: -------------------------------------------------------------------------------- 1 | % Get the indices from the FFT that contain data carriers (not guards or DC) 2 | % 3 | % @param sample_rate Sampling rate (in Hz) that the mapping should be generated for 4 | % @return indices Column vector of 600 integers where each integer represents the index that should be extracted from a 5 | % call to `fftshift(fft(samples))` 6 | function [indices] = get_data_carrier_indices(sample_rate) 7 | fft_size = get_fft_size(sample_rate); 8 | 9 | % DroneID uses 600 carriers 10 | data_carrier_count = 600; 11 | 12 | % Define the location of the DC carrier (which is not used as a data carrier) 13 | dc = (fft_size / 2) + 1; 14 | 15 | % Create an initial mapping of all 0's, then set then set all of the data carrier indices to 1 16 | mapping = zeros(fft_size, 1); 17 | 18 | % As an example: With an FFT size of 2048, 1025 would be DC, 725-1024 and 1026-1325 would be data carriers, and the 19 | % rest would be guards 20 | mapping(dc - (data_carrier_count / 2):dc - 1) = ones(data_carrier_count / 2, 1); 21 | mapping(dc + 1:dc + (data_carrier_count / 2)) = ones(data_carrier_count / 2, 1); 22 | 23 | % Get the indices of `mapping` that contain a 1 value (should be all of the data carriers) 24 | indices = find(mapping == 1); 25 | end 26 | 27 | -------------------------------------------------------------------------------- /matlab/updated_scripts/get_fft_size.m: -------------------------------------------------------------------------------- 1 | % Get the FFT size based on the sampling rate 2 | % 3 | % @param sample_rate Sample rate (in Hz) 4 | % @return fft_size FFT size required for the provided sample rate 5 | function [fft_size] = get_fft_size(sample_rate) 6 | % DroneID's carrier spacing is the same as LTE 7 | fft_size = sample_rate / 15e3; 8 | end 9 | 10 | -------------------------------------------------------------------------------- /matlab/updated_scripts/get_sample_count_of_file.m: -------------------------------------------------------------------------------- 1 | % Get the number of complex values in the specified file using the provided sample type 2 | % 3 | % @param file_path Path to the file that contains complex 32-bit floating point samples 4 | % @param sample_type MATLAB numeric class type for each I and Q value in the file. Ex: 'single', 'int16', 'int8', etc 5 | % @return sample_count Number of complex 32-bit floating point samples in the provided input file 6 | function [sample_count] = get_sample_count_of_file(file_path, sample_type) 7 | handle = fopen(file_path, "r"); 8 | if (handle == 0) 9 | error("Could not open input file '%s'"', file_path); 10 | end 11 | 12 | % Get how many bytes are required to make a single I or Q value 13 | bytes_per_sample = get_bytes_per_sample(sample_type); 14 | 15 | % Move to the end of the file 16 | fseek(handle, 0, 'eof'); 17 | 18 | % Ask how many bytes into the file the pointer is currently 19 | byte_count = ftell(handle); 20 | fclose(handle); 21 | 22 | % Get the number of complex samples in the file. Flooring to ensure that if there are bytes missing those are not 23 | % counted towards the total number of samples 24 | sample_count = floor(byte_count / bytes_per_sample / 2); 25 | end -------------------------------------------------------------------------------- /matlab/updated_scripts/is_octave.m: -------------------------------------------------------------------------------- 1 | % Used to check if the script is running in Octave or MATLAB 2 | % 3 | % Logic is from https://stackoverflow.com/questions/2246579/how-do-i-detect-if-im-running-matlab-or-octave 4 | % 5 | % @return val True if the current script is running inside Octave, false if MATLAB 6 | function [val] = is_octave() 7 | val = exist('OCTAVE_VERSION', 'builtin') ~= 0; 8 | end 9 | -------------------------------------------------------------------------------- /matlab/updated_scripts/must_be_member.m: -------------------------------------------------------------------------------- 1 | % Replacement for the mustBeMember function that's in MATLAB, but not Octave 2 | % 3 | % Throws an assertion failure if `vector` contains any values not in `valid_values` 4 | % 5 | % @param vector Vector/Matrix of values to check against the list of valid values 6 | % @param valid_values List of value values that can be in any one of the elements of `vector` 7 | function [] = must_be_member(vector, valid_values) 8 | assert(isequal(ismember(vector, valid_values), ones(size(vector)))); 9 | end 10 | -------------------------------------------------------------------------------- /matlab/updated_scripts/normalized_xcorr_fast.m: -------------------------------------------------------------------------------- 1 | % A cross correlation function that takes some shortcuts to be faster than xcorr(x,y,0,'normalized') with some small 2 | % tradeoffs in accuracy. Return values are normalized to be between 0 and 1.0 with 1.0 being a perfect match 3 | % 4 | % Will return a vector that is `length(filter)` samples shorter than the input 5 | % 6 | % Correlation peaks point to the *beginning* of the `filter` sequence 7 | % 8 | % @param input_samples Complex row/column vector of samples (must have at least as many samples as `filter`) 9 | % @param filter Complex row/column vector to correlate for in `input_samples` 10 | % @param varargin Variable arguments (see above) 11 | % @return scores Vector of correlation scores as complex values (use `abs(x).^2` to get score in range 0-1.0) 12 | function [scores] = normalized_xcorr_fast(input_samples, filter, varargin) 13 | assert(isrow(input_samples) || iscolumn(input_samples), "Input samples must be row or column vector"); 14 | assert(isrow(filter) || iscolumn(filter), "Filter must be a row or column vector"); 15 | assert(mod(length(varargin), 2) == 0, "Varargs length must be a multiple of 2"); 16 | 17 | % Placeholder for any varargs that might be needed in the future 18 | for idx=1:2:length(varargin) 19 | key = varargin{idx}; 20 | val = varargin{idx+1}; 21 | 22 | switch(key) 23 | otherwise 24 | error("Invalid vararg key '%s'", key); 25 | end 26 | end 27 | 28 | % Create the output vector using the same dimensions as the input samples vector. Not all samples can be 29 | % computed, so don't include the last `length(filter)` samples 30 | dims = size(input_samples); 31 | if (dims(1) == 1) 32 | scores = zeros(1, length(input_samples) - length(filter)); 33 | else 34 | scores = zeros(length(input_samples) - length(filter), 1); 35 | end 36 | 37 | % Make the filter zero mean 38 | filter = filter - mean(filter); 39 | 40 | % Will be using dot product, so the conjugate is needed 41 | filter_conj = conj(filter); 42 | 43 | % Pre-calculate the variance, and the square root of the variance 44 | filter_conj_var = var(filter_conj); 45 | filter_conj_var_sqrt = sqrt(filter_conj_var); 46 | 47 | % Pre-calculate and convert to multiplication, the divisions that will need to happen later 48 | window_size = length(filter); 49 | recip_window_size = 1 / window_size; 50 | recip_window_size_minus_one = 1 / (window_size - 1); 51 | 52 | % To prevent needing an if statement in the critical path, start the running sum with the first element 53 | % missing, and the value being removed first in the loop set to 0. This means that on startup, the loop 54 | % will work properly without needing a conditional 55 | temp_window = input_samples(2:window_size - 1); 56 | running_sum = sum(temp_window); 57 | prev_val = 0; 58 | 59 | % The same trick above is applied to the 60 | running_abs_sqrd = sum(real(temp_window).^2 + imag(temp_window).^2); 61 | running_abs_sqd_prev = 0; 62 | 63 | for idx=1:length(scores) 64 | % Get the next `window_size` samples starting at the current offset 65 | window = input_samples(idx:idx + window_size - 1); 66 | 67 | % Since the window is shifting to the right, subtract off the left-most value that was just removed and add 68 | % on the new value on the right 69 | running_sum = running_sum - prev_val + window(end); 70 | 71 | % The value that will be removed on the next iteration is the left-most value of the current window 72 | prev_val = window(1); 73 | 74 | % Make the window zero mean by subtracting the average power 75 | window = window - (running_sum * recip_window_size); 76 | 77 | % Compute the dot product 78 | prod = sum(window .* filter_conj) * recip_window_size; 79 | 80 | % Compute the running abs(window).^2 estimate by removing the previous left-most value, and adding on the new 81 | % right-most value. Then make the new left-most value the prev value for the next iteration 82 | running_abs_sqrd = running_abs_sqrd - running_abs_sqd_prev + real(window(end)).^2 + imag(window(end)).^2; 83 | running_abs_sqd_prev = real(window(1)).^2 + imag(window(1)).^2; 84 | 85 | % Get the variance of the window 86 | variance = running_abs_sqrd * recip_window_size_minus_one; 87 | 88 | % Divide the dot product result by the square root of the std deviation of both windows combined 89 | scores(idx) = prod / (sqrt(variance) * filter_conj_var_sqrt); 90 | end 91 | 92 | end 93 | 94 | -------------------------------------------------------------------------------- /matlab/updated_scripts/process_file.m: -------------------------------------------------------------------------------- 1 | % This script takes in a floating point complex IQ recording containing DroneID bursts and demodulates each burst 2 | % The steps are: 3 | % - Find all bursts in the file using the first ZC sequence 4 | % - Low pass filter each burst 5 | % - Adjust for frequency offset based on the offset found using the first OFDM symbol's cyclic prefix 6 | % - Extract each OFDM symbol 7 | % - Quantize/Demodulate all data carriers 8 | % - Validate that the first symbol XOR's to all zeros 9 | % - Pass XOR'd bits from all other data symbols to a C++ program that removes the LTE and rate matching 10 | % - Print out each frame in hex 11 | 12 | %% Path Info 13 | if (is_octave) 14 | this_script_path = fileparts(mfilename('fullpath')); 15 | else 16 | this_script_path = fileparts(matlab.desktop.editor.getActiveFilename); 17 | end 18 | 19 | % Create a directory to store the constellation plots for debugging 20 | % THIS CAN BE COMMENTED OUT IF NEEDED!!! JUST MAKE SURE TO COMMENT OUT THE `saveas` CALL LATER AS WELL 21 | mkdir(fullfile(this_script_path, "images")); 22 | 23 | turbo_decoder_path = fullfile(this_script_path, filesep, '..', filesep, '..', filesep, 'cpp', filesep, 'remove_turbo'); 24 | if (~ isfile(turbo_decoder_path)) 25 | error("Could not find Turbo decoder application at '%s'. Check that the program has been compiled",... 26 | turbo_decoder_path); 27 | end 28 | 29 | %% File Parameters 30 | enable_plots = true; % Set to false to prevent the plots from popping up 31 | correlation_threshold = 0.7; % The SNR is pretty good, so using a high correlation score (must be between 0.0 and 1.0) 32 | chunk_size = 10e6; % Number of samples to process at a time 33 | enable_equalizer = true; % Enable/disable the frequency domain equalizer 34 | 35 | %% Paramters that the user must change 36 | sample_type = 'single'; 37 | file_path = 'YOUR_FILE_NAME_HERE'; 38 | file_sample_rate = YOUR_SAMPLE_RATE_HERE; 39 | file_freq_offset = 0e6; 40 | 41 | %% Low Pass Filter Setup 42 | signal_bandwidth = 10e6; % The actual occupied bandwidth of the DroneID signal 43 | filter_tap_count = 50; % Number of filter taps to use for the low pass filter 44 | filter_taps = fir1(filter_tap_count, signal_bandwidth/file_sample_rate); % Create the low pass filter taps 45 | 46 | %% Burst Extraction 47 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(file_sample_rate); 48 | cyclic_prefix_schedule = [ 49 | long_cp_len, ... 50 | short_cp_len, ... 51 | short_cp_len, ... 52 | short_cp_len, ... 53 | short_cp_len, ... 54 | short_cp_len, ... 55 | short_cp_len, ... 56 | short_cp_len, ... 57 | long_cp_len]; 58 | fft_size = get_fft_size(file_sample_rate); 59 | 60 | % A correlation figure number of -1 will prevent plotting by the find_zc_indices_by_file function 61 | correlation_fig_number = -1; 62 | if (enable_plots) 63 | correlation_fig_number = 456; 64 | end 65 | 66 | % Making sure that the bursts that are extracted have enough padding for the low pass filter to start up and terminate 67 | bursts = extract_bursts_from_file(file_path, file_sample_rate, file_freq_offset, correlation_threshold, chunk_size,... 68 | filter_tap_count, 'SampleType', sample_type, 'CorrelationFigNum', correlation_fig_number); 69 | 70 | assert(~isempty(bursts), "Did not find any bursts"); 71 | 72 | frames = {}; 73 | 74 | % Get a list of the indices from the shifted FFT outputs that contain data carriers 75 | data_carrier_indices = get_data_carrier_indices(file_sample_rate); 76 | 77 | % Initial value for the second LFSR in the scrambler 78 | scrambler_x2_init = fliplr([0 0 1, 0 0 1 0, 0 0 1 1, 0 1 0 0, 0 1 0 1, 0 1 1 0, 0 1 1 1, 1 0 0 0]); 79 | 80 | % This determines which OFDM symbol's cyclic prefix is used to determine the coarse frequency offset. Some drones use 9 81 | % OFDM symbols, and some use 8. It seems that those drones that use 8 OFDM symbols have a short cyclic prefix in the 82 | % first symbol. Skipping the first symbol for those drones that have 9 OFDM symbols results in the new "first" symbol 83 | % having a short cyclic prefix as well. So, since the burst extractor always assumes that there are 9 symbols, the 84 | % first symbol is skipped for the purposes of coarse CFO. The second symbol is assumed to have a short cyclic prefix 85 | cfo_estimation_symbol_idx = 2; 86 | 87 | %% Burst Processing 88 | for burst_idx=1:size(bursts, 1) 89 | % Get the next burst 90 | burst = bursts(burst_idx,:); 91 | 92 | if (enable_plots) 93 | figure(43); 94 | subplot(2, 1, 1); 95 | plot(10 * log10(abs(burst).^2)); 96 | title('Time domain abs^2 10log10 (original)'); 97 | 98 | % Plot the FFT, but average it with a single pole IIR filter to make it smoother 99 | figure(1000); 100 | fft_bins = 10 * log10(abs(fftshift(fft(burst))).^2); 101 | running = fft_bins(1); 102 | beta = 0.06; 103 | for idx = 2:length(fft_bins) 104 | running = (running * (1 - beta)) + (fft_bins(idx) * beta); 105 | fft_bins(idx) = running; 106 | end 107 | x_axis = file_sample_rate / 2 * linspace(-1, 1, length(burst)); 108 | plot(x_axis, fft_bins); 109 | title('Frequency Spectrum (averaged)'); 110 | grid on; 111 | end 112 | 113 | %% Find Integer Frequency Offset 114 | 115 | % Exploiting the fact that during the first ZC sequence the DC carrier will be much lower in amplitude than the 116 | % surrounding samples. Steps: 117 | % 1. Extract just the time domain samples used in the first ZC sequence 118 | % 2. Interpolate those time domain samples to increase the frequency resolution of the measurement 119 | % 3. Get the power spectrum (abs squared of the FFT) 120 | % 4. Look N elements around the center of the FFT for the lowest point (this is the center of the signal) 121 | % 5. Calculate how far off from 0 Hz the lowest bin was, and frequency shift the upsampled signal by that value 122 | % 6. Decimate the samples back to the original sample rate for further processing 123 | 124 | % Calculate the first sample index for the first ZC sequence (skipping the cyclic prefix) 125 | offset = sum(cyclic_prefix_schedule(1:4)) + (fft_size * 3) + filter_tap_count; 126 | 127 | % Upsample (interpolate and filter) the ZC sequence samples 128 | interp_rate = 10; 129 | burst = resample(burst, interp_rate, 1); 130 | 131 | % Extract out just the samples for the first ZC sequence 132 | zc_samples = burst((offset * interp_rate):(offset * interp_rate) + (fft_size * interp_rate) - 1); 133 | 134 | % Convert the time domain ZC sequence samples to the frequency domain 135 | fft_bins = 10 * log10(abs(fftshift(fft(zc_samples))).^2); 136 | 137 | % Loop for the lowest bin in the middle of the frequency domain spectrum 138 | bin_count = 15; % How far left and right to look for the lowest carrier 139 | 140 | % Set all of the FFT bins on the outside to infinity so they can't possibly be the minimum value 141 | fft_bins(1:(fft_size * interp_rate / 2) - bin_count) = Inf; 142 | fft_bins((fft_size * interp_rate / 2) + bin_count - 1:end) = Inf; 143 | 144 | % Find the index of the FFT bin with the lowest amplitude 145 | [~, center_offset] = min(fft_bins); 146 | 147 | % Calculate the frequency needed to correct the integer offset, then conver that to radians 148 | integer_offset = ((fft_size * interp_rate / 2) - center_offset + 1) * 15e3; 149 | radians = 2 * pi * integer_offset / (file_sample_rate * interp_rate); 150 | 151 | % Apply a frequency adjustment 152 | burst = burst .* exp(1j * radians * [0:length(burst) - 1]); 153 | 154 | % Downsample (filter and decimate) the burst samples 155 | burst = resample(burst, 1, interp_rate); 156 | 157 | %% Apply low pass filter 158 | burst = filter(filter_taps, 1, burst); 159 | 160 | if (enable_plots) 161 | figure(43); 162 | subplot(2, 1, 2); 163 | plot(10 * log10(abs(burst).^2)); 164 | title('Time domain abs^2 10log10 (filtered)') 165 | end 166 | 167 | %% Interpolate and find the true starting sample offset 168 | interp_factor = 1; 169 | burst = resample(burst, interp_factor, 1); 170 | true_start_index = find_sto_cp(burst, file_sample_rate * interp_factor); 171 | burst = resample(burst(true_start_index:end), 1, interp_factor); 172 | 173 | % Plot cyclic prefixes overlayed with the replica from the end of the OFDM symbol 174 | if (enable_plots) 175 | offset = 1; 176 | figure(7777); 177 | for cp_idx=1:length(cyclic_prefix_schedule) 178 | subplot(3, 3, cp_idx); 179 | symbol = burst(offset:offset + cyclic_prefix_schedule(cp_idx) + fft_size - 1); 180 | left = symbol(1:cyclic_prefix_schedule(cp_idx)); 181 | right = symbol(end - cyclic_prefix_schedule(cp_idx) + 1:end); 182 | plot(abs(left)); 183 | hold on 184 | plot(abs(right)); 185 | hold off; 186 | title(['Cyclic Prefix Overlay ', mat2str(cp_idx)]); 187 | 188 | offset = offset + length(symbol); 189 | end 190 | end 191 | 192 | %% Coarse frequency offset adjustment using one of the OFDM symbols (see coarse_cfo_symbol_sample_offset definition) 193 | 194 | % Get the expected starting index of the symbol to be used for CFO estimation 195 | zc_start = long_cp_len + (fft_size * 3) + (short_cp_len * 3); 196 | 197 | % Extract out the full OFDM symbol (cyclic prefix included) 198 | cfo_est_symbol = burst(zc_start - short_cp_len:zc_start + fft_size - 1); 199 | 200 | % Get the cyclic prefix, and then the copy of the cyclic prefix that exists at the end of the OFDM symbol 201 | cyclic_prefix = cfo_est_symbol(1:short_cp_len); 202 | symbol_tail = cfo_est_symbol(end - short_cp_len + 1:end); 203 | 204 | skip = 0; 205 | cyclic_prefix = cyclic_prefix(skip+1:end-skip); 206 | symbol_tail = symbol_tail(skip+1:end-skip); 207 | 208 | % Calculate the frequency offset by taking the dot product of the two copies of the cyclic prefix and dividing out 209 | % the number of samples in between each cyclic prefix sample (the FFT size) 210 | offset_radians = angle(dot(cyclic_prefix, symbol_tail)) / fft_size; 211 | offset_hz = offset_radians * file_sample_rate / (2 * pi); 212 | 213 | if (enable_plots) 214 | figure(999); 215 | plot(abs(cyclic_prefix).^2); 216 | hold on; 217 | plot(abs(symbol_tail).^2, '*-', 'Color', 'red'); 218 | hold off; 219 | title('Cyclic Prefix Overlay - CFO Estimate') 220 | end 221 | 222 | % Apply the inverse of the estimated frequency offset back to the signal 223 | burst = burst .* exp(1j * -offset_radians * [1:length(burst)]); 224 | 225 | %% OFDM Symbol Processing 226 | 227 | % Extract the individual OFDM symbols without the cyclic prefix for both time and frequency domains 228 | [time_domain_symbols, freq_domain_symbols] = extract_ofdm_symbol_samples(burst, file_sample_rate); 229 | 230 | % Calculate the channel for both of the ZC sequnces 231 | channel1 = calculate_channel(freq_domain_symbols(4,:), file_sample_rate, 4); 232 | channel2 = calculate_channel(freq_domain_symbols(6,:), file_sample_rate, 6); 233 | 234 | % Only select the data carriers from each channel estimate 235 | channel1 = channel1(data_carrier_indices); 236 | channel2 = channel2(data_carrier_indices); 237 | 238 | % Calculate the average phase offset of each channel estimate 239 | channel1_phase = sum(angle(channel1)) / length(data_carrier_indices); 240 | channel2_phase = sum(angle(channel2)) / length(data_carrier_indices); 241 | 242 | % This doesn't seem right, but taking the difference of the two channels and dividing by two yields the average 243 | % walking phase offset between the two. That value can be used to correct for the phase offsets caused by not being 244 | % exactly spot on with the true first sample 245 | channel_phase_adj = (channel1_phase - channel2_phase) / 2; 246 | 247 | if (enable_plots) 248 | figure(441); 249 | subplot(2, 1, 1); 250 | plot(abs(channel1).^2, '-'); 251 | title('ZC Sequence 1 Channel') 252 | subplot(2, 1, 2); 253 | plot(abs(channel2).^2, '-'); 254 | title('ZC Sequence 2 Channel') 255 | end 256 | 257 | % Only use the fisrt ZC sequence to do the initial equaliztion. Trying to use the average of both ends up with 258 | % strange outliers in the constellation plot 259 | channel = channel1; 260 | 261 | % Place to store the demodulated bits 262 | bits = zeros(9, 1200); 263 | 264 | % Walk through each OFDM symbol and extract the data carriers and demodulate the QPSK inside 265 | % This is done for symbols 4 and 6 even though they contain ZC sequences. It's just to keep the logic clean 266 | 267 | for idx=1:size(bits, 1) 268 | data_carriers = freq_domain_symbols(idx,data_carrier_indices); 269 | 270 | if (enable_equalizer) 271 | % Equalize just the data carriers 272 | data_carriers = data_carriers .* channel; 273 | end 274 | 275 | % Demodulate/quantize the QPSK to bits 276 | bits(idx,:) = quantize_qpsk(data_carriers); 277 | 278 | if (enable_plots) 279 | figure(1); 280 | subplot(3, 3, idx); 281 | plot(data_carriers, 'o'); 282 | title(['Symbol ', mat2str(idx), ' IQ']); 283 | 284 | figure(111); 285 | subplot(3, 3, idx); 286 | plot(10 * log10(abs(time_domain_symbols(idx,:)).^2), '-'); 287 | title(['Symbol ', mat2str(idx), ' Time Domain']); 288 | 289 | figure(112); 290 | subplot(3, 3, idx); 291 | plot(10 * log10(abs(freq_domain_symbols(idx,:)).^2)); 292 | title(['Symbol ', mat2str(idx), ' Freq Domain']); 293 | end 294 | end 295 | 296 | if (enable_plots) 297 | % Save the constellation plots to disk for debugging 298 | % THIS CAN BE COMMENTED OUT IF NEEDED 299 | png_path = sprintf('%s/images/ofdm_symbol_%d.png', this_script_path, burst_idx); 300 | 301 | try 302 | saveas(gcf, png_path); 303 | catch 304 | error('Could not write out PNG file to "%s"', png_path); 305 | end 306 | end 307 | 308 | % The remaining bits are descrambled using the same initial value, but more bits 309 | second_scrambler = generate_scrambler_seq(7200, scrambler_x2_init); 310 | 311 | % Only descramble the remaining data symbols (ignoring the ZC sequences in 4 and 6, and the first data symbol) 312 | bits = bits([2,3,5,7,8,9],:); 313 | 314 | % Just converting the bits matrix into a vector to make XOR'ing easier 315 | bits = reshape(bits.', 1, []); 316 | 317 | % Run the actual XOR 318 | bits = bitxor(bits, second_scrambler); 319 | 320 | % Write the descrambled bits to disk as 8-bit integers 321 | handle = fopen("/tmp/bits", "wb"); 322 | fwrite(handle, bits, 'int8'); 323 | fclose(handle); 324 | 325 | % Run the Turbo decoder and rate matcher 326 | [retcode, out] = system(sprintf("%s %s", turbo_decoder_path, "/tmp/bits")); 327 | if (retcode ~= 0) 328 | warning("Failed to run the final processing step"); 329 | end 330 | 331 | % Save off the hex values for the frame 332 | frames{burst_idx} = out; 333 | end 334 | 335 | % Print out all frames in hex 336 | for idx=1:size(bursts, 1) 337 | frame = frames{idx}; 338 | 339 | fprintf('FRAME: %s', frame); 340 | end 341 | -------------------------------------------------------------------------------- /matlab/updated_scripts/quantize_qpsk.m: -------------------------------------------------------------------------------- 1 | % Take in a vector of complex samples representing individual QPSK constellation points with the constellation rotated 2 | % such that the points are ideally at 1+i, -1+i, -1-i, and 1-i. 3 | % 4 | % This function uses hard decision, so it's not what you want to use in low SNR environments 5 | % 6 | % The constellation mapping is: 7 | % 1+i == 0b00 8 | % 1-i == 0b01 9 | % -1+i == 0b10 10 | % -1-i == 0b11 11 | % 12 | % Which comes from https://github.com/ttsou/openphy/blob/master/src/lte/qam.c#L35 13 | % 14 | % @param data_carriers Row or column vector of complex samples 15 | % @return quantized_bits Vector of 1/0 values that make up the bits demapped from the provided sample vector 16 | function [quantized_bits] = quantize_qpsk(data_carriers) 17 | assert(iscolumn(data_carriers) || isrow(data_carriers), "Data carriers must be row/column vector"); 18 | 19 | quantized_bits = zeros(length(data_carriers), 1); 20 | 21 | % Track where in the `quantized_bits` vector the new bits should be placed 22 | bits_offset = 1; 23 | 24 | % Walk through each complex sample in the input vector 25 | for sample_idx = 1:length(data_carriers) 26 | sample = data_carriers(sample_idx); 27 | 28 | % Determine bit mapping based on the quadrant that the sample is located 29 | if (real(sample) > 0 && imag(sample) > 0) 30 | bits = [0, 0]; 31 | elseif (real(sample) > 0 && imag(sample) < 0) 32 | bits = [0, 1]; 33 | elseif (real(sample) < 0 && imag(sample) > 0) 34 | bits = [1, 0]; 35 | elseif (real(sample) < 0 && imag(sample) < 0) 36 | bits = [1, 1]; 37 | else 38 | bits = [0, 0]; 39 | end 40 | 41 | % Save off the quatized bits and move the counter ahead by 2 42 | quantized_bits(bits_offset:bits_offset+1) = bits; 43 | bits_offset = bits_offset + 2; 44 | end 45 | end 46 | 47 | -------------------------------------------------------------------------------- /matlab/updated_scripts/read_complex.m: -------------------------------------------------------------------------------- 1 | % Read in complex IQ samples from the specified file 2 | % 3 | % @param input_file Path to input file. Must be string, char array, or cell string 4 | % @param sample_offset How many complex samples to skip from the beginning of the file 5 | % @param sample_count Number of samples after `sample_offset` to extract from the file. Must be -1 or inf to read all 6 | % samples after `sample_offset` or > 0 to limit the number of samples read 7 | % @param sample_type Data type of each real/imaginary value. Example: 'single' for 32-bit float, 'int16' for 16-bit 8 | % shorts 9 | % @return samples Samples read from the input file as a column vector of complex values 10 | function [samples] = read_complex(input_file, sample_offset, sample_count, sample_type) 11 | assert(isstring(input_file) || ischar(input_file) || iscellstr(input_file), ... 12 | 'Input file must be a string, char array, or cell string'); 13 | assert(isnumeric(sample_offset), 'Sample offset must be a number') 14 | assert(sample_offset >= 0, 'Sample offset must be >= 0') 15 | assert(isnumeric(sample_count) || isinf(sample_count), 'Sample count must be a number or `inf`'); 16 | assert(sample_count == -1 || sample_count > 0 || sample_count == inf, 'Sample count must be -1, inf, or > 0'); 17 | assert(isstring(sample_type) || ischar(sample_type) || iscellstr(sample_type), ... 18 | 'Sample type must be a string, char array, or cell string'); 19 | 20 | % Open the sample file and verify that was successful 21 | file_handle = fopen(input_file, 'r'); 22 | assert(file_handle ~= -1, "Could not open input file '%s'", input_file); 23 | 24 | try 25 | % Create a value using the sample type 26 | sample_type_example = cast(1, sample_type); 27 | 28 | % Get information about the created variable 29 | sample_type_info = whos('sample_type_example'); 30 | 31 | % Get the number of bytes in sample_type from the type info 32 | bytes_per_real_sample = sample_type_info.bytes; 33 | catch 34 | error('Failed to determine byte size of type "%s"', sample_type); 35 | end 36 | 37 | % Get the total number of samples (reals and complex) in the file 38 | fseek(file_handle, 0, 'eof'); 39 | total_real_samples = floor(ftell(file_handle) / bytes_per_real_sample); 40 | total_complex_samples = total_real_samples / 2; 41 | 42 | % Sanity check that the sample offset requested actually exists 43 | assert(sample_offset < total_complex_samples, ... 44 | "Asked for sample offset of %d, but there are only %d samples", sample_offset, total_complex_samples); 45 | 46 | % Move the file handle to the starting sample offset (each real is 2 bytes, and a complex value is 4) 47 | fseek(file_handle, sample_offset * (bytes_per_real_sample * 2), 'bof'); 48 | 49 | % If the user supplied -1 or inf update the sample count to include all remaining samples in the file 50 | if (sample_count == -1 || sample_count == inf) 51 | sample_count = total_complex_samples - sample_offset; 52 | end 53 | 54 | % Warn the user when asking for more samples than are available 55 | if (sample_count + sample_offset > total_complex_samples) 56 | warning('Attempting to read %d samples with an offset of %d when only %d total samples are available', ... 57 | sample_count, sample_offset, total_complex_samples); 58 | end 59 | 60 | % Read in samples from the file. These come in as individual int16 values, not complex. Using an explicit cast 61 | % here because MATLAB seems to treat everything as double precision floats unless told not to 62 | real_samples = cast(fread(file_handle, sample_count * 2, sample_type), sample_type); 63 | fclose(file_handle); 64 | 65 | % Convert the interleaved samples into a column vector of complex values 66 | samples = complex(real_samples(1:2:end), real_samples(2:2:end)); 67 | end 68 | 69 | -------------------------------------------------------------------------------- /matlab/updated_scripts/transmit/calculate_crc.m: -------------------------------------------------------------------------------- 1 | % Calculate the 16-bit CRC used on the payload for DroneID 2 | % 3 | % This code is a MATLAB port of 4 | % https://github.com/dji-sdk/Guidance-SDK/blob/master/examples/uart_example/crc16.cpp 5 | % 6 | % This function can be used to validate a CRC by running with all bytes including the CRC. If the purpose is to 7 | % calculate what the CRC should be, then do not include the additional two bytes of zeros and instead just remove the 8 | % bytes that will be the CRC 9 | % 10 | % @param byte_vector Row/Column vector of uint8 values 11 | % @return checksum uint16 CRC output value 12 | function [checksum] = calculate_crc(byte_vector) 13 | 14 | crc_table = [ 15 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, ... 16 | 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, ... 17 | 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, ... 18 | 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, ... 19 | 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, ... 20 | 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, ... 21 | 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, ... 22 | 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, ... 23 | 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, ... 24 | 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, ... 25 | 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, ... 26 | 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, ... 27 | 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, ... 28 | 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, ... 29 | 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, ... 30 | 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, ... 31 | 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, ... 32 | 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, ... 33 | 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, ... 34 | 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, ... 35 | 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, ... 36 | 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, ... 37 | 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, ... 38 | 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, ... 39 | 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, ... 40 | 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, ... 41 | 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, ... 42 | 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, ... 43 | 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, ... 44 | 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, ... 45 | 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, ... 46 | 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 ... 47 | ]; 48 | 49 | assert(~isempty(byte_vector), "Input vector was empty"); 50 | assert(isrow(byte_vector) || iscolumn(byte_vector), "Input vector must be row/col"); 51 | 52 | checksum = uint16(0x3692); 53 | 54 | for idx=1:length(byte_vector) 55 | % Very verbose due to the amount of strange things that need to be done to make MATLAB play nice with bit ops 56 | new_byte = uint8(byte_vector(idx)); 57 | table_idx = bitxor(checksum, uint16(new_byte)); 58 | table_idx = bitand(table_idx, uint16(0x00ff)); 59 | val = crc_table(table_idx + 1); 60 | temp_checksum = bitshift(checksum, -8); 61 | checksum = bitxor(temp_checksum, val); 62 | end 63 | end 64 | 65 | -------------------------------------------------------------------------------- /matlab/updated_scripts/transmit/create_burst.m: -------------------------------------------------------------------------------- 1 | function [samples] = create_burst(sample_rate, frame_configuration, add_turbo_path, temp_path, show_debug_plots) 2 | frame_bytes = create_frame_bytes(frame_configuration{:}); 3 | 4 | %% Turbo encoding and rate matching 5 | if (~ isfile(add_turbo_path)) 6 | error("Could not find Turbo encoder application at '%s'. Check that the program has been compiled",... 7 | add_turbo_path); 8 | end 9 | 10 | % Where to store the files used to talk to the add_turbo binary 11 | frame_bin_file = fullfile(temp_path, 'frame.bin'); % Will write payload bytes here 12 | turbo_encoded_file = fullfile(temp_path, 'frame.bin.encoded'); % The add_turbo binary will create this 13 | 14 | % Write out the payload to Turbo encode and rate match 15 | file_handle = fopen(frame_bin_file, "w"); 16 | assert(file_handle ~= -1, 'Could not open output temp file "%s"', frame_bin_file); 17 | 18 | try 19 | fwrite(file_handle, frame_bytes, 'uint8'); 20 | catch 21 | error('Could not write to temp file "%s"', frame_bin_file); 22 | end 23 | 24 | fclose(file_handle); 25 | 26 | % Call the Turbo encoder 27 | [retcode, out] = system(sprintf("%s '%s' '%s'", add_turbo_path, frame_bin_file, turbo_encoded_file)); 28 | if (retcode ~= 0) 29 | warning("Failed to run the Turbo encoder. Details: %s", out); 30 | end 31 | 32 | % Remove the Turbo encoder input temp file 33 | delete(frame_bin_file); 34 | 35 | % Verify that it created the expected output file 36 | if (~ isfile(turbo_encoded_file)) 37 | error("Turbo encoder did not produce an output file"); 38 | end 39 | 40 | % Read in all bytes from the encoded output file 41 | file_handle = fopen(turbo_encoded_file, "r"); 42 | assert(file_handle ~= -1, 'Could not open Turbo encoder output file "%s"', turbo_encoded_file); 43 | encoded_bits = uint8(fread(file_handle, inf, 'uint8')); 44 | fclose(file_handle); 45 | 46 | % Remove the Turbo encoder output temp file 47 | delete(turbo_encoded_file); 48 | 49 | % Make sure that there are exactly the right number of bytes in the file created by the Turbo encoder 50 | required_bytes = 7200; 51 | if (length(encoded_bits) ~= required_bytes) 52 | error("Expected %d bytes, but got %d", required_bytes, length(encoded_bits)) 53 | end 54 | 55 | %% Scrambler Application 56 | 57 | % Convert to a matrix of 8 rows so that each symbol has it's own row 58 | encoded_bits = reshape(encoded_bits, [], 6).'; 59 | 60 | % Initial value for the second LFSR in the scrambler 61 | scrambler_x2_init = fliplr([0 0 1, 0 0 1 0, 0 0 1 1, 0 1 0 0, 0 1 0 1, 0 1 1 0, 0 1 1 1, 1 0 0 0]); 62 | 63 | % Create scrambler for all data bits and reshape so that each OFDM symbol has a row 64 | scrambler = uint8(reshape(generate_scrambler_seq(7200, scrambler_x2_init), [], 6).'); 65 | 66 | encoded_bits = [scrambler(1,:); encoded_bits]; 67 | 68 | % Apply the scrambler to each OFDM symbol's data bits 69 | for idx=1:6 70 | encoded_bits(idx+1,:) = bitxor(encoded_bits(idx+1,:), scrambler(idx,:)); 71 | end 72 | 73 | %% OFDM Symbol Creation Setup 74 | 75 | % Get some required params 76 | fft_size = get_fft_size(sample_rate); 77 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate); 78 | 79 | % Define the size of each symbols cyclic prefix 80 | cyclic_prefix_schedule = [ 81 | long_cp_len, ... 82 | short_cp_len, ... 83 | short_cp_len, ... 84 | short_cp_len, ... 85 | short_cp_len, ... 86 | short_cp_len, ... 87 | short_cp_len, ... 88 | short_cp_len, ... 89 | long_cp_len ... 90 | ]; 91 | 92 | % Number of data carriers per OFDM symbol 93 | data_carrier_count = 600; 94 | 95 | %% Bits to Symbol Mapping 96 | 97 | % Create the QPSK samples for each OFDM symbol 98 | symbol_data = zeros(9, data_carrier_count); 99 | 100 | encoded_bits_ptr = 1; 101 | for symbol_idx=1:9 102 | % There's no work to be done for symbols 4 and 6 as they are ZC sequences 103 | if (symbol_idx == 4) 104 | elseif (symbol_idx == 6) 105 | else 106 | % Convert from bits to QPSK constellation 107 | symbol_data(symbol_idx,:) = to_qpsk(encoded_bits(encoded_bits_ptr,:)); 108 | encoded_bits_ptr = encoded_bits_ptr + 1; 109 | end 110 | 111 | if (show_debug_plots) 112 | figure(1); 113 | subplot(3, 3, symbol_idx); 114 | plot(symbol_data(symbol_idx,:), 'o'); 115 | end 116 | end 117 | 118 | %% Frequency Domain Symbol Creation 119 | % Create the frequency domain representation of each OFDM symbol 120 | freq_domain_symbols = zeros(9, fft_size); 121 | 122 | % Get the carriers that will contain the QPSK samples 123 | data_carrier_indices = get_data_carrier_indices(sample_rate); 124 | 125 | for symbol_idx=1:9 126 | % Copy the freq domain samples to the buffer 127 | freq_domain_symbols(symbol_idx,data_carrier_indices) = symbol_data(symbol_idx,:); 128 | 129 | if (show_debug_plots) 130 | figure(2); 131 | subplot(3, 3, symbol_idx); 132 | plot(abs(freq_domain_symbols(symbol_idx,:)).^2); 133 | 134 | figure(3); 135 | subplot(3, 3, symbol_idx); 136 | plot(freq_domain_symbols(symbol_idx,:), 'o'); 137 | end 138 | end 139 | 140 | %% Time Domain Symbol Creation 141 | % Create the time domain representation of each OFDM symbol (no cyclic prefix at this point) 142 | time_domain_symbols = zeros(9, fft_size); 143 | 144 | for symbol_idx=1:9 145 | % Move to the time domain 146 | samples = ifft(fftshift(freq_domain_symbols(symbol_idx,:))); 147 | 148 | % Normalize the output of the FFT 149 | samples = samples / fft_size; 150 | 151 | time_domain_symbols(symbol_idx,:) = samples; 152 | 153 | if (show_debug_plots) 154 | figure(4); 155 | subplot(3, 3, symbol_idx); 156 | plot(10 * log10(abs(time_domain_symbols(symbol_idx,:)).^2)) 157 | end 158 | end 159 | 160 | % Replace the contents of symbols 4 and 6 with the correct ZC sequence 161 | time_domain_symbols(4,:) = create_zc(fft_size, 4) / fft_size; 162 | time_domain_symbols(6,:) = create_zc(fft_size, 6) / fft_size; 163 | 164 | %% Add Cyclic Prefix 165 | 166 | samples = zeros(1, sum(cyclic_prefix_schedule) + (fft_size * length(cyclic_prefix_schedule))); 167 | samples_ptr = 1; 168 | for symbol_idx=1:9 169 | cp_len = cyclic_prefix_schedule(symbol_idx); 170 | symbol = time_domain_symbols(symbol_idx,:); 171 | cp = symbol(end-cp_len+1:end); 172 | symbol_with_prefix = [cp, symbol]; 173 | samples(samples_ptr:samples_ptr + length(symbol_with_prefix) - 1) = symbol_with_prefix; 174 | samples_ptr = samples_ptr + length(symbol_with_prefix); 175 | end 176 | 177 | if (show_debug_plots) 178 | figure(7); 179 | plot(10 * log10(abs(time_domain).^2)); 180 | 181 | filter_out = filter(conj(create_zc(fft_size, 4)), 1, samples); 182 | figure(8); 183 | plot(abs(filter_out).^2); 184 | end 185 | end -------------------------------------------------------------------------------- /matlab/updated_scripts/transmit/create_frame_bytes.m: -------------------------------------------------------------------------------- 1 | % Valid ending arguments: 2 | % - SequenceNumber (0 - 65535) 3 | % - StateInfo0 (1 byte) 4 | % - StateInfo1 (1 byte) 5 | % - SerialNumber (14-16 characters) 6 | % - Longitude (double) 7 | % - Latitude (double) 8 | % - Height (-32768 to 32767) 9 | % - Altitude (-32768 to 32767) 10 | % - VelocityNorth (-32768 to 32767) 11 | % - VelocityEast (-32768 to 32767) 12 | % - VelocityUp (-32768 to 32767) 13 | % - Yaw (-32768 to 32767) 14 | % - PhoneAppGPSTime (milliseconds since Epoch) 15 | % - PhoneAppLatitude (double) 16 | % - PhoneAppLongitude (double) 17 | % - HomeLatitude (double) 18 | % - HomeLongitude (double) 19 | % - ProductType (1 byte) 20 | % - UUID (19 bytes) 21 | 22 | function [frame] = create_frame_bytes(varargin) 23 | % MIN_SAMPLE_RATE = 15.36e6; 24 | % 25 | % assert(sample_rate >= MIN_SAMPLE_RATE, "Sample rate must be at least 15.36 MSPS"); 26 | % assert(round(sample_rate / MIN_SAMPLE_RATE) * MIN_SAMPLE_RATE == sample_rate, ... 27 | % "Sample rate must be multiple of %d", MIN_SAMPLE_RATE); 28 | 29 | assert(isempty(varargin) || mod(length(varargin), 2) == 0, ... 30 | "Variable argument list must have even number of elements"); 31 | 32 | sequence_number = 0; 33 | state_info = [0, 0]; 34 | serial_number = '0123456789abcd'; 35 | longitude = 0.0; 36 | latitude = 0.0; 37 | height = 0; 38 | altitude = 0; 39 | velocity_north = 0; 40 | velocity_east = 0; 41 | velocity_up = 0; 42 | yaw = 0; 43 | phone_app_gps_time = 0; 44 | phone_app_latitude = 0.0; 45 | phone_app_longitude = 0.0; 46 | home_latitude = 0.0; 47 | home_longitude = 0.0; 48 | product_type = 0; 49 | uuid = zeros(1, 19); 50 | 51 | 52 | for idx=1:2:length(varargin) 53 | key = varargin{idx}; 54 | val = varargin{idx + 1}; 55 | 56 | switch(key) 57 | case 'SequenceNumber' 58 | assert(isnumeric(val), 'Sequence number must be an integer'); 59 | assert(val >= -32768 && val <= 32767, 'Invalid sequence number'); 60 | 61 | sequence_number = uint16(val); 62 | case 'StateInfo0' 63 | assert(isinteger(val), 'StateInfo0 must be an integer'); 64 | assert(val >= 0 && val <= 255, 'Invalid StateInfo0 value'); 65 | 66 | state_info(1) = uint8(val); 67 | case 'StateInfo1' 68 | assert(isinteger(val), 'StateInfo1 must be an integer'); 69 | assert(val >= 0 && val <= 255, 'Invalid StateInfo1 value'); 70 | 71 | state_info(2) = uint8(val); 72 | case 'SerialNumber' 73 | assert(isstring(val) || ischar(val), 'SerialNumber must be a string') 74 | assert(length(val) >= 14 && length(val) <= 16, ... 75 | 'SerialNumber length %d is not valid. Must be between 14 and 16 chars', length(val)) 76 | 77 | serial_number = val; 78 | case 'Longitude' 79 | assert(isnumeric(val), 'Longitude must be a number') 80 | assert(abs(val) <= 180, 'Longitude must be between -180 and 180') 81 | 82 | longitude = double(val); 83 | case 'Latitude' 84 | assert(isnumeric(val), 'Latitude must be a number') 85 | assert(abs(val) <= 90, 'Latitude must be between -90 and 90') 86 | 87 | latitude = double(val); 88 | case 'Height' 89 | assert(isinteger(val), 'Height must be an integer') 90 | assert(val >= -32768 && val <= 32767, 'Height must be between -32768 and 32767') 91 | 92 | height = int16(val); 93 | case 'Altitude' 94 | assert(isinteger(val), 'Altitude must be an integer') 95 | assert(val >= -32768 && val <= 32767, 'Altitude must be between -32768 and 32767') 96 | 97 | altitude = int16(val); 98 | case 'VelocityNorth' 99 | assert(isinteger(val), 'VelocityNorth must be an integer') 100 | assert(val >= -32768 && val <= 32767, 'VelocityNorth must be between -32768 and 32767') 101 | 102 | velocity_north = int16(val); 103 | case 'VelocityEast' 104 | assert(isinteger(val), 'VelocityEast must be an integer') 105 | assert(val >= -32768 && val <= 32767, 'VelocityEast must be between -32768 and 32767') 106 | 107 | velocity_east = int16(val); 108 | case 'VelocityUp' 109 | assert(isinteger(val), 'VelocityUp must be an integer') 110 | assert(val >= -32768 && val <= 32767, 'VelocityUp must be between -32768 and 32767') 111 | 112 | velocity_up = int16(val); 113 | case 'Yaw' 114 | assert(isinteger(val), 'Yaw must be an integer') 115 | assert(val >= -32768 && val <= 32767, 'Yaw must be between -32768 and 32767') 116 | 117 | yaw = int16(val); 118 | case 'PhoneAppGPSTime' 119 | assert(isinteger(val), 'PhoneAppGPSTime must be an integer') 120 | assert(val >= 0 && val <= (2^64)-1, 'PhoneAppGPSTime must be between 0 and (2^64)-1') 121 | 122 | phone_app_gps_time = uint64(val); 123 | case 'PhoneAppLatitude' 124 | assert(isnumeric(val), 'PhoneAppLatitude must be a number') 125 | assert(abs(val) <= 90, 'PhoneAppLatitude must be between -90 and 90') 126 | 127 | phone_app_latitude = double(val); 128 | case 'PhoneAppLongitude' 129 | assert(isnumeric(val), 'PhoneAppLongitude must be a number') 130 | assert(abs(val) <= 180, 'PhoneAppLongitude must be between -180 and 180') 131 | 132 | phone_app_longitude = double(val); 133 | case 'HomeLatitude' 134 | assert(isnumeric(val), 'HomeLatitude must be a number') 135 | assert(abs(val) <= 90, 'HomeLatitude must be between -90 and 90') 136 | 137 | home_latitude = double(val); 138 | case 'HomeLongitude' 139 | assert(isnumeric(val), 'HomeLongitude must be a number') 140 | assert(abs(val) <= 180, 'HomeLongitude must be between -180 and 180') 141 | 142 | home_longitude = double(val); 143 | case 'ProductType' 144 | assert(isinteger(val), 'ProductType must be an integer') 145 | assert(val >= 0 && val <= 255, 'ProductType must be between 0 and 255') 146 | 147 | product_type = uint8(val); 148 | case 'UUID' 149 | assert(isstring(val) || ischar(val), 'UUID must be a string') 150 | assert(length(val) == 19, 'UUID must be 19 characters long (THIS IS TEMPORARY!!)') 151 | 152 | uuid = val; 153 | otherwise 154 | error('Invalid key "%s"', key); 155 | end 156 | end 157 | 158 | coord_adj = 10000000 / 57.2957795785523; 159 | 160 | % Pack the fields into bytes 161 | sequence_number_bytes = to_bytes(sequence_number, 2); 162 | state_info_bytes = state_info; 163 | serial_number_bytes = [uint8(serial_number) zeros(1, 16 - length(serial_number))]; 164 | longitude_bytes = to_bytes(int32(round(longitude * coord_adj)), 4); 165 | latitude_bytes = to_bytes(int32(round(latitude * coord_adj)), 4); 166 | height_bytes = to_bytes(height, 2); 167 | altitude_bytes = to_bytes(altitude, 2); 168 | velocity_north_bytes = to_bytes(velocity_north, 2); 169 | velocity_east_bytes = to_bytes(velocity_east, 2); 170 | velocity_up_bytes = to_bytes(velocity_up, 2); 171 | yaw_bytes = to_bytes(yaw, 2); 172 | phone_app_gps_time_bytes = to_bytes(phone_app_gps_time, 8); 173 | phone_app_latitude_bytes = to_bytes(int32(round(phone_app_latitude * coord_adj)), 4); 174 | phone_app_longitude_bytes = to_bytes(int32(round(phone_app_longitude * coord_adj)), 4); 175 | home_latitude_bytes = to_bytes(int32(round(home_latitude * coord_adj)), 4); 176 | home_longitude_bytes = to_bytes(int32(round(home_longitude * coord_adj)), 4); 177 | product_type_bytes = product_type; 178 | uuid_bytes = uint8(uuid); 179 | 180 | version = uint8(2); 181 | message_type = uint8(16); 182 | uuid_len = uint8(length(uuid_bytes)); 183 | 184 | 185 | frame = [ ... 186 | message_type, version, sequence_number_bytes, state_info_bytes, serial_number_bytes, longitude_bytes, ... 187 | latitude_bytes, height_bytes, altitude_bytes, velocity_north_bytes, velocity_east_bytes, velocity_up_bytes, ... 188 | yaw_bytes, phone_app_gps_time_bytes, phone_app_latitude_bytes, phone_app_longitude_bytes, home_longitude_bytes, ... 189 | home_latitude_bytes, product_type_bytes, uuid_len, uuid_bytes, uint8(0) ... 190 | ]; 191 | 192 | frame = [uint8(length(frame)) frame]; 193 | 194 | crc_bytes = to_bytes(calculate_crc(frame), 2); 195 | frame = [frame, crc_bytes]; 196 | end -------------------------------------------------------------------------------- /matlab/updated_scripts/transmit/get_seconds_since_epoch.m: -------------------------------------------------------------------------------- 1 | % Gets the number of seconds since Jan 1 1970 (epoch) 2 | % 3 | % @return seconds Number of seconds since epoch 4 | function [seconds] = get_seconds_since_epoch() 5 | if (is_octave()) 6 | seconds = uint64(time()); 7 | else 8 | seconds = uint64(convertTo(datetime('now', 'TimeZone', 'UTC'), 'epochtime')); 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /matlab/updated_scripts/transmit/to_bytes.m: -------------------------------------------------------------------------------- 1 | function [byte_array] = to_bytes(value, byte_count) 2 | byte_array = zeros(1, byte_count); 3 | 4 | % Octave doesn't like negative values being used with dec2bin. So, for negative values typecast them to their 5 | % unsigned counterpart. 6 | % https://www.mathworks.com/matlabcentral/answers/98649-how-can-i-convert-a-negative-integer-to-a-binary-string-in-other-words-how-can-i-find-two-s-comple 7 | if (value < 0) 8 | value = typecast(value, ['u' class(value)]); 9 | end 10 | 11 | bits = dec2bin(value, byte_count * 8); 12 | 13 | for idx=1:byte_count 14 | bits = circshift(bits, 8); 15 | byte_array(idx) = uint8(bin2dec(bits(1:8))); 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /matlab/updated_scripts/transmit/to_qpsk.m: -------------------------------------------------------------------------------- 1 | function [samples] = to_qpsk(bitvec) 2 | assert(iscolumn(bitvec) || isrow(bitvec), "Bit vector must be a row or column vector"); 3 | assert(mod(length(bitvec), 2) == 0, "Length of bit vector must be an even number") 4 | 5 | % Keep the power of each point at 1.0 6 | amplitude = 1/sqrt(2); 7 | 8 | samples = zeros(length(bitvec) / 2, 1); 9 | 10 | for idx=1:2:length(bitvec) 11 | bits = bitvec(idx:idx+1); 12 | 13 | if (bits == [0, 0]) 14 | sample = amplitude + amplitude * 1j; 15 | elseif (bits == [0 1]) 16 | sample = amplitude - amplitude * 1j; 17 | elseif (bits == [1 0]) 18 | sample = -amplitude + amplitude * 1j; 19 | else 20 | sample = -amplitude - amplitude * 1j; 21 | end 22 | 23 | samples((idx + 1) / 2) = sample; 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/apply_freq_offset_time_domain.m: -------------------------------------------------------------------------------- 1 | % Applies a frequency shift to time domain samples 2 | % 3 | % @param samples Input samples (must be row or column vector) 4 | % @param sample_rate Sample rate (in Hz) that the frequency offset should be relative to 5 | % @param freq_offset Frequency offset (in Hz) to apply 6 | % @return samples Frequency shifted time domain samples 7 | function [samples] = apply_freq_offset_time_domain(samples, sample_rate, freq_offset) 8 | assert(isrow(samples) || iscolumn(samples), "Samples must be a row or column vector"); 9 | 10 | if (isrow(samples)) 11 | samples = samples .* exp(1j * 2 * pi * freq_offset / sample_rate * [0:length(samples)-1]); 12 | else 13 | samples = samples .* exp(1j * 2 * pi * freq_offset / sample_rate * [0:length(samples)-1].'); 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/convert_complex_file_type.m: -------------------------------------------------------------------------------- 1 | % Convert the samples in the input file to a different format (ex: float complex to int16 complex) 2 | % 3 | % The input and output types need to be string names like 'single', 'int16', or 'int8'. 4 | % To convert a file containing complex 32-bit floats to complex 16-bit ints for a radio with a 12-bit DAC: 5 | % convert_complex_file_type('/tmp/foo.fc32', 'single', '/tmp/foo.sc16', 'int16', 2^12); 6 | % 7 | % @param input_file_path Path to the input file containing complex samples of `input_type` 8 | % @param input_type Sample type of each I and Q value in `input_file_path` 9 | % @param output_file_path Path to the new file containing samples from the input file converted to `output_type` 10 | % @param output_type New sample type of each I and Q value (converted from `input_type`) 11 | % @param scalar Value to scale the input samples by before writing to disk 12 | % @return sample_count Number of complex samples written to `output_file_path` 13 | function [sample_count] = convert_complex_file_type(input_file_path, input_type, output_file_path, output_type, scalar) 14 | assert(isstring(input_file_path) || ischar(input_file_path) || iscellstr(input_file_path), ... 15 | 'Input file path must be a string, char array, or cell string'); 16 | assert(isstring(input_type) || ischar(input_type), 'Input type must be a string or char array'); 17 | assert(isstring(output_file_path) || ischar(output_file_path) || iscellstr(output_file_path), ... 18 | 'Output file path must be a string, char array, or cell string'); 19 | assert(isstring(output_file_path) || ischar(output_file_path), 'Output type must be a string or char array'); 20 | assert(isnumeric(scalar), 'Scalar must be a number'); 21 | 22 | input_file_handle = fopen(input_file_path, 'r'); 23 | assert(input_file_handle ~= -1, 'Could not open input file "%s"', input_file_path); 24 | 25 | output_file_handle = fopen(output_file_path, 'w'); 26 | assert(output_file_handle ~= -1, 'Could not open output file "%s" for writing', output_file_path); 27 | 28 | input_file_sample_bytes = get_bytes_per_sample(input_type); 29 | 30 | % How many samples to work on at a time. Keep this value as high as possible to reduce runtime. But, don't go 31 | % crazy with it as it will plow through RAM 32 | chunk_size = 1e6; 33 | 34 | % Track the number of samples written 35 | sample_count = 0; 36 | 37 | % Read until there are no samples left in the input file 38 | while (~ feof(input_file_handle)) 39 | % Read up to one chunk of samples (could be less if there aren't enough samples left in the file) 40 | input_samples = fread(input_file_handle, chunk_size * input_file_sample_bytes, input_type); 41 | 42 | % Scale and cast the input samples to the output 43 | output_samples = cast(input_samples * scalar, output_type); 44 | 45 | % Write the scaled samples to disk and update how many samples have been written 46 | sample_count = sample_count + fwrite(output_file_handle, output_samples, output_type); 47 | end 48 | 49 | % Convert to complex sample count 50 | sample_count = sample_count / 2; 51 | end 52 | 53 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/create_zc_seq.m: -------------------------------------------------------------------------------- 1 | function [zc_seq] = create_zc_seq(fft_size, root) 2 | assert(log2(fft_size) == round(log2(fft_size)), "Invalid FFT size. Must be power of 2"); 3 | 4 | % Would use MATLAB's zadoffChuSeq function, but Octave doesn't have that 5 | % The logic below was tested against the MATLAB function 6 | zc_seq = exp(-1j * pi * root * (0:600) .* (1:601) / 601); 7 | end 8 | 9 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/cyclic_prefix_sto_test.m: -------------------------------------------------------------------------------- 1 | clear all; 2 | 3 | % This script is meant to test using the cyclic prefix to find the starting time offset (STO) of a 4 | % DroneID burst 5 | 6 | %% Parameters 7 | sample_rate = 15.36e6; 8 | fft_size = get_fft_size(sample_rate); 9 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate); 10 | cyclic_prefix_length_schedule = [... 11 | long_cp_len, ... 12 | short_cp_len, ... 13 | short_cp_len, ... 14 | short_cp_len, ... 15 | short_cp_len, ... 16 | short_cp_len, ... 17 | short_cp_len, ... 18 | short_cp_len, ... 19 | long_cp_len]; 20 | num_data_carriers = 600; 21 | left_guards = (fft_size / 2) - (num_data_carriers / 2); 22 | right_guards = left_guards - 1; 23 | num_ofdm_symbols = 9; 24 | carrier_spacing = sample_rate / fft_size; 25 | zero_padding_count = 100; 26 | modulation_order = 4; 27 | 28 | % User definable parameters 29 | integer_freq_offset = -58; 30 | snr = 0; 31 | enable_plots = 1; 32 | 33 | %% Create Time Domain Samples 34 | modulator = comm.OFDMModulator( ... 35 | "FFTLength", fft_size, ... 36 | "NumGuardBandCarriers", [left_guards; right_guards], ... 37 | "CyclicPrefixLength", cyclic_prefix_length_schedule, ... 38 | "NumSymbols", num_ofdm_symbols, ... 39 | "InsertDCNull", true); 40 | 41 | % Generate the PSK that will be used in the data carriers of each OFDM sym 42 | data_carriers = pskmod(... 43 | randi([0, log2(modulation_order)], num_data_carriers, num_ofdm_symbols), ... 44 | modulation_order); 45 | 46 | % Create the time domain samples and convert to a row vector 47 | samples = modulator(data_carriers); 48 | samples = reshape(samples, 1, []); 49 | 50 | % Add in zeros to the left and right 51 | samples = [zeros(1, zero_padding_count), samples, zeros(1, zero_padding_count)]; 52 | 53 | if (enable_plots) 54 | figure(1); 55 | plot(10 * log10(abs(samples).^2)); 56 | title('Original Time Domain'); 57 | end 58 | 59 | %% Apply Integer Frequency Offset 60 | int_freq_offset_hz = integer_freq_offset * carrier_spacing; 61 | int_offset_vec = exp(1j * 2 * pi * int_freq_offset_hz / sample_rate * [0:(length(samples)-1)]); 62 | 63 | samples = samples .* int_offset_vec; 64 | 65 | %% Add Noise 66 | samples = awgn(samples, snr, 'measured'); 67 | 68 | if (enable_plots) 69 | figure(2); 70 | plot(10 * log10(abs(samples).^2)); 71 | title('Samples With Noise & Int Freq Offset'); 72 | end 73 | 74 | %% Find STO with Cyclic Prefix 75 | 76 | % How far from the first sample to search for the STO 77 | num_offsets = zero_padding_count * 1.5; 78 | scores_cp_sto = zeros(1, num_offsets); 79 | 80 | for idx=1:length(scores_cp_sto) 81 | offset = idx; 82 | scores = zeros(1, num_ofdm_symbols); 83 | 84 | % Extract and correlate the samples that each cyclic prefix is expected 85 | % to be at 86 | for cp_idx=1:num_ofdm_symbols 87 | cp_len = cyclic_prefix_length_schedule(cp_idx); 88 | 89 | % Extract the full OFDM symbol including cyclic prefix 90 | window = samples(offset:offset + fft_size + cp_len - 1); 91 | 92 | % Extract the cyclic prefix and the final samples of the symbol 93 | left = window(1:cp_len); 94 | right = window(end - cp_len + 1:end); 95 | 96 | % Correlate the two windows 97 | scores(cp_idx) = abs(xcorr(left, right, 0, 'normalized')); 98 | 99 | % Move the sample pointer forward by the full symbol size 100 | offset = offset + cp_len + fft_size; 101 | end 102 | 103 | % In the real DroneID the first OFDM symbol needs to be ignored since 104 | % it isn't always present. So, just average the correlation scores of 105 | % all but the first element 106 | scores_cp_sto(idx) = sum(scores(2:end)) / (length(scores) - 1); 107 | end 108 | 109 | if (enable_plots) 110 | figure(3); 111 | plot(scores_cp_sto); 112 | title('STO Scores (Cyclic Prefix)') 113 | end 114 | 115 | % Find the index of the highest score 116 | [~, sto_est_cp] = max(scores_cp_sto); 117 | fprintf("True offset: %d, estimated offset: %d\n", zero_padding_count + 1, sto_est_cp); 118 | 119 | if (enable_plots) 120 | figure(4); 121 | plot(10 * log10(abs(fftshift(fft(samples))).^2)); 122 | title('FFT of Noisy Signal'); 123 | end 124 | 125 | % XCorr for ZC -> Extract Burst -> CP STO -> Coarse CFO -> Integer CFO -> Equalization 126 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/est_integer_freq_offset.m: -------------------------------------------------------------------------------- 1 | % Estimates the integer frequency offset using the time domain samples of 2 | % the first ZC sequence (root index 600) 3 | % 4 | % @param received_zc_td Time domain of the received ZC sequence with no 5 | % cyclic prefix (should be row vector) 6 | % @param sample_rate Sample rate of the samples in Hz 7 | % @param max_carrier_offset How much integer frequency offset to search for 8 | % (must be positive non-zero value) 9 | % @return integer_offset_hz Best estimate for the integer frequency offset 10 | % in Hz 11 | function [integer_offset_hz] = est_integer_freq_offset(received_zc_td, sample_rate, max_carrier_offset) 12 | fft_size = get_fft_size(sample_rate); 13 | 14 | assert(isrow(received_zc_td), "ZC time domain samples must be row vector"); 15 | assert(length(received_zc_td) == fft_size, "ZC time domain samples must be FFT samples wide"); 16 | assert(isnumeric(sample_rate) && sample_rate > 0, "Sample rate must be positive and non-zero"); 17 | assert(isnumeric(max_carrier_offset) && max_carrier_offset > 0, "Max carrier offset must be positive and non-zero"); 18 | 19 | % Calculate all of the cyclic shifts that will be done to search for 20 | % the integer frequency offset value 21 | shift_range = -max_carrier_offset:1:max_carrier_offset; 22 | 23 | % Allocate storage for the correlation scores from each shift 24 | scores = zeros(1, length(shift_range)); 25 | 26 | % Move the ZC sequence to the frequency domain 27 | zc_fd = fft(received_zc_td); 28 | 29 | for idx=1:length(shift_range) 30 | % Circularly shift the FFT 31 | zc_fd_shifted = circshift(zc_fd, shift_range(idx)); 32 | 33 | % Take the upper and lower `(fft_size / 2) - 1` samples 34 | left = zc_fd_shifted(1:(fft_size / 2) - 1); 35 | right = zc_fd_shifted((fft_size / 2) + 2:end); 36 | 37 | % Correlate the lower samples with the mirror of the upper samples 38 | scores(idx) = xcorr(left, fliplr(right), 0); 39 | end 40 | 41 | % Find the index of the max score 42 | [~, index] = max(abs(scores)); 43 | 44 | % Calculate the number of Hz between each FFT bin 45 | carrier_spacing = sample_rate / fft_size; 46 | 47 | % Get how many carriers the best score was shifted by and multiply that 48 | % value by the carrier spacing to get the integer frequency offset 49 | integer_offset_hz = shift_range(index) * carrier_spacing; 50 | end 51 | 52 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/ifo_test.m: -------------------------------------------------------------------------------- 1 | % This script is for testing how to detect integer frequeny offsets (IFO) in OFDM 2 | 3 | clear all; 4 | 5 | %% Parameters 6 | sample_rate = 15.36e6; 7 | fft_size = get_fft_size(sample_rate); 8 | carrier_spacing = sample_rate / fft_size; 9 | data_carriers = 600; 10 | starting_zeros = 100; 11 | cp_len = 80; 12 | frac_frequency_offset = 0e3; 13 | carrier_offset = -1; 14 | freq_offset = carrier_spacing * carrier_offset; 15 | cp_backoff = 0; 16 | max_allowed_int_offset = 5; 17 | enable_plots = 0; 18 | snr = 15; 19 | 20 | if (abs(carrier_offset) > max_allowed_int_offset) 21 | warning("TEST WILL FAIL!! OFFSET TOO HIGH"); 22 | end 23 | 24 | %% Get Data Carrier Indices 25 | left_carriers = (fft_size / 2) - (data_carriers / 2); 26 | right_carriers = left_carriers - 1; 27 | carrier_indices = (left_carriers + 1):(left_carriers + 1 + data_carriers); 28 | 29 | %% Create OFDM Symbol 30 | 31 | % Generate ZC sequence and zero the middle sample (will become FFT DC) 32 | zc_seq = zadoffChuSeq(600, 601); 33 | zc_seq((data_carriers / 2) + 1) = 0; 34 | 35 | % Build the FFT freq domain by placing the ZC sequence in the middle with 36 | % the nulled out ZC sample at DC 37 | golden_freq_domain = reshape([zeros(left_carriers, 1); zc_seq; zeros(right_carriers, 1)], 1, []); 38 | 39 | % Convert to time domain 40 | golden_time_domain = ifft(golden_freq_domain); 41 | 42 | % Create and prepend the cyclic prefix 43 | cp = golden_time_domain(end-cp_len+1:end); 44 | ofdm_symbol = [cp, golden_time_domain]; 45 | 46 | %% Create Sample Vector with Burst 47 | 48 | % Create a "burst" with zeros on either side 49 | samples = [zeros(1, starting_zeros), ofdm_symbol, zeros(1, starting_zeros)]; 50 | 51 | % Add some noise 52 | samples = awgn(samples, snr, 'measured'); 53 | 54 | if (enable_plots) 55 | figure(3); 56 | plot(10 * log10(abs(samples).^2)); 57 | end 58 | 59 | %% Apply Integer Frequency Offset 60 | 61 | % Generate the vector that will be used to rotate each sample 62 | freq_offset_vector = exp(1j * 2 * pi * freq_offset / sample_rate * [1:length(samples)]); 63 | samples = samples .* freq_offset_vector; 64 | 65 | %% Apply Fractional Frequency Offset 66 | frac_offset_vector = exp(1j * 2 * pi * frac_frequency_offset / sample_rate * [1:length(samples)]); 67 | samples = samples .* frac_offset_vector; 68 | 69 | %% Find Starting Sample Index 70 | 71 | % Use autocorrelation to search for the start of the ZC sequence. This 72 | % takes advantage of the fact that the ZC sequence is symmetrical about the 73 | % center in both the time and frequency domains. Each iteration of the 74 | % loop below will look at one `fft_size` window of samples sliding to the 75 | % right by one sample on each iteration. The window is then split in two 76 | % with the second half fliped so that it's backwards. These two windows 77 | % are then correlated with each other, and the score saved off 78 | sto_search_window = fft_size; 79 | sto_scores = zeros(1, starting_zeros * 2); 80 | for td_idx=1:length(sto_scores) 81 | sto_window = samples(td_idx:td_idx + fft_size - 1); 82 | sto_left_window = sto_window(1:(fft_size / 2)); 83 | sto_right_window = sto_window((fft_size / 2) + 1:end); 84 | sto_scores(td_idx) = xcorr(sto_left_window, fliplr(sto_right_window), 0, 'normalized'); 85 | end 86 | 87 | if (enable_plots) 88 | figure(1); 89 | plot(abs(sto_scores)); title('STO Scores'); 90 | end 91 | 92 | % Find the index of the maximum value from the autocorrelation above 93 | [~, sto_est] = max(abs(sto_scores)); 94 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/normalize.m: -------------------------------------------------------------------------------- 1 | % Normalize the input samples such that the max I or Q value is within -1.0 to 1.0 (inclusive) 2 | % 3 | % @param samples Input samples as a row or column vector 4 | % @param backoff Multiplier that is applied to the scalar value to bring the amplitudes down. Must be between 0 and 5 | % 1.0. Primary use is to back off of the power before transmitting to prevent overdriving the amplifier 6 | % @return scaled_samples Normalized samples 7 | % @return scalar Scalar that was used to scale the samples (includes the backoff) 8 | function [scaled_samples, scalar] = normalize(samples, backoff) 9 | assert(isrow(samples) || iscolumn(samples), "Input samples must be a row or column vector"); 10 | assert(~isempty(samples), "Input samples vector was empty"); 11 | assert(isnumeric(backoff), "Backoff must be a number"); 12 | assert(backoff >= 0.0 && backoff <= 1.0, "Backoff value must be between 0.0 and 1.0 (inclusive)"); 13 | 14 | scalar = (1 / max(abs(samples))) * backoff; 15 | 16 | scaled_samples = cast(samples .* scalar, class(samples)); 17 | end 18 | 19 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/plot_fft.m: -------------------------------------------------------------------------------- 1 | % Create a plot containing the `10*log10(abs(fftshift(fft(samples))).^2))` power spectrum with the X axis in Hz 2 | % 3 | % @param samples Input complex samples (must be row/column vector) 4 | function [figure_handle] = plot_fft(samples, figure_number, title, sample_rate) 5 | assert(isrow(samples) || iscolumn(samples), "Samples must be a row/column vector"); 6 | 7 | figure_handle = figure(figure_number); 8 | 9 | sample_count = length(samples); 10 | x_axis_values = sample_rate * (-sample_count/2:sample_count/2-1)/sample_count; 11 | plot(x_axis_values, 10 * log10(abs(fftshift(fft(samples))).^2)); 12 | set(gcf, 'Name', title); 13 | end 14 | 15 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/plot_symbol_boundaries.m: -------------------------------------------------------------------------------- 1 | function [] = plot_symbol_boundaries(samples, sample_rate, figure_num) 2 | long_cp_len = round(1/192000 * sample_rate); 3 | short_cp_len = round(0.0000046875 * sample_rate); 4 | fft_size = sample_rate / 15e3; 5 | 6 | % Plot the time domain 7 | figure(figure_num); 8 | plot(10 * log10(abs(samples))); 9 | 10 | % Tranceparency of each box (so the waveform is still visible) 11 | face_opacity = 0.25; 12 | 13 | % Don't show the lines between the boxes. Just gets in the way 14 | edge_color = [0, 0, 0, 0]; 15 | 16 | % Define the box colors (with transparency) 17 | face_color_red = [1, 0, 0]; 18 | face_color_green = [0, 1, 0]; 19 | 20 | % The Y axis of the plot is in dB, so draw a box that starts at -30 dB and 21 | % goes up by 25 dB (-5 dB). These numbers just happened to work out with 22 | % the test data and will likely change for other collects 23 | box_y_start = -30; 24 | box_y_height = 25; 25 | 26 | for symbol_idx=0:8 27 | if (mod(symbol_idx, 2) == 0) 28 | color = face_color_red; 29 | else 30 | color = face_color_green; 31 | end 32 | 33 | relative_offset = symbol_idx * (fft_size + short_cp_len); 34 | r = rectangle('Position', [1 + relative_offset, box_y_start, fft_size + short_cp_len, box_y_height]); 35 | 36 | if (exist('OCTAVE_VERSION', 'builtin')) 37 | set(r, "FaceColor", color); 38 | set(get(r, 'children'), "facealpha", face_opacity); 39 | else 40 | set(r, "FaceColor", [color, face_opacity]); 41 | end 42 | end 43 | 44 | title('OFDM Symbol Boundaries') 45 | end -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/read_complex_floats.m: -------------------------------------------------------------------------------- 1 | % Reads in a window of complex samples from the provided file 2 | % 3 | % @param file_path Path to file containing 32-bit floating point interleaved complex samples (I,Q,I,Q,...) 4 | % @param sample_offset How many samples into the file to skip before reading in samples 5 | % @param sample_count How many samples to extract after the `sample_offset` 6 | % @return samples Column vector of `sample_count` complex samples (might be less if not enough samples are avilable 7 | % in the provided file 8 | function [samples] = read_complex_floats(file_path, sample_offset, sample_count) 9 | samples = double(read_complex(file_path, sample_offset, sample_count, 'single')); 10 | end 11 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/test_integer_freq_offset.m: -------------------------------------------------------------------------------- 1 | clear all; 2 | 3 | % This script is used to test time and frequency estimation / correction of 4 | % a simulated DroneID burst. No equalization is done in this script. 5 | % There is a simulated fractional time offset applied, but the script will 6 | % fail if that time offset is anything other than 0 or 0.5. This is 7 | % something that will likely have to be fixed by the equalizer or by 8 | % upsampling to find the most correct fractional starting time offset 9 | 10 | %% Parameters 11 | sample_rate = 15.36e6; 12 | fft_size = get_fft_size(sample_rate); 13 | carrier_spacing = round(sample_rate / fft_size); 14 | 15 | % Pick a worst case Parts Per Million (PPM) to support. 20 PPM is the 16 | % value for the HackRF SDR's oscialltor. Then choose the worst case 17 | % frequency that the SDR will operate at. In this case 5.9 GHz is the 18 | % highest frequency the radio will need to tune to for DroneID. Use that 19 | % to then figure out what the worst case frequency offset (in Hz) can be. 20 | % That will be used as the search space for the Integer Frequency Offset 21 | % (IFO) which is done later. 22 | worst_case_ppm = 20; 23 | worst_case_freq = 5.9e9; 24 | max_allowed_freq_offset = worst_case_freq * worst_case_ppm / 1e6; 25 | max_allowed_int_freq_offset = ceil(max_allowed_freq_offset / carrier_spacing); 26 | 27 | % Pick a frequency offset to test with 28 | frequency_offset = max_allowed_freq_offset - 3.3e3; 29 | 30 | % Calculate the long and short cyclic prefix lengths 31 | [long_cp_len, short_cp_len] = get_cyclic_prefix_lengths(sample_rate); 32 | 33 | % Below are the cyclic prefix lengths for each OFDM symbol 34 | cyclic_prefixes = [ 35 | long_cp_len, ... 36 | short_cp_len, ... 37 | short_cp_len, ... 38 | short_cp_len, ... 39 | short_cp_len, ... 40 | short_cp_len, ... 41 | short_cp_len, ... 42 | short_cp_len, ... 43 | long_cp_len 44 | ]; 45 | 46 | % Calculate how many samples there are in one DroneID burst 47 | total_burst_sample_count = (fft_size * length(cyclic_prefixes)) + sum(cyclic_prefixes); 48 | 49 | % Calculate how many guard carriers there are in one OFDM symbol 50 | [left_guards, right_guards] = get_num_guard_carriers(sample_rate); 51 | 52 | % Number of constellation points in the underlying PSK 53 | modulation_order = 4; 54 | 55 | % Number of occupied OFDM carriers per symbol 56 | num_data_carriers = 600; 57 | 58 | %% OFDM Time Domain Creation 59 | modulator = comm.OFDMModulator( ... 60 | "FFTLength", fft_size, ... 61 | "NumGuardBandCarriers", [left_guards; right_guards], ... 62 | "CyclicPrefixLength", cyclic_prefixes, ... 63 | "NumSymbols", length(cyclic_prefixes), ... 64 | "InsertDCNull", true); 65 | 66 | % Modulate 9 random OFDM symbols worth of PSK samples 67 | symbols = pskmod( ... 68 | randi([0, modulation_order - 1], num_data_carriers, length(cyclic_prefixes)), ... 69 | modulation_order); 70 | 71 | % Create the OFDM time domain 72 | modulated_samples = modulator(symbols); 73 | 74 | %% Insert ZC Sequence at symbol 4 75 | 76 | % Figure out where in the modulated samples that the ZC sequence should 77 | % start (this includes the cyclic prefix) 78 | symbol_4_offset = (fft_size * 3) + sum(cyclic_prefixes(1:3)); 79 | 80 | % Create the time domain ZC sequence for symbol 4 81 | zc_seq = create_zc(fft_size, 4); 82 | 83 | % Tack on the cyclic prefix to build the full symbol 84 | full_zc_seq = [zc_seq(end-cyclic_prefixes(4)+1:end); zc_seq]; 85 | 86 | % Insert the full ZC sequence symbol in place of the modulated symbol 4 87 | modulated_samples(symbol_4_offset:symbol_4_offset + length(full_zc_seq) - 1) = full_zc_seq; 88 | 89 | % Convert to row vector 90 | modulated_samples = reshape(modulated_samples, 1, []); 91 | 92 | %% Prepend/Append Zeros 93 | 94 | % These zeros are needed to simulate an actual received sample and the 95 | % ambiguity of where the burst actually starts. The number of samples 96 | % isn't critical, but should be something > about 30 samples 97 | num_zeros = 50; 98 | 99 | zero_vec = zeros(1, num_zeros); 100 | modulated_samples = [zero_vec, modulated_samples, zero_vec]; 101 | 102 | %% Add in a 0.5 sample time offset 103 | 104 | % This simulates the clocks of the transmitter and receiver not being 105 | % perfectly synced up. Upsample by 4, then skip the first two samples, and 106 | % downsample by 4 107 | modulated_samples = resample(modulated_samples, 4, 1); 108 | modulated_samples = [modulated_samples(2:end), 0, 0]; 109 | modulated_samples = resample(modulated_samples, 1, 4); 110 | 111 | %% Add Noise 112 | 113 | % This is not an exact SNR. Just add gaussian noise to the signal to 114 | % simulate real world reception 115 | modulated_samples = awgn(modulated_samples, 10, 'measured'); 116 | 117 | figure(1); 118 | plot(10 * log10(abs(modulated_samples).^2)); 119 | title('Time Domain with Noise'); 120 | %% Apply Frequency Offset 121 | 122 | freq_offset_vector = exp(1j * 2 * pi * frequency_offset / sample_rate * [1:length(modulated_samples)]); 123 | freq_offset_samples = modulated_samples .* freq_offset_vector; 124 | 125 | figure(2); 126 | subplot(3, 1, 1); plot(10 * log10(abs(fftshift(fft(modulated_samples))).^2)); 127 | title('Original Modulated Samples (FFT)') 128 | subplot(3, 1, 2); plot(10 * log10(abs(fftshift(fft(freq_offset_vector))).^2)); 129 | title('Frequency Offset Vector (FFT)') 130 | subplot(3, 1, 3); plot(10 * log10(abs(fftshift(fft(freq_offset_samples))).^2)); 131 | title('Frequency Offset Samples (FFT)') 132 | 133 | %% Find the ZC sequence 134 | 135 | % Find the start of the burst by looking at the cyclic prefixes. Make sure 136 | % to look PAST `num_zeros` since in the real world it will not be known 137 | % where the burst actually starts, so the search space will be rather large 138 | sto_estimate = est_sto(freq_offset_samples, sample_rate, num_zeros * 1.5); 139 | 140 | % Calculate how many samples after the start of the burst that the ZC 141 | % sequence should be located (this offset includes the ZC sequence cyclic 142 | % prefix) 143 | zc_seq_offset = (fft_size * 3) + long_cp_len + (short_cp_len * 3); 144 | 145 | % Calculate where in the sample vector the ZC sequence starts 146 | auto_corr_index = sto_estimate + zc_seq_offset; 147 | 148 | fprintf("Sample offset error: %d\n", sto_estimate - num_zeros - 1); 149 | %% Estimate coarse freq offset and adjust 150 | 151 | % Use the cyclic prefix of the ZC sequence to do coarse frequency offset 152 | % estimation (the ZC sequence is the 4th OFDM symbol). 153 | coarse_cp_len = cyclic_prefixes(4); 154 | cp_len_window_size = coarse_cp_len; 155 | 156 | % The auto_corr_index points to the start of the ZC sequence, so back off 157 | % by the length of the 4th cyclic prefix 158 | coarse_offset_start = auto_corr_index - coarse_cp_len; 159 | 160 | % Extract out `fft_size + coarse_cp_len` samples 161 | window = freq_offset_samples(coarse_offset_start:coarse_offset_start + fft_size + coarse_cp_len - 1); 162 | 163 | % Extract the first and last `coarse_cp_len` samples from the window 164 | window_left = window(1:coarse_cp_len); 165 | window_right = window(end - coarse_cp_len + 1:end); 166 | 167 | % Calculate the coarse frequency offset in radians and Hz by taking the dot 168 | % product of the two windows 169 | cfo_est_radians = angle(sum(window_left .* conj(window_right))) / fft_size; 170 | cfo_est_hz = cfo_est_radians * sample_rate / (2 * pi); 171 | 172 | % Adjust for the frequency offset 173 | cfo_est_adj_vector = exp(1j * cfo_est_radians * [1:length(freq_offset_samples)]); 174 | freq_offset_samples = freq_offset_samples .* cfo_est_adj_vector; 175 | 176 | %% Integer frequency offset estimation 177 | % The ZC sequence will be used to find the integer frequency offset. So, 178 | % extract out just the samples for the ZC sequence (not including the 179 | % cyclic prefix) 180 | received_zc_sym = freq_offset_samples(auto_corr_index:auto_corr_index + fft_size - 1); 181 | 182 | figure(3); 183 | plot(abs(received_zc_sym)); 184 | title('ZC Sequence (Time Domain Magnitude)'); 185 | 186 | % Estimate the integer frequency offset 187 | est_int_offset_hz = est_integer_freq_offset(received_zc_sym, sample_rate, max_allowed_int_freq_offset); 188 | 189 | % Adjust for the integer frequency offset 190 | ifo_est_adj_vector = exp(1j * 2 * pi * est_int_offset_hz / sample_rate * [1:length(freq_offset_samples)]); 191 | freq_offset_samples = freq_offset_samples .* ifo_est_adj_vector; 192 | 193 | % Print out how far off the combination of coarse and integer freq offset 194 | % was from the true value 195 | residual_freq_error_hz = est_int_offset_hz + cfo_est_hz + frequency_offset; 196 | fprintf("Remaining Frequency Offset (Hz): %f\n", residual_freq_error_hz); 197 | 198 | %% Extract OFDM Data Carriers 199 | demod = comm.OFDMDemodulator( ... 200 | "FFTLength", fft_size, ... 201 | "NumGuardBandCarriers", [left_guards; right_guards], ... 202 | "CyclicPrefixLength", cyclic_prefixes, ... 203 | "NumSymbols", length(cyclic_prefixes), ... 204 | "RemoveDCCarrier", true); 205 | 206 | % Extract out just the samples in the burst and run those through the OFDM 207 | % demodulator 208 | burst_samples = freq_offset_samples(sto_estimate:sto_estimate + total_burst_sample_count - 1); 209 | demod_symbols = demod(reshape(burst_samples, [], 1)); 210 | 211 | % Plot just the data carrying OFDM symbol constellations (the 4th symbol is 212 | % the ZC sequence) 213 | figure(4); 214 | plot(demod_symbols(:,[1,2,3,5,6,7,8,9]), 'o'); 215 | title('Demodulated Constellations') 216 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/write_complex.m: -------------------------------------------------------------------------------- 1 | % Write arbitrary type complex samples as interleaved I/Q values (Real(1),Imag(1),Real(2),Imag(2),...) 2 | % 3 | % Variable arguments are as follows: 4 | % - Append: Can be '1' or 'true' to append to an existing file, or '0' or 'false' to overwrite. Defaults to overwrite 5 | % 6 | % @param output_path Path to output the samples to. Must be string, char array, or cell string 7 | % @param samples Vector (row or column) of samples to write 8 | % @param sample_type Data type the samples should be written out as. Example: 'single' for floats, 'int16' for shorts 9 | % @param varargin Variable arguments (see above) 10 | % @return Number of complex samples written 11 | 12 | function [written] = write_complex(output_path, samples, sample_type, varargin) 13 | assert(isstring(output_path) || ischar(output_path) || iscellstr(output_path), ... 14 | 'Output path must be a string, char array, or cell string'); 15 | assert(~ isempty(output_path), 'Output path was empty'); 16 | assert(~ isempty(samples), 'No samples provided'); 17 | assert(iscolumn(samples) || isrow(samples), 'Input samples must be row or column vector'); 18 | assert(isstring(sample_type) || ischar(sample_type) || iscellstr(sample_type), ... 19 | 'Sample type must be string, char array, or cell string'); 20 | assert(mod(length(varargin), 2) == 0, 'Variable argument length must be even. Got %d arguments', length(varargin)); 21 | 22 | % Default to overwriting the output file 23 | fopen_mode = 'w'; 24 | 25 | % Parse varargs 26 | for idx=1:2:length(varargin) 27 | key = varargin{idx}; 28 | value = varargin{idx+1}; 29 | 30 | switch(key) 31 | case 'Append' 32 | if (value == 1 || value == true) 33 | % When appending in MATLAB the mode must be 'a', not 'wa' 34 | fopen_mode = 'a'; 35 | elseif (value ~= 0 && value ~= false) 36 | error('Invalid value "%s" for Append', mat2str(value)); 37 | end 38 | end 39 | end 40 | 41 | % Open the output file and check the operation succeeded 42 | handle = fopen(output_path, fopen_mode); 43 | assert(handle ~= -1, 'Could not open output file "%s" for writing', output_path); 44 | 45 | % Attempt to write out the samples as interleaved I/Q 46 | written = fwrite(handle, reshape([real(samples); imag(samples)], 1, []), sample_type); 47 | 48 | % Warn the user if not all samples could be written 49 | if (written ~= length(samples) * 2) 50 | warning('Attempted to write %d samples to "%s" but only wrote %d', length(samples * 2), output_path, written); 51 | end 52 | 53 | fclose(handle); 54 | end 55 | 56 | -------------------------------------------------------------------------------- /matlab/updated_scripts/unused_scripts/write_complex_floats.m: -------------------------------------------------------------------------------- 1 | function [] = write_complex_floats(output_path, samples) 2 | write_complex(output_path, samples, 'single'); 3 | end 4 | 5 | --------------------------------------------------------------------------------