dx command

This commit is contained in:
SpudGunMan
2025-10-26 05:37:04 -07:00
parent 9c9e9a02e6
commit 6d90d6f207
6 changed files with 178 additions and 0 deletions

View File

@@ -299,6 +299,7 @@ time =
# using Hamlib rig control will monitor and alert on channel use
enabled = False
rigControlServerAddress = localhost:4532
dxspotter_enabled = True
# device interface to send the message to
sigWatchBroadcastInterface = 1
# broadcast channel can also be a comma separated list of channels

View File

@@ -50,6 +50,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
"dx": lambda: handledxcluster(message, message_from_id, deviceID),
"ea": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
"echo": lambda: handle_echo(message, message_from_id, deviceID, isDM, channel_number),
"ealert": lambda: handle_emergency_alerts(message, message_from_id, deviceID),

View File

@@ -875,4 +875,62 @@ bbslink_enabled = True
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
```
# DX Spotter Module
The DX Spotter module allows you to fetch and display recent DX cluster spots from [spothole.app](https://spothole.app) directly in your mesh-bot.
## Command
| Command | Description |
|---------|------------------------------|
| `dx` | Show recent DX cluster spots |
## Usage
Send a message to the bot containing the `dx` command. You can add filters to narrow down the results:
- **Basic usage:**
```
dx
```
Returns the latest DX spots.
- **With filters:**
```
dx band=20m mode=SSB
dx xota=WWFF
dx by=K7MHI
```
- `band=`: Filter by band (e.g., 20m, 40m)
- `mode=`: Filter by mode (e.g., SSB, CW, FT8)
- `xota=`: Filter by source/group (e.g., WWFF, POTA, SOTA)
- `by=`: Filter by callsign of the spotter
## Example Output
```
K7ABC @14.074 MHz FT8 WWFF KFF-1234 by:N0CALL CN87 Some comment
W1XYZ @7.030 MHz CW SOTA W7W/WE-001 by:K7MHI CN88
```
- Each line shows:
`DX_CALL @FREQUENCY MODE GROUP GROUP_REF by:SPOTTER_CALL SPOTTER_GRID COMMENT`
## Notes
- Returns up to 4 of the most recent spots matching your filters.
- Data is fetched from [spothole.app](https://spothole.app/).
- If no spots are found, youll see:
`No DX spots found.`
## Configuration
No additional configuration is required. The module is enabled if present in your `modules/` directory.
---
**Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025**
Happy meshing!

112
modules/dxspot.py Normal file
View File

@@ -0,0 +1,112 @@
#meshing-around modules/dxspot.py
import requests
import datetime
from modules.log import logger
trap_list_dxspotter = ["dx"]
def handledxcluster(message, nodeID, deviceID):
from modules.dxspot import get_spothole_spots
if "DX" in message.upper():
logger.debug(f"System: DXSpotter: Device:{deviceID} Handler: DX Spot Request Received from Node {nodeID}")
band = None
mode = None
source = None
dx_call = None
parts = message.split()
for part in parts:
if part.lower().startswith("band="):
band = part.split("=")[1]
elif part.lower().startswith("mode="):
mode = part.split("=")[1]
elif part.lower().startswith("xota="):
source = part.split("=")[1]
elif part.lower().startswith("by="):
dx_call = part.split("=")[1]
# Build params dict
params = {}
if source:
params["source"] = source.upper()
if band:
params["band"] = band.lower()
if mode:
params["mode"] = mode.upper()
if dx_call:
params["dx_call"] = dx_call.upper()
# Fetch spots
spots = get_spothole_spots(**params)
if spots:
response_lines = []
for spot in spots[:5]:
callsign = spot.get('dx_call', spot.get('callsign', 'N/A'))
freq_hz = spot.get('freq', spot.get('frequency', None))
frequency = f"{float(freq_hz)/1e6:.3f} MHz" if freq_hz else "N/A"
mode_val = spot.get('mode', 'N/A')
comment = spot.get('comment', '')
sig = spot.get('sig', '')
de_grid = spot.get('de_grid', '')
de_call = spot.get('de_call', '')
sig_ref_name = spot.get('sig_refs_names', [''])[0] if spot.get('sig_refs_names') else ''
line = f"{callsign} @{frequency} {mode_val} {sig} {sig_ref_name} by:{de_call} {de_grid} {comment}"
response_lines.append(line)
response = "\n".join(response_lines)
else:
response = "No DX spots found."
return response
return "Error: No DX command found."
def get_spothole_spots(source=None, band=None, mode=None, date=None, dx_call=None, de_continent=None, de_location=None):
"""
Fetches spots from https://spothole.app/api/v1/spots with optional filters.
Returns a list of spot dicts.
"""
url = "https://spothole.app/api/v1/spots"
params = {}
# Add administrative filters if provided
qrt = False # Always fetch active spots
needs_sig = False # Always need spots wth a group ike Xota
limit = 4
dedupe = True
params["dedupe"] = str(dedupe).lower()
params["limit"] = limit
params["qrt"] = str(qrt).lower()
params["needs_sig"] = str(needs_sig).lower()
params["needs_sig_ref"] = 'true'
# Only get spots from last 9 hours
received_since_dt = datetime.datetime.utcnow() - datetime.timedelta(hours=9)
received_since = int(received_since_dt.timestamp())
params["received_since"] = received_since
# Add spot filters if provided
if de_continent:
params["de_continent"] = de_continent
if de_location:
params["de_location"] = de_location
if source:
params["source"] = source
if band:
params["band"] = band
if mode:
params["mode"] = mode
if dx_call:
params["dx_call"] = dx_call
if date:
# date should be a string in YYYY-MM-DD or datetime.date
if isinstance(date, datetime.date):
params["date"] = date.isoformat()
else:
params["date"] = date
try:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko"}
response = requests.get(url, params=params, headers=headers)
response.raise_for_status()
spots = response.json()
except Exception as e:
logger.debug(f"Error fetching spots: {e}")
spots = []
return spots

View File

@@ -385,6 +385,7 @@ try:
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
dxspotter_enabled = config['radioMon'].getboolean('dxspotter_enabled', True) # default True
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
sigWatchBroadcastInterface = config['radioMon'].getint('sigWatchBroadcastInterface', 1) # default interface 1

View File

@@ -141,6 +141,11 @@ if dad_jokes_enabled:
trap_list = trap_list + ("joke",)
help_message = help_message + ", joke"
if dxspotter_enabled:
from modules.dxspot import handledxcluster
trap_list = trap_list + ("dx",)
help_message = help_message + ", dx"
# Wikipedia Search Configuration
if wikipedia_enabled:
from modules.wiki import * # from the spudgunman/meshing-around repo