#!/bin/bash # # Author : Gaston # Date : 30 November 2025 # Updated : 1 December 2025 # Purpose : Finds Winlink RMS stations by channel near a given location set -o pipefail source /opt/emcomm-tools/bin/et-common # Temporary file to hold location for all supported location types LOCATION_FILE=/tmp/location.json [[ -e ${TMP_FILE} ]] && rm ${TMP_FILE} usage() { echo "Usage: $(basename $0) [OPTIONS]" echo echo "Location options (choose one):" echo " --call CALLSIGN Search near callsign" echo " --grid GRID Search near Maidenhead grid" echo " --latlon LAT,LON Search near coordinates (use decimal degrees)" echo echo "If no location option is specified, your position will be pulled from a supported" echo "ETC GPS unit or fallback to the grid square configured by 'et-user'." echo echo "Other options (optional):" echo " -b BAND Band [70cm|2m|6m|10m|12m|15m|17m|20m|30m|40m|80m|160m]" echo " -m MODE Mode [ardop|vara|packet]" echo exit 1 } loc_type="" loc_value="" band="" mode="" while [[ $# -gt 0 ]]; do case "$1" in --call) loc_type="call"; loc_value="$2"; shift 2 ;; --grid) loc_type="grid"; loc_value="$2"; shift 2 ;; --latlon) loc_type="latlon"; loc_value="$2"; shift 2 ;; -b) band="$2"; shift 2 ;; -m) mode="$2"; shift 2 ;; -*|--*) echo "Unknown option: $1"; usage ;; -h|--help) usage ;; *) echo "Unexpected argument: $1"; usage ;; esac done # Use local ETC user position if location is not defined if [[ -z $loc_type ]]; then loc_type="gps" fi #echo "$loc_type = $loc_value" case "$loc_type" in gps) curl -f -s "http://localhost:1981/api/geo/position" | jq .position > ${LOCATION_FILE} if [[ $? -ne 0 ]]; then echo -e "${RED}Can't determine your position for use as the transmitting station.${NC}" >&2 echo "Try specifying your callsign, grid or lat,lon." >&2 echo " --call CALLSIGN Search near callsign" >&2 echo " --grid GRID Search near Maidenhead grid" >&2 echo " --latlon LAT,LON Search near coordinates (use decimal degrees)" >&2 exit 1 fi ;; call) curl -f -s "http://localhost:1981/api/license?callsign=${loc_value}" > ${LOCATION_FILE} if [[ $? -ne 0 ]]; then echo -e "${RED}Location not found for callsign: ${loc_value}.${NC}" >&2 exit 1 fi ;; grid) curl -f -s "http://localhost:1981/api/geo/grid?gridSquare=${loc_value}" | jq '{lat: .position.lat, lon: .position.lon}' > ${LOCATION_FILE} if [[ $? -ne 0 ]]; then echo -e "${RED}Error converting grid to lat/lon using value: ${loc_value}.${NC}" >&2 exit 1 fi ;; latlon) # Write directly to JSON for consistency IFS=',' read -r tmpLat tmpLon <<< "$loc_value" if [[ -z $tmpLat || -z $tmpLon ]]; then echo -e "${RED}Invalid lat,lon format: $value. Expected 'lat,lon'.${NC}" >&2 exit 1 fi jq -n --arg lat "$tmpLat" --arg lon "$tmpLon" '{lat: ($lat|tonumber), lon: ($lon|tonumber)}' > ${LOCATION_FILE} ;; *) echo -e "${RED}Invalid location type: $loc_type${NC}" >&2 exit 1 ;; esac validate_latitude() { local lat="$1" if [[ -z "$lat" ]]; then echo -e "${RED}Latitude can't be an empty value${NC}" >&2 return 1 fi if ! [[ "$lat" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then echo -e "${RED}Latitude must be a number${NC}" >&2 return 1 fi local latf latf=$(printf "%.10f" "$lat" 2>/dev/null) || { echo -e "${RED}Error converting latitude${NC}" >&2 return 1 } # Check range: -90 <= lat <= 90 if (( $(echo "$latf < -90" | bc -l) )) || (( $(echo "$latf > 90" | bc -l) )); then echo -e "${RED}Latitude must be in range between -90 and 90 degrees${NC}" >&2 return 1 fi } validate_longitude() { local lon="$1" if [[ -z "$lon" ]]; then echo -e "${RED}Longitude can't be an empty value${NC}" >&2 return 1 fi if ! [[ "$lon" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then echo -e "${RED}Longitude must be a number${NC}" >&2 return 1 fi local lonf lonf=$(printf "%.10f" "$lon" 2>/dev/null) || { echo -e "${RED}Error converting longitude ${NC}" >&2 return 1 } # Check range: -180 <= lat <= 1800 if (( $(echo "$lon < -180" | bc -l) )) || (( $(echo "$lon > 180" | bc -l) )); then echo -e "${RED}Longitude must be in range between -180 and 180 degrees${NC}" >&2 return 1 fi } LAT=$(jq .lat $LOCATION_FILE) LON=$(jq .lon $LOCATION_FILE) validate_latitude "$LAT" || exit 1 validate_longitude "$LON" || exit 1 QUERY="http://localhost:1981/api/winlink/near?lat=${LAT}&lon=${LON}" [[ -n "${band}" ]] && QUERY="${QUERY}&band=${band}" [[ -n "${mode}" ]] && QUERY="${QUERY}&mode=${mode}" OUT=$(curl -s "${QUERY}") if [[ -z "${OUT}" ]] || [[ "${OUT}" = "[]" ]]; then echo "No Winlink RMS channels found." exit 1 fi echo echo "Nearby Winlink RMS Channels:" echo "----------------------------" echo "${OUT}" | jq -r ' (["Station","Callsign","Lat","Lon","Mode","Freq","Band"] | @tsv), (.[] | [ .callsign, .base_callsign, (.lat | tostring), (.lon | tostring), .mode, (.freq | tostring), .band ] | @tsv) ' | column -t -s $'\t'