commit 564fc27a470d40198e1c12b745fd9b14e196c8d3 Author: N4IRS Date: Thu Jul 9 13:45:14 2026 -0400 feat: migrate bookworm binaries to filesystem-mirror structure diff --git a/etc/cron.daily/DVSM_Update b/etc/cron.daily/DVSM_Update new file mode 120000 index 0000000..dcf616a --- /dev/null +++ b/etc/cron.daily/DVSM_Update @@ -0,0 +1 @@ +/usr/local/sbin/DVSM_Update.sh \ No newline at end of file diff --git a/etc/logrotate.d/MMDVM_Bridge b/etc/logrotate.d/MMDVM_Bridge new file mode 100644 index 0000000..9f01a0f --- /dev/null +++ b/etc/logrotate.d/MMDVM_Bridge @@ -0,0 +1,27 @@ +/var/log/mmdvm/MMDVM_Bridge.log +{ + firstaction + # $1 = the full path to the log file is passed to this script + # The path contains ".log" and a trailing space, so strip it. + parameter=$1 + parameter="${parameter%%.*}" + today=$(date +"%Y-%m-%d") + filename=$parameter-$today.log + echo $filename $1 >/tmp/out.txt + if [ -f "$filename" ]; then + mv $filename $1 + fi + endscript + daily + rotate 7 + maxage 7 + dateext + dateformat -%Y-%m-%d + extension .log + ifempty + create + postrotate + systemctl reload mmdvm_bridge + endscript +} + diff --git a/lib/systemd/system/mmdvm_bridge.service b/lib/systemd/system/mmdvm_bridge.service new file mode 100644 index 0000000..60d62ec --- /dev/null +++ b/lib/systemd/system/mmdvm_bridge.service @@ -0,0 +1,25 @@ +[Unit] +Description=MMDVM_Bridge Service +# Description=Place this file in /lib/systemd/system +# Description=N4IRS 07/01/2024 + +After=netcheck.service +Requires=netcheck.service + +[Service] +Type=simple +Restart=always +RestartSec=3 +Restart=on-abort +StandardOutput=null +WorkingDirectory=/opt/MMDVM_Bridge +ExecStartPre = /usr/bin/sh -c 'echo "Starting MMDVM_Bridge: [`date +%%T.%%3N`]" >> /var/log/netcheck' +ExecStart=/opt/MMDVM_Bridge/MMDVM_Bridge /opt/MMDVM_Bridge/MMDVM_Bridge.ini +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process + +[Install] +WantedBy=multi-user.target + + + diff --git a/opt/MMDVM_Bridge/DVSwitch.ini b/opt/MMDVM_Bridge/DVSwitch.ini new file mode 100644 index 0000000..22d0ff4 --- /dev/null +++ b/opt/MMDVM_Bridge/DVSwitch.ini @@ -0,0 +1,99 @@ +; MMDVM_Bridge export / import configuration file. +; This file should be placed along side of MMDVM_Bridge or you can supply +; an absolute path in the DVSWITCH environment variable, e.g: +; DVSWITCH=/etc/MMDVM_Bridge/DVSwitch.ini +; before executing MMDVM_Bridge +; +; Another method to set the enviorment variable is to use the systemd unit file +; by adding: +; Environment=DVSWITCH=/etc/MMDVM_Bridge/DVSwitch.ini +; to /lib/systemd/system/mmdvm_bridge.service + +; Configure the Quantar Repeater Partner +; Note that the TX and RX ports are already reversed for MMDVM_Bridge <--> Quantar_Bridge +[QUANTAR] +address = 0.0.0.0 ; Address to send IMBE TLV frames to (export) +txPort = 34103 ; Port to send IMBE TLV frames to (export) +rxPort = 34100 ; Port to listen for IMBE TLV frames on (import) +quantarPort = 1994 ; HDLC frames To/From the Quantar repeater +debug = 0 ; Debug 0 = off, 1 = on (adds lots of additional messages) +logLevel = 2 ; Logging levels, 0=No logging, 1=Debug, 2=Message, 3=Info, 4=Warning, 5=Error, 6=Fatal +logFilePath = /var/log/dvswitch/Quantar_Bridge.log + +; Configure the DMR Partner +; Audio format is AMBE 72 bit +[DMR] +address = 127.0.0.1 ; Address to send AMBE TLV frames to (export) +txPort = 31100 ; Port to send AMBE TLV frames to (export) +rxPort = 31103 ; Port to listen on (import) +slot = 2 ; Export slot +exportTG = 0 ; Which TG to export +hangTimerInFrames = 0 ; Use 50 for 3 seconds of hang time (3000 / 60) +talkerAlias = %callsign %location %description ; Get callsign location and description from MMDVM_Bridge.ini + +; Configure the D-Star Partner +; Audio format is AMBE 48 bit (DSAMBE) +[DSTAR] +address = 127.0.0.1 ; Address to send AMBE TLV frames to (export) +txPort = 32100 ; Port to send AMBE TLV frames to (export) +rxPort = 32103 ; Port to listen on (import) +fallbackID = 1234567 ; In case we can not find a valid DMR id in the database, export this one +exportTG = 9 ; Which TG to export +slot = 2 ; Export slot +RemotePort = 54321 ; Port to send Gateway commands to +message = %location %description ; Get location and description from MMDVM_Bridge.ini + +; Configure the NXDN Partner +; Audio format is AMBE 72 bit +[NXDN] +address = 127.0.0.1 ; Address to send AMBE TLV frames to (export) +txPort = 33100 ; Port to send AMBE TLV frames to (export) +rxPort = 33103 ; Port to listen on (import) +fallbackID = 1234567 ; In case we can not find a valid DMR id in the database, export this one +nxdnFallbackID = 12345 ; Use this ID when the input DMR ID is not found in the database +translate = 1234=4321 ; Translate NXDN TG < -- > DMR TG (bidirectional) +slot = 2 ; Export slot +RemotePort = 6075 ; Port to send Gateway commands to + +; Configure the P25 Partner +; Audio format is IMBE 88 bit +[P25] +address = 127.0.0.1 ; Address to send AMBE TLV frames to (export) +txPort = 34100 ; Port to send AMBE TLV frames to (export) +rxPort = 34103 ; Port to listen on (import) +slot = 2 ; Export slot +RemotePort = 6074 ; Port to send Gateway commands to + +; Configure the Yaesu Fusion Partner +; Audio format is AMBE 72 bit +; Audio format is IMBE 88 bit +[YSF] +address = 127.0.0.1 ; Address to send AMBE TLV frames to (export) +txPort = 35100 ; Port to send AMBE TLV frames to (export) +rxPort = 35103 ; Port to listen on (import) +txWidePort = 35105 ; Port to send IMBE TLV frames to for YSFw (export) +fallbackID = 1234567 ; In case we can not find a valid DMR id in the database, export this one +exportTG = 9 ; Which TG to export +slot = 2 ; Export slot +RemotePort = 6073 ; Port to send Gateway commands to + +; Configure the BrandMeister connection +; Simple Terminal Feature Update +; Audio format is AMBE 72 bit +[STFU] ; Brandmeister Open DMR Terminal (ODMRT) Protocol +BMAddress = 3102.repeater.net ; Brandmeister ODMRT server address +BMPort = 54006 ; Brandmeister ODMRT port +BMPassword = passw0rd ; Your Brandmeister password +Address = 127.0.0.1 ; Address to send AMBE TLV frames to (export) +txPort = 36100 ; Port to send AMBE TLV frames to (export) +rxPort = 36103 ; Port to listen on (import) +UserID = 1234567 ; Your DMR ID +TalkerAlias = N0CALL Name City ; Max 27 characters +StartTG = 3166 ; Startup talk group +LogLevel = 3 ; STFU log level (0=No logging, 1=Debug, 2=Message, 3=Info, 4=Warning, 5=Error) + +[ASL] +address = 127.0.0.1 +txPort = 30100 +rxPort = 30103 + diff --git a/opt/MMDVM_Bridge/MMDVM_Bridge.amd64 b/opt/MMDVM_Bridge/MMDVM_Bridge.amd64 new file mode 100755 index 0000000..bad68e1 Binary files /dev/null and b/opt/MMDVM_Bridge/MMDVM_Bridge.amd64 differ diff --git a/opt/MMDVM_Bridge/MMDVM_Bridge.arm64 b/opt/MMDVM_Bridge/MMDVM_Bridge.arm64 new file mode 100755 index 0000000..d2d1f71 Binary files /dev/null and b/opt/MMDVM_Bridge/MMDVM_Bridge.arm64 differ diff --git a/opt/MMDVM_Bridge/MMDVM_Bridge.armhf b/opt/MMDVM_Bridge/MMDVM_Bridge.armhf new file mode 100755 index 0000000..edc6c23 Binary files /dev/null and b/opt/MMDVM_Bridge/MMDVM_Bridge.armhf differ diff --git a/opt/MMDVM_Bridge/MMDVM_Bridge.i386 b/opt/MMDVM_Bridge/MMDVM_Bridge.i386 new file mode 100644 index 0000000..38d7066 --- /dev/null +++ b/opt/MMDVM_Bridge/MMDVM_Bridge.i386 @@ -0,0 +1 @@ +Binary not available for i386 diff --git a/opt/MMDVM_Bridge/MMDVM_Bridge.ini b/opt/MMDVM_Bridge/MMDVM_Bridge.ini new file mode 100644 index 0000000..fbf384c --- /dev/null +++ b/opt/MMDVM_Bridge/MMDVM_Bridge.ini @@ -0,0 +1,104 @@ +[General] +Callsign=N0CALL +Id=1234567 +Timeout=180 +Duplex=0 + +[Info] +RXFrequency=222340000 +TXFrequency=224940000 +Power=1 +Latitude=41.7333 +Longitude=-50.3999 +Height=0 +Location=Iceberg, North Atlantic +Description=MMDVM_Bridge +URL=https://groups.io/g/DVSwitch + +[Log] +# Logging levels, 0=No logging, 1=Debug, 2=Message, 3=Info, 4=Warning, 5=Error, 6=Fatal +DisplayLevel=1 +FileLevel=2 +FilePath=/var/log/mmdvm +FileRoot=MMDVM_Bridge + +[DMR Id Lookup] +File=/var/lib/mmdvm/DMRIds.dat +Time=24 + +[NXDN Id Lookup] +File=/var/lib/mmdvm/NXDN.csv +Time=24 + +[Modem] +Port=/dev/null +RSSIMappingFile=/dev/null +Trace=0 +Debug=0 + +[D-Star] +Enable=0 +Module=B + +[DMR] +Enable=0 +ColorCode=1 +EmbeddedLCOnly=1 +DumpTAData=0 + +[System Fusion] +Enable=0 + +[P25] +Enable=0 +NAC=293 + +[NXDN] +Enable=0 +RAN=1 +Id=12345 + +[D-Star Network] +Enable=0 +GatewayAddress=127.0.0.1 +GatewayPort=20010 +LocalPort=20011 +Debug=0 + +[DMR Network] +Enable=0 +Address=hblink.dvswitch.org +Port=62031 +Jitter=360 +Local=62032 +Password=passw0rd +# for DMR+ see https://github.com/DVSwitch/MMDVM_Bridge/blob/master/DOC/DMRplus_startup_options.md +# for XLX the syntax is: Options=XLX:4009 +# Options= +Slot1=0 +Slot2=1 +Debug=0 + +[System Fusion Network] +Enable=0 +LocalAddress=0 +LocalPort=3200 +GatewayAddress=127.0.0.1 +GatewayPort=4200 +Debug=0 + +[P25 Network] +Enable=0 +GatewayAddress=127.0.0.1 +GatewayPort=42020 +LocalPort=32010 +Debug=0 + +[NXDN Network] +Enable=0 +#LocalAddress=127.0.0.1 +Debug=0 +LocalPort=14021 +GatewayAddress=127.0.0.1 +GatewayPort=14020 + diff --git a/opt/MMDVM_Bridge/RemoteCommand.amd64 b/opt/MMDVM_Bridge/RemoteCommand.amd64 new file mode 100755 index 0000000..138b0fc Binary files /dev/null and b/opt/MMDVM_Bridge/RemoteCommand.amd64 differ diff --git a/opt/MMDVM_Bridge/RemoteCommand.arm64 b/opt/MMDVM_Bridge/RemoteCommand.arm64 new file mode 100755 index 0000000..a2ecfcd Binary files /dev/null and b/opt/MMDVM_Bridge/RemoteCommand.arm64 differ diff --git a/opt/MMDVM_Bridge/RemoteCommand.armhf b/opt/MMDVM_Bridge/RemoteCommand.armhf new file mode 100755 index 0000000..a2ecfcd Binary files /dev/null and b/opt/MMDVM_Bridge/RemoteCommand.armhf differ diff --git a/opt/MMDVM_Bridge/RemoteCommand.i386 b/opt/MMDVM_Bridge/RemoteCommand.i386 new file mode 100644 index 0000000..38d7066 --- /dev/null +++ b/opt/MMDVM_Bridge/RemoteCommand.i386 @@ -0,0 +1 @@ +Binary not available for i386 diff --git a/opt/MMDVM_Bridge/dvswitch.sh b/opt/MMDVM_Bridge/dvswitch.sh new file mode 100755 index 0000000..e8173c3 --- /dev/null +++ b/opt/MMDVM_Bridge/dvswitch.sh @@ -0,0 +1,1364 @@ +#!/bin/bash + +################################################################# +# /* +# * Copyright (C) 2019, 2020, 2021 N4IRR +# * +# * Permission to use, copy, modify, and/or distribute this software for any +# * purpose with or without fee is hereby granted, provided that the above +# * copyright notice and this permission notice appear in all copies. +# * +# * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# * AND FITNESS. IN NO EVENT SHALL N4IRR BE LIABLE FOR ANY SPECIAL, DIRECT, +# * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# * PERFORMANCE OF THIS SOFTWARE. +# */ +################################################################# + +#DEBUG=echo +#set -xv # this line will enable debug + + +SCRIPT_VERSION="1.6.2" + +AB_DIR=${AB_DIR:-"/var/lib/dvswitch"} +MMDVM_DIR=${MMDVM_DIR:-"/var/lib/mmdvm"} +DVSWITCH_INI=${DVSWITCH_INI:-"/opt/MMDVM_Bridge/DVSwitch.ini"} +MMDVM_INI=${MMDVM_INI:-"/opt/MMDVM_Bridge/MMDVM_Bridge.ini"} +NODE_DIR=${NODE_DIR:-"/tmp"} + +# Default server and port assignment, but overridden by value in ABInfo +TLV_PORT=36000 +USRP_PORT=32001 +SERVER=127.0.0.1 + +# HTTP_PORT is used for the simple server that supports data file uploads +HTTP_PORT=9042 + +# Error codes defined below +SUCCESSS=0 +ERROR_FILE_NOT_FOUND=-1 +ERROR_INVALID_ARGUMENT=-2 +ERROR_EMPTY_FILE=-3 +ERROR_DIR_NOT_FOUND=-4 +ERROR_INVALID_FILE=-5 +ERROR_LOOKUP_FAILED=-6 +ERROR_INI_FAILURE=-7 +_ERRORCODE=$SUCCESSS + +################################################################# +# Return value from ABInfo_xxxx.json +# The value may be an value, object/value or object/object/value +################################################################# +function getABInfoValue() { + declare _json_file=`getABInfoFileName` +python3 - </dev/null | head -1` + else + declare _json_file=$ABINFO # Use the environment variable (probably set by AB) + fi + echo $_json_file +} + +################################################################# +# Parse and print out an ini file value +# parseIniFile fileName stanza tag +################################################################# +function parseIniFile() { +python3 - < 1 will increase audio level above unity +################################################################# +function setUSRPGain() { + if [ $# -eq 0 ]; then + getABInfoValue usrp to_pcm gain + else + remoteControlCommand "usrpGain=$1" + fi +} + +################################################################# +# Set the digital audio gain +################################################################# +function setTLVGain() { + if [ $# -eq 0 ]; then + getABInfoValue usrp to_ambe gain + else + remoteControlCommand "tlvGain=$1" + fi +} + +################################################################# +# Set the USRP agc params to threshold, slope and decay +################################################################# +function setUSRPAgc() { + if [ $# -eq 0 ]; then + echo "Argument required: AGC parameters (threshold, slope and decay)" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + remoteControlCommand "agcUSRP=$1,$2,$3" + fi +} + +################################################################# +# Set the TLV agc params to threshold, slope and decay +################################################################# +function setTLVAgc() { + if [ $# -eq 0 ]; then + echo "Argument required: AGC parameters (threshold, slope and decay)" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + remoteControlCommand "agcTLV=$1,$2,$3" + fi +} + +################################################################# +# Set the USRP audio codec to {SLIN|ULAW|ADPCM} +################################################################# +function setUSRPCodec() { + if [ $# -eq 0 ]; then + echo "Argument required: codec" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + string='|SLIN|ULAW|ADPCM|slin|ulaw|adpcm|' + if [[ $string == *"|$1|"* ]]; then + remoteControlCommand "codec=$1" + else + echo "Invalid argument: {slin|ulaw|adpcm}" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + fi + fi +} + +################################################################# +# set the AB listener port +################################################################# +function setTLVRxPort() { + if [ $# -eq 0 ]; then + getABInfoValue tlv rx_port + else + remoteControlCommand "rxport=$1" + sleep 1 + TLV_PORT=`getTLVPort` # We have changed the listener on AB, so we must adjust our sending port + fi +} + +################################################################# +# Set the AB -> MB transmit port +################################################################# +function setTLVTxPort() { + if [ $# -eq 0 ]; then + getABInfoValue tlv tx_port + else + remoteControlCommand "txport=$1" + fi +} + +################################################################# +# Send the info packet to a USRP client (DVSM/UC) +################################################################# +function getInfo() { + if [ $# -eq 0 ]; then + remoteControlCommand "info" + else + getABInfoValue $1 $2 + fi +} + +################################################################# +# mute AB ("OFF", "USRP", "TLV", "BOTH") +################################################################# +function setMute() { + if [ $# -eq 0 ]; then + getABInfoValue mute + else + remoteControlCommand "mute=$1" + fi +} + +################################################################# +# Send "text" message to Mobile +################################################################# +function sendMessage() { + if [ -z "$1" ]; then + echo "Argument required: text" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + remoteControlCommand "message=$1" + fi +} + +################################################################# +# Send a macro definition or file to Mobile +################################################################# +function sendMacro() { + if [ -z "$2" ]; then + echo "Argument required: file or text" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + remoteControlCommand "$1=$2" + fi +} + +################################################################# +# Set the ping timer (keep alive) +################################################################# +function setPingTimer() { + if [ -z "$1" ]; then + getABInfoValue usrp ping + else + remoteControlCommand "ping=$1" + fi +} + +################################################################# +# Tell AB to reload database files from disk into memory +################################################################# +function reloadDatabase() { + remoteControlCommand "reloadDatabase" +} + +################################################################# +# Send the remote control TLV command to Analog_Bridge +################################################################# +function remoteControlCommand() { + if [ ! -z "${DEBUG}" ]; then + echo "remoteControlCommand $1" + else +PYTHON_ARG="$1" python3 - <iiiiiii',usrpSeq, 0, 0, 0, packetType << 24, 0, 0)) + cmd.encode("utf-8") + struct.pack('>i',0) + usrpSeq = (usrpSeq + 1) & 0xffff + udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + udp.sendto(usrp, ('$SERVER', $USRP_PORT)) + udp.close() +except: + sys.stderr.write("USRPCommand: error sending command\n") + traceback.print_exc() +END +} + +################################################################# +# +################################################################# +function setCallAndID() { + if [ ! -z "${DEBUG}" ]; then + echo "setCallAndID $1" + else +python3 - <> 16) & 0xff),((dmr_id >> 8) & 0xff),(dmr_id & 0xff),0,0,0,0,0,0,0,0,0)[0:14] + call + chr(0) + _sock.sendto(cmd, ('$SERVER', $TLV_PORT)) + _sock.close() +except: + sys.stderr.write("setCallAndID: error sending command\n") + exit(1) +END + fi +} + +################################################################# +# Tell AB to upload a file to the Mobile client +################################################################# +function pushFileToClient() { + if [ ! -z "${DEBUG}" ]; then + echo "remoteControlCommand pushFileToClient $1" + else + if [ ! -f $1 ]; then + echo "File $1 does not exist, abort transfer" + return + fi + size=`wc -c $1 | awk '{print $1}'` + if (($size == 0)); then + echo "file is empty, abort transfer" + return + fi + +python3 - < 0 bytes. Arguments are +# Directory, Server IP and file name. +################################################################# +function pushLocalFileAsURLToClient() { + + if [ ! -f "$1/$3" ]; then + echo "File $1/$3 does not exist, abort transfer" + _ERRORCODE=$ERROR_FILE_NOT_FOUND + return + fi + declare size=`wc -c "$1/$3" | awk '{print $1}'` + if (($size == 0)); then + echo "file is empty, abort transfer" + _ERRORCODE=$ERROR_EMPTY_FILE + return + fi + pushURLToClient "$2/$3" +} + +################################################################# +# Send the URL of a file to download to DVSM. DVSM knows that if +# the name begins with http it is a URL. +################################################################# +function pushURLToClient() { +python3 - < "$NODE_DIR/$1" + if [ ${_OS} == Darwin ]; then + sed -i '' 's/||/||/g' "$NODE_DIR/$1" + else + sed -i 's/||/||/g' "$NODE_DIR/$1" + fi + declare isValid=`grep -i N4IRS "$NODE_DIR/$1"` + if [ -z "${isValid}" ]; then + rm "$NODE_DIR/$1" + echo "ASL node list is not valid, ignoring" + fi +} + +################################################################# +# +################################################################# +function collectProcessDataFiles() { + + declare gitURL="https://raw.githubusercontent.com/g4klx" + echo "Processing NXDN" + ParseNodeFile NXDN_Hosts.txt "${gitURL}/NXDNClients/master/NXDNGateway/NXDNHosts.txt" > $NODE_DIR/NXDN_node_list.txt 2>/dev/null + + echo "Processing P25" + ParseNodeFile P25_Hosts.txt "${gitURL}/P25Clients/master/P25Gateway/P25Hosts.txt" > $NODE_DIR/P25_node_list.txt 2>/dev/null + + echo "Processing DMR" + ParseTGFile TGList_BM.txt > $NODE_DIR/DMR_node_list.txt 2>/dev/null + + echo "Processing TGIF" + ParseTGFileTGIF TGList_TGIF.txt > $NODE_DIR/TGIF_node_list.txt 2>/dev/null + + echo "Processing YSF" + ParseYSFile YSF_Hosts.txt > $NODE_DIR/YSF_node_list.txt 2>/dev/null + + echo "Processing DStar" + ParseDStarFile DSTAR_Hosts.txt > $NODE_DIR/DSTAR_node_list.txt 2>/dev/null + + echo "Processing ASL" + DownloadAndValidateASLNodeList node_list.txt 2>/dev/null +} + +################################################################# +# Get all mobile data files, proces them into proper format and +# push them to the device +################################################################# +function collectProcessPushDataFiles() { + + collectProcessDataFiles + + echo "Pushing NXDN" + pushFileToClient "$NODE_DIR/NXDN_node_list.txt" + + echo "Pushing P25" + pushFileToClient "$NODE_DIR/P25_node_list.txt" + + echo "Pushing DMR" + pushFileToClient "$NODE_DIR/DMR_node_list.txt" + + echo "Pushing TGIF" + pushFileToClient "$NODE_DIR/TGIF_node_list.txt" + + echo "Pushing YSF" + pushFileToClient "$NODE_DIR/YSF_node_list.txt" + + echo "Pushing DStar" + pushFileToClient "$NODE_DIR/DSTAR_node_list.txt" + + echo "Pushing ASL" + pushFileToClient "$NODE_DIR/node_list.txt" +} + +################################################################# +# Utility function to get the primary IP address +################################################################# +function getMyIP() { + declare _ip _line + while IFS=$': \t' read -a _line ;do + [ -z "${_line%inet}" ] && + _ip=${_line[${#_line[1]}>4?1:2]} && + [ "${_ip#127.0.0.1}" ] && echo $_ip && return 0 + done< <(LANG=C /sbin/ifconfig) +} + +################################################################# +# Get all mobile data files, proces them into proper format and +# push the URL to the device. Starts a simple web server on port +# $HTTP_PORT (9042). +################################################################# +function collectProcessPushDataFilesHTTP() { + + declare processID=`ps aux | grep "python3 -m http.server $HTTP_PORT" | grep -v grep | awk '{print $2}'` + kill $processID 2>/dev/null + pushd "$NODE_DIR" + python3 -m http.server $HTTP_PORT & + popd + declare _MYIP=`getMyIP` + PSERVER="http://${_MYIP}:$HTTP_PORT" + + collectProcessDataFiles + + echo "Pushing NXDN" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "NXDN_node_list.txt" + sleep 5 + + echo "Pushing P25" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "P25_node_list.txt" + sleep 5 + + echo "Pushing DMR" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "DMR_node_list.txt" + sleep 5 + + echo "Pushing TGIF" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "TGIF_node_list.txt" + sleep 5 + + echo "Pushing YSF" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "YSF_node_list.txt" + sleep 5 + + echo "Pushing DStar" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "DSTAR_node_list.txt" + sleep 5 + + echo "Pushing ASL" + pushLocalFileAsURLToClient "$NODE_DIR" "$PSERVER" "node_list.txt" + sleep 10 + + processID=`ps aux | grep "python3 -m http.server $HTTP_PORT" | grep -v grep | awk '{print $2}'` + kill $processID 2>/dev/null + + sendMessage "Database update complete" + +} + +################################################################# +# Download and validate a file. This function will use curl to download +# a file from a server and test for valid data. The tests include +# a warning on download failure, and errors for file size and valid contents. +################################################################# +function downloadAndValidate() { + ${DEBUG} curl --fail -o "$MMDVM_DIR/$1" -s "http://www.pistar.uk/downloads/$2" + if (( $? != 0 )); then + echo "Warning, download failure" + _ERRORCODE=$ERROR_FILE_NOT_FOUND + fi + if [ ! -f $MMDVM_DIR/$1 ]; then + echo "Error, $1 file does not seem to exist" + _ERRORCODE=$ERROR_INVALID_FILE + else + declare _fileSize=`wc -c $MMDVM_DIR/$1 | awk '{print $1}'` + if (( ${_fileSize} < 10 )); then + echo "Error, $1 file has no contents" + _ERRORCODE=$ERROR_INVALID_FILE + else + declare isValid=`grep $3 "$MMDVM_DIR/$1"` + if [ -z "$isValid" ]; then + echo "Error, $1 file does not seem to be valid" + _ERRORCODE=$ERROR_INVALID_FILE + fi + fi + fi +} + +################################################################# +# Download all user databases +################################################################# +function downloadDatabases() { + if [ -d "${MMDVM_DIR}" ] && [ -d "${AB_DIR}" ]; then + + ${DEBUG} curl -s -N "https://database.radioid.net/static/user.csv" | awk -F, 'NR>1 {if ($1 > "") print $1,$2,$3}' > "${MMDVM_DIR}/DMRIds.dat" + ${DEBUG} curl -s -N "https://database.radioid.net/static/user.csv" | awk -F, 'BEGIN{OFS=",";} NR>1 {if ($1 > "") print $1,$2,$3}' > "${AB_DIR}/subscriber_ids.csv" + ${DEBUG} curl -s -N "https://database.radioid.net/static/nxdn.csv" > "${MMDVM_DIR}/NXDN.csv" + ${DEBUG} curl -s -N "http://www.pistar.uk/downloads/DMR_Hosts.txt" > "${MMDVM_DIR}/DMR_Hosts.txt" + + downloadAndValidate "NXDNHosts.txt" "NXDN_Hosts.txt" "dvswitch.org" + downloadAndValidate "P25Hosts.txt" "P25_Hosts.txt" "dvswitch.org" + downloadAndValidate "TGList_BM.txt" "TGList_BM.txt" "DVSWITCH" + downloadAndValidate "YSFHosts.txt" "YSF_Hosts.txt" "dvswitch.org" + + downloadAndValidate "TGList_TGIF.txt" "TGList_TGIF.txt" "TGIF" +# TG list direct from BM +# curl -X 'GET' 'https://api.brandmeister.network/v2/talkgroup' -H 'accept: */*' +# TG list from TGIF +# curl -X 'GET' 'https://api.tgif.network/dmr/talkgroups/json' +# TG list from FreeDMR +# curl -X 'GET' 'https://freedmr.cymru/talkgroups/talkgroup_ids_json.php' + + downloadAndValidate "FCSRooms.txt" "FCS_Hosts.txt" "FCS00106" + downloadAndValidate "DCS_Hosts.txt" "DCS_Hosts.txt" "DCS006" + downloadAndValidate "DPlus_Hosts.txt" "DPlus_Hosts.txt" "REF030" + downloadAndValidate "DExtra_Hosts.txt" "DExtra_Hosts.txt" "XRF012" + downloadAndValidate "XLXHosts.txt" "XLXHosts.txt" "001;" + downloadAndValidate "APRS_Hosts.txt" "APRS_Hosts.txt" "noam.aprs2.net" + + declare isValid=`grep 3113043 "${MMDVM_DIR}/DMRIds.dat"` + if [ -z "$isValid" ]; then + + ${DEBUG} curl -s -N "http://registry.dstar.su/dmr/DMRIds.php" > "${MMDVM_DIR}/DMRIds.dat" + ${DEBUG} curl -s -N "http://registry.dstar.su/dmr/DMRIds.php" | awk -F, 'BEGIN{FS=" ";OFS=",";} NR>1 {if ($1 > "") print $1,$2,$3}' > "${AB_DIR}/subscriber_ids.csv" + + isValid=`grep 3113043 "${MMDVM_DIR}/DMRIds.dat"` + if [ -z "$isValid" ]; then + echo "Error, DMR ID file does not seem to be valid" + _ERRORCODE=$ERROR_INVALID_FILE + fi + fi + else + echo "Destination directory does not exist, aborting" + _ERRORCODE=$ERROR_DIR_NOT_FOUND + fi +} + +################################################################# +# Set digital mode of AB/MB getting the proper ports from DVSwitch.ini +################################################################# +function setMode() { + if [ $# -eq 0 ]; then # No argument passed, just return the current value + echo `getABInfoValue tlv ambe_mode` + else + declare _MODE=`echo $1 | tr '[:lower:]' '[:upper:]'` + if [[ "|DMR|YSF|P25|NXDN|DSTAR|ASL|STFU|" == *"$_MODE"* ]]; then + ${DEBUG} setTLVRxPort 30000 # cause AB to stop listening + _MBTX=`parseIniFile "$DVSWITCH_INI" "$_MODE" "TXPort"` + _MBRX=`parseIniFile "$DVSWITCH_INI" "$_MODE" "RXPort"` + if [ ! -z $_MBTX ]; then + sendMessage "Setting mode to $_MODE" + ${DEBUG} setAmbeMode $_MODE + ${DEBUG} setTLVTxPort ${_MBRX} + ${DEBUG} setTLVRxPort ${_MBTX} + if [ $# -ge 2 ]; then ${DEBUG} setTLVGain $2; setTLVAudioType AUDIO_USE_GAIN; fi + if [ $# -ge 3 ]; then ${DEBUG} setUSRPGain $3; setUSRPAudioType AUDIO_USE_GAIN; fi + ${DEBUG} getInfo + else + echo "Error, DVSwitch.ini file not found" + _ERRORCODE=$ERROR_FILE_NOT_FOUND + fi + else + echo "Error, Mode must be DMR or YSF or P25 or DSTAR, NXDN, ASL or STFU" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + fi + fi +} + +################################################################# +# Show pretty ABInfo json file +################################################################# +function prettyPrintInfo() { + declare _abname=`getABInfoFileName` + if [ -f ${_abname} ]; then + python3 -mjson.tool ${_abname} + else + echo ABInfo file not found + fi +} + +################################################################# +# Lookup info in database file +################################################################# +function lookup() { + declare databaseName="${MMDVM_DIR}/DMRIds.dat" + if [ -f "${databaseName}" ]; then + found=`grep -i $1 "${databaseName}"` + if [ -z "$found" ]; then + _ERRORCODE=$ERROR_LOOKUP_FAILED + else + echo $found + fi + else + echo DMR ID database file not found at ${databaseName} + _ERRORCODE=$ERROR_LOOKUP_FAILED + fi +} + +################################################################# +# Get version information from AB and MB +################################################################# +function appVersion() { + if [ $# -eq 0 ]; then + echo "dvswitch.sh version $SCRIPT_VERSION" + else + case $1 in + ab|AB|Analog_Bridge) + if [ -f "/opt/Analog_Bridge/Analog_Bridge" ]; then + "/opt/Analog_Bridge/Analog_Bridge" -v + else + declare _ver=`getABInfoValue ab version` + echo "Analog_Bridge version ${_ver}" + fi + ;; + mb|MB|MMDVM_Bridge) + if [ -f "/opt/MMDVM_Bridge/MMDVM_Bridge" ]; then + "/opt/MMDVM_Bridge/MMDVM_Bridge" -v + else + echo "MMDVM_Bridge version UNKNOWN" + fi + ;; + gw|GW) + for gw in P25Gateway NXDNGateway YSFGateway; do + if [ -f "/opt/$gw/$gw" ]; then + "/opt/$gw/$gw" -v + fi + done + ;; + path) + if [ -f "$2" ]; then + "$2" -v + fi + ;; + all|ALL) + appVersion + for app in ab mb gw; do + appVersion $app + done + for app in Analog_Reflector STFU; do + appVersion path "/opt/${app}/${app}" + done + ;; + esac + fi +} + +################################################################# +# Echo the list of "enabled" modes in MB.ini +################################################################# +function getEnabledModes() { + # For each mode, disable the main section and the network + declare _MODE="" + declare _NET="" + declare enabledModes="" + for mode in DMR "System Fusion" P25 D-Star NXDN; do + _MODE=`parseIniFile "$MMDVM_INI" "${mode}" "Enable"` + _NET=`parseIniFile "$MMDVM_INI" "${mode} Network" "Enable"` + #echo "${mode} mode = ${_MODE} and Network = ${_NET}" + if [ ${_MODE} == "1" ] && [ ${_NET} == "1" ]; then + enabledModes=`echo ${enabledModes}${mode}" " ` + fi + done + echo "$1${enabledModes}" +} + +################################################################# +# Print out the owner for a specified UDP port +################################################################# +function getUDPPortOwner() { + if [ -z "$1" ]; then + echo "Argument required: port number" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + declare port=":$1" + declare _OS=$(uname -s) + + if [ ${_OS} == Darwin ]; then + declare pid=$(lsof -i udp$port -P +c 0 | awk 'NR>1 {print $2}') + if [ -z "$pid" ]; then + echo "No processes listening on port $port" + else + ps -f $pid | awk 'NR>1 {print $8 " " $9 " " $10}' + fi + else + declare pid=$(sudo netstat -unap | grep "$port" | awk '{print $6}' | cut -d'/' -f1) + if [ -z "$pid" ]; then + echo "No processes listening on port $port" + else + ps -f $pid | awk 'NR>1 {print $9 " " $10 " " $11}' + fi + fi + fi +} + +################################################################# +# Print out the ports owned by a specified process +################################################################# +function getUDPPortsForProcess() { + if [ -z "$1" ]; then + echo "Argument required: process name" + _ERRORCODE=$ERROR_INVALID_ARGUMENT + else + declare process="$1" + declare _OS=$(uname -s) + + set -f; + if [ ${_OS} == Darwin ]; then + declare ports=($(lsof -i udp -P +c 0 | grep -i "${process:0:14}" | awk '{if ($9 != "*:*") print $9}' | cut -d':' -f2)) + if [ ${#ports[@]} -gt 0 ]; then + echo "$process owns UDP ports: ${ports[@]}" + fi + else + declare ports=($(sudo netstat -unap | grep -i "${process:0:14}" | awk '{split($4, a, ":"); print a[2]}')) + if [ ${#ports[@]} -gt 0 ]; then + echo "$process owns UDP ports: ${ports[@]}" + fi + fi + set +f; + fi +} + +################################################################# +# Print out the ports for all DVSwitch processes +################################################################# +function getUDPPortsForDVSwitch() { + for i in Analog_Bridge MMDVM_Bridge Quantar_Bridge Analog_Reflector STFU P25Gateway NXDNGateway DMRGateway YSFGateway ircddbgateway YSFParrot NXDNParrot md380-emu; do + getUDPPortsForProcess "$i" + done +} + +################################################################# +# +################################################################# +function updateINIFileValue() { + declare _file="$1" + declare _section="$2" + declare _tag="$3" + declare _value="${@:4}" + + if [ $# -ge 2 ]; then # Do we have the correct number of arguments? + if [ -f ${_file} ]; then # Check if the file exists (better error message then parseIniFile) + declare _secFound=$(grep -i "^\\[${_section}\\]" "${_file}") + if [ ! -z "${_secFound}" ]; then # See if the section exists + if [ ! -z ${_tag} ]; then + declare _tagLine=$(sed -n "/^\[${_section}\]/,/^\[/ p" "${_file}" | sed -n "/${_tag}/p") + if [ ! -z "${_tagLine}" ]; then + if [ ! -z "${_value}" ]; then + declare _oldValue=`parseIniFile "${_file}" "${_section}" "${_tag}"` + declare _oldLine="^${_tag}.*=.*${_oldValue}" + declare _equal=`[[ "${_tagLine}" == *" = "* ]] && echo " = " || echo "="` + declare _newLine="${_tag}${_equal}${_value}" + sed -i -e "/^\[${_section}\]/,/^\[/ s/${_oldLine}/${_newLine}/i" "${_file}" + else + echo "${_tagLine}" + fi + else + echo "Error Tag \"${_tag}\" was not found in section \"${_section}\" of file \"${_file}\"" + _ERRORCODE=$ERROR_INI_FAILURE + fi + else + declare _fullSection=$(sed -n "/^\[${_section}\]/,/^\[/ p" "${_file}") + echo "${_fullSection}" + fi + else + echo "Error, section \"${_section}\" was not found in file \"${_file}\"" + _ERRORCODE=$ERROR_INI_FAILURE + fi + else + echo "INI File \"${_file}\" not found" + _ERRORCODE=$ERROR_INI_FAILURE + fi + else + echo "Error, argument number: file section {tag} {value}" + _ERRORCODE=$ERROR_INI_FAILURE + fi +} + +################################################################# +# +################################################################# +function setGpsToIP() { + declare ip=$(curl -s ifconfig.me) + declare json=$(curl -s -L ipvigilante.com/$ip) +latlon=(`python3 - < DVSM/UC audio codec" + echo -e "\t tlvPorts rxport txport\t\t\t\t Set Analog_Bridge receive and transmit ports" + echo -e "\t info \t\t\t\t\t\t Update ABInfo and send to DVSM/UC" + echo -e "\t show \t\t\t\t\t\t Pretty print the ABInfo json file" + echo -e "\t lookup \t\t\t\t\t Lookup a DMR ID/call in the local database" + echo -e "\t mute {OFF|USRP|TLV|BOTH}\t\t\t Cause Aanlog_Bridge to mute a stream" + echo -e "\t message msg\t\t\t\t\t Send a text message to DVSM/UC" + echo -e "\t macro {file|text}\t\t\t\t Send a macro collection to DVSM" + echo -e "\t pushfile file\t\t\t\t\t Push file to DVSM" + echo -e "\t pushurl url\t\t\t\t\t Push URL to DVSM" + echo -e "\t collectProcessDataFiles \t\t\t Collect and prepare DVSM data files" + echo -e "\t collectProcessPushDataFiles \t\t\t Collect, prepare and upload DVSM data files" + echo -e "\t collectProcessPushDataFilesHTTP \t\t Collect, prepare and upload DVSM data files over http" + echo -e "\t reloadDatabase \t\t\t\t Tell AB to reload database files into memory" + echo -e "\t getEnabledModes \t\t\t\t Return the list of "enabled" modes in MB.ini" + echo -e "\t getUDPPortOwner {UDP port}\t\t\t Print out the process owner for the specified port" + echo -e "\t getUDPPortsForProcess {process name|ALL}\t Print out the ports owned by the specified process (or all DVSwitch processes)" + echo -e "\t updateINIFileValue file section {tag} {value}\t Display or edit a tag in an INI file" + echo -e "\t gps lat long \t\t\t\t\t Set GPS coordinates for YSF to lat and long" + echo -e "\t setGpsToIP \t\t\t\t\t Set GPS coordinates for YSF to the lat and long of your public IP address" + exit 1 +} + +################################################################# +# The main application +################################################################# +if [ $# -eq 0 ]; then + usage # No arguments, so just report usage information +else + case $1 in + -h|--help|"-?"|help) + usage + ;; + update) + downloadDatabases + ;; + lookup) + lookup $2 + ;; + collectProcessDataFiles|collectprocessdatafiles|cpdf) + collectProcessDataFiles + ;; + version|-v) + appVersion $2 + ;; + getEnabledModes|getenabledmodes|gem) + if [ $# -eq 1 ]; then # No argument passed, just return the current value + getEnabledModes "Enabled Modes: " + else + getEnabledModes "$2" + fi + ;; + getUDPPortOwner|getudpportowner|gupo) + getUDPPortOwner "$2" + ;; + getUDPPortsForProcess|getudpportsforprocess|gupfp) + if [ -z "$2" ] || [ $2 == "all" ] || [ $2 == "ALL" ]; then + getUDPPortsForDVSwitch + else + for i in "${@:1}"; do + getUDPPortsForProcess "$i" + done + fi + ;; + updateINIFileValue|updateinifilevalue|uifv) + updateINIFileValue "$2" "$3" $4 $5 ${@:6} + ;; + parseIniFile|parseinifile|pif) + parseAnyIniFile "$2" $3 $4 + ;; + *) + # All the commands below require that a valid ABInfo file exists. + TLV_PORT=`getTLVPort` # Get the communications port to use before we go further + USRP_PORT=`getUSRPPort` # Get the communications port to use before we go further + if [ $TLV_PORT == "ERROR" ]; then + echo "Can not find /tmp/ABInfo file (have you run Analog_Bridge?), aborting" + exit 1 + fi + case $1 in + mode) + setMode $2 $3 $4 + ;; + tune) + ${DEBUG} tune $2 + ${DEBUG} getInfo + ;; + ambeSize|ambesize) + ${DEBUG} setAmbeSize $2 + ;; + ambeMode|ambemode) + ${DEBUG} setAmbeMode $2 + ;; + slot) + ${DEBUG} setSlot $2 + ;; + setCallAndId|setcallandid) + setCallAndID $2 $3 + getInfo + ;; + tlvAudio|tlvaudio) + setTLVAudioType $2 + setTLVGain $3 + ;; + usrpAudio|usrpaudio) + setUSRPAudioType $2 + setUSRPGain $3 + ;; + USRPAgc|usrpagc) + setUSRPAgc $2 $3 $4 + ;; + TLVAgc|tlvagc) + setTLVAgc $2 $3 $4 + ;; + usrpCodec|usrpcodec) + setUSRPCodec $2 + ;; + tlvPorts|tlvports) + setTLVRxPort $2 + setTLVTxPort $3 + ;; + info) + # no arguments fill just tell AB to update the json file + # two arguments returns the value of "object" and "name" object{name:value} + getInfo $2 $3 + ;; + show) + prettyPrintInfo + ;; + mute) + setMute $2 + ;; + pushFile|pushfile|pf) + pushFileToClient "$2" + ;; + collectProcessPushDataFiles|collectprocesspushdatafiles|cppdf) + collectProcessPushDataFiles + ;; + pushUrl|pushurl) + pushURLToClient "$2" + ;; + collectProcessPushDataFilesHTTP|collectprocesspushdatafileshttp|cppdfh) + collectProcessPushDataFilesHTTP + ;; + reloadDatabase|reloaddatabase) + reloadDatabase + ;; + message) + sendMessage "$2" + ;; + macro) + sendMacro macro "$2" + ;; + menu) + sendMacro menu "$2" + ;; + ping) + setPingTimer "$2" + ;; + gps) + remoteControlCommand "gps=$2,$3" + ;; + exit) + remoteControlCommand "exit=0 0" + ;; + setGpsToIP) + setGpsToIP + ;; + exitAB|exitab) + exitAnalogBridge $2 $3 + ;; + usrpCommand|usrp) # undocumented ATM/WIP + USRPCommand "$2" "$3" + ;; + *) + # unknown option, update branch info (no option is specified, just ordered by placement) + echo "Unknown command line option:" $1 + usage + ;; + esac + ;; + esac +fi +exit $_ERRORCODE \ No newline at end of file diff --git a/usr/local/sbin/DVSM_Update.sh b/usr/local/sbin/DVSM_Update.sh new file mode 100755 index 0000000..dbeb5e5 --- /dev/null +++ b/usr/local/sbin/DVSM_Update.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# set -o errexit + +# N4IRS 10/30/2020 +# Version 1.6.0 + +################################################# +# # +# Download updated host files and setup links # +# # +################################################# + +# Use dvswitch.sh to get the host files +/opt/MMDVM_Bridge/dvswitch.sh update + +# create symbolic links for ircddbgateway +DIR="/usr/share/ircddbgateway/" +if [ -d "$DIR" ]; then + rm /usr/share/ircddbgateway/DCS_Hosts.txt + rm /usr/share/ircddbgateway/DExtra_Hosts.txt + rm /usr/share/ircddbgateway/DPlus_Hosts.txt + + ln -s /var/lib/mmdvm/DCS_Hosts.txt /usr/share/ircddbgateway/DCS_Hosts.txt + ln -s /var/lib/mmdvm/DExtra_Hosts.txt /usr/share/ircddbgateway/DExtra_Hosts.txt + ln -s /var/lib/mmdvm/DPlus_Hosts.txt /usr/share/ircddbgateway/DPlus_Hosts.txt +fi + +# if private host file(s) do not exist create empty ones +FILE="/var/lib/mmdvm/private_NXDNHosts.txt" +if [ ! -f "$FILE" ]; then + touch $FILE +fi + +FILE="/var/lib/mmdvm/private_P25Hosts.txt" +if [ ! -f "$FILE" ]; then + touch $FILE +fi + +exit 0