ASL3 callsigns instead of node numbers

I would like to know if anyone has successfully been able to run write_node_callsigns.sh with ASL3. I am legally blind and have been using this feature on one of my HamVoip nodes. Many thanks, Steve WB4IZC

#!/bin/bash
# N5LSN
# Make app_rpt telemetry use ASL callsigns instead of node numbers
# Intended for use on ASL3
# Directories and files
SRCDIR="/var/log/asterisk"
DESTDIR="/usr/share/asterisk/sounds/en/rpt/nodenames"
RPTSOUNDS="/usr/share/asterisk/sounds/en/rpt"
LETTERS="/usr/share/asterisk/sounds/en/letters"
NUMBERS="/usr/share/asterisk/sounds/en/digits"
PREV_DB="/tmp/previous_astdb.txt"
# Start time tracking
start_time=$(date +%s%3N)
# Usage instructions
usage() {
    cat << EOF
Usage: write_node_callsigns.sh options
OPTIONS:
   -h        Show this message
   -a        Process all nodes
   -i        Include node number with call
   -n node   Process a single node
   -d path   Specify destination directory (default: /usr/share/asterisk/sounds/en/rpt/nodenames)
   -v        Verbose output
   -f        Force run without user confirmation
Examples:
    ./write_node_callsigns.sh -a               # Process all nodes
    ./write_node_callsigns.sh -n 40000         # Process single node 40000
    ./write_node_callsigns.sh -f               # Force execution without confirmation
EOF
}
STRING=""
VERBOSE=""
INCNODE=""
FORCE_RUN=0
MAX_PREVIEW=10
# Create directories if missing
ensure_directory_exists() {
    if [ ! -d "$1" ]; then
        echo "Creating directory: $1"
        mkdir -p "$1" || { echo "Failed to create directory $1"; exit 1; }
    fi
}
# Find the correct audio file (.gsm or .ulaw)
find_audio_file() {
    basepath=$1
    if [ -f "${basepath}.gsm" ]; then
        echo "${basepath}.gsm"
    elif [ -f "${basepath}.ulaw" ]; then
        echo "${basepath}.ulaw"
    else
        echo ""
    fi
}
# Process each character in the callsign and form the audio filenames
make_call() {
    local foo=${1,,}
    STRING=""
    for (( i=0; i<${#foo}; i++ )); do
        local char=${foo:$i:1}
        case $char in
            [0-9]) FILENAME=$(find_audio_file "$NUMBERS/$char") ;;
            "/")   FILENAME=$(find_audio_file "$LETTERS/slash") ;;
            "-")   FILENAME=$(find_audio_file "$LETTERS/dash") ;;
            [a-z]) FILENAME=$(find_audio_file "$LETTERS/$char") ;;
        esac
        if [ -n "$FILENAME" ]; then
            STRING="$STRING $FILENAME"
        else
            echo "Error: Audio file for '$char' not found."
        fi
    done
}
# Handle .ulaw files with sox
process_file() {
    local file=$1
    if [[ "$file" == *.ulaw ]]; then
        echo "-t raw -e u-law -r 8000 -c 1 $file"
    else
        echo "$file"
    fi
}
# Concatenate the audio files into the final output and measure time
write_call() {
    local output_file="$DESTDIR/$f1.gsm"
    local start=$(date +%s%3N)  # Get start time in milliseconds
    # Generate processed file paths for sox
    local processed_files=""
    for file in $STRING; do
        processed_files="$processed_files $(process_file $file)"
    done
    # Execute sox to concatenate files (always overwrite)
    sox $processed_files $output_file
    local end=$(date +%s%3N)  # Get end time in milliseconds
    local duration=$((end - start))  # Calculate processing time
    # Output in the required format
    echo "[$(printf "%04d" $duration)ms] - $f1 - $f2"
}
# Load previous database into associative array for fast lookup
declare -A previous_callsigns
load_previous_database() {
    if [ -f "$PREV_DB" ]; then
        while IFS='|' read -r node callsign _; do
            # Ensure the node ID is valid (non-empty)
            if [ -n "$node" ]; then
                previous_callsigns["$node"]="$callsign"
            fi
        done < "$PREV_DB"
    fi
}
# Compare current astdb.txt with previous_db.txt to find new or modified nodes
compare_databases() {
    echo "Comparing databases..."
    new_nodes=()  # Array to store nodes that are new or changed
    changes=()    # Array to store changes for preview
    declare -A latest_node_callsigns  # To handle duplicate node numbers
    while IFS='|' read -r f1 f2 _; do
        # Skip lines starting with a semicolon or empty lines
        [[ "$f1" =~ ^\; ]] || [ -z "$f1" ] && continue
        # Handle duplicates by keeping the last occurrence of each node
        latest_node_callsigns["$f1"]="$f2"
    done < "$SRCDIR/astdb.txt"
    # Now compare the latest callsigns with the previous database
    for node in "${!latest_node_callsigns[@]}"; do
        current_callsign="${latest_node_callsigns[$node]}"
        old_callsign="${previous_callsigns[$node]}"
        if [ -z "$old_callsign" ]; then
            # New node
            new_nodes+=("$node|$current_callsign")
            changes+=("$node: NEW -> $current_callsign")
        elif [ "$old_callsign" != "$current_callsign" ]; then
            # Callsign has changed
            new_nodes+=("$node|$current_callsign")
            changes+=("$node: $old_callsign -> $current_callsign")
        fi
    done
}
# Prompt the user to confirm before proceeding, show the first 10 nodes with changes
confirm_processing() {
    local node_count=$1
    if [ $FORCE_RUN -eq 1 ]; then
        return  # Skip confirmation if forced to run
    fi
    echo "$node_count nodes need to be processed."
    echo "Preview of changes:"
    local i=0
    for change in "${changes[@]}"; do
        echo "  $change"
        ((i++))
        if [ $i -ge $MAX_PREVIEW ]; then
            echo "  ...and more."
            break
        fi
    done
    read -p "Continue? [y/n]: " response
    case "$response" in
        [yY][eE][sS]|[yY])
            echo "Starting processing..."
            ;;
        *)
            echo "Aborting."
            exit 0
            ;;
    esac
}
# Update the previous_db.txt file with the current astdb.txt data
update_previous_db() {
    cp "$SRCDIR/astdb.txt" "$PREV_DB"
}
# Format the total execution time into the appropriate unit (seconds, minutes, hours)
format_total_time() {
    local total_time_ms=$1
    if (( total_time_ms < 1000 )); then
        # Less than 1 second: display in milliseconds
        echo "${total_time_ms}ms"
    elif (( total_time_ms < 60000 )); then
        # Less than 1 minute: display in seconds
        local total_time_sec=$(echo "scale=1; $total_time_ms / 1000" | bc)
        echo "${total_time_sec}s"
    elif (( total_time_ms < 3600000 )); then
        # Less than 1 hour: display in minutes
        local total_time_min=$(echo "scale=1; $total_time_ms / 60000" | bc)
        echo "${total_time_min}min"
    else
        # More than 1 hour: display in hours
        local total_time_hr=$(echo "scale=1; $total_time_ms / 3600000" | bc)
        echo "${total_time_hr}h"
    fi
}
# Main processing logic for new or modified nodes
process_nodes() {
    # Load the previous database into memory for fast lookups
    load_previous_database
    # Compare current and previous databases
    compare_databases
    local node_count=${#new_nodes[@]}
    if [ $node_count -eq 0 ]; then
        echo "No new or changed nodes to process."
        return
    fi
    # Confirm processing with user
    confirm_processing "$node_count"
    # Process new or modified nodes
    for node_data in "${new_nodes[@]}"; do
        IFS='|' read -r f1 f2 <<< "$node_data"
        make_call "$f2"
        if [ "$INCNODE" ]; then
            STRING="$STRING $(find_audio_file "$RPTSOUNDS/node")"
            make_call "$f1"
        fi
        write_call
    done
    # Update the previous database with the current data
    update_previous_db
}
# Parse command-line options
while getopts "hail:vn:d:fv" OPTION; do
    case $OPTION in
        h) usage; exit 0 ;;
        a) ;;
        i) INCNODE=1 ;;
        n) node=$OPTARG ;;
        d) DESTDIR=$OPTARG ;;
        f) FORCE_RUN=1 ;;  # Force execution without confirmation
        v) VERBOSE=1 ;;
        ?) usage; exit 1 ;;
    esac
done
# Ensure necessary directories exist
ensure_directory_exists "$DESTDIR"
# Verify that the source file exists
if [ ! -f "$SRCDIR/astdb.txt" ]; then
    echo "$SRCDIR/astdb.txt not found. Please verify the location."
    exit 1
fi
# Start processing nodes
process_nodes
# Calculate total script execution time
end_time=$(date +%s%3N)
total_duration=$((end_time - start_time))
formatted_duration=$(format_total_time $total_duration)
echo "Total script execution time: $formatted_duration"
1 Like

Thank You Mason, I will give this a try. Many thanks and 73, Steve WB4IZC

I run it every midnight via cron. The first time you run it, it will take forever. After that, it only updates the callsigns for nodes with new/updated info. It uses astdb.txt, so you’ll need to read this: Other Software Products - AllStarLink Manual

Wanted to come and say Thanks...have been using this and meant to extend my gratitude.

1 Like

Hey Mason-- Does that script need to live in a particular directory when I execute it..?
The only thing I remember from using it on HamVOIP was my default root partition was insufficient so I had to gpart it bigger; doesn't look like it'll be a problem on ASL3...

LATER: Nevermind, all OK, though OMG, you're correct; that takes a long time to process all the callsigns (+/‐ 1hr on a RPi-4 2MB). Working great!

I found this script on a Web search. It seems the -n option is ignored. For example, I am passing -n 519321 to the script but it proceeds to process the entire file! Is this a bug?

Great script. I just got it working. The link for enabling the needed services was very helpful. Thanks and 73, N9KIW.

I was telling a friend about this script. He’s running a ClearNode with Hamvoip. Will this script work with Hamvoip or can it be modified to do so? Thanks.

HamVOIP (I think) comes with it’s own version, /usr/local/sbin/write_node_callsigns sh. If not bundled, than certainly there’s a reference to add it on the HamVOIP website. Sorry, it’s been years since I did it last. If memory serves, I had to expand the / (root) filesystem because it wasn’t big enough as created. YMMV.

HamVoIP has a built-in script to handle this.

/usr/local/sbin/write_node_callsigns.sh

The switches are similar to, but not identical to this script.

For example, there is no -f to force the script running, I.E. for automation purposes, because it doesn’t prompt for anything, so there is no need for it.

The bad thing about HamVoIP’s version, though, is it doesn’t do any comparing. So if a node’s call sign field has changed, it will skip that node if a file has already been generated for it, unless you specifically update just that node number, or all of them. So it doesn’t take long before some things become stale, even if you keep it updating regularly.

It is, however, much faster than the referenced script for processing all nodes, because it just sticks files together using cat, not SoX, and doesn’t do any kind of encoding or processing to the raw input or output files.

Speaking of that:

Since I make it a point to change the sound files on my ASL and HamVoIP nodes (I really hate Allison, the stock Asterisk voice), I also modified my local copy of this script to write out ulaw files instead of GSM, because the artifacts of GSM compression really bugs me. Like, seriously, I have an actual, visceral reaction to GSM artifacts. I wonder if I’m the only one.

The files I use are much faster than stock, so I’m not too much concerned about space, even with 43000 plus nodes.