I picked up a remote control training collar for our 7 year old Maltese to help train her in order to curb barking. In the spirit of keeping everything humane, we only use two of the three functions (alarm, vibrate, and shock), avoiding the shock function for obvious reasons. I became interested in the modulation used to encode the functions, so I used my Real Time Spectrum Analyzer to analyze the transmitted signal. I also recorded some IQ data and analyzed it using Matlab, reconstructing the functions as IQ data and finally transmitting 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 of the three functions. The BB60C is an amazing device, and I plan on writing up a review in a future post. I also wanted to broadcast reconstructed signals to test the validity of the decoded data. In order to do that, I chose to use the Nuand bladeRF 2.0 micro xA4.
Using the Spike software, I discovered that the signal generated by the dog remote occupies a small amount of bandwidth at approximately 433 MHz. The carrier was not extremely stable, moving randomly +/- a few kHz around 433 MHz. 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 the AM vs Time plot, it is fairly obvious that the signal is modulated by On/Off Keying (OOK) which is the simplest form of amplitude-shift keying (ASK) modulation, representing digital data as the presence or absence of a carrier.
I set the capture bandwidth to 5 MHz and collected 8 seconds of data as I pressed the three buttons on the remote. Following the recording of the data, I loaded the collected data in Matlab and analyzed the results.
There appear to be three different types signals within a packet: a start sequence, one, and zero represented by differing lengths of carrier on/off times.
The start sequence is 2.2 ms in duration with the carrier “on” 1.4 ms followed by “off” for 782 us. Each bit is 1 ms in duration with a “1” defined by the carrier “on” for 750 us followed by “off” for 250 us. A “0” is defined by the carrier “on” for 250 us followed by “off” for 750 us.
I wanted to reconstruct the signals in MATLAB, generate IQ data, send the data to the Blade RF, and transmit the signals to test if the receiver is activated. The MATLAB code is listed below.
close all
clear all
% Load IQ data
xml = parseXML('IQREC-08-05-21-16h33m57s643.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
t = 0:1/Fs:(sampleCount/Fs)-1/Fs;
s = load_iq_bb60c('IQREC-08-05-21-16h33m57s643.iq',scaleFactor).';
% Decimate by 10 using a Chebyshev filter of order 4
s1 = decimate(s.',10,4).';
Fs = Fs/10;
t = 0:1/Fs:8-1/Fs;
figure
plot(t,10*log10(abs(s1.^2)))
grid on
ylim([-65 -20])
xlabel('time (s)')
ylabel('Power (dBm)')
title('Received Signal for Three Functions ')
figure
plot(t,10*log10(abs(s1.^2)))
grid on
xlim([2.76 3.39])
ylim([-65 -20])
xlabel('time (s)')
ylabel('Power (dBm)')
title('Received Signal for Audio Function ')
figure
plot(t,10*log10(abs(s1.^2)),'Linewidth',2)
grid on
xlim([2.7726 2.8188])
ylim([-65 -20])
xlabel('time (s)')
ylabel('Power (dBm)')
title('One Packet for Audio Function ')
figure
subplot(3,1,1)
plot(t,10*log10(abs(s1.^2)),'Linewidth',2)
grid on
xlim([2.7726 2.8188])
ylim([-65 -20])
xlabel('time (s)')
ylabel('Power (dBm)')
title('One Packet for Audio Function ')
subplot(3,1,2)
plot(t,10*log10(abs(s1.^2)),'Linewidth',2)
grid on
xlim([4.84125 4.8874])
ylim([-65 -20])
xlabel('time (s)')
ylabel('Power (dBm)')
title('One Packet for Vibrate Function ')
subplot(3,1,3)
plot(t,10*log10(abs(s1.^2)),'Linewidth',2)
grid on
xlim([6.55593 6.60213])
ylim([-65 -20])
xlabel('time (s)')
ylabel('Power (dBm)')
title('One Packet for Shock Function ')
%% Reconstruct the signals
% Audio signal bit sequence
abit = [1,1,1,1,0,1,1,0,1,1,1,0,0,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,0,0,0,0];
% Vibrate signal bit sequence
vbit = [1,1,1,1,0,1,1,0,1,1,1,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0];
% Shock signal bit sequence
sbit = [1,1,1,1,0,1,1,0,1,1,1,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0,1,1,1,1,0,0,0,0];
% The start sequence is 2.2 ms in duration
% Carrier "on" 1.4 ms followed by "off" for 782 us
% Each bit is 1 ms in duration
% A "1" is carrier "on" for 750 us followed by "off" for 250 us
% A "0" is carrier "on" for 250 us followed by "off" for 750 us
% Use a sample rate of 5 MSPS
Fs = 5e6;
startSeqLen = Fs*1.4e-3 + Fs*782e-6;
startSeq = [ones(1,Fs*1.4e-3) zeros(1,Fs*782e-6)];
oneSeq = [ones(1,Fs*750e-6) zeros(1,Fs*250e-6)];
zeroSeq = [ones(1,Fs*250e-6) zeros(1,Fs*750e-6)];
% Audio, vibrate, and shock sequences
audioSeq = startSeq;
vibrateSeq = startSeq;
shockSeq = startSeq;
for ii = 1:length(abit)
if abit(ii) == 0
audioSeq = [audioSeq zeroSeq];
else
audioSeq = [audioSeq oneSeq];
end
if vbit(ii) == 0
vibrateSeq = [vibrateSeq zeroSeq];
else
vibrateSeq = [vibrateSeq oneSeq];
end
if sbit(ii) == 0
shockSeq = [shockSeq zeroSeq];
else
shockSeq = [shockSeq oneSeq];
end
end
% 10 copies
audioSeq = repmat(audioSeq,1,10);
vibrateSeq = repmat(vibrateSeq,1,10);
shockSeq = repmat(shockSeq,1,10);
t = 0:1/Fs:length(audioSeq)/Fs - 1/Fs;
% IQ Data...for OOK it can cycle between 0 and 1 while Q = 0
audioIq = complex(audioSeq, zeros(1,length(audioSeq)));
save_sc16q11('audioSeq.bin',audioIq);
vibrateIq = complex(vibrateSeq, zeros(1,length(vibrateSeq)));
save_sc16q11('vibrateSeq.bin',vibrateIq);
shockIq = complex(shockSeq, zeros(1,length(shockSeq)));
save_sc16q11('shockSeq.bin',shockIq);
figure
plot(t,real(audioIq),t,imag(audioIq),'Linewidth',2)
xlim([0 0.0451])
ylim([0 1.2])
grid on
legend('In-phase (I)','Quadrature (Q)')
xlabel('time (s)')
ylabel('I')
title('IQ for One Reconstructed Packet for Audio Function')
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_sc16q11(filename, signal)
% SAVE_SC16Q11 Write a normalized complex signal to a binary file in the
% bladeRF "SC16 Q11" format.
%
% [RET] = save_sc16q11(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) .* 2048.0);
sig_i(sig_i > 2047) = 2047;
sig_i(sig_i < -2048) = -2048;
sig_q = round(imag(signal) .* 2048.0);
sig_q(sig_q > 2047) = 2047;
sig_q(sig_q < -2048) = -2048;
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, 'int16');
fclose(f);
if count == sig_len;
ret = 1;
else
end
else
error(err_msg);
ret = 0;
end
end
After running the code and transferring the IQ binary data file to my Linux machine, I used the Nuand bladeRF-cli software to transmit the reconstructed IQ modulating a carrier frequency of 433 MHz.