This is a continuation of a previous post https://radix2tech.com/decoding-an-rf-dog-barking-control-collar/ in which I examined the signal characteristics of a non-branded dog training collar. I purchased an additional training collar which has two functions: warning, and shock. Our 5 year old lab mix has a dangerous habit of chasing vehicles, and I wanted to curb his behavior as well as keep him safe if he bolted from our front door or gate.
With the same intentions as the previous post, I was interested in the modulation used to encode the functions. I used my Real Time Spectrum Analyzer to analyze the transmitted signal along with recording the IQ data. I analyzed it using Matlab, reconstructed the functions, saved the data, and transmitted the signals using a Software Defined Radio (SDR) to see if they would trigger the receiver.
To record the IQ data, I used my Signal Hound BB60C and captured data from each function. To broadcast the signal, I chose to use the HackRF One, since the bottom of the transmit frequency capability of the Nuand bladeRF 2.0 micro xA4 doesn’t reach the 27.145 MHz required. The bladeRF frequency range is 47 MHz to 6 GHz, while the HackRF One has a range of 1 MHz to 6 GHz.
Since this device is manufactured in Canada, it has a RF identification number affixed to the transmitter. That’s usually the first step in identifying the transmitter frequency, bandwidth, modulation, etc. Looking up the information revealed some good information.
In the United States, the FCC ID lookup usually provides more detailed information, such as modulation schemes, testing documentation, and sometimes schematics. This was useful enough to get started, however. Using the Spike software, I discovered that the signal occupies about 6 kHz bandwidth at approximately 27.145 MHz. The carrier was very stable. It appeared to be a form of Frequency Shift Keying (FSK) with peaks at -2 kHz and +2 kHz offset from the center frequency. Selecting the Zero Span mode in the Spike software and choosing a video trigger, I captured a snapshot of the waveform. Judging from the IQ plot and phase modulation vs. time plot, it was very evident that the modulation is 2FSK.
If you look at the zero span plot, the IQ/time plot shows distinct breaks in the waveform where the markers are present. This displays a reversal of the phasor with the same wavelength, indicating a an identical frequency with the opposite sign. This makes sense, since we have chosen the center of the captured bandwidth to fall in the exact center of the device’s transmitted signal. Frequencies on the left of the carrier are negative, while positive frequencies lie to the right. Choosing the center frequency to be a lower frequency (i.e. 27.140 MHz) than the carrier, there are two distinct positive frequency peaks, and the IQ plot shows two distinct wavelengths.
I captured a segment of IQ data while pressing the warning button, loaded it into Matlab, and trimmed it to the start of the message. You can see the phase shifts occurring at intervals of 1 ms depending of whether the signal represents a “0” or “1”. Feeding the signal through Matlab’s FSK demodulator and plotting the data shows the series of digital data representing the warning function.
I reconstructed the signal from the digital data using Matlab in the script below. Note that I created a two complex exponential signals at two frequencies -2 kHz and +2 kHz, and combined them depending on the bit information: +2 kHz for a ‘1’ and -2 kHz for a ‘0’ offset from the carrier.
close all
clear all
% Load IQ data
xml = parseXML('dog_remote_2-10-24-20-17h41m42s224.xml');
Fs = 0;
t = 0;
sampleCount = 0;
scaleFactor = 0;
for ii = 1:length(xml.Children)
% Sample rate
if strcmp(xml.Children(ii).Name, 'SampleRate')
Fs = str2double(xml.Children(ii).Children.Data);
end
% Sample count
if strcmp(xml.Children(ii).Name, 'SampleCount')
sampleCount = str2double(xml.Children(ii).Children.Data);
end
% Scale factor
if strcmp(xml.Children(ii).Name, 'ScaleFactor')
scaleFactor = str2double(xml.Children(ii).Children.Data);
end
end
s = load_iq_bb60c('dog_remote_2-10-24-20-17h41m42s224.iq',scaleFactor).';
% Trim to the warning button press
s = s(2923000:5407000);
t = 0:1/Fs:length(s)/Fs-1/Fs;
figure
plot(t,10*log10(abs(s)))
xlabel('time (s)')
ylabel('Power (dBm)')
title('Received Signal for Warning Function')
% Decimate by 100 using a Chebyshev filter of order 10
s1 = decimate(s.',100,10).';
% Trim to make it a multiple of the SamplesPerSymbol property
s1 = s1(42:end);
Fs = Fs/100;
sz = length(s1)/Fs;
t = 0:1/Fs:sz-1/Fs;
figure
plot(t,real(s1),t,imag(s1))
xlabel('time (s)')
ylabel('amplitude (scaled)')
title('Received Signal for Warning Function')
% Demodulate the 2FSK signal
M = 2;
freqSep = 4e3;
SamplesPerSymbol = 50;
fskDemod = comm.FSKDemodulator(M,'FrequencySeparation',freqSep,'SamplesPerSymbol',SamplesPerSymbol,'SymbolRate',1000);
rcdData = step(fskDemod,s1.');
tc = 0:SamplesPerSymbol/Fs:t(end);
figure
stem(tc,rcdData)
grid on
ylim([0 1.1])
xlabel('time (s)')
ylabel('Digital Data')
title('Demodulated Digital Data for Warning Function')
% Construct our 2FSK data
Fc1 = 2000;
Fc2 = -2000;
t = 0:1/Fs:length(s1)/Fs-1/Fs;
sC1 = exp(1i*2*pi*Fc1*t);
sC2 = exp(1i*2*pi*Fc2*t);
data = kron(rcdData.',ones(1,50));
% A '1' is +2 kHz and a '0' is -2 kHz
sC1 = data.*sC1;
sC2 = abs(data-1).*sC2;
sC = sC1+sC2;
figure
plot(t,real(sC),t,imag(sC))
grid on
ylim([-1.1 1.1])
xlabel('time (s)')
ylabel('amplitude (scaled)')
title('Reconstructed Signal for Warning Function')
figure
pwelch(sC,[],[],[],Fs,'centered','power');
title('Spectrum for Reconstructed Signal - Warning Function')
% Resample by 160 to get a sample rate of 8 MS/s
sC = interp(sC,160);
Fs = Fs*160;
figure
pwelch(sC,[],[],[],Fs,'centered','power');
% Save the file for playing back with the HackRF
save_sc8('dog_remote_warning.bin',sC);
function [ signal, signal_i, signal_q ] = load_iq_bb60c(filename, scaleFactor)
% Read a normalized complex signal from a binary file in the
% BB60C format
%
[f, err_msg] = fopen(filename, 'r', 'ieee-le');
if f ~= -1
data = fread(f, Inf, 'int16');
signal_i = data(1:2:end - 1, :);
signal_i(signal_i<0) = signal_i(signal_i<0)./32768;
signal_i(signal_i>0) = signal_i(signal_i>0)./32767;
signal_i = signal_i ./ scaleFactor;
signal_q = data(2:2:end, :);
signal_q(signal_q<0) = signal_q(signal_q<0)./32768;
signal_q(signal_q>0) = signal_q(signal_q>0)./32767;
signal_q = signal_q ./ scaleFactor;
signal = signal_i + 1j .* signal_q;
fclose(f);
else
error(err_msg);
end
end
function [ret] = save_sc8(filename, signal)
% SAVE_SC8 Write a normalized complex signal to a binary file in the
% hackrf "SC8" format.
%
% [RET] = save_sc8(FILENAME, SIGNAL)
%
% RET is 1 on success, and 0 on failure.
%
% FILENAME is the target filename. The file will be overwritten if it
% already exists. The file data is written in little-endian format.
%
% SIGNAL is a complex signal with the real and imaginary components
% within the range [-1.0, 1.0).
[f, err_msg] = fopen(filename, 'w', 'ieee-le');
if f ~= -1
sig_i = round(real(signal) .* 127.0);
sig_i(sig_i > 127) = 127;
sig_i(sig_i < -127) = -127;
sig_q = round(imag(signal) .* 127.0);
sig_q(sig_q > 127) = 127;
sig_q(sig_q < -127) = -127;
assert(length(sig_i) == length(sig_q));
sig_len = 2 * length(sig_i);
sig_out(1:2:sig_len - 1) = sig_i;
sig_out(2:2:sig_len) = sig_q;
count = fwrite(f, sig_out, 'int8');
fclose(f);
if count == sig_len
ret = 1;
else
end
else
error(err_msg);
ret = 0;
end
end