A universal communication bridge that seamlessly connects amateur radio (APRS) with modern messaging platforms like Discord, enabling bidirectional message routing with intelligent protocol adaptation.
- Protocol Abstraction: Send one message to multiple protocols automatically
- Smart Adaptation: Messages automatically fit each protocol's capabilities and limits
- Content Prioritization: Critical information preserved when space is limited
- Bidirectional Communication: Messages flow seamlessly in both directions
- 📡 APRS-IS: Amateur radio packet network with position and messaging
- 💬 Discord: Rich embeds, threading, and webhook integration
- 📁 File Logging: Structured logging with rotation
- 🔧 Extensible: Easy to add new protocols
- RARSMS Prefix Filtering: Only route APRS messages intended for the system
- Message Deduplication: Intelligent filtering prevents duplicate APRS retransmissions
- Geographic Filtering: Location-based APRS packet filtering
- Callsign Authorization: Whitelist-based access control with proper SSID handling
- Configurable Rules: Custom routing between any protocols
W4ABC>APRS,TCPIP*::RARSMS :Emergency at I-40 mile marker 123
→ Discord embed with location map, emergency icon, and formatted message
@user: RARSMS Anyone monitoring 146.52?
→ APRS: USER:Anyone monitoring 146.52?
Create once, deliver everywhere:
emergency_msg = create_emergency_message(
"aprs_main", "W4ABC",
"Vehicle accident, need assistance",
35.7500, -78.7000
)
# Automatically adapts to APRS (67 chars), Discord (rich embed)git clone <repository>
cd rarsms
cp config.example.yaml config.yamlEdit config.yaml with your credentials:
# APRS Configuration (for amateur radio)
aprs_callsign: "YOUR-CALL"
aprs_passcode: "12345"
# Discord Configuration (for bidirectional communication)
discord_bot_token: "your_bot_token"
discord_channel_id: "your_channel_id"
discord_guild_id: "your_guild_id" # optional
# Message filtering
message_prefix: "RARSMS"
require_prefix: true
block_position_updates: true
# Deduplication settings
deduplication_timeout: 300 # seconds (5 minutes default)config.yaml to version control! It contains sensitive credentials.
Edit callsigns.txt:
# One callsign per line (base callsign only)
W4ABC
KJ4XYZ
N4DEF
docker-compose up -d# View logs
docker-compose logs -f
# Check status
docker-compose psThe bridge implements smart filtering to reduce noise and ensure only intended messages are routed:
Only messages from authorized callsigns that meet one of these criteria:
- Addressed to RARSMS:
:RARSMS :Hello from the field! - Start with RARSMS:
:CQ :RARSMS Anyone on frequency?
W4ABC>APRS,TCPIP*::RARSMS :Meeting tonight at 7 PM
KJ4XYZ>APRS,TCPIP*::CQ :RARSMS Weather update needed
N4DEF>APRS,TCPIP*::DISCORD :RARSMS: Emergency at grid FM15
W4ABC>APRS,TCPIP*::CQ :Regular APRS chat
KJ4XYZ>APRS,TCPIP*::W4XYZ :Direct message without prefix
RARSMS intelligently prevents duplicate messages from APRS retransmissions:
- Tracks unique message content for configurable time window (default: 5 minutes)
- Removes APRS message numbers and technical metadata for accurate comparison
- Logs when duplicates are blocked:
🚫 Blocked duplicate message from KK4PWJ-10
- Position packets from authorized callsigns (regardless of content)
To enable bidirectional communication, you'll need to create a Discord bot:
- Go to Discord Developer Portal
- Click "New Application" and give it a name (e.g., "RARSMS Bridge")
- Go to the "Bot" section and click "Add Bot"
- Copy the bot token (keep this secret!)
In the "Bot" section, enable these privileged intents:
- ✅ Message Content Intent
- ✅ Server Members Intent (optional)
- Go to "OAuth2" → "URL Generator"
- Select scopes:
bot - Select bot permissions:
- ✅ Send Messages
- ✅ View Channels
- ✅ Read Message History
- ✅ Add Reactions
- Use the generated URL to invite the bot to your server
- Enable Developer Mode in Discord (User Settings → Advanced → Developer Mode)
- Right-click on your channel → Copy ID
- Use this as
discord_channel_idin your config
Copy config.example.yaml to config.yaml and configure the following:
aprs_callsign: Your amateur radio callsignaprs_passcode: APRS-IS passcode (calculate here)aprs_server: APRS-IS server (default: "rotate.aprs2.net")aprs_port: APRS-IS port (default: 14580)
discord_bot_token: Discord bot token from Discord Developer Portaldiscord_channel_id: Discord channel ID for bidirectional communicationdiscord_guild_id: (Optional) Discord server ID for faster startup
message_prefix: Custom prefix (default: "RARSMS")require_prefix: Enable/disable prefix requirement (default: true)block_position_updates: Block noisy position updates (default: true)deduplication_timeout: Seconds to remember messages for duplicate filtering (default: 300)
filter_lat: Latitude for APRS packet filteringfilter_lon: Longitude for APRS packet filteringfilter_distance: Filter radius in kilometers
You can also use environment variables to override config.yaml values (useful for Docker):
# Core settings
APRS_CALLSIGN="KK4PWJ-0"
APRS_PASSCODE="12345"
DISCORD_BOT_TOKEN="your_bot_token"
DISCORD_CHANNEL_ID="your_channel_id"
# Message filtering
MESSAGE_PREFIX="RARSMS"
REQUIRE_PREFIX="true"
BLOCK_POSITION_UPDATES="true"
DEDUPLICATION_TIMEOUT="300"protocols:
aprs_emergency:
type: "aprs"
aprs_callsign: "EMERGENCY-1"
message_prefix: "EMERGENCY"
discord_alerts:
type: "discord"
discord_webhook_url: "https://discord.com/api/webhooks/..."
discord_bot_token: "bot_token"routing:
emergency_alerts:
source_protocols: ["aprs_main"]
target_protocols: ["discord_alerts", "email_emergency"]
message_types: ["emergency"]
bidirectional: false
position_sharing:
source_protocols: ["aprs_main"]
target_protocols: ["discord_main"]
message_types: ["position"]
source_filter: "^(W4|K4|N4).*" # Only specific call areasW4ABC-9>APRS,TCPIP*:!3547.12N/07838.45W>Mobile station
→ Discord embed with:
- 📍 Interactive map link
- 🚗 Station status
- ⏰ Timestamp
- 📡 Technical details
KJ4XYZ>APRS,TCPIP*::RARSMS :Anyone monitoring 146.52?
→ Discord: [APRS] KJ4XYZ: Anyone monitoring 146.52?
Reply to APRS message: APRS W4ABC Weather is clearing up, going mobile
→ APRS: USERNAME:Weather is clearing up, going mobile
When using bot mode, RARSMS tracks APRS messages in Discord and allows replies:
- APRS message appears in Discord with reply instructions
- Use Discord's reply function (right-click → Reply)
- Format your reply:
APRS <CALLSIGN> <your message> - Bot validates the callsign matches the original sender
- Message routes back to APRS with confirmation reaction
APRS W4ABC Yes, I'm monitoring 146.52
APRS KJ4XYZ-9 Thanks for the weather update
APRS N4DEF Roger, see you at the meeting
W4ABC Yes (missing "APRS" prefix)
APRS WRONG-CALL Message (wrong callsign)
Just replying normally (not using reply function)
- Message Tracking: Remembers last 100 APRS messages for replies
- Callsign Validation: Ensures replies go to correct station
- Visual Feedback: 📡 for success, ❌ for errors
- Rich Formatting: APRS messages shown with maps and metadata
- QRZ.com Integration: Clickable callsigns link to operator profiles
- Clean Message Display: Technical metadata automatically filtered out
APRS messages appear in Discord with a clean, 3-line format designed for maximum readability:
📻 [**KK4PWJ-10**](https://www.qrz.com/db/KK4PWJ) *14:34 UTC*
Testing the new bridge system
Reply: `APRS KK4PWJ-10 your message here`
Features:
- 📻 Visual indicator: Radio emoji draws attention to new APRS activity
- 🔗 Clickable callsigns: Direct links to QRZ.com profiles (link previews suppressed)
- ⏰ Timestamp first: Logical flow - when → who → what → how to reply
- 🧹 Clean content: Technical debug information automatically removed
- 💬 Simple reply format: Clear instructions for responding back to APRS
Original (multiple content blocks):
- 🚨 Emergency text (CRITICAL priority)
- 📍 GPS coordinates (HIGH priority)
- 📋 Additional details (MEDIUM priority)
- 🏷️ Metadata tags (LOW priority)
APRS (67 char limit):
🚨 Emergency text GPS:35.7796,-78.6382 Details...
Discord (2000 char limit):
🚨 **EMERGENCY**
Emergency text
📍 **Location:** [35.7796, -78.6382](https://maps.google.com/?q=35.7796,-78.6382)
📋 Additional details
🏷️ Metadata: event_type=emergency, priority=high
# Start the bridge
docker-compose up -d
# View logs in real-time
docker-compose logs -f
# Stop the bridge
docker-compose down
# Rebuild after changes
docker-compose up -d --build
# Check status
docker-compose ps
# Restart specific service
docker-compose restart rarsms✅ SUCCESSFULLY CONNECTED: 2 protocol(s)
🟢 aprs_main
🟢 discord_main
🚀 BRIDGING MODE ACTIVE
→ Messages will be routed between all connected protocols
→ APRS ↔ Discord bridging enabled
→ RARSMS prefix filtering active for APRS messages
⚠ APRS protocol not configured - missing: APRS_CALLSIGN
→ APRS will not be available for message routing
⚠ Discord protocol not configured - missing: DISCORD_WEBHOOK_URL
→ Set DISCORD_WEBHOOK_URL to enable Discord integration
| Issue | Cause | Solution |
|---|---|---|
| No messages routed | Missing RARSMS prefix | Check message format: :RARSMS :text |
| APRS login failed | Wrong callsign/passcode | Verify credentials and passcode calculation |
| Discord not working | Invalid webhook URL | Check webhook URL format and permissions |
| Messages filtered | Callsign not authorized | Add callsign to callsigns.txt |
| Duplicate messages | APRS retransmissions | Adjust deduplication_timeout in config |
| SSID missing in APRS | Callsign formatting | Ensure aprs_callsign includes SSID (e.g., KK4PWJ-0) |
# Check message filtering
docker-compose logs -f | grep "Blocking message"
# Monitor duplicate filtering
docker-compose logs -f | grep "Blocked duplicate message"
# Monitor protocol connections
docker-compose logs -f | grep "protocol.*connected"
# View routing statistics
docker-compose logs -f | grep "Statistics"
# Check APRS packet formatting
docker-compose logs -f | grep "Sending APRS packet"- BaseProtocol: Common interface for all communication protocols
- UniversalMessage: Standard message format with intelligent adaptation
- MessageAdapter: Automatic protocol-specific formatting and truncation
- ProtocolManager: Handles routing, authentication, and error recovery
- Receive: Protocol-specific message parsing
- Convert: Transform to universal message format
- Route: Apply filtering and routing rules
- Adapt: Protocol-specific formatting and truncation
- Send: Deliver via target protocol
Adding new protocols requires implementing the BaseProtocol interface:
class NewProtocol(BaseProtocol):
def get_capabilities(self) -> ProtocolCapabilities
async def connect(self) -> bool
async def send_message(self, message: Message) -> bool
def parse_incoming_message(self, raw_data) -> Optional[Message]- Callsign Whitelist: Only authorized amateur radio operators
- Prefix Filtering: Prevents accidental message routing
- Input Validation: Sanitizes all incoming messages
- Non-Root Container: Runs with minimal privileges
- Environment Variables: Credentials never stored in code
- Route emergency APRS messages to multiple platforms instantly
- Automatic position sharing with interactive maps
- Cross-platform coordination during events
- Bridge club meetings between radio and digital platforms
- Share announcements across multiple channels
- Archive conversations for later reference
- Coordinate repeater maintenance across platforms
- Share technical information between operators
- Real-time status updates during testing
- Protocol Development: Add support for new messaging platforms
- Message Adapters: Improve protocol-specific formatting
- Filter Rules: Enhance message routing and filtering
- Documentation: Improve setup guides and examples
GPL 3.0 License - see LICENSE file for details.
- APRS Information: aprs.org
- APRS Passcode Calculator: apps.magicbug.co.uk/passcode
- Discord Webhooks: Discord Developer Documentation
- Amateur Radio: arrl.org
Note: This project is designed for licensed amateur radio operators. Please ensure compliance with your local amateur radio regulations when using APRS features.