Hands-on Digital transmission with your soundcard

This article is part of the fundamentals of my real-world tutorial on digital communications using a cheap soundcard as the radio. If this notebook is interesting to you, check out the full tutorial!

08 - Our first end-to-end data transmission

In the previous notebook, we designed a simple frame structure for aligning data into packets which can be decoded independently. The frame consisted of a header and a payload, where the payload was protected with a channel code and a checksum to ensure successful detection. We implemented the packet creating and decoding in a Packeting class.

In this notebook, we will use this class and all the previous blocks to create an end-to-end digital transmission using your soundcard. However, in this notebook, we will keep the transmitted data constant. We will extend the system to textual data received from a UDP stream in the next notebook.

First, let's have some standard imports:

In [1]:
# Source code has been redacted in online version
# Omitting 9 lines of source code
In [2]:
try:
    %load_ext tikzmagic
except ModuleNotFoundError:
    print ("Did not find tikzmagic. You will not be able to compile the tex code!")
In [3]:
%matplotlib notebook
In [4]:
# Source code has been redacted in online version
# Omitting 7 lines of source code

Then, let's define our transmitter. For now, we fix the payload it transmits to a given value of [1,1,1,1,0,0,0,0,1,1,0,0,1,0,1,0]. The principle is simple: Whenever the transmitter is asked to generate a transmit signal, it creates a frame with the constant payload given in the createFrame method.

In [5]:
class TransmitSignal(TX):
    def __init__(self, environment, Ts, packeting):
        super().__init__( environment)
        self._Ts = Ts
        self._packeting = packeting
        
        t = np.arange(-5*Ts, 5*Ts, 1/environment._samplerate)
        g_samples = get_filter('rc', Ts, rolloff=0.995)(t)
        
        self._filter = StatefulFilter(g_samples, [1])
        self._samplerate = environment._samplerate
    
    def __createFrame(self):
        # Use a fixed payload 
        packet = self._packeting.createPacket(np.array([1,1,1,1,0,0,0,0,1,1,0,0,1,0,1,0]))
        
        # perform BPSK modulation of the bits
        frame = 1 - 2.0*packet
        return frame
        
        
    def _generateSignal(self):
        Ns = int(self._Ts*self._samplerate)
        
        # get the BPSK symbols and filter them through the TX filter
        data = self.__createFrame()
        
        dataUps = np.zeros(int(Ns)*len(data))
        dataUps[::Ns] = data
        
        filtered = self._filter.filter(dataUps)
        
        tx = np.real(filtered * 0.2).astype(np.float32)

        return tx

At the receiver side, we create a component which reads a stream of bits and attempts to decode the packets from the stream. In order to do this, for each received bit the decoder is run on the last N bits, where N is the length of one PHY packet. If the decoder found a packet, the contained payload data is forwarded to the next component:

In [6]:
class DetectFrames(Component):
    def __init__(self, environment, packeting):
        super().__init__(environment, None)
        self._buffer = RingBuffer(packeting._packetLength, dtype=np.int8)        
        self._packeting = packeting
        
    def receive(self, data):
        bits = np.sign(data) == -1
        for b in bits:
            self._buffer.add(np.array([b]))
            d = self._buffer.data()
            packet = self._packeting.decodePacket(d)
            if packet is not None:
                self._send(packet)

As the data sink, we simple write a component that just prints out the bits in a received frame:

In [7]:
# Source code has been redacted in online version
# Omitting 6 lines of source code

Already now, we can set up our transmission chain. Within the chain, we use all the blocks from the previous notebooks: Upconversion, Downconversion and Timing Recovery. Pictorially, we have the following connections, with the blocks relevant from this notebook marked:

In [8]:
# Source code has been redacted in online version
# Omitting 29 lines of source code
In [9]:
# Source code has been redacted in online version
# Omitting 28 lines of source code

Let's try our system in the simulated channel. If everything works well, the PrintFrames component should print frames with exactly the payload we setup in the TransmitSignal component.

In [10]:
runTransmission(18000, lambda env: SimulatedChannel(env, channelEffect=HighpassChannelEffect(gain=0.2)))
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Stop received from external

Great! After some settling of the internal gain parameters, the eye diagram stabilizes and the receiver prints out the expected payload! We have successfully created a rudimentary digital transmission system!

Let's try the system in the real audio channel:

In [16]:
runTransmission(8000, AudioChannel)
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Stop received from external

Yeah! It also works over the audio channel. After moving the microphone towards the speaker and letting the gains settle, the eye diagram stabilizes. Then, the frames are correctly detected!

Summary

In this notebook, we have successfully integrated our packet encoding and decoding into the transmission system. With this packeting mechanism, we are now able to really transmit digital information over our audio channel.

In the next notebook, we will extend this system to receive data from a UDP network stream, such that we can control the transmitted payload on the fly.

This article is part of the fundamentals of my real-world tutorial on digital communications using a cheap soundcard as the radio. If this notebook is interesting to you, check out the full tutorial!

Copyright (C) 2018 - dspillustrations.com


DSPIllustrations.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com, amazon.de, amazon.co.uk, amazon.it.