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

Recent Posts

  • 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