#!/usr/bin/env bash # CRDOP launcher — native MAX25-SoftModem (audio-dummyd / M25 host). # ARDOP is a separate optional MAX25-Stack plugin — not part of CRDOP. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" MAX25_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" PREFIX_SHARE="${SCRIPT_DIR}/../share/crdop" if [[ -n "${CRDOP_INI:-}" ]]; then INI="${CRDOP_INI}" elif [[ -f "${HOME}/.config/crdop/crdop.ini" ]]; then INI="${HOME}/.config/crdop/crdop.ini" elif [[ -f "${PREFIX_SHARE}/crdop.ini.example" ]]; then INI="${PREFIX_SHARE}/crdop.ini.example" else INI="${ROOT}/share/crdop.ini.example" fi ini_get() { local section="$1" key="$2" default="$3" file="$4" [[ -f "${file}" ]] || { echo "${default}"; return; } awk -F= -v sec="[${section}]" -v k="${key}" -v d="${default}" ' $0 ~ /^[[:space:]]*[#;]/ { next } $1 ~ /^\[/ { cur=$0; gsub(/[][]/, "", cur); next } cur == substr(sec, 2, length(sec)-2) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1) if ($1 == k) { v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", v); gsub(/^"|"$/, "", v) print v; found=1; exit } } END { if (!found) print d } ' "${file}" } resolve_ini() { if [[ ! -f "${INI}" && -f "${ROOT}/share/crdop.ini.example" ]]; then INI="${ROOT}/share/crdop.ini.example" fi } normalize_profile() { case "${1,,}" in cb|dual|amateur) echo "${1,,}" ;; *) echo "warn: unknown radio_profile '${1}', using cb" >&2 echo "cb" ;; esac } normalize_duplex() { case "${1,,}" in full|half) echo "${1,,}" ;; *) echo "warn: unknown duplex '${1}', using half" >&2 echo "half" ;; esac } enforce_host_audio() { local backend backend="$(ini_get audio backend "" "${INI}")" if [[ -z "${backend}" ]]; then case "$(uname -s)" in FreeBSD) backend="oss" ;; *) backend="alsa" ;; esac fi if [[ "${backend}" == "alsa" ]]; then local no_pulse no_pulse="$(ini_get audio no_pulse yes "${INI}")" case "${no_pulse,,}" in 1|yes|true|on) unset PULSE_SERVER PULSE_COOKIE PIPEWIRE_RUNTIME_DIR 2>/dev/null || true export PULSE_SERVER= export ALSA_CARD="${ALSA_CARD:-}" ;; esac fi } audio_cli_args() { local cap pb backend backend="$(ini_get audio backend "" "${INI}")" if [[ -z "${backend}" ]]; then case "$(uname -s)" in FreeBSD) backend="oss" ;; *) backend="alsa" ;; esac fi cap="$(ini_get audio capture "" "${INI}")" pb="$(ini_get audio playback "" "${INI}")" local args=() [[ -n "${cap}" ]] && args+=("${cap}") [[ -n "${pb}" ]] && args+=("${pb}") if [[ ${#args[@]} -gt 0 ]]; then if [[ "${backend}" == "alsa" ]]; then for a in "${args[@]}"; do if [[ "${a}" == *pulse* || "${a}" == *pipewire* || "${a}" == default ]]; then echo "error: audio device '${a}' — use hw:/plughw: kernel ALSA" >&2 exit 1 fi done fi printf '%s\n' "${args[@]}" elif [[ "${backend}" == "oss" ]]; then echo "/dev/dsp" echo "/dev/dsp" fi } find_audio_dummyd() { for candidate in \ "${MAX25_ROOT}/build/bin/audio-dummyd" \ "${MAX25_ROOT}/stacks/crdop/tools/audio-dummyd" \ "$(command -v audio-dummyd 2>/dev/null || true)"; do [[ -n "${candidate}" && -x "${candidate}" ]] || continue echo "${candidate}" return done return 1 } resolve_ini if [[ -f "${PREFIX_SHARE}/VERSION" ]]; then CRDOP_VERSION="$(tr -d '[:space:]' < "${PREFIX_SHARE}/VERSION")" elif [[ -f "${ROOT}/VERSION" ]]; then CRDOP_VERSION="$(tr -d '[:space:]' < "${ROOT}/VERSION")" else CRDOP_VERSION="CUR999" fi PROFILE="$(normalize_profile "$(ini_get profile radio_profile cb "${INI}")")" DUPLEX="$(normalize_duplex "$(ini_get modem duplex half "${INI}")")" MYCALL="$(ini_get mycall call NOCALL-0 "${INI}")" HOST_PORT="$(ini_get host port 8515 "${INI}")" DATA_PORT=$((HOST_PORT + 1)) SAMPLE_RATE="$(ini_get audio sample_rate 48000 "${INI}")" if [[ $# -ge 1 && "${1}" =~ ^[0-9]+$ ]]; then HOST_PORT="$1" DATA_PORT=$((HOST_PORT + 1)) shift fi mapfile -t AUDIO_ARGS < <(audio_cli_args || true) enforce_host_audio DUMMY="$(find_audio_dummyd || true)" if [[ -z "${DUMMY}" ]]; then echo "error: audio-dummyd not found — build MAX25 or set path" >&2 exit 1 fi DUMMY_ARGS=(--ctrl-port "${HOST_PORT}" --data-port "${DATA_PORT}" -r "${SAMPLE_RATE}") if [[ ${#AUDIO_ARGS[@]} -ge 1 ]]; then DUMMY_ARGS+=(-D "${AUDIO_ARGS[0]}") fi if [[ ${#AUDIO_ARGS[@]} -ge 2 ]]; then DUMMY_ARGS+=(-P "${AUDIO_ARGS[1]}") fi AUDIO_BACKEND="$(ini_get audio backend "" "${INI}")" [[ -z "${AUDIO_BACKEND}" ]] && case "$(uname -s)" in FreeBSD) AUDIO_BACKEND="oss" ;; *) AUDIO_BACKEND="alsa" ;; esac echo "CRDOP ${CRDOP_VERSION} profile=${PROFILE} duplex=${DUPLEX} ctrl=:${HOST_PORT} data=:${DATA_PORT} call=${MYCALL} audio=${AUDIO_BACKEND} (native M25)" exec "${DUMMY}" "${DUMMY_ARGS[@]}" "$@"