#!/bin/bash # # -------------------------------- # VMODEM - Virtual Modem bootstrap # -------------------------------- # Original by Oliver Molini 2021 # http://www.steptail.com/guides:virtual_modem:script # # Billy Stoughton II for bug fixes and contributions # # Modified and partially rewritten by Snep (contact-snep@diskcat.com) 2025 # for the CGHMN project # # Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License # https://creativecommons.org/licenses/by-nc-sa/4.0/ # Move to directory of script cd "$(dirname "$0")" || exit # Script version vmodver=cghmn-1.0 # CONFIGURATION # ----------------------- # Variable: SERIAL_PORT # SERIAL_PORT specifies which local serial device to use. # For example, "ttyUSB0" will tell the script to use # to use /dev/ttyUSB0 for communication. # Common values: ttyUSB0 or ttyAMA0 # SERIAL_PORT="${SERIAL_PORT:-ttyUSB0}" # Variable: BAUD # BAUD will tell the script to open the serial port at # specified symbol rate. When connecting, make sure # your client computer uses the same BAUD than what # has been specified here. # Common BAUD rates: 9600, 19200, 38400, 57600 # # Default: # BAUD=57600 # BAUD="${BAUD:-57600}" # Variable: LOGIN_ENABLED # If set to "yes", the LOGIN command is enabled, # which allows remote users to log in to this # machine with their Unix username and password LOGIN_ENABLED="no" # Variable: PPP_NUMBER # If set to a number, when this number is called # by the computer on the serial line, a PPP daemon # will be spawned PPP_DIAL_NUMBER="1" # Variable: TERM # Tells the script and environment which type of terminal to emulate. # It is only useful to change this, if you're using a serial # terminal to connect to this script. If you're connecting form a ANSI # cabable machine such as DOS, you may want to use TERM="ansi" # TERM="vt100" # EXPORT SHELL VARS # ----------------- export SERIAL_PORT export BAUD export TERM # FUNCTIONS # --------- # #INITIALIZE SERIAL SETTINGS ttyinit () { exec 99>&- stty -F "/dev/${SERIAL_PORT}" "${BAUD}" stty -F "/dev/${SERIAL_PORT}" sane stty -F "/dev/${SERIAL_PORT}" raw stty -F "/dev/${SERIAL_PORT}" -echo -icrnl clocal exec 99<>"/dev/${SERIAL_PORT}" } # SEND MESSAGE ON SCREEN AND OVER SERIAL sendtty () { NEWLINE_STR_CONSOLE="\n" NEWLINE_STR_SERIAL="\x0d\x0a" if [ "${1}" = "-n" ]; then NEWLINE_STR_CONSOLE="" NEWLINE_STR_SERIAL="" shift fi echo -en "${1}${NEWLINE_STR_CONSOLE}"; echo -en "${1}${NEWLINE_STR_SERIAL}" > "/dev/${SERIAL_PORT}" } # Sends an AT status message or status code as reply sendstatus () { # Default to ERROR if not code given RESULT_CODE="${1:-ERROR}" if [ "${VERBOSITY:-verbose}" = "verbose" ]; then sendtty "${RESULT_CODE}"; elif [ "${VERBOSITY}" = "numeric" ]; then # Translate readable result codes into numeric ones if requested if [ "${RESULT_CODE}" == "OK" ]; then sendtty 0 elif [ "${RESULT_CODE}" == "CONNECT" ]; then sendtty 1 elif [ "${RESULT_CODE}" == "RING" ]; then sendtty 2 elif [ "${RESULT_CODE}" == "NO CARRIER" ]; then sendtty 3 elif [ "${RESULT_CODE}" == "ERROR" ]; then sendtty 4 elif [ "${RESULT_CODE}" == "CONNECT 1200" ]; then sendtty 5 elif [ "${RESULT_CODE}" == "NO DIALTONE" ]; then sendtty 6 elif [ "${RESULT_CODE}" == "BUSY" ]; then sendtty 7 elif [ "${RESULT_CODE}" == "NO ANSWER" ]; then sendtty 8 else echo "> Unspecified result code '${RESULT_CODE}'" >&2 sendtty "${RESULT_CODE}" fi fi } # Launch a script with getty exec_with_getty () { /sbin/getty -L "${SERIAL_PORT}" "${BAUD}" "${TERM}" -n -l "${1}" return $? } # Dials a number (calls a number) from an ATD* command dial () { AT_COMMAND="${1}" NUMBER="$( grep -Eo "[0-9]+" <<< "${AT_COMMAND}" )" # Ensure number is not blank if [ -z "${NUMBER}" ]; then echo "> Blank number dialed" >&2 sendstatus "ERROR" return 1 fi DIAL_SCRIPT="" # Show ringing status if verbose dialing is enabled if [ "${VERBOSE_DIALING:-yes}" = "yes" ]; then sendtty "RINGING" sleep 1 fi # Check if number is PPP dial number if [ "${NUMBER}" = "${PPP_DIAL_NUMBER}" ]; then if [ -f "${PWD}/ppp-cghmn.sh" ]; then DIAL_SCRIPT="${PWD}/ppp-cghmn.sh" elif [ -f "${PWD}/ppp.sh" ]; then DIAL_SCRIPT="${PWD}/ppp.sh" else echo "> No pppd handler script found in '${PWD}'" >&2 sendstatus "NO CARRIER" return fi else # If not a PPP dial number, find script with called number if [ -f "${PWD}/DIAL_${NUMBER}.sh" ]; then DIAL_SCRIPT="${PWD}/DIAL_${NUMBER}.sh" elif [ -f "${PWD}/${NUMBER}.sh" ]; then DIAL_SCRIPT="${PWD}/${NUMBER}.sh" else sendstatus "NO CARRIER" return fi fi # For compatibility, explicitly tell the terminal to default to CR/LF # when pressing enter, to avoid cases where the terminal just sends CR. echo -en "\x1b[20h" > "/dev/${SERIAL_PORT}" echo "> Executing dial script '${DIAL_SCRIPT}' for number '${NUMBER}'" >&2 # Show connect message if [ "${VERBOSITY}" = "numeric" ]; then sendtty "1" elif [ "${VERBOSE_DIALING:-yes}" = "yes" ]; then sendtty "CONNECT ${BAUD}" else sendtty "CONNECT" fi # Call script with getty exec_with_getty "${DIAL_SCRIPT}" echo "> Dial script returned with exit code ${?}" >&2 ttyinit sendstatus "NO CARRIER" } export -f sendtty export -f ttyinit export -f sendstatus # Open serial port for use. Allocate file descriptor # and treat the serial port as a file. ttyinit #exec 99<>"/dev/${SERIAL_PORT}" # Init message sendtty "" sendtty "VMODEM - Virtual Modem bootstrap for PPP link v${vmodver}" sendtty "Connection speed set to ${BAUD} baud" sendtty "" sendtty "TYPE HELP FOR COMMANDS" sendtty "READY." # Main script loop while true; do CHARHEX="$(head -c 1 "/dev/${SERIAL_PORT}" | xxd -p -)" CHAR="$(echo -e "\x${CHARHEX}")" # Echo recevied CHARacter to console echo -n "${CHAR}" # Echo recevied CHARacter back to serial if [ "${ECHO_SERIAL:-yes}" = "yes" ]; then echo -n "${CHAR}" > "/dev/${SERIAL_PORT}" fi # Check for end of line on CR or LF if [ "$CHARHEX" = "0d" ] || [ "$CHARHEX" = "0a" ]; then # Upper-case entire line cmd="${BUFFER^^}" # Clear temporary BUFFERs BUFFER= CHAR= # Write new line to console and serial if [ "${ECHO_SERIAL:-yes}" = "yes" ]; then sendtty "" else echo "" fi # # --- HAYES EMULATION --- # if [[ $cmd == AT* ]]; then # Handle known AT commands case "$cmd" in # Commands that do nothing but return OK # shall also return OK # ATA = Answer incoming call # ATH0 = Modem Hang up (Go On-Hook) # ATH1 = Modem Pick up (Go Off-Hook) # ATM0 = Modem speaker always off # ATM1 = Modem speaker on until carrier detected # ATM2 = Modem speaker always on # ATM3 = Modem speaker only on whilst answering # AT&C0 = Force Carrier-Detect high # AT&C1 = Let Modem set Carrier-Detect signal # AT&D0 = Modem ignores DTR line from computer # AT&D1 = Modem switches to AT mode on DTR on->off transition # AT&D1 = Modem hangs up and switches to AT mode on DTR on->off transition # AT&D3 = Modem resets itself on DTR on->off transition # AT&S0 = Force DSR line high # AT&S1 = Modem will set DSR high during connected state # ATSn = Change current register to n # ATSn=i = Change current register to n and store value i ""|AT|ATA|ATH*|ATM*|AT\&C*|AT\&S*|ATS*) ;; # ATZ = Restore modem # ATZn = Restore modem to profile n # AT&F = Restore modem to factory settings # AT&Fn = Restore modem to factory settings in profile n ATZ*|AT\&F*) ECHO_SERIAL="yes" VERBOSITY="verbose" ;; # ATE0 = Disable echo # ATE1 = Enable echo ATE|ATE0) ECHO_SERIAL="no" ;; ATE1) ECHO_SERIAL="yes" ;; # ATV0 = Disable verbose responses # ATV1 = Enable verbose responses ATV|ATV0) VERBOSITY="numeric" ;; ATV1) VERBOSITY="verbose" ;; # ATQ0 = Modem returns result codes # ATQ1 = Modem is quiet, returns no result codes ATQ|ATQ0) VERBOSITY="verbose" ;; ATQ1) VERBOSITY="quiet" ;; # ATX0 = Blind dial, no busy detection, CONNECT when connection established (Hayes Smartmodem 300 compatible result code) # ATX1 = Blind dial, no busy detection, CONNECT with BAUD rate appended when connection established # ATX2 = Dial tone detection, no busy detection, CONNECT with BAUD rate appened when connection established # ATX3 = Blind dial, busy detection, CONNECT with BAUD rate appended when connection established # ATX4 = Dial tone detection, busy detection, CONNECT with BAUD rate appended when connection established ATX|ATX0) VERBOSE_DIALING="no" ;; ATX1|ATX2|ATX3|ATX4) VERBOSE_DIALING="yes" ;; # ATDn = Dial number ATD*) dial "${cmd}" continue ;; # Return ERROR on all other (unknown) commands *) sendstatus "ERROR" continue ;; esac # Send OK if no other status code was returned earlier sendstatus "OK" elif [ "${cmd}" = "HELP" ]; then sendtty "Command Reference for Virtual Modem Bootstrap v$vmodver" sendtty "" sendtty "AT......Tests modem link, prints OK if successful" sendtty "ATE0....Switch terminal echo off" sendtty "ATE1....Switch terminal echo on" sendtty "ATD?....Fork program ?.sh and output on terminal" sendtty "ATDT1...Open PPPD connection" sendtty "ATZ.....Reset modem settings" sendtty "HELP....Display command reference" [ "${LOGIN_ENABLED}" = "yes" ] && \ sendtty "LOGIN...Fork a new linux login on serial" sendtty "EXIT....End this script" sendtty "" sendtty "To establish connection over PPP, dial 1 using tone dialing (ATDT1)" sendtty "" sendtty "READY." elif [ "${cmd}" = "LOGIN" ]; then if [ "${LOGIN_ENABLED:-no}" = "yes" ]; then # Spawn login command on serial if enabled above exec 99>&- /sbin/getty -L "${SERIAL_PORT}" "${BAUD}" "${TERM}" # Reset tty after logout ttyinit sendtty "" sendtty "READY." else sendtty "LOGIN is not enabled on this connection." sendstatus "ERROR" fi elif [ "${cmd}" = "EXIT" ] || [ "${cmd}" = "QUIT" ]; then # Exit script # If managed by an init-system, this script *should* restart to serve the next connection sendstatus "OK" sendtty "BYE" # Close serial port exec 99>&- exit 0 fi else # If not CR or LF, append CHARacter to BUFFER BUFFER="${BUFFER}${CHAR}" fi done # Close serial port exec 99>&-