Adds support for Huawei NCM modems, which expect ndisdup commands at a separate cdc-wdm device, supported by kmod-usb-net-huawei-cdc-ncm. This builds on the work of Oskari Rauta at https://sites.google.com/site/variousopenwrt/huawei-e3267 . In particular, i added a lot of resilience. It has provided me with a stable connection on Huawei E303 and E398 modems during the last few months.
Signed-off-by: Maarten Deprez <[email protected]> diff --git a/net/huawei-ncm/Makefile b/net/huawei-ncm/Makefile new file mode 100644 index 0000000..27a0bcc --- /dev/null +++ b/net/huawei-ncm/Makefile @@ -0,0 +1,52 @@ +# +# Copyright (C) 2015 Maarten Deprez <[email protected] +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=huawei-ncm +PKG_VERSION:=1 +PKG_RELEASE:=0 +PKG_LICENSE:=GPLv2 + +PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) + +include $(INCLUDE_DIR)/package.mk + +define Package/huawei-ncm + SECTION:=net + CATEGORY:=Network + DEPENDS:=+kmod-usb-net-huawei-cdc-ncm + TITLE:=Huawei NCM protocol + PKGARCH:=all + MAINTAINER:=Maarten Deprez <[email protected]> +endef + +define Package/huawei-ncm/description + Protocol scripts to make and maintain connections to 3G/4G networks over huawei NCM modems +endef + +define Build/Prepare +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/huawei-ncm/install + $(INSTALL_DIR) $(1)/lib/netifd/proto + $(INSTALL_BIN) ./files/lib/netifd/proto/huawei_ncm.sh $(1)/lib/netifd/proto/huawei_ncm.sh + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/* $(1)/usr/bin/ + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/* $(1)/usr/sbin/ + $(INSTALL_DIR) $(1)/usr/lib/huawei-ncm + $(INSTALL_DATA) ./files/usr/lib/huawei-ncm/*.sh $(1)/usr/lib/huawei-ncm/ +endef + +$(eval $(call BuildPackage,huawei-ncm)) diff --git a/net/huawei-ncm/files/lib/netifd/proto/huawei_ncm.sh b/net/huawei-ncm/files/lib/netifd/proto/huawei_ncm.sh new file mode 100755 index 0000000..a75880c --- /dev/null +++ b/net/huawei-ncm/files/lib/netifd/proto/huawei_ncm.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +. /lib/functions.sh +. ../netifd-proto.sh +init_proto "$@" + +proto_huawei_ncm_init_config() { + renew_handler=1 + + proto_config_add_string 'pin' + proto_config_add_string 'apn' + proto_config_add_string 'mode' + proto_config_add_string 'freq' + proto_config_add_string 'timeout' + proto_config_add_string 'interval' +} + +proto_huawei_ncm_setup() { + local config="$1" + local iface="$2" + + local pin apn mode freq timeout interval + json_get_vars pin apn mode freq timeout interval + + proto_export "INTERFACE=$config" + proto_run_command "$config" /usr/sbin/huawei-ncm-connect -vvv \ + -p /var/run/huawei-ncm-$iface.pid \ + -s /lib/netifd/dhcp.script \ + -m "${mode:-2,0}" \ + -f "${freq:-0}" \ + -n "$pin" \ + -t "${timeout:-15}" \ + -i "${interval:-60}" \ + "$iface" \ + "$apn" + +} + +proto_huawei_ncm_renew() { + local config="$1" + proto_kill_command "$config" SIGHUP +} + +proto_huawei_ncm_teardown() { + local config="$1" + proto_kill_command "$config" +} + +add_protocol huawei_ncm diff --git a/net/huawei-ncm/files/usr/bin/huawei-ncm-cells b/net/huawei-ncm/files/usr/bin/huawei-ncm-cells new file mode 100755 index 0000000..9443a96 --- /dev/null +++ b/net/huawei-ncm/files/usr/bin/huawei-ncm-cells @@ -0,0 +1,33 @@ +#!/bin/sh + +VERBOSITY=1 +IFACE="$1" + + +### includes + +. /usr/lib/huawei-ncm/error.sh +. /usr/lib/huawei-ncm/find-modem.sh +. /usr/lib/huawei-ncm/modem.sh + + +### find and init modem + +find_modem +MODEM="$PCUI" +init_modem + +m=$(modem '^CELLINFO=?') || exit $? + +for i in $(echo "$m" | sed -e 's/[()]//g' -e 's/,/\n/g'); do + + n="$(modem "^CELLINFO=$i" '^[0-9]\+,[0-9]\+,-[0-9]\+,-[0-9]\+$')" || exit $? + + [ -n "$n" ] || continue + + printf '%s [%d dBm / %d dBm]\n' \ + "$(echo "$n" | cut -d, -f1)" \ + "$(echo "$n" | cut -d, -f3)" \ + "$(echo "$n" | cut -d, -f4)" + +done diff --git a/net/huawei-ncm/files/usr/bin/huawei-ncm-info b/net/huawei-ncm/files/usr/bin/huawei-ncm-info new file mode 100755 index 0000000..1b358cd --- /dev/null +++ b/net/huawei-ncm/files/usr/bin/huawei-ncm-info @@ -0,0 +1,116 @@ +#!/bin/sh + +VERBOSITY=1 +IFACE="$1" + + +### includes + +. /usr/lib/huawei-ncm/error.sh +. /usr/lib/huawei-ncm/find-modem.sh +. /usr/lib/huawei-ncm/modem.sh + + +### find and init modem + +find_modem +init_modem "$PCUI" + + +### product information + +modem I . | +while read i; do + + case "$i" in + Manufacturer:*) + echo -n "vendor ";; + Model:*) + echo -n "model ";; + Revision:*) + echo -n "firmware ";; + IMEI:*) + echo -n "imei ";; + *) continue;; + esac + + echo "$i" | sed 's/^[^:]*: //' + +done + + +### network + +m="$(modem '+COPS?')" || exit $? +echo -n "provider " +echo "$m" | cut -d, -f3 | sed 's/"//g' + + +### mode + +m="$(modem '^SYSCFG?')" || exit $? +echo -n "mode " + +case "$(echo "$m" | cut -d, -f1,2)" in + 2,0) echo "Auto";; + 2,1) echo "GSM, WCDMA";; + 2,2) echo "WCDMA, GSM";; + 13,*) echo "GSM";; + 14,*) echo "WCDMA";; + *) echo "Unknown";; +esac + + +### data bandwidth + +m="$(modem '+CGEQNEG=1')" || exit $? +printf "uplink %s kbps\n" "$(echo "$m" | cut -d, -f3)" +printf "downlink %s kbps\n" "$(echo "$m" | cut -d, -f4)" + + +### frequency lock + +m="$(modem '^FREQLOCK?')" || exit $? +echo -n "freqlock " +echo "$m" | cut -d, -f2 + + +### cell + +modem '+CREG=2' +m="$(modem '+CREG?')" || exit $? +i="$(echo "$m" | cut -d, -f3 | sed 's/"//g')" +[ -n "$i" ] && printf "lac 0x%x (%d)\n" $((0x$i)) $((0x$i)) +i="$(echo "$m" | cut -d, -f4 | sed 's/"//g')" +[ -n "$i" ] && printf "ci 0x%x (%d)\n" $((0x$i)) $((0x$i)) + + +### cell info + +if m="$(modem '^CELLINFO=0' '^[0-9]\+,[0-9]\+,-[0-9]\+,-[0-9]\+$')"; then + echo -n "freq " + echo "$m" | cut -d, -f1 +fi + + +### network technology + +#m="$(modem '^SYSINFOEX')" || exit $? +#printf "network %s / %s\n" \ +# "$(echo "$m" | cut -d, -f7 | sed 's/"//g')" \ +# "$(echo "$m" | cut -d, -f9 | sed 's/"//g')" + + +### reception: csq / rssi + +m="$(modem '+CSQ')" || exit $? +i="$(echo "$m" | cut -d, -f1)" +[ -n "$i" ] && printf "signal %d%% (%d / 31)\n" $((($i * 100 + 16) / 31)) $i +[ -n "$i" ] && printf "rssi %d dBm\n" $(($i * 2 - 113)) + + +### reception: rcsp / ecio + +m="$(modem '^CSNR?')" || exit $? +printf "rcsp %d dBm\n" "$(echo "$m" | cut -d, -f1)" +printf "ecio %d dBm\n" "$(echo "$m" | cut -d, -f2)" diff --git a/net/huawei-ncm/files/usr/lib/huawei-ncm/error.sh b/net/huawei-ncm/files/usr/lib/huawei-ncm/error.sh new file mode 100644 index 0000000..9affe46 --- /dev/null +++ b/net/huawei-ncm/files/usr/lib/huawei-ncm/error.sh @@ -0,0 +1,14 @@ +### error function + +VERBOSITY="${VERBOSITY:-1}" + +error () { + [ $VERBOSITY -ge 1 ] && echo "$@" >&2 + exit 1 +} + +vecho () { + v=$1; shift + [ $VERBOSITY -ge $v ] && + echo "$@" >&2 +} diff --git a/net/huawei-ncm/files/usr/lib/huawei-ncm/find-modem.sh b/net/huawei-ncm/files/usr/lib/huawei-ncm/find-modem.sh new file mode 100644 index 0000000..b06df97 --- /dev/null +++ b/net/huawei-ncm/files/usr/lib/huawei-ncm/find-modem.sh @@ -0,0 +1,223 @@ +#include error.sh + +### load module with checking + +# load_module (module, [timeout]) + +load_module () { + + vecho 2 -n "Loading $1 module... " + + local t=0 + + while ! [ -d "/sys/module/$1" ]; do + + if ! [ $((t++)) -lt ${2:-3} ]; then + vecho 2 "timeout!" || vecho 1 "timeout loading $1 module" + return 1 + fi + + if ! modprobe "$1"; then + vecho 2 "failed!" || vecho 1 "unable to load $1 module" + return 1 + fi + + sleep 1 + + done + + vecho 2 "ok!" + return 0 + +} + + +### find huawei modem device + +# is_huawei_modem (iface) +is_huawei_modem () { + [ "$(basename "$(readlink "/sys/class/net/$1/device/driver")")" = huawei_cdc_ncm ] +} + +# find_modem_iface ([iface], [timeout]) +find_modem_iface () { + + vecho 2 -n "Looking for huawei-cdc-ncm modem... " + + local i t=0 + + while true; do + + for i in $([ -n "$1" ] && echo "$1" || ls /sys/class/net); do + is_huawei_modem "$i" || continue + IFACE="$i"; break 2 + done + + if ! [ $((t++)) -lt ${2:-5} ]; then + vecho 2 "failed" || vecho 1 "no huawei-cdc-ncm modem found" + return 1 + fi + + sleep 1 + + done + + usbpath="$(readlink -f "/sys/class/net/$IFACE/device")" + + vecho 2 "ok!" + return 0 + +} + + +### find interfaces +# use AT^SETPORT="FF;1,16,3,2" and restart +# to enable (only) necessary interfaces + +check_interfaces () { + [ -c "$MODEM" -a -c "$PCUI" -a -c "$WDM" ] +} + +find_interfaces () { + + vecho 2 -n "Enumerating interfaces on $IFACE... " + + local i + + unset MODEM PCUI DIAG WDM + + for i in /sys/class/net/"$IFACE"/device/../*:*; do + case $(cat "$i"/bInterfaceProtocol) in + 01) MODEM="/dev/$(basename "$i"/tty*)";; + 02) PCUI="/dev/$(basename "$i"/tty*)";; + 03) DIAG="/dev/$(basename "$i"/tty*)";; + 16) WDM="/dev/$(basename "$i/usbmisc"/*)";; + esac + done + + + # check if necessary interfaces are present + + if ! check_interfaces; then + vecho 1 "missing interfaces from $IFACE:" \ + "$( [ -c "$MODEM" ] || echo modem )" \ + "$( [ -c "$PCUI" ] || echo pcui )" \ + "$( [ -c "$WDM" ] || echo wdm )" + return 1 + fi + + vecho 2 "ok!" + return 0 + +} + + +### usb connection path + +cumpath () { + + local p i + + read p + + while read i; do + p="$p/$i" + echo "$i" | grep -qx "$1" && + echo "$p" + done + +} + +usb_path () { + echo "$1" | sed -e 's@\(^.*/usb[0-9]\+\|[^/]\+\)/@\1\n@g' | + cumpath '[0-9.-]\+' +} + + +### reset last usb hub in path + +usbpath= + +reset_usb () { + + [ -x /usr/bin/usbreset ] || return 1 + [ -n "$usbpath" ] || return 1 + + local dev i + + vecho 2 -n "Trying to recover usb connectivity... " + + usb_path "$usbpath" | tac | + + while read i; do + + vecho 3 -n "($(basename "$i")) " + + [ -d "$i" ] || continue + [ -f "$i/busnum" -a -f "$i/devnum" ] || continue + + dev=$(printf '%03d/%03d\n' $(cat "$i/busnum") $(cat "$i/devnum")) + vecho 3 -n "--> [$dev] " + + usbreset $dev || break + + vecho 2 "ok!" + return 0 + + done + + vecho 2 "failed!" + return 1 + +} + + +### reset modem device + +reset_modem () { + + [ -c "$MODEM" ] || return 1 + + vecho 2 -n "Resetting modem $IFACE... " + + echo -en "AT+CFUN=4\r\n" > "$MODEM" + sleep 1 + echo -en "AT+CFUN=6,0\r\n" > "$MODEM" + + vecho 2 "ok!" + return 0 + +} + + +### find modem + +iface="$IFACE" + +find_modem () { + find_modem_iface "$iface" "${1:-10}" && + find_interfaces +} + + +### find modem with resilience + +find_modem_hard () { + + local t=0 + + load_module huawei_cdc_ncm || return 1 + + while true; do + + find_modem ${1:-30} && return 0 + + [ $((t++)) -lt 2 ] || return 1 + + reset_modem || + #reset_usb || + return 1 + + done + +} diff --git a/net/huawei-ncm/files/usr/lib/huawei-ncm/modem.sh b/net/huawei-ncm/files/usr/lib/huawei-ncm/modem.sh new file mode 100644 index 0000000..0cba57c --- /dev/null +++ b/net/huawei-ncm/files/usr/lib/huawei-ncm/modem.sh @@ -0,0 +1,70 @@ +#include error.sh + +### open modem device + +modem= + +init_modem () { + local m="${1:-$MODEM}" + [ -c "$m" ] || error "modem $m does not exist" + [ $VERBOSITY -ge 4 ] && echo "Opening modem $m" >&2 + exec 5<> "$m" || error "failed to open modem: $m" + echo -en 'ATE0\r\n' >&5 + while read -rt1 -u5 i; do true; done + modem="$m" +} + + +### send command and read response + +modem() { + + [ -n "$modem" ] || error "modem not initialized" + + echo -en "AT$1\r\n" >&5 + + if [ -n "$2" ]; then + match="$2" + select= + else + match="^$(echo "$1" | grep -o "^\W\w\+"):" + select='s/^.*: *//' + fi + + err=timeout; t0=$(($(date +%s)+10)) + while [ $((t=t0-$(date +%s))) -gt 0 ] && read -r -t$t -u5 i; do + + i="$(echo "$i" | sed 's/\r$//')" + + vecho 4 "modem: $1: $i" >&2 + + #echo "$i" | grep -q "^AT" && continue + + if echo "$i" | grep -q '^OK'; then + err=; break + fi + + if echo "$i" | grep -q '^ERROR'; then + err="command error"; break + fi + + if echo "$i" | grep -q '^CME ERROR:'; then + err="cme error ${i#*:}"; break + fi + + if echo "$i" | grep -q "$match"; then + echo "$i" | sed "$select" + continue + fi + + n=$((n+1)) + + done + + if [ -n "$err" ]; then + vecho 1 "modem error: $1: $err" >&2 + return 1 + fi + + return 0 +} diff --git a/net/huawei-ncm/files/usr/sbin/huawei-ncm-connect b/net/huawei-ncm/files/usr/sbin/huawei-ncm-connect new file mode 100755 index 0000000..90f8152 --- /dev/null +++ b/net/huawei-ncm/files/usr/sbin/huawei-ncm-connect @@ -0,0 +1,367 @@ +#!/bin/sh + + +### error handling and logging + +. /usr/lib/huawei-ncm/error.sh + + +### interrupt handlers + +int= +run=1 +renew= +connection=0 + +sleep= + +wsleep () { + sleep "$@" & sleep=$! + wait $sleep; sleep= + [ -n "$run" ] +} + +trap 'run=; [ -n "$sleep" ] && kill $sleep; vecho 2 "interrupt!"' TERM +trap 'renew=1; [ -n "$sleep" ] && kill $sleep; vecho 2 "renew!"' HUP +trap 'kill -TERM $$; int=1' INT +trap '[ -n "$PID" ] && rm -f "$PID"' EXIT + + +### check arguments + +PID= +PIN= +MODE= +FREQ= +SCRIPT=/lib/netifd/dhcp.script +TIMEOUT=15 +INTERVAL=60 +VERBOSITY=1 + +usage () { + + if [ $VERBOSITY -ge 1 ]; then + echo "Usage: $0 [ options ] <interface> <apn>" >&2 + echo "Options:" >&2 + echo " -p <pidfile> save PID in <pidfile>" >&2 + echo " -m <mode> set modem to network <mode>" >&2 + echo " -f <freq> lock frequency to <freq>" >&2 + echo " -n <pin> pin code of the card" >&2 + echo " -s <script> use <script> as DHCP script" >&2 + echo " -t <timeout> time to wait for network and data connection" >&2 + echo " -i <interval> connection checking interval" >&2 + fi + + exit 2 +} + +while getopts ":p:m:f:n:s:t:i:qvh" i; do + case $i in + p) PID="$OPTARG";; + m) MODE="$OPTARG";; + f) FREQ="$OPTARG";; + n) PIN="$OPTARG";; + s) SCRIPT="$OPTARG";; + t) TIMEOUT="$OPTARG";; + i) INTERVAL="$OPTARG";; + q) VERBOSITY=$((VERBOSITY-1));; + v) VERBOSITY=$((VERBOSITY+1));; + h) usage;; + ?) vecho 1 "Invalid option: -$OPTARG"; usage;; + :) vecho 1 "Option -$OPTARG needs an argument"; usage;; + esac +done + +shift $(($OPTIND-1)) + +if [ $# -ne 2 ]; then + vecho 1 "Expecting two arguments" + usage +fi + +IFACE="$1" +APN="$2" + + +### save pid + +if [ -n "$PID" ]; then + echo $$ > "$PID" +fi + + +### include modem functions + +. /usr/lib/huawei-ncm/find-modem.sh +. /usr/lib/huawei-ncm/modem.sh + + +### check pin state + +check_pin () { + + vecho 2 -n "Checking PIN... " + + m="$(modem '^CPIN?')" || exit $? + pin="$(echo "$m" | cut -d, -f1)" + times="$(echo "$m" | cut -d, -f2)" + + case "$pin" in + + READY) + vecho 2 "ok!" + ;; + + SIM\ PIN) + + ### don't try to unlock in face of failed tries + if [ "$times" != 3 ]; then + vecho 1 "SIM card locked with non-virgin 'times' record; won't try to unlock" + exit 1 + fi + + ### check if PIN was given + if [ -z "$PIN" ]; then + vecho 1 "SIM card locked but no PIN specified" + exit 1 + fi + + + ### try to unlock + + modem "^CPIN=\"$PIN\"" || exit $? + + ;; + + *) + vecho 1 "SIM card locked with $pin code ($m)" + exit 1 + ;; + esac +} + +### set mode if requested + +set_mode () { + + [ -z "$MODE" ] && return + + vecho 2 -n "Setting mode... " + + ### default: no change + mode=16,3 + + ### calculate mode + + case "$MODE" in + gsm) mode=13,0 ;; + wcdma) mode=14,0 ;; + gsmfirst) mode=2,1 ;; + wcdmafirst) mode=2,2 ;; + auto) mode=2,0 ;; + esac + + ### set mode + modem "^SYSCFG=$mode,3fffffff,2,4" || exit $? + + vecho 2 "ok!" +} + + +### lock to frequency if requested + +lock_frequency () { + + [ -n "$FREQ" ] || return 0 + + vecho 2 -n "Locking frequency... " + + if [ "$FREQ" = "0" ]; then + modem "^FREQLOCK=0" || exit $? + else + modem "^FREQLOCK=1,$FREQ" || exit $? + fi + + vecho 2 "ok!" + return 0 + +} + + +### wait for network registration + +register () { + + vecho 2 -n "Waiting for network... " + + for i in $(seq "$TIMEOUT"); do + stat="$(modem '+CREG?' | cut -d, -f2)" || exit $? + [ "$stat" == 1 ] && break + wsleep 1 || return 1 + vecho 2 -en "\b. " + done + + if [ "$stat" != 1 ]; then + vecho 2 "timeout" || vecho 1 "timeout while waiting for network" + return 1 + fi + + vecho 2 "ok!" + return 0 +} + + +### check +CGPADDR response +is_connected () { + [ -n "$1" -a "$1" != '"0.0.0.0"' ] +} + + +### establish data connection + +connect () { + + vecho 2 -n "Establishing data connection... " + + echo -en "AT^NDISDUP=1,1,\"$APN\"\r\n" > "$WDM" + connection=1 + + for i in $(seq "$TIMEOUT"); do + ip="$(modem '+CGPADDR=1' | cut -d, -f2)" || exit $? + is_connected "$ip" && break + wsleep 1 || return 1 + vecho 2 -en "\b. " + done + + if ! is_connected "$ip"; then + vecho 2 "timeout" || vecho 1 "timeout waiting for data connection" + return 1 + fi + + vecho 2 "ok" + return 0 + +} + + +### request address + +dhcp () { + + vecho 2 -n "Running udhcpc... " + + ifconfig "$IFACE" up && + udhcpc -i "$IFACE" -s "$SCRIPT" -f -t 0 -q + + [ -n "$run" ] || return 1 + + if [ $? -gt 0 ]; then + vecho 1 "failed to get ip address" + return 1 + fi + + connection=2 + vecho 2 "ok" + return 0 + +} + +### take a break + +nap () { + + ### sleep... + + vecho 3 -n "Taking a nap... " + + wsleep "$INTERVAL" || return 1 + [ -z "$renew" ] || return 1 + + vecho 3 "wake up" + return 0 + +} + + +### check connection +check () { + + [ $connection -ne 2 ] && return 1 + + vecho 3 -n "Checking connection... " + + ip="$(modem '+CGPADDR=1' | cut -d, -f2)" || exit $? + + if ! is_connected "$ip"; then + connection=0 + vecho 2 "lost connection" + return 1 + fi + + vecho 3 "ok!" + return 0 + +} + +### disconnect + +disconnect () { + + [ $connection -ge 1 ] || return + + vecho 2 -n "Disconnecting... " + + echo -en "AT^NDISDUP=1,0\r\n" > "$WDM" + + for i in $(seq "$TIMEOUT"); do + ip="$(modem '+CGPADDR=1' | cut -d, -f2)" || exit $? + [ -z "$ip" ] && break + wsleep 1 + vecho 2 -en "\b. " + done + + if [ -n "$ip" ]; then + vecho 2 "timeout" || vecho 1 "timeout waiting for disconnection" + return 1 + fi + + vecho 1 "disconnected" + connection=0 + return 0 + +} + + +### connection loop + +while [ -n "$run" ]; do + + if ! find_modem_hard; then + vecho 1 "Couldn't find modem; sleeping 10 minutes" + wsleep 600 + continue + fi + + init_modem + + check_pin + set_mode + lock_frequency + + register && + connect && + dhcp + + renew= + while [ -n "$run" ]; do + nap && check || break + done + + check_interfaces && + disconnect + +done + +if [ -n "$int" ]; then + kill -INT -$$ +fi _______________________________________________ openwrt-devel mailing list [email protected] https://lists.openwrt.org/cgi-bin/mailman/listinfo/openwrt-devel
