Python APRS Terminal

A simple Python GUI program to view APRS data from radio via serial port or Bluetooth. Easy to send messages and update status. Serial port and Bluetooth settings is available in settings panel. Autoscrolling feature is included to program so latest messages are always visible.

Features

  • Graphical user interface for those who not like text only mode.
  • Decodes KISS frame
  • Decodes ax.25 U frame
  • Decodes MIC-E format
  • Decodes APRS symbol and displays it on screen
  • Decodes APRS compressed data formats
  • Removes old items from the map
  • Removes traces of moving objects on the map

Supported and tested Hardware

  • Supports virtually any TNC which supports real or virtual serial port and KISS protocol.
  • Tested with Mobilinkd TNC2 through Bluetooth (with virtual serial port).

How does it work

Program decodes KISS frame and then ax.25 frame and lastly MIC-E data if available. Result is printed on window with colorful notation.

HELP – What to do if it does not work somehow?

Program should work on Python version 2.7.xx and onward. Run it with command “sudo python APRSterminal.py” without quotes. Contact me. I’m willing to help on any questions and can even add some features to program if necessary.

Source code is available from GitHub.

UART-controlled 7-segment display with Python GUI

This project uses this 7-segment display. It is based on Arduino. Display shows current gasoline price in Tampere, Finland. Gasoline price is middle price for 95E10 and it’s downloaded from www.polttoaine.net with Python code.

GUI (graphical user interface) is made with wxPython and wxFormBuilder. Update interval and serial port is user selectable.

Python code:

#! /usr/bin/python
# -*- coding: utf-8 -*-

import wx, wx.xrc
import threading
import serial
import urllib
from lxml.html import fromstring

class MyApp(wx.App):

    delay = 0
    runTime = 0

    def OnInit(self):
        self.res = wx.xrc.XmlResource("gui.xrc")
        self.frame = self.res.LoadFrame(None, "MyFrame1")
        self.text1 = wx.xrc.XRCCTRL(self.frame, "m_textCtrl1")
        self.text2 = wx.xrc.XRCCTRL(self.frame, "m_textCtrl2")
        self.slider1 = wx.xrc.XRCCTRL(self.frame, "m_slider1")
        self.frame.Bind(wx.EVT_BUTTON, self.on_evt_button, id=wx.xrc.XRCID("m_button1"))

        self.SetTopWindow(self.frame)
        self.frame.Show()
        
        self.delay = self.slider1.GetValue()*60
        self.updatePrice()
        self.thread = threading.Thread(target=self.delayTimer)
        self.thread.daemon = True
        self.thread.start()
        return True

    def on_evt_button(self, evt):
        self.delay = self.slider1.GetValue()*60

    def updatePrice(self):
        fopen = urllib.urlopen("https://www.polttoaine.net/Tampere")
        content = fopen.read()
        doc = fromstring(content)
        price = doc.find_class("Hinnat")[3].text_content()
        self.text1.SetValue(price)

        try:
            with serial.Serial(self.text2.GetValue(), 9600, timeout=1) as ser:
                time.sleep(5)
                ser.write(price+"\r")
        except IOError:
            print("Port cannot be opened")

    def delayTimer(self):
        self.runTime += 1
        if self.runTime >= self.delay:
            self.runTime = 0
            self.updatePrice()
        self.thread = threading.Timer(1,self.delayTimer)
        self.thread.daemon = True
        self.thread.start()       

app = MyApp(False)
app.MainLoop()

XRC layout file generated from wxFormBuilder:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<resource xmlns="http://www.wxwindows.org/wxxrc" version="2.3.0.1">
  <object class="wxFrame" name="MyFrame1">
    <style>wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL</style>
    <size>380,165</size>
    <title>95E10 Gasoline Price Display</title>
    <centered>1</centered>
    <aui_managed>0</aui_managed>
    <object class="wxFlexGridSizer">
      <rows>3</rows>
      <cols>3</cols>
      <minsize>380,165</minsize>
      <vgap>0</vgap>
      <hgap>0</hgap>
      <growablecols></growablecols>
      <growablerows></growablerows>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxStaticText" name="m_staticText1">
          <label>Current output</label>
          <wrap>-1</wrap>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxTextCtrl" name="m_textCtrl1">
          <style>wxTE_READONLY</style>
          <value></value>
        </object>
      </object>
      <object class="spacer">
        <option>1</option>
        <flag>wxEXPAND</flag>
        <border>5</border>
        <size>0,0</size>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxStaticText" name="m_staticText2">
          <label>Update interval (minutes)</label>
          <wrap>-1</wrap>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_BOTTOM|wxALIGN_CENTER_HORIZONTAL</flag>
        <border>5</border>
        <object class="wxSlider" name="m_slider1">
          <style>wxSL_HORIZONTAL|wxSL_LABELS</style>
          <size>100,-1</size>
          <value>30</value>
          <min>10</min>
          <max>120</max>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxButton" name="m_button1">
          <label>Save settings</label>
          <default>0</default>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxStaticText" name="m_staticText3">
          <label>Serial port</label>
          <wrap>-1</wrap>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL</flag>
        <border>5</border>
        <object class="wxTextCtrl" name="m_textCtrl2">
          <value>/dev/ttyUSB0</value>
        </object>
      </object>
    </object>
  </object>
</resource>

 

Reading UART with Python GUI application

Finally, I can say that I can program in the Python language. I learned Python from this tutorial. It is a good combination of the languages that I previously knew (PHP, Javascript and SQL). Structure also reminds me of Delphi and Basic languages.

Here is a program that reads 1-wire temperature sensor DS18B20 and displays it’s value on GUI (graphical user interface). GUI is made with wxPython library and with wxFormBuilder software.

In between sensor and computer there is Arduino Nano which transforms data from 1-wire to UART.

Python code:

#! /usr/bin/python
# -*- coding: utf-8 -*-

import wx
import wx.xrc
import serial
from thread import start_new_thread

class MyApp(wx.App):

    def OnInit(self):
        self.res = wx.xrc.XmlResource("gui.xrc")
        self.frame = self.res.LoadFrame(None, "MyFrame1")
        self.text1 = wx.xrc.XRCCTRL(self.frame, "m_textCtrl1")
        self.text2 = wx.xrc.XRCCTRL(self.frame, "m_textCtrl2")
        self.button1 = wx.xrc.XRCCTRL(self.frame, "m_button1")
        self.frame.Bind(wx.EVT_BUTTON, self.on_evt_button, id=wx.xrc.XRCID("m_button1"))
        
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

    def on_evt_button(self, evt):
        self.button1.SetLabel("Wait...")
        self.button1.Enable(False)
        start_new_thread(readSerial,(self,))

def readSerial(self):
    try:
        with serial.Serial(self.text2.GetValue(), 9600, timeout=1) as ser:
            line = ""
            while len(line) == 0:
                ser.write("\n")
                line = ser.readline()
            self.text1.SetValue(line.splitlines()[0] + u"°C")
    except IOError:
        print("Port cannot be opened")
    self.button1.Enable(True)
    self.button1.SetLabel("Read sensor")

app = MyApp(False)
app.MainLoop()

XRC layout file generated from wxFormBuilder:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<resource xmlns="http://www.wxwindows.org/wxxrc" version="2.3.0.1">
  <object class="wxFrame" name="MyFrame1">
    <style>wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL</style>
    <size>-1,100</size>
    <title>1-Wire Temperature</title>
    <centered>1</centered>
    <aui_managed>0</aui_managed>
    <object class="wxGridSizer">
      <minsize>-1,100</minsize>
      <rows>2</rows>
      <cols>3</cols>
      <vgap>0</vgap>
      <hgap>0</hgap>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT</flag>
        <border>5</border>
        <object class="wxStaticText" name="m_staticText1">
          <label>Outdoor temp</label>
          <wrap>-1</wrap>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL</flag>
        <border>5</border>
        <object class="wxTextCtrl" name="m_textCtrl1">
          <style>wxTE_READONLY</style>
          <size>100,-1</size>
          <value>---</value>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxButton" name="m_button1">
          <label>Read sensor</label>
          <default>0</default>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL</flag>
        <border>5</border>
        <object class="wxStaticText" name="m_staticText2">
          <label>Port addr</label>
          <wrap>-1</wrap>
        </object>
      </object>
      <object class="sizeritem">
        <option>0</option>
        <flag>wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL</flag>
        <border>5</border>
        <object class="wxTextCtrl" name="m_textCtrl2">
          <size>100,-1</size>
          <value>/dev/ttyUSB0</value>
        </object>
      </object>
    </object>
  </object>
</resource>

Arduino code:

#include <OneWire.h>
#include <DallasTemperature.h>

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2

// Setup a oneWire instance to communicate with any OneWire devices
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);

void setup(void) {
  Serial.begin(9600); // start serial port
  sensors.begin(); // Start up the library
}

void loop(void) { 
  while (Serial.available() > 0) { // if there's any serial available, read it
    if (Serial.read() == '\n') { // look for the newline
      sensors.requestTemperatures(); // Send the command to get temperatures
      Serial.println(sensors.getTempCByIndex(0)); // get temperature from first sensor only
    }
  }
}

 

APRS Weather Station

Automatic Packet Reporting System (APRS) is amateur radio -based system for sending and receiving short telemetry data locally. Local APRS stations can be seen at www.aprs.fi.

Picture of my station.

My hardware is based on Raspberry Pi computer and home made antenna with Baofeng UV-5R+Plus handheld transceiver. There is also a diy cable between Raspberry and radio. It is made from handsfree headset. There is a galvanic isolation between audio lines and opto-isolator on the PTT line. PTT is connected to Raspberrys GPIO pin 26.

Add this line to Dire Wolf config file:

PTT GPIO 26

Galvanic isolation and attenuation is made with following circuit:

DS18B20 temperature sensor is connected to Raspberrys GPIO pin 4 with parasitic power supply.

You must also activate parasitic power supply by adding following lines to associated files:

In /etc/modules

w1-gpio pullup=1
w1-therm

In /boot/config.txt

dtoverlay=w1-gpio,gpiopin=4,pullup=on

And then reboot.

On Raspberry Pi there is Xastir and Dire Wolf software installed. They are connected together with networked AGWPE. Temperature data is gathered with self made Python code. It talks with Xastir by emulating WX200 weather station. Code is here.

# WX200emu.py
#
# wx200, wx-200 weather station emulation and server
#
# Author:	Juha-Pekka Varjonen
#		 juvar@mbnet.fi
#
# License:	GNU General Public License, Version 3

version = '1.0'

import argparse, socket, sys, datetime, time

parser = argparse.ArgumentParser(prog='WX200Emu.py', description='WX200emu is a weather station emulator and server for client software. Listens for client connections and sends the 1-wire temperature data out to those clients.')
parser.add_argument("host", 
          help="host name or ip address, e.g. localhost")
parser.add_argument("port", 
          help="port number, e.g. 9753",
                    type=int)
parser.add_argument("id", help="1-wire sensor 64-bit serial number")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose",
          help="increase output verbosity",
                    action="store_true")
group.add_argument("-q", "--quiet",
          help="do not display any message",
                    action="store_true")
parser.add_argument('-V', '--version', 
          action='version', 
          version='%(prog)s ' + version)
args = parser.parse_args()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the port
server_address = (args.host, args.port)
if args.verbose:
  print('starting up on %s port %s' % server_address)
try:
  sock.bind(server_address)
except:
  if args.quiet is False:
    print('cannot bind socket')
    print('reason: ', sys.exc_info()[1])
  exit()

# Listen for incoming connections
sock.listen(1)

while True:
  try:
    # Wait for a connection
    if args.verbose:
      print('waiting for a connection')
    elif args.quiet is False:
      print('server is up and running')
    connection, client_address = sock.accept()
  except:
    if args.quiet is False:
      print('exception occurred')
      print('reason: ', sys.exc_info()[0])
    exit()
  try:
    if args.verbose:
      print('connection from', client_address)
    while True:
      # current date and time
      i = datetime.datetime.now()
      second = int(str(i.second),16).to_bytes(1, byteorder='big')
      minute = int(str(i.minute),16).to_bytes(1, byteorder='big')
      hour = int(str(i.hour),16).to_bytes(1, byteorder='big')
      day = int(str(i.day),16).to_bytes(1, byteorder='big')
      format_month = (0b00010000 + i.month).to_bytes(1, byteorder='big')

      # temperature to human readable format from 1-wire
      w1file = open('/sys/bus/w1/devices/' + args.id + '/w1_slave', 'r')
      text = w1file.read()
      w1file.close()
      out_temp = float("%.1f" % float(float(text.split("t=")[1])/1000))
      
      # outdoor temperature to wx200 format
      out_temp_sign = 0
      if out_temp < 0:
        out_temp_sign = 8
      out_temp_a = int((abs(out_temp) / 10) + out_temp_sign).to_bytes(1, byteorder='big')
      t = str(int(abs(out_temp) * 10))
      out_temp_bc = int(t[-2:len(t)],16).to_bytes(1, byteorder='big')

      # weather data wx200 format
      # http://wx200.planetfall.com/wx200.txt
      wx200file = [b''.join([b'\x8f', second, minute, hour, day, format_month, b'\0\0\xee\0\0\0\0\0\0\0\0\0\0\0\xee\0\0\0\0\0\0\0\0\0\0\0\0\0']),
          b''.join([b'\x9f\xee\0\0\0\0\0\0\0\0\0\0\0\0\0\0', out_temp_bc, out_temp_a, b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0']),
          b'\xaf\xee\xee\xee\xee\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0',
          b'\xbf\0\0\0\0\0\0\0\0\0\0\0\0',
          b'\xcf\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0']

      if args.verbose:
        print('sending data to the client')

      # send data, normal range is 0,2 (first two lines)
      for i in range(1,2):
        Cksum = 0
        for ch in wx200file[i]:
            Cksum += ch
        connection.sendall(b''.join([wx200file[i],(Cksum & 0xff).to_bytes(1, byteorder='big')]))
        
      time.sleep(10)

  except FileNotFoundError:
    if args.quiet is False:
      print('cannot open 1-wire connection')
    exit()
  except:
    if args.quiet is False:
      print('exception occurred')
      print('reason: ', sys.exc_info()[0])
    exit()
  finally:
    # Clean up the connection
    if args.verbose:
      print('closing connection')
    connection.close()

Usage example. Reading command line help:

python3 wx200emu.py -h

Using with temperature sensor installed:

python3 wx200emu.py localhost 8008 10-000801b5a7a6

You can use any free port. Change sensor serial number according with your sensor.

Antenna is made according to these drawings: http://www.users.on.net/~endsodds/jpole.htm

I’m very happy with this antenna. I can receive station over a hundred kilometers.

I bought 3.5 inch touchscreen display and housing for Raspberry Pi, but display turned out to be too small and insensitive for anything useful use. So SSH and VNC connections came to good use. SSH works straight out of the box but VNC needs some fine-tuning.

First install RealVNC both to the host and client computers. Then setting up VNC server to Raspberry Pi is easy as pie with following command:

vncserver-virtual :1

Virtual display number can be any, but it must be the same as next command on client computer:

vncviewer xxx.xxx.xxx.xxx:1

Use Raspberry Pi’s IP address here.

Update – forget Xastir

It is possible to send and receive packets with Dire Wolf alone. First here is custom config file for Dire Wolf:

# OH1FWW config file for Dire Wolf

ACHANNELS 1

CHANNEL 0
MYCALL OH1FWW
MODEM 1200
PTT GPIO 26
PBEACON DELAY=0:30 EVERY=30 VIA=WIDE2-2 LAT=61^25.95N LONG=23^48.85E commentcmd="python ~/read1wire.py"

Use your own call sign, not mine! All commands must be on single line. commentcmd is undocumented feature. It makes it possible to run scripts from Dire Wolf and print results to APRS packet comment line.

Every=30 means that it is repeated every 30 minutes.

Run with custom config file:

direwolf -c custom-config.conf

Dont forget to save this read1wire.py file too:

import time
def readtemp():
    try:
        w1file = open('/sys/bus/w1/devices/28-0000021ebb20/w1_slave', 'r')
        text = w1file.read()
        w1file.close()
        return float("%.1f" % float(float(text.split("t=")[1])/1000))
    except:
        print('cannot read 1-wire sensor. E2')
        exit()
i=0
while True:
    out_temp = readtemp()
    if out_temp == 85.0:
        i = i+1
        time.sleep(2)
    else:
        print('out temp: {} deg.C'.format(out_temp))
        exit()
    if i == 10:
        print('cannot read 1-wire sensor. E1')
        exit()

Change sensor ID to yours.

After that it is again useful to use 3.5 inch built in display because there is not any graphical interface.