12 DJI Mavic 3 DroneID Analysis
proto17 edytuje tę stronę 2022-09-05 18:30:22 -04:00

This page outlines the work done to get from the DJI Mini 2, to the DJI Mavic 3

Initial Analysis

The first thing do to is have a look at an example collect's time domain.

Step one here is to load the collected samples into MATLAB. I was provided a collect of a single burst that was done with a very high SNR at 15.36 MSPS. The following code will load the samples (using the functions in this repo) and plot the time domain

clear all;

burst = read_complex('collect.fc32', 0, Inf, 'single');
figure(1); 
plot(10 * log10(abs(burst).^2)); 
title('Orig Time Domain dB');
xlabel('Sample Index');
ylabel('Power (dB)');

The code above produces:

mavic_3_time_domain

The image above shows that there are now 4 ZC sequences, and it looks like they are both repeated. The two on the right appear to be the same root 600 sequences from the DJI Mini 2, and the ones on the left look a lot like the root 147 sequences, but the structure is harder to see.

ZC Sequences

To test the theory above, first look for the root 600 sequence:

clear all;

burst = read_complex('collect.fc32', 0, Inf, 'single');
figure(1); 
plot(10 * log10(abs(burst).^2)); 
title('Orig Time Domain dB');
xlabel('Sample Index');
ylabel('Power (dB)');

%% Define constants
sample_rate = 15.36e6;
carrier_spacing = 15e3;
fft_size = sample_rate / carrier_spacing;
data_carrier_indices = get_data_carrier_indices(sample_rate);

%% Create the root index 600 ZC sequence for time domain correlation
% Create the frequency domain ZC sequence with root 600 and remove the middle sample
zc_600 = reshape(exp(-1j * pi * 600 * (0:600) .* (1:601) / 601), [], 1);
zc_600(301) = [];

zc_600_samples_fd = zeros(fft_size, 1);
zc_600_samples_fd(data_carrier_indices) = zc_600;

zc_600_samples_td = ifft(fftshift(zc_600_samples_fd));

%% Correlate for the root index 600 ZC sequence
zc_600_xcorr_results = normalized_xcorr_fast(burst, zc_600_samples_td);
figure(2);
plot(abs(zc_600_xcorr_results));
title('ZC Root Index 600 XCorr Scores');
xlabel('Starting Sample Index');
ylabel('Normalized Correlation Score');

Running the code above shows that the root index 600 sequences are still present

zc_600_xcorr_results

The indices of the sequences are 7354 and 8450. Looking at where the suspected root 600 sequences start in the time domain shows that the second two are almost certainly root 600 sequences.

zc_600_time_domain_indices

That leaves the other sequence. Assuming for a moment that it's not going to be the same, how about brute forcing for the sequence? WARNING: THIS WILL TAKE A LOT OF CPU POWER TO RUN!!

clear all;

burst = read_complex('collect.fc32', 0, Inf, 'single');
figure(1); 
plot(10 * log10(abs(burst).^2)); 
title('Orig Time Domain dB');
xlabel('Sample Index');
ylabel('Power (dB)');

%% Define constants
sample_rate = 15.36e6;
carrier_spacing = 15e3;
fft_size = sample_rate / carrier_spacing;
data_carrier_indices = get_data_carrier_indices(sample_rate);

%% Create the root index 600 ZC sequence for time domain correlation
% Create the frequency domain ZC sequence with root 600 and remove the middle sample
zc_600 = reshape(exp(-1j * pi * 600 * (0:600) .* (1:601) / 601), [], 1);
zc_600(301) = [];

zc_600_samples_fd = zeros(fft_size, 1);
zc_600_samples_fd(data_carrier_indices) = zc_600;

zc_600_samples_td = ifft(fftshift(zc_600_samples_fd));

%% Correlate for the root index 600 ZC sequence
zc_600_xcorr_results = normalized_xcorr_fast(burst, zc_600_samples_td);
figure(2);
plot(abs(zc_600_xcorr_results));
title('ZC Root Index 600 XCorr Scores');
xlabel('Starting Sample Index');
ylabel('Normalized Correlation Score');

%% Brute force for root index(es) of the other sequence(s)

% Attempt to find root indices from 1 to 700 inclusive
zc_search_scores = zeros(1, 700);
for root_idx = 1:length(zc_search_scores)
    % Calculate the ZC sequence for the current root index, remove the
    % middle element, map to FFT bins, and convert to time domain (no
    % cyclic prefix)
    zc = reshape(exp(-1j * pi * root_idx * (0:600) .* (1:601) / 601), [], 1);
    zc(301) = [];

    zc_samples_fd = zeros(fft_size, 1);
    zc_samples_fd(data_carrier_indices) = zc;

    zc_samples_td = ifft(fftshift(zc_samples_fd));
    
    % Search for the time domain version of the ZC sequence throughout the
    % entire burst
    xcorr_results = abs(normalized_xcorr_fast(burst, zc_samples_td));
    
    % Save off just the highest correlation value.  This will not say how
    % many of each ZC sequence root index are present, but that will be
    % handled later
    [value, ~] = max(xcorr_results);
    zc_search_scores(root_idx) = value;
    
    % This search will take quite a while, so print out some information
    % about how far into the search the script is.
    if (mod(root_idx, 100) == 0)
        fprintf(1, '%d\n', root_idx);
    end
end

% Plot the search results
figure(3);
plot(zc_search_scores);
title('Root Index Search Results');
xlabel('Root Index');

The code above will produce the following new plot

zc_brute_force

The plot shows that there are two root indices present: 600 (which was verified earlier) and 385 which is new. Since there are not three spikes present, the assumption is that root index 385 is present twice just like 600. To verify:

clear all;

burst = read_complex('collect.fc32', 0, Inf, 'single');
figure(1); 
plot(10 * log10(abs(burst).^2)); 
title('Orig Time Domain dB');
xlabel('Sample Index');
ylabel('Power (dB)');

%% Define constants
sample_rate = 15.36e6;
carrier_spacing = 15e3;
fft_size = sample_rate / carrier_spacing;
data_carrier_indices = get_data_carrier_indices(sample_rate);

%% Create the root index 600 ZC sequence for time domain correlation
% Create the frequency domain ZC sequence with root 600 and remove the middle sample
zc_600 = reshape(exp(-1j * pi * 600 * (0:600) .* (1:601) / 601), [], 1);
zc_600(301) = [];

zc_600_samples_fd = zeros(fft_size, 1);
zc_600_samples_fd(data_carrier_indices) = zc_600;

zc_600_samples_td = ifft(fftshift(zc_600_samples_fd));

%% Correlate for the root index 600 ZC sequence
zc_600_xcorr_results = normalized_xcorr_fast(burst, zc_600_samples_td);
figure(2);
plot(abs(zc_600_xcorr_results));
title('ZC Root Index 600 XCorr Scores');
xlabel('Starting Sample Index');
ylabel('Normalized Correlation Score');

%% Brute force for root index(es) of the other sequence(s)

% % Attempt to find root indices from 1 to 700 inclusive
% zc_search_scores = zeros(1, 700);
% for root_idx = 1:length(zc_search_scores)
%     % Calculate the ZC sequence for the current root index, remove the
%     % middle element, map to FFT bins, and convert to time domain (no
%     % cyclic prefix)
%     zc = reshape(exp(-1j * pi * root_idx * (0:600) .* (1:601) / 601), [], 1);
%     zc(301) = [];
% 
%     zc_samples_fd = zeros(fft_size, 1);
%     zc_samples_fd(data_carrier_indices) = zc;
% 
%     zc_samples_td = ifft(fftshift(zc_samples_fd));
%     
%     % Search for the time domain version of the ZC sequence throughout the
%     % entire burst
%     xcorr_results = abs(normalized_xcorr_fast(burst, zc_samples_td));
%     
%     % Save off just the highest correlation value.  This will not say how
%     % many of each ZC sequence root index are present, but that will be
%     % handled later
%     [value, ~] = max(xcorr_results);
%     zc_search_scores(root_idx) = value;
%     
%     % This search will take quite a while, so print out some information
%     % about how far into the search the script is.
%     if (mod(root_idx, 100) == 0)
%         fprintf(1, '%d\n', root_idx);
%     end
% end
% 
% % Plot the search results
% figure(3);
% plot(zc_search_scores);
% title('Root Index Search Results');
% xlabel('Root Index');
% ylabel('Normalized Correlation Score');

%% Create the root index 385 ZC sequence for time domain correlation
% Create the frequency domain ZC sequence with root 385 and remove the middle sample
zc_385 = reshape(exp(-1j * pi * 385 * (0:600) .* (1:601) / 601), [], 1);
zc_385(301) = [];

zc_385_samples_fd = zeros(fft_size, 1);
zc_385_samples_fd(data_carrier_indices) = zc_385;

zc_385_samples_td = ifft(fftshift(zc_385_samples_fd));

%% Correlate for the root index 385 ZC sequence
zc_385_xcorr_results = normalized_xcorr_fast(burst, zc_385_samples_td);
figure(4);
plot(abs(zc_385_xcorr_results));
title('ZC Root Index 385 XCorr Scores');
xlabel('Starting Sample Index');
ylabel('Normalized Correlation Score');

The brute forcing of the ZC sequence was commented out since it's no longer needed. The above code should produce the following new plot

zc_385_xcorr_results

The two highest peaks are 5162 and 6258. Using the FFT size of 1024 (sample rate of 15.36e6 / carrier spacing 15e3) the locations of the sequences can be validated:

If the first sequence starts at 5162, the next should be cyclic_prefix + fft_size away. Assuming that the short cyclic prefix is being used, that's 72 + 1024 which means the start of the second instance of the 385 root index should be at 5162 + 72 + 1024 which is 6258. So far so good. Now, the time domain shows that the first root index 600 starts at sample offset 7354. Doing the same addition to the second root 385 sequence would put the first root 600 sequence at 6258 + 1024 + 72 or 7354. And finally, the second root 600 sequence should start at 7354 + 1024 + 72 or 8450. So, all of the starting indices work out perfectly!

Data Symbols

Since there are 4 ZC sequences in total, that leaves what appears to be 6 data OFDM symbols. Handily, that's exactly what the DJI Mini 2 uses!

This concludes the initial look at the Mavic 3 DroneID signal. The next part requires a fair amount of time as it gets into STO, CFO, symbol extraction, and equalization. All of which are a pain =\