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
- HamShield: A shield that enables the Arduino to operate as a VHF/UHF transceiver.
- Arduino: Used to interface with the HamShield and send/receive data over the air.
- Python GUI Application: Provides a graphical interface to interact with the Arduino via a serial connection.
- pySerial: A Python library for communicating with the Arduino over serial ports.
Overview of Communication Process
- 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.
- 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
- 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.
- 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.