In this part of the lab we are going to experiment with Digital modulation and communication. Network Communication systems have layered architechture. The bottom layer is the physical which implements the modulation. Here we will use AFSK, which is a form of BFSK in the audio range (hence the 'A'). We will write a modulator/demodulator for AFSK. In addition, we will leverage AX.25, which is an amateur-radio data-link layer protocol. AX.25 is a packet based protocol that will help us transmit data using packets. It implements basic synchronization, addressing, data encapsulation and some error detection. In the ham world, an implementation of AFSK and AX.25 together is also called a TNC ( Terminal Node Controller ). In the past TNC's were separate boxes that hams used to attach to their radios to communicate with packet-based-communication. Today, it is easy to implement TNC's in software using the computer's soundcard.... as you will see here!
# Import functions and libraries
import numpy as np
import matplotlib.pyplot as plt
import pyaudio
import Queue
import threading,time
import sys
from numpy import pi
from numpy import sin
from numpy import zeros
from numpy import r_
from scipy import signal
from scipy import integrate
import threading,time
import multiprocessing
from rtlsdr import RtlSdr
from numpy import mean
from numpy import power
from numpy.fft import fft
from numpy.fft import fftshift
from numpy.fft import ifft
from numpy.fft import ifftshift
import bitarray
from scipy.io.wavfile import read as wavread
%matplotlib inline
# Plot an image of the spectrogram y, with the axis labeled with time tl,
# and frequency fl
#
# t_range -- time axis label, nt samples
# f_range -- frequency axis label, nf samples
# y -- spectrogram, nf by nt array
# dbf -- Dynamic range of the spect
def sg_plot( t_range, f_range, y, dbf = 60) :
eps = 1e-3
# find maximum
y_max = abs(y).max()
# compute 20*log magnitude, scaled to the max
y_log = 20.0 * np.log10( abs( y ) / y_max + eps )
fig=figure(figsize=(15,6))
plt.imshow( np.flipud( 64.0*(y_log + dbf)/dbf ), extent= t_range + f_range ,cmap=plt.cm.gray, aspect='auto')
plt.xlabel('Time, s')
plt.ylabel('Frequency, Hz')
plt.tight_layout()
def myspectrogram_hann_ovlp(x, m, fs, fc,dbf = 60):
# Plot the spectrogram of x.
# First take the original signal x and split it into blocks of length m
# This corresponds to using a rectangular window %
isreal_bool = isreal(x).all()
# pad x up to a multiple of m
lx = len(x);
nt = (lx + m - 1) // m
x = append(x,zeros(-lx+nt*m))
x = x.reshape((m/2,nt*2), order='F')
x = concatenate((x,x),axis=0)
x = x.reshape((m*nt*2,1),order='F')
x = x[r_[m//2:len(x),ones(m//2)*(len(x)-1)].astype(int)].reshape((m,nt*2),order='F')
xmw = x * hanning(m)[:,None];
# frequency index
t_range = [0.0, lx / fs]
if isreal_bool:
f_range = [ fc, fs / 2.0 + fc]
xmf = np.fft.fft(xmw,len(xmw),axis=0)
sg_plot(t_range, f_range, xmf[0:m/2,:],dbf=dbf)
print 1
else:
f_range = [-fs / 2.0 + fc, fs / 2.0 + fc]
xmf = np.fft.fftshift( np.fft.fft( xmw ,len(xmw),axis=0), axes=0 )
sg_plot(t_range, f_range, xmf,dbf = dbf)
return t_range, f_range, xmf
For the following tasks you will need the functions:
sg_plot
myspectrogram_hann_ovlp
play_audio
record_audio
audioDevNumbers
text2Morse
(to Identify yourself before transmission)
def play_audio( Q, p, fs , dev):
# play_audio plays audio with sampling rate = fs
# Q - A queue object from which to play
# p - pyAudio object
# fs - sampling rate
# dev - device number
# Example:
# fs = 44100
# p = pyaudio.PyAudio() #instantiate PyAudio
# Q = Queue.queue()
# Q.put(data)
# Q.put("EOT") # when function gets EOT it will quit
# play_audio( Q, p, fs,1 ) # play audio
# p.terminate() # terminate pyAudio
# open output stream
ostream = p.open(format=pyaudio.paFloat32, channels=1, rate=int(fs),output=True,output_device_index=dev)
# play audio
while (1):
data = Q.get()
if data=="EOT" :
break
try:
ostream.write( data.astype(np.float32).tostring() )
except:
break
def record_audio( queue, p, fs ,dev,chunk=1024):
# record_audio records audio with sampling rate = fs
# queue - output data queue
# p - pyAudio object
# fs - sampling rate
# dev - device number
# chunk - chunks of samples at a time default 1024
#
# Example:
# fs = 44100
# Q = Queue.queue()
# p = pyaudio.PyAudio() #instantiate PyAudio
# record_audio( Q, p, fs, 1) #
# p.terminate() # terminate pyAudio
istream = p.open(format=pyaudio.paFloat32, channels=1, rate=int(fs),input=True,input_device_index=dev,frames_per_buffer=chunk)
# record audio in chunks and append to frames
frames = [];
while (1):
try: # when the pyaudio object is distroyed stops
data_str = istream.read(chunk) # read a chunk of data
except:
break
data_flt = np.fromstring( data_str, 'float32' ) # convert string to float
queue.put( data_flt ) # append to list
def audioDevNumbers(p):
# din, dout, dusb = audioDevNumbers(p)
# The function takes a pyaudio object
# The function searches for the device numbers for built-in mic and
# speaker and the USB audio interface
# some devices will have the name “Generic USB Audio Device”. In that case, replace it with the the right name.
dusb = 'None'
din = 'None'
dout = 'None'
if sys.platform == 'darwin':
N = p.get_device_count()
for n in range(0,N):
name = p.get_device_info_by_index(n).get('name')
if name == u'USB PnP Sound Device':
dusb = n
if name == u'Built-in Microph':
din = n
if name == u'Built-in Output':
dout = n
# Windows
else:
N = p.get_device_count()
for n in range(0,N):
name = p.get_device_info_by_index(n).get('name')
if name == u'USB PnP Sound Device':
dusb = n
if name == u'Microsoft Sound Mapper - Input':
din = n
if name == u'Microsoft Sound Mapper - Output':
dout = n
if dusb == 'None':
print('Could not find a usb audio device')
return din, dout, dusb
def genPTT(plen,zlen,fs):
Nz = floor(zlen*fs)
Nt = floor(plen*fs)
pttsig = zeros(Nz)
t=r_[0.0:Nt]/fs
pttsig[:Nt] = 0.5*sin(2*pi*t*2000)
return pttsig
def text2Morse(text,fc,fs,dt):
CODE = {'A': '.-', 'B': '-...', 'C': '-.-.',
'D': '-..', 'E': '.', 'F': '..-.',
'G': '--.', 'H': '....', 'I': '..',
'J': '.---', 'K': '-.-', 'L': '.-..',
'M': '--', 'N': '-.', 'O': '---',
'P': '.--.', 'Q': '--.-', 'R': '.-.',
'S': '...', 'T': '-', 'U': '..-',
'V': '...-', 'W': '.--', 'X': '-..-',
'Y': '-.--', 'Z': '--..',
'0': '-----', '1': '.----', '2': '..---',
'3': '...--', '4': '....-', '5': '.....',
'6': '-....', '7': '--...', '8': '---..',
'9': '----.',
' ': ' ', "'": '.----.', '(': '-.--.-', ')': '-.--.-',
',': '--..--', '-': '-....-', '.': '.-.-.-',
'/': '-..-.', ':': '---...', ';': '-.-.-.',
'?': '..--..', '_': '..--.-'
}
Ndot= 1.0*fs*dt
Ndah = 3*Ndot
sdot = sin(2*pi*fc*r_[0.0:Ndot]/fs)
sdah = sin(2*pi*fc*r_[0.0:Ndah]/fs)
# convert to dit dah
mrs = ""
for char in text:
mrs = mrs + CODE[char.upper()] + "*"
sig = zeros(1)
for char in mrs:
if char == " ":
sig = concatenate((sig,zeros(Ndot*7)))
if char == "*":
sig = concatenate((sig,zeros(Ndot*3)))
if char == ".":
sig = concatenate((sig,sdot,zeros(Ndot)))
if char == "-":
sig = concatenate((sig,sdah,zeros(Ndot)))
return sig
AFSK1200 encodes digital binary data at a data-rate of 1200b/s. It uses the frequencies 1200Hz and 2200Hz ( center frequency of \(1700\)Hz \(\pm 500\) Hz) to encode the '0's and '1's (also known as space and mark) bits. Even though it has a relatively low bit-rate it is still the dominant standard for amature packet radio over VHF. It is a common physical layer for the AX.25 packet protocol and hence a physical layer for the Automatic Packet Reporting System (APRS), which we will describe later.
The exact frequency spectrum of a general FSK signal is difficult to obtain. But, when the mark and space frequency difference \(\Delta f\) is much larger than the bit-rate, \(B\), then the bandwidth of FSK is approximately \(2\Delta f + B\). This is not exactly the case for AFSK1200 where the spacing between the frequencies is 1000Hz and the bit-rate is 1200 baud.
Note, that for the (poor) choice of 1200/2200Hz for frequencies, a synchronous phase (starting each bit with the same phase) is not going to be continuous. For the Bandwidth to be narrow, it is important that the phase in the modulated signal is continuous. For this reason, AFSK1200 has to be generated in the following way: \[ s(t) = cos\left(2\pi f_c t + 2\pi \Delta f \int_{\infty}^t m(\tau)d\tau \right),\] where \(m(t)\) has the value =1 for a duration of a mark bit, and a value =-1 for a duration of a space bit. Such \(m(t)\) signal is called an Non-Return-to-Zero (NRZ) signal in the digital communication jargon. Here's a link to some notes provided by Fred Nicolls from the university of Cape Town
The integration guarentees that the phase in continuous. In addition, the instantaneous frequency of \(s(t)\) is the derivative of its phase, which is \(2\pi f_c + 2\pi \Delta f m(t)\), which is exactly what we need.
Write a function sig = afsk1200(bits)
the function will take a bitarray of bits and will output an AFSK1200 modulated signal of them, sampled at 44100Hz. Note that Mark frequency is 1200Hz and Space Frequency is 2200Hz. If your USB device prefers 48000Hz, then use 48KHz for the sampling rate.
Note, that 44100 does not divide by 1200. Make sure that you produce signals that have the right rate over time. (44100 does not divide by 1200, but 44100*4 does).
def afsk1200(bits):
#the function will take a bitarray of bits and will output an AFSK1200 modulated signal of them, sampled at 44100Hz
return sig
bits=bitarray.bitarray((rand(64)>0.5).tolist())
sig = afsk1200(bits)
# Code to plot the figure
#
AFSK is a form of digital frequency modulation. As such it can be demodulated like FM. Because AFSK alternates between two frequencies, we can place two bandpass filters around the frequency of the Mark and Space and use envelope detection to determine which frequency is active in a bit period. This is a non-coherent AFSK demodulation, because the receiver phase does not need to be synced to the transmitter phase in order to demodulate the signal. The implementation we will use here is loosly based on on the one by Sivan Tolede (4X6IZ), a CS faculty in Tel-Aviv university who has written a nice article on a high-performance AX.25 modem. You can find it Here.
# Design Filters:
#
# Filter the stream,compute the envelope, compute the NRZ signal which is the difference of the envelopes and plot it.
#
sign
) function of the difference.NRZ = sign(NRZa)
# find the index of the bits
# compute time vector
fig = figure(figsize=(16,4))
plot(t, NRZ)
stem(t[idx],NRZ[idx],'r')
# find the decoded bits
bit_dec = bitarray.bitarray((NRZ[idx]>0).tolist())
print(bit_dec)
print(bits)
We can also demodulate AFSK using FM demodulation. Recall that the bandwidth of AFSK is approximately \(2\Delta f + B\) where \(B\) is the bitrate (1200Hz for AFSK1200). The afsk signal is \(s(t) = cos\left(2\pi f_c t + 2\pi \Delta f \int_{\infty}^t m(\tau)d\tau \right)\). If we filter the signal by passing only the positive frequencies we will get \(s(t) = \frac{1}{2}e^{2\pi j f_c t + 2\pi j \Delta f \int_{\infty}^t m(\tau)d\tau}\). This is the analytic signal and we can FM demodulate it in a similar way as we did for the FM radio Lab by taking the derivative of the phase. After appropriate scaling and offset we can extract the NRZ signal.
# code for FM demodulation
#
bit_dec_fm = bitarray.bitarray((NRZ_fm[idx]>0).tolist())
print(bit_dec_fm)
print(bits)
def nc_afskDemod(sig, TBW=2.0, N=74):
# non-coherent demodulation of afsk1200
# function returns the NRZI (without rectifying it)
return NRZI
def fm_afskDemod(sig, TBW=4, N=74):
# non-coherent demodulation of afsk1200
# function returns the NRZI (without rectifying it)
return NRZ_fma
One of the ways to evaluate the properties of a digital modulation scheme is to compute the bit-error-rate (BER) curves with as a function of signal-to-noise ratio (SNR). The BER is the number of bit errors (received bits that have been altered due to decoding error) divided by the total number of transmitted bits.
Let's calculate the BER for AFSK with both the non-coherent and FM demodulations:
# Generate a random bit steam
bits=bitarray.bitarray((rand(10000)>0.5).tolist())
# modulate
sig = afsk1200(bits)
# add noise
sig_n = sig + 1*randn(len(sig))
# demodulate and decode bits with non-coherent and FM demodulators
# plot the first 3000 bits
fig = figure(figsize=(16,4))
plot(NRZa_nc[:3000])
title('result of non-coherent demodulation')
fig = figure(figsize=(16,4))
plot(NRZa_fm[:3000])
axis((0,3000,-2,2))
title('analog NRZ of fm demodulation')
# compute error bit rate
#
print("BER of non-coherent:",1.0*E_nc/len(bits))
print("BER of FM:",1.0*E_fm/len(bits))
Your Bit error rate will depend also on the quality of the reconstruction. You can try to repeat the experiment for different choices of filters if you like.
BER curves are usually displayed in log log of the BER vs SNR. SNR is measured by energy per bit over noise power spectral density. Since we are just interested in the trend, we will plot the BER vs 1/noise standard deviation
# Compute Error Bit Rate Curves
# plot
loglog(1/(r_[0.1:8.0:0.2]),BER_nc)
loglog(1/(r_[0.1:8.0:0.2]),BER_fm,'r')
title("empirical BER for AFSK demodulation")
xlabel("SNR")
ylabel("BER")
legend(("non-coherent","FM"))
Before we go to demodulate AFSK1200 we will construct data in the form of AX.25 packets. The structure of the AX.25 packet, and in particular the flag that starts and ends a frame will help us sync to the beginning of the packet for accurate demodulation. The AX.25 protocol uses several measures for standartization and to improve detection and demodulation of packets. We will go over them now:
AX.25 does not encode NRZ '1's and '0's in the usual mark and space frequencies. Instead it uses a scheme called NRZI, or non-return to zero inverted. NRZI encodes a '0' bit as a change from mark to space, or space to mark. A '1' is encoded as no change. To encode an AX.25 packet we need to convert our bit sequence to an NRZI one. For example, an original bit stream of 11011000 should be first converted to 11000101 (initial state is 1).
Because a '1' is represented by no change, sending a long string of '1's would result in a constant signal. This may result in a receiver drifting out of sync with the transmitter. In order to circumvent this, the encoder performs bit stuffing before by placing a bit '0' after every fifth '1' in the the stream. The decoder does bit unstuffing and removes the extra '0's. Bit stuffing is performed before converting to NRZI.
Bytes are sent least-significant-bit first
A Flag field starts and ends each packet. It is a unique sequence and is used to detect the beginning and the end of packets. The flag consists of the bit sequence: 0x7E or: 01111110. The flag is an exception in which no bit stuffing is performed. Therefore it is the only place in the packet in which 6 consequitive '1's appear. In NRZI it will translate to a time interval of 7 bits between zero-corssing of the non-coerent detector output. This means that we can use the flag sequence to uniquly detect a packet.
The Ax.25 protocol defines several type of frames. We will use the Unnumbered Information (UI) frame as defined in the protocol. UI frame is used for connectionless mode, where AX.25 frmaes are transmitted without expecting any response, and reception is not quaranteed. The UI frame that is used in the Automatic Positioning and Reporting System (APRS) protocol and has 9 fields of data:
flag | Dest. Addr. | Src. Addr. | Digipeter Addresses | Control field | ID | Information Field | FCS | Flag |
---|---|---|---|---|---|---|---|---|
1 | 7 | 7 | 56 | 1 | 1 | 256 | 2 | 1 |
Of importance are the Source address, which are your call sign, the Information Field which is the packet payload, the FCS which is a error checksum.
The FCS field is always the last two bytes in the packet. They are a checksum that can be used to determine the integrity of the packet.
APRS is a ham packet-based system for real-time tactical digital communication for local area. APRS uses the AX.25 protocol in its core. Using APRS you can report position, status, and send messages that other hams or stations with APRS capability will able to decode, interpret and display the information. APRS also provides means of packet repeating (Digipeters) alongside with internet terminal nodes. Some radio manufacturers saw the potential and included APRS in some of their products as well. Go to this website: https://aprs.fi to see the APRS activity in the surrounding area that is aggregated by from the internet nodes. You will see fixed stations, weather stations as well as mobile operators in your area. We will use the website to confirm that our transmitted packets were received.
The national APRS frequency is 144.39MHz (ch-117 on your radio). There is much activity and infrastracture transmitting and listenning to that frequency. The international space station also has an APRS digipeter on board operating at 145.825MHZ (ch-50 on your radio). You can also use AX.25 on any of the digital or experimental channels in the bandplan -- though you will have to coordinate if you want anybody to hear you!
For APRS packets, the Destination address field is not used for routing. Instead it represents the version of the APRS software you are using. In order to be decoded by receivers in the APRS network it must start with the letters AP. We will use APDSP just for fun. The source address is your call sign. The digipeter addresses require some explenation but the fields 'WIDE1-1,WIDE2-1' will result in the packet being digipeted a maximum of 2 hops. In dense population areas like the bay area 'WIDE1-1' is often enough to get your packet to its destination. The Control and ID fields are fixed to "03" and "0" respectively. The FCS field is the checksum field as defined by AX.25. The flag fields are the usual 01111110.
We have prepared for you code that generates valid bitstream of AX.25 packets from the appropriate fields as well as decode the fields from a bitstream. The code is a modification of code oroginally written by: Greg Albrecht W2GMD. You can download the code from: https://inst.eecs.berkeley.edu/~ee123/sp14/lab3/ax25.py.
The information field of the packet are 256 Bytes payload that contain the information you want to send. We will go over some of the information that is needed to construct valid and useful information field messeges. There are several digipeters in the bay area that have internet terminal nodes. These implement several "fun" and useful services. For example, you can send a position report that would show up on a google map. You can also send a short EMAIL by sending an APRS packet.
How a node or a client interprets your packet depends on the information field structure. There are three types of packets: Position, Status and Messages #### Messages
Just begin your packet line with a Colon and a 9 character TOASDDRESS, another colon and then text. The TOADDRESS must have 9 characters and should be filled with spaces if it is smaller.
Examples of messages:
The "-----" are blank spaces to fill the space to 9 characters.
You can report your position to people on the APRS system. If your report is picked up by a node it will show up on http://www.aprs.fi. The basic format of a position packet is:
! or = symbols | Lattitude 8 chars | / | Longitude 9 chars | icon 1 char | Comment max 43 chars |
---|---|---|---|---|---|
= | 3752.50N | / | 12215.43W | K | Shows a school symbol on Cory Hall position |
= | 3752.45N | / | 12215.98W | [ | Shows a person walking on Oxford and Hearst |
= | 2759.16N | / | 08655.30E | [ | I'm on the top of the world! (Mt. Everest) |
The latitude format is expressed as a fixed 8-character field, in degrees and decimal minutes (to two decimal places), followed by a letter N for north and S for south. Latitude minutes are expressed as whole minutes and hundredths of a minute, separated by a decimal point. Longitude is expressed as a fixed 9-character field, in degrees and decimal minutes (to two decimal places), followed by the letter E for east or W for west. Longitude degrees are in the range 000 to 180. Longitude minutes are expressed as whole minutes and hundredths of a minute, separated by a decimal point.
In generic format:
For example Cory Hall is at N37° 52.5022', W122° 15.4395'. So the position is encoded as: 3752.50N/12215.43W
You can go to http://www.gpsvisualizer.com/geocode to find the coordinates of an address. Note: use the degree, minutes representation, not the decimal one.
a 1 character icon is provided after the coordinates. This will show an icon on the http://aprs.fi maps. Here are some useful ones:
Examples:
a school symbol * =3752.50N/12215.43WKShows a school symbol on Cory Hall position. * =3752.45N/12215.98W[Shows a person walking on Oxford and Hearst * =3752.❏❏N/12215.❏❏-Shows a house symbol somewhere in Berkeley.
A status packet starts with '>' character. It wil show on APRS equipped radios.
Examples:
In the following section we will construct a valid (bitstuffed) bitstream from the different APRS packet fields. We will convert to an NRZI representation and modulate to generate a valid AFSK1200 APRS Packet. We will transmit it over the radio and loog at http://aprs.fi to see if it was received by a node.
The following code shows you how to construct a message packet that will tell a digipeter to send you an email. Make sure you fill the correct information in the fields. The bitstream will already be bitsuffed with zeroes.
import ax25
# Enter your callsign here
callsign = ""
Digi =b'WIDE1-1,WIDE2-1'
dest = "APDSP"
# Uncomment to Send Email
info = ":EMAIL :youremail@berkeley.edu What a great lab!"
#Uncomment to show yourself on top of Mt everest
#info = "=2759.16N/08655.30E[I'm on the top of the world"
#uncomment to send to everyone on the APRS system near you
#info = ":ALL : CQCQCQ I would like to talk to you!"
# uncomment to report position
#info = "=3752.50N/12215.43WKThis is Cory Hall!"
# uncomment to send a status message
# info = ">I like radios"
packet = ax25.UI(
destination=dest,
source=callsign,
info=info,
digipeaters=Digi.split(b','),
)
print(packet.unparse())
Recall that AX.25 packets are sent with NRZI encoding in which a '0' is a change and a '1' is no change.
NRZI = NRZ2NRZI(bits)
, takes a standard bitarray stream and converts it to a bitarray stream representing '0's as change and '1's as unchanged. For example, an input of 0000111100 will result in 0101111101. This assume an initial state of '1'.def NRZ2NRZI(NRZ):
NRZI = NRZ.copy()
current = True
for n in range(0,len(NRZ)):
if NRZ[n] :
NRZI[n] = current
else:
NRZI[n] = not(current)
current = NRZI[n]
return NRZI
# construct message
msg = afsk1200(NRZ2NRZI(bitarray.bitarray(zeros(160).tolist())+packet.unparse()))
# display spectrogram
# play message on your speaker
p = pyaudio.PyAudio()
din, dout, dusb = audioDevNumbers(p)
Q = Queue.Queue()
# generate a pttsignal
pttsig = genPTT(xxx,yyy,44100.0)
Q.put(pttsig)
Q.put(msg/2.0)
Q.put(msg/2.0)
t_play = threading.Thread(target = play_audio, args = (Q, p, 44100, dusb ))
t_play.start()
time.sleep(4)
p.terminate()
Now that we know how to create AX.25 and APRS packets, know how to AFSK1200 modulate them, know how to demodulate AFSK1200 as well, we can move forward to receiving and decoding packets. By the end of that we will have a fully functioning communication system.
Download the file ISSpkt.wav. It contains an APRS packet I recorded on one of the ISS flybyes. Load it to your workspace using the function wavread
, which we imported from scipy.io
. (ISSpkt.wav)
sig = wavread("ISSpkt.wav")[1]
We will now automate the packet decoding by writing some functions that implement portions of the process.
nc_afskDemod
on the ISS to get the demodulated "analog" NRZINRZIa = nc_afskDemod(sig)
fig = figure(figsize(16,4))
plot(NRZIa)
NRZI = sign(NRZIa)
We know how to get the NRZI signal, but the main issue that is left to resolve is to detect when a packet is received and synchronize to the beginning of it so we can correctly interpret its bits.
There are many ways to do this. Here we will use a simple decoder that is based on zero-crossings of the NRZI sequence. Recall that a '1' is encoded by no change and a '0' is encoded by change of sign. Therefore, if we see a 1 bit time-length between zero crossing we can decode that transition as a '0'. If we see 2 bit time-length between zero crossings of the NRZI we can decode this as '10'. Similarly if we see a 3 bit time-length we decode it all as '110', 4 as '1110', 5 as '11110' and 6 as '111110'. Because of the bit-stuffing, the only 7 bit time-length valid sequence is the flag sequence that marks the begining and the end of the packet. Of course we can not expect the transitions to be accurate due to noise and other corruptions. We therefore give an error margin of 1/2 a bit-length for decoding. For example, we will decode a '0' for zero-crossing interval between 1/2 a bit and 3/2 bits., a '10' for zero-crossing interval between 3/2 and 5/2 bits, etc.
We have implemented the decoding as a state machine for you. We first look for a flag sequence. When it is found we start collecting the bits of the packet. If at any time we get an invalid interval length we discard the data and start looking again for a flag. If the second flag is detected, a packet is announced. We then check if the checksum is valid. If it is, the packet is recorded and returned.
# function to generate a checksum for validating packets
def genfcs(bits):
# Generates a checksum from packet bits
fcs = ax25.FCS()
for bit in bits:
fcs.update_bit(bit)
digest = bitarray.bitarray(endian="little")
digest.frombytes(fcs.digest())
return digest
# function to parse packet bits to information
def decodeAX25(bits):
ax = ax25.AX25()
ax.info = "bad packet"
bitsu = ax25.bit_unstuff(bits[8:-8])
if (genfcs(bitsu[:-16]).tobytes() == bitsu[-16:].tobytes()) == False:
#print("failed fcs")
return ax
bytes = bitsu.tobytes()
ax.destination = ax.callsign_decode(bitsu[:56])
source = ax.callsign_decode(bitsu[56:112])
if source[-1].isdigit() and source[-1]!="0":
ax.source = b"".join((source[:-1],'-',source[-1]))
else:
ax.source = source[:-1]
digilen=0
if bytes[14]=='\x03' and bytes[15]=='\xf0':
digilen = 0
else:
for n in range(14,len(bytes)-1):
if ord(bytes[n]) & 1:
digilen = (n-14)+1
break
ax.digipeaters = ax.callsign_decode(bitsu[112:112+digilen*8])
ax.info = bitsu[112+digilen*8+16:-16].tobytes()
return ax
def detectFrames(NRZI):
# function looks for packets in an NRZI sequence and validates their checksum
# compute finite differences of the digital NRZI to detect zero-crossings
dNRZI = NRZI[1:] - NRZI[:-1]
# find the position of the non-zero components. These are the indexes of the zero-crossings.
transit = nonzero(dNRZI)[0]
# Transition time is the difference between zero-crossings
transTime = transit[1:]-transit[:-1]
# loop over transitions, convert to bit streams and extract packets
dict = { 1:bitarray.bitarray([0]), 2:bitarray.bitarray([1,0]), 3:bitarray.bitarray([1,1,0]),
4:bitarray.bitarray([1,1,1,0]),5:bitarray.bitarray([1,1,1,1,0]),6:bitarray.bitarray([1,1,1,1,1,0])
,7:bitarray.bitarray([1,1,1,1,1,1,0])}
state = 0; # no flag detected yet
packets =[]
tmppkt = bitarray.bitarray([0])
lastFlag = 0 # position of the last flag found.
for n in range(0,len(transTime)):
Nb = round(transTime[n]/36.75) # maps intervals to bits. Assume 44100Hz and 1200baud
if (Nb == 7 and state ==0):
# detected flag frame, start collecting a packet
tmppkt = tmppkt + dict[7]
state = 1 # packet detected
lastFlag = transit[n-1]
continue
if (Nb == 7 and state == 1):
# detected end frame successfully
tmppkt = tmppkt + dict[7]
# validate checksum
bitsu = ax25.bit_unstuff(tmppkt[8:-8]) # unstuff bits
if (genfcs(bitsu[:-16]).tobytes() == bitsu[-16:].tobytes()) :
# valid packet
packets.append(tmppkt)
tmppkt = bitarray.bitarray([0])
state = 0
continue
if (state == 1 and Nb < 7 and Nb > 0):
# valid bits
tmppkt = tmppkt + dict[Nb]
continue
else:
# not valid bits reset
state = 0
tmppkt = bitarray.bitarray([0])
continue
if state == 0:
lastFlag = -1
# if the state is 1, which means that we detected a packet, but the buffer ended, then
# we return the position of the beginning of the flag within the buffer to let the caller
# know that there's a packet that overlapps between two buffer frames.
return packets, lastFlag
Here's the code to decode the packet:
packets ,lastflag = detectFrames(NRZI)
ax = decodeAX25(packets[0])
print("Dest: %s | Source: %s | Digis: %s | %s |" %(ax.destination ,ax.source ,ax.digipeaters,ax.info))
The idea in the next task is to implement a stream processing in which a continuous stream of data is coming and we would like to process and decode packet in real time. The idea is simple, we get a buffer of samples and decode packet within it. There are two issues: 1. Our filtering needs to mitigate with boundaries between packets. We will solve it by overlapp and save type of an approach. 2. A packet may start towards the end of the buffer and continue to the next. The length of a packet is much bigger than the filter. Therefore, if that situation happens, the function detectFrames
will return the position of the lastFlag within the current buffer. If there is no such flag, it will return -1.
For simplicity we will work on 1 second length buffers.
The following code will put chunks of 1024 samples into a queue. It also adds the string "END" to the end of the queue. You can use it to break the function so it does not hang. This simulates what the audio receive function would do:
sig = wavread("ISSpkt_full.wav")[1]
Qin = Queue.Queue()
for n in r_[0:len(sig):1024]:
Qin.put(sig[n:n+1024])
Qin.put("END")
Task:
# code for stream processing of the Queue data
#
info = raw_input("MSG:")
). When a message is typed the application will construct an APRS packet (add also 160 zeros to the beginning), modulate it (add PTT signal of course) and transmit it through the radioThe packet parameters should be set to:
Don't forget to update your call sign!
You can either operate on the APRS frequency, decode packets and send in real time, try to communicate through the ISS, or use one of the digital channels and text your friends!