Skip to content
KC3SMW
KC3SMW

Adventures in Ham Radio

  • Welcome
  • HF Propagation
  • Posts
  • Packet Radio Maps
  • Contact
KC3SMW

Adventures in Ham Radio

Building a Packet Radio Chat Interface with Python, HamShield, and Arduino

admin, September 24, 2024September 24, 2024

In this guide, we’ll walk through how to build a packet radio communication interface using Python, Arduino with HamShield, and how to integrate the two for seamless serial communication. This setup will enable us to send and receive messages via a VHF/UHF transceiver while using a Python-based GUI for user interaction.

Key Components

  1. HamShield: A shield that enables the Arduino to operate as a VHF/UHF transceiver.
  2. Arduino: Used to interface with the HamShield and send/receive data over the air.
  3. Python GUI Application: Provides a graphical interface to interact with the Arduino via a serial connection.
  4. pySerial: A Python library for communicating with the Arduino over serial ports.

Overview of Communication Process

  1. Arduino and HamShield: The Arduino, equipped with a HamShield, will send and receive packets using the AFSK (Audio Frequency Shift Keying) modulation. The packets will contain callsigns and a message, which will be transmitted over the air.
  2. Python Interface: The Python GUI acts as a user-friendly interface to send messages from a computer to the Arduino, which transmits them over the radio. It also displays any incoming messages received by the Arduino.

Step-by-Step Breakdown

Arduino Code for Packet Transmission

The following code for the Arduino sets up the HamShield to receive commands over a serial connection and transmit AFSK-encoded packets over the air.

#include <HamShield.h>
#include <DDS.h>
#include <packet.h>
#include <avr/wdt.h>

#define MIC_PIN 3
#define RESET_PIN A3
#define SWITCH_PIN 2

HamShield radio;
DDS dds;
AFSK afsk;
String messagebuff = "";
String origin_call = "";  // Originating callsign
String destination_call = "";  // Destination callsign
String textmessage = "";
int msgptr = 0;

void setup() {
  pinMode(MIC_PIN, OUTPUT);
  digitalWrite(MIC_PIN, LOW);

  pinMode(SWITCH_PIN, INPUT_PULLUP);
  pinMode(RESET_PIN, OUTPUT);
  digitalWrite(RESET_PIN, HIGH);
  delay(5); 

  Serial.begin(9600);

  radio.initialize();
  radio.frequency(145550);  // Default APRS frequency
  radio.setRfPower(0);
  radio.setVolume1(0xFF);
  radio.setVolume2(0xFF);
  radio.setSQHiThresh(-100);
  radio.setSQLoThresh(-100);
  radio.bypassPreDeEmph();

  dds.start();
  afsk.start(&dds);
  delay(100);
  radio.setModeReceive();
  Serial.println("Initialization Complete. Ready...");
}

void loop() {
  if (Serial.available()) { 
    char temp = (char)Serial.read();
    if (temp == '`') { 
      parseAndPrepMessage(); 
      msgptr = 0; 
      messagebuff = ""; 
    } else { 
      messagebuff += temp;
      msgptr++;
    }
  }

  processIncomingPackets();
}

void processIncomingPackets() {
  if (afsk.decoder.read() || afsk.rxPacketCount()) {
    while (afsk.rxPacketCount()) {
      AFSK::Packet *packet = afsk.getRXPacket();
      Serial.print(F(">> "));
      if (packet) {
        packet->printPacket(&Serial);  
        AFSK::PacketBuffer::freePacket(packet);
      }
    }
  }
}

void parseAndPrepMessage() { 
  int firstComma = messagebuff.indexOf(',');
  int secondComma = messagebuff.indexOf(',', firstComma + 1);
  int colonIndex = messagebuff.indexOf(':');

  if (firstComma == -1 || secondComma == -1 || colonIndex == -1) {
    Serial.println("Invalid message format!");
    return;
  }

  origin_call = messagebuff.substring(0, firstComma); 
  destination_call = messagebuff.substring(firstComma + 1, secondComma); 
  textmessage = messagebuff.substring(colonIndex + 1); 

  Serial.print(destination_call); 
  Serial.print(" > "); 
  Serial.print(origin_call); 
  Serial.print(": "); 
  Serial.println(textmessage);

  prepMessage();
}

void prepMessage() { 
   radio.setModeTransmit();
   delay(1000);

   AFSK::Packet *packet = AFSK::PacketBuffer::makePacket(22 + 32);
   packet->start();
   packet->appendCallsign(origin_call.c_str(), 0); 
   packet->appendCallsign(destination_call.c_str(), 0, true); 
   packet->appendFCS(0x03);
   packet->appendFCS(0xf0);
   packet->print(textmessage);
   packet->finish();

   bool ret = afsk.putTXPacket(packet);

   if (afsk.txReady()) {
     radio.setModeTransmit();
     if (afsk.txStart()) {
     } else {
       radio.setModeReceive();
     }
   }

   for (int i = 0; i < 500; i++) {
     if (afsk.encoder.isDone())
        break;
     delay(50);
   }

   radio.setModeReceive();
}

ISR(TIMER2_OVF_vect) {
  TIFR2 = _BV(TOV2);
  static uint8_t tcnt = 0;
  if (++tcnt == 8) {
    dds.clockTick();
    tcnt = 0;
  }
}

ISR(ADC_vect) {
  static uint8_t tcnt = 0;
  TIFR1 = _BV(ICF1);
  dds.clockTick();
  if (++tcnt == 1) {
    afsk.timer();
    tcnt = 0;
  }
}

Python GUI for Serial Communication

The Python GUI allows users to:

  • Select a serial port to connect to the Arduino.
  • Enter both the From and To callsigns.
  • Type messages to be sent over the packet radio network.
  • Display received messages from the Arduino in real-time.

Python Code

import serial
import serial.tools.list_ports
import tkinter as tk
from tkinter import scrolledtext, messagebox
import threading

class ArduinoApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Arduino Packet Messenger")

        # Serial port connection variables
        self.serial_port = None
        self.read_thread = None
        self.running = False

        # Setup GUI components
        self.setup_gui()

        # Get available serial ports
        self.refresh_serial_ports()

    def setup_gui(self):
        # Create a main frame to contain all the widgets
        main_frame = tk.Frame(self.root, padx=10, pady=10)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # Add frames for different sections (Port Selection, Callsigns, Messaging)
        port_frame = tk.Frame(main_frame, pady=5)
        port_frame.pack(fill=tk.X)

        callsign_frame = tk.Frame(main_frame, pady=5)
        callsign_frame.pack(fill=tk.X)

        message_frame = tk.Frame(main_frame, pady=5)
        message_frame.pack(fill=tk.BOTH, expand=True)

        send_frame = tk.Frame(main_frame, pady=5)
        send_frame.pack(fill=tk.X)

        # Serial Port Selection section
        self.port_label = tk.Label(port_frame, text="Select Port:", font=("Arial", 10))
        self.port_label.grid(row=0, column=0, padx=5, pady=5)

        self.port_menu_var = tk.StringVar(self.root)
        self.port_menu = tk.OptionMenu(port_frame, self.port_menu_var, [])
        self.port_menu.grid(row=0, column=1, padx=5, pady=5)

        self.refresh_button = tk.Button(port_frame, text="Refresh Ports", font=("Arial", 10), command=self.refresh_serial_ports)
        self.refresh_button.grid(row=0, column=2, padx=5, pady=5)

        self.connect_button = tk.Button(port_frame, text="Connect", font=("Arial", 10), command=self.connect_to_arduino)
        self.connect_button.grid(row=0, column=3, padx=5, pady=5)

        # Callsign section
        self.from_label = tk.Label(callsign_frame, text="From Callsign:", font=("Arial", 10))
        self.from_label.grid(row=0, column=0, padx=5, pady=5)

        self.from_entry = tk.Entry(callsign_frame, width=20)
        self.from_entry.grid(row=0, column=1, padx=5, pady=5)
        self.from_entry.insert(0, "PKTCHT")  # Default From Callsign

        self.to_label = tk.Label(callsign_frame, text="To Callsign:", font=("Arial", 10))
        self.to_label.grid(row=0, column=2, padx=5, pady=5)

        self.to_entry = tk.Entry(callsign_frame, width=20)
        self.to_entry.grid(row=0, column=3, padx=5, pady=5)
        self.to_entry.insert(0, "KC3SMW")  # Default To Callsign

        # Message Display section
        self.message_area = scrolledtext.ScrolledText(message_frame, width=60, height=10, font=("Arial", 10))
        self.message_area.pack(fill=tk.BOTH, expand=True)



        # Message sending section
        self.message_entry = tk.Entry(send_frame, width=50, font=("Arial", 10))
        self.message_entry.grid(row=0, column=0, padx=5, pady=5)
        self.message_entry.bind('<Return>', lambda event: self.send_message())  # Bind Enter key to send_message

        self.send_button = tk.Button(send_frame, text="Send", font=("Arial", 10), command=self.send_message)
        self.send_button.grid(row=0, column=1, padx=5, pady=5)

    def refresh_serial_ports(self):
        """Refresh the list of available serial ports."""
        ports = serial.tools.list_ports.comports()
        port_names = [port.device for port in ports]

        self.port_menu_var.set('')
        menu = self.port_menu['menu']
        menu.delete(0, 'end')

        for port in port_names:
            menu.add_command(label=port, command=lambda p=port: self.port_menu_var.set(p))

    def connect_to_arduino(self):
        """Connect to the selected Arduino serial port."""
        port = self.port_menu_var.get()
        if port == "":
            messagebox.showerror("Error", "No port selected")
            return

        try:
            self.serial_port = serial.Serial(port, 9600, timeout=1)
            self.message_area.insert(tk.END, f"Connected to {port}\n")
            self.start_reading_thread()  # Start the thread to read from the serial port
        except serial.SerialException as e:
            messagebox.showerror("Connection Error", f"Failed to connect to {port}\nError: {str(e)}")

    def start_reading_thread(self):
        """Start a separate thread to continuously read from the serial port."""
        self.running = True
        self.read_thread = threading.Thread(target=self.read_from_serial, daemon=True)
        self.read_thread.start()

    def read_from_serial(self):
        """Continuously read from the serial port in a separate thread."""
        while self.running:
            try:
                if self.serial_port and self.serial_port.is_open:
                    response = self.serial_port.readline().decode('utf-8').strip()
                    if response:
                        self.message_area.insert(tk.END, f"Received: {response}\n")
                        self.message_area.yview(tk.END)  # Auto-scroll to the end of the message area
            except Exception as e:
                print(f"Error reading from serial port: {e}")

    def send_message(self):
        """Send a message to the Arduino."""
        if self.serial_port is None or not self.serial_port.is_open:
            messagebox.showerror("Error", "Not connected to any port")
            return

        from_callsign = self.from_entry.get()
        to_callsign = self.to_entry.get()
        message = self.message_entry.get()

        if message == "":
            return

        # Format the message with From Callsign, To Callsign, and Message, and append a backtick (`) to the end
        formatted_message = f"{from_callsign},{to_callsign},:{message}`"

        self.serial_port.write(formatted_message.encode('utf-8'))
        self.message_area.insert(tk.END, f"Sent: {formatted_message}\n")
        self.message_entry.delete(0, tk.END)

    def on_closing(self):
        """Handle closing of the application."""
        self.running = False
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        self.root.quit()

if __name__ == "__main__":
    root = tk.Tk()
    app = ArduinoApp(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)  # Ensure proper closure of the app
    root.mainloop()

How It Works

  1. Python GUI:
    • The GUI allows users to input “From” and “To” callsigns, compose a message, and send it via the HamShield.
    • Messages are sent in the format: From Callsign,To Callsign,:Message.
    • A separate thread continuously reads incoming messages from the Arduino and displays them in the GUI.
  2. Arduino:
    • The Arduino receives messages over the serial port, formats them for AFSK packet transmission, and sends them via the HamShield.
    • It also receives and decodes incoming AFSK packets, sending the data back to the Python interface over serial.

Conclusion

This project demonstrates a simple way to build a packet radio communication system using HamShield, Arduino, and a Python GUI. It allows users to send and receive packet messages over ham radio frequencies using their computer.

Ham radio Linux Packet Radio

Post navigation

Previous post
Next post

Recent Posts

  • Tinker’s Corner: Playing with Time – LED Patterns Using FOR Loops
  • Sparks, Mics, and Wires: Packet Radio and APRS
  • Building a Packet Radio Chat Interface with Python, HamShield, and Arduino
  • Command Line Ham Prop Data
  • Tape Measure Yagi
  • Building FLDIGI from source on Debian, Ubuntu, and Linux Mint
  • Unlocking the World of Ham Radio: Your Technician Class Adventure Begins
  • Packet Radio With Windows
  • SSTV Connecting Images Through the Airwaves
  • Pat Winlink Using AX25
  • Pat Winlink on Linux Using AGW
  • Winlink
  • APRS
  • Using Linux Packet Radio (4 of 4)
  • Setting Up AXPorts (3 of 4)
  • Configuring Direwolf (2 of 4)
  • Installing Software for Linux Packet Radio (1 of 4)
  • Exploring the Power of Digital Communication in Ham Radio, What is Packet Radio
©2025 KC3SMW | WordPress Theme by SuperbThemes