]> git.ipfire.org Git - people/ms/network.git/blob - src/functions/functions.modem
modem: Support LTE as connection type
[people/ms/network.git] / src / functions / functions.modem
1 #!/bin/bash
2 ###############################################################################
3 # #
4 # IPFire.org - A linux based firewall #
5 # Copyright (C) 2012 IPFire Network Development Team #
6 # #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
11 # #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
16 # #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
19 # #
20 ###############################################################################
21
22 # Exit codes from the chat(8) command:
23 CHAT_OK=0
24 CHAT_INVALID=1
25 CHAT_ERROR=2
26 CHAT_TIMEOUT=3
27
28 modem_chat() {
29 local answer="OK"
30 local device
31 local timeout=2
32 local quiet="false"
33
34 while [ $# -gt 0 ]; do
35 case "${1}" in
36 --timeout=*)
37 timeout=$(cli_get_val ${1})
38 ;;
39 --answer=*)
40 answer=$(cli_get_val ${1})
41 ;;
42 --quiet)
43 quiet="true"
44 ;;
45 *)
46 device=${1}
47 shift; break
48 ;;
49 esac
50 shift
51 done
52
53 assert serial_exists ${device}
54 assert serial_is_unlocked ${device}
55 assert isset answer
56 assert isset timeout
57
58 local command=$@
59
60 log DEBUG "Sending command to ${device}: ${command}"
61
62 (
63 # This cannot be run with -x.
64 set +x 2>/dev/null
65
66 chat -V -s -t ${timeout} "" "${command}" "${answer}" \
67 < ${device} > ${device} || exit $?
68 print # Print line feed.
69 ) 2>&1 | __modem_chat_process_output "${answer}" ${quiet}
70
71 local ret=${PIPESTATUS[0]}
72
73 # Return the exit code of the chat command.
74 case "${ret}" in
75 ${CHAT_OK})
76 return ${EXIT_OK}
77 ;;
78
79 # When the timeout condition hit, the expected string has not
80 # been received in the given amount of time.
81 ${CHAT_TIMEOUT}|${CHAT_ERROR})
82 return ${EXIT_ERROR}
83 ;;
84
85 ${CHAT_INVALID})
86 return ${EXIT_CONF_ERROR}
87 ;;
88 esac
89
90 log WARNING "Received unknown exit code from chat(8): ${ret}"
91 return ${EXIT_ERROR}
92 }
93
94 __modem_chat_process_output() {
95 local answer=${1}
96 local quiet=${2}
97
98 if enabled quiet; then
99 # Just throw everything away.
100 cat >/dev/null
101 return ${EXIT_OK}
102 fi
103
104 local counter=0
105
106 local line
107 while read -r line; do
108 # Also skip empty lines.
109 [ -n "${line}" ] || continue
110
111 # Ignore all volatile messages.
112 [ "${line:0:1}" = "^" ] && continue
113
114 log DEBUG "Output[${counter}]: ${line}"
115 counter=$(( ${counter} + 1 ))
116
117 # Skip the first line, because that's out command.
118 [ ${counter} -eq 1 ] && continue
119
120 # Filter out the expected answer.
121 [ "${line}" = "${answer}" ] && continue
122
123 # Print the rest.
124 print "${line}"
125 done
126 }
127
128 modem_initialize() {
129 local device="${1}"
130 assert isset device
131 shift
132
133 # Sleep for $sleep seconds, to give
134 # the modem a moment to initialize itself.
135 local sleep=1
136
137 while [ $# -gt 0 ]; do
138 case "${1}" in
139 --sleep=*)
140 sleep="$(cli_get_val "${1}")"
141 ;;
142 *)
143 warning "Unrecognized argument: ${1}"
144 ;;
145 esac
146 shift
147 done
148 assert isinteger sleep
149
150 log INFO "Initializing modem ${device}"
151
152 # Reset the modem.
153 modem_chat "${device}" "${AT_INITIALIZE}"
154
155 # Wait...
156 if [ ${sleep} -gt 0 ]; then
157 sleep ${sleep}
158 fi
159 }
160
161 # Exit codes of the sim_status function.
162 EXIT_SIM_READY=0
163 EXIT_SIM_PIN=1
164 EXIT_SIM_PUK=2
165 EXIT_SIM_UNKNOWN=3
166
167 modem_sim_status() {
168 local device=${1}
169 assert isset device
170
171 local output
172 output=$(modem_chat ${device} "AT+CPIN?")
173 assert_check_retval $?
174
175 # Strip leading +CPIN: from the output.
176 output=${output#*: }
177
178 case "${output}" in
179 "READY")
180 log DEBUG "${device}'s SIM is unlocked or doesn't need a PIN."
181 return ${EXIT_SIM_READY}
182 ;;
183 "SIM PIN")
184 log DEBUG "${device}'s SIM is waiting for a PIN."
185 return ${EXIT_SIM_PIN}
186 ;;
187 "SIM PUK")
188 log DEBUG "${device}'s SIM is PUK locked."
189 return ${EXIT_SIM_PUK}
190 ;;
191 esac
192
193 log WARNING "${device}: Invalid output of the AT+CPIN? command."
194 return ${EXIT_SIM_UNKNOWN}
195 }
196
197 modem_sim_unlocked() {
198 local device=${1}
199 assert isset device
200
201 modem_sim_status "${device}"
202 local ret=$?
203
204 [ ${ret} -eq ${EXIT_SIM_READY} ] && return ${EXIT_TRUE} || return ${EXIT_FALSE}
205 }
206
207 modem_sim_locked() {
208 modem_sim_unlocked $@ && return ${EXIT_FALSE} || return ${EXIT_TRUE}
209 }
210
211 modem_sim_unlock() {
212 local device=${1}
213 assert isset device
214
215 local pin=${2}
216 assert isset pin
217
218 local command="AT+CPIN=${pin}"
219
220 local new_pin=${3}
221 if isset new_pin; then
222 command="${command},${new_pin}"
223 fi
224
225 modem_chat --timeout=2 --quiet "${device}" "${command}"
226
227 local ret=$?
228 case "${ret}" in
229 ${EXIT_OK})
230 log INFO "Successfully unlocked SIM card on ${device}."
231 ;;
232 *)
233 log ERROR "Could not unlock SIM card on ${device}."
234 ret=${EXIT_ERROR}
235 ;;
236 esac
237
238 return ${ret}
239 }
240
241 modem_sim_auto_unlock() {
242 local device="${1}"
243 assert isset device
244
245 local pin="${2}"
246 assert isset pin
247
248 # Get the current state the SIM card is in.
249 modem_sim_status "${device}" &>/dev/null
250 local sim_status_code=$?
251
252 case "${sim_status_code}" in
253 ${EXIT_SIM_READY})
254 # Everything's fine. The SIM card is
255 # already unlocked.
256 return ${EXIT_OK}
257 ;;
258 ${EXIT_SIM_PIN})
259 # Try to unlock the device.
260 if modem_sim_unlock ${device} ${pin}; then
261 return ${EXIT_OK}
262 else
263 return ${EXIT_ERROR}
264 fi
265 ;;
266 ${EXIT_SIM_PUK})
267 log ERROR "SIM card is PUK locked. Please unlock manually."
268 return ${EXIT_ERROR}
269 ;;
270 esac
271
272 return ${EXIT_ERROR}
273 }
274
275 # Returns the vendor of the modem.
276 # For example: "huawei"
277 modem_get_manufacturer() {
278 local device=${1}
279 assert isset device
280
281 local output
282 output=$(modem_chat ${device} "AT+GMI")
283 assert_check_retval $?
284
285 [ "${output:0:1}" = "+" ] && output=${output#*: }
286 output=${output//\"/}
287
288 print "${output}"
289 }
290
291 modem_get_model() {
292 local device=${1}
293 assert isset device
294
295 local output
296 output=$(modem_chat ${device} "AT+GMM")
297 assert_check_retval $?
298
299 [ "${output:0:1}" = "+" ] && output=${output#*: }
300 output=${output//\"/}
301
302 print "${output}"
303 }
304
305 modem_get_software_version() {
306 local device=${1}
307 assert isset device
308
309 local output
310 output=$(modem_chat ${device} "AT+GMR")
311 assert_check_retval $?
312
313 [ "${output:0:1}" = "+" ] && output=${output#*: }
314 output=${output//\"/}
315
316 print "${output}"
317 }
318
319 modem_get_sim_imsi() {
320 local device=${1}
321 assert isset device
322
323 # Make sure the SIM card is unlocked for this operation.
324 assert modem_sim_unlocked ${device}
325
326 modem_chat ${device} "AT+CIMI"
327 }
328
329 modem_get_device_imei() {
330 local device=${1}
331 assert isset device
332
333 local output
334 output=$(modem_chat --timeout=1 ${device} "AT+CGSN") || assert_check_retval $?
335 local ret=$?
336
337 if [ ${ret} -eq ${EXIT_OK} ]; then
338 print "${output}"
339 fi
340
341 return ${ret}
342 }
343
344 modem_is_mobile() {
345 local device=${1}
346 assert isset device
347
348 # Check if the device can return it's IMEI.
349 # If not, it's probably a serial 56k modem or something
350 # in that category.
351
352 modem_get_device_imei ${device} &>/dev/null
353 }
354
355 # Exit codes of the network registration function.
356 EXIT_REG_REGISTERED_TO_HOME_NETWORK=0
357 EXIT_REG_NOT_REGISTERED_NOT_SEARCHING=1
358 EXIT_REG_NOT_REGISTERED_SEARCHING=2
359 EXIT_REG_REGISTRATION_DENIED=3
360 EXIT_REG_REGISTERED_ROAMING=4
361 EXIT_REG_UNKNOWN=5
362
363 modem_get_network_registration() {
364 local device=${1}
365 assert isset device
366
367 # Make sure the SIM card is unlocked for this operation.
368 assert modem_sim_unlocked ${device}
369
370 local output
371 output=$(modem_chat ${device} "AT+CREG?")
372 assert_check_retval $?
373
374 # Cut out unneeded parts of the message.
375 output=${output#*: }
376
377 case "${output}" in
378 [0-2],[0-5])
379 local status=${output%,*}
380
381 # The status variable must be zero.
382 [ ${status} -ne 0 ] && break
383
384 local stat=${output#*,}
385 case "${stat}" in
386 0)
387 return ${EXIT_REG_NOT_REGISTERED_NOT_SEARCHING}
388 ;;
389 1)
390 return ${EXIT_REG_REGISTERED_TO_HOME_NETWORK}
391 ;;
392 2)
393 return ${EXIT_REG_NOT_REGISTERED_SEARCHING}
394 ;;
395 3)
396 return ${EXIT_REG_REGISTRATION_DENIED}
397 ;;
398 5)
399 return ${EXIT_REG_REGISTERED_ROAMING}
400 ;;
401 *)
402 return ${EXIT_REG_UNKNOWN}
403 ;;
404 esac
405 ;;
406 esac
407
408 # Apparently the output of the CREG? command was not in
409 # the right format. The modem will be tried to be set to the
410 # right mode.
411
412 modem_set_network_registration ${device} 0
413 modem_get_network_registration ${device}
414 }
415
416 modem_set_network_registration() {
417 local device=${1}
418 assert isset device
419
420 # Make sure the SIM card is unlocked for this operation.
421 assert modem_sim_unlocked ${device}
422
423 local mode=${2}
424 assert isset mode
425
426 modem_chat ${device} "AT+CREG=${mode}"
427 }
428
429 modem_scan_networks() {
430 local device=${1}
431 assert isset device
432
433 # Make sure the SIM card is unlocked for this operation.
434 assert modem_sim_unlocked ${device}
435
436 local output
437 output=$(modem_chat --timeout=60 ${device} "AT+COPS=?")
438 assert_check_retval $?
439
440 output=${output#*: }
441
442 # XXX the output is not very nice to parse.
443 }
444
445 __modem_get_network_operator() {
446 local device=${1}
447 assert isset device
448
449 # Make sure the SIM card is unlocked for this operation.
450 assert modem_sim_unlocked ${device}
451
452 local argument=${2}
453 assert isset argument
454
455 local output
456 output=$(modem_chat ${device} "AT+COPS?")
457 assert_check_retval $?
458
459 output=${output#*: }
460 output=${output//,/ }
461
462 local arg mode format operator act
463 local i=0
464 while read -r arg; do
465 case "${i}" in
466 0)
467 mode="${arg}"
468 ;;
469 1)
470 format="${arg}"
471 ;;
472 2)
473 operator="$(strip ${arg})"
474 ;;
475 3)
476 act="${arg}"
477 ;;
478 *)
479 break
480 ;;
481 esac
482 i="$(( ${i} + 1 ))"
483 done <<< "$(args ${output})"
484
485 print "${!argument}"
486 return ${EXIT_OK}
487 }
488
489 modem_get_network_operator() {
490 local device=${1}
491 assert isset device
492
493 __modem_get_network_operator ${device} operator
494 }
495
496 # Exit codes of the network operator mode function.
497 EXIT_OPMODE_GSM=0
498 EXIT_OPMODE_COMPACTGSM=1
499 EXIT_OPMODE_UMTS=2
500 EXIT_OPMODE_GSM_WITH_EGPRS=3
501 EXIT_OPMODE_UMTS_WITH_HSDPA=4
502 EXIT_OPMODE_UMTS_WITH_HSUPA=5
503 EXIT_OPMODE_UMTS_WITH_HSDPA_AND_HSUPA=6
504 EXIT_OPMODE_LTE=7
505 EXIT_OPMODE_UNKNOWN=8
506
507 modem_get_network_mode() {
508 local device=${1}
509 assert isset device
510
511 local output
512 output=$(__modem_get_network_operator ${device} act)
513 assert_check_retval $?
514
515 case "${output}" in
516 0)
517 print "GSM"
518 return ${EXIT_OPMODE_GSM}
519 ;;
520 1)
521 print "Compact GSM"
522 return ${EXIT_OPMODE_COMPACTGSM}
523 ;;
524 2)
525 print "UMTS"
526 return ${EXIT_OPMODE_UMTS}
527 ;;
528 3)
529 print "EDGE (GSM+EGPRS)"
530 return ${EXIT_OPMODE_GSM_WITH_EGPRS}
531 ;;
532 4)
533 print "UMTS +HSDPA"
534 return ${EXIT_OPMODE_UMTS_WITH_HSDPA}
535 ;;
536 5)
537 print "UMTS +HSUPA"
538 return ${EXIT_OPMODE_UMTS_WITH_HSUPA}
539 ;;
540 6)
541 print "UMTS +HSDPA +HSUPA"
542 return ${EXIT_OPMODE_UMTS_WITH_HSDPA_AND_HSUPA}
543 ;;
544 7)
545 print "LTE"
546 return ${EXIT_OPMODE_LTE}
547 ;;
548 *)
549 print "Unknown"
550 return ${EXIT_OPMODE_UNKNOWN}
551 ;;
552 esac
553 }
554
555 __modem_get_signal_quality() {
556 local device=${1}
557 assert isset device
558
559 # Make sure the SIM card is unlocked for this operation.
560 assert modem_sim_unlocked ${device}
561
562 local argument=${2}
563 assert isset argument
564
565 local output
566 output=$(modem_chat ${device} "AT+CSQ")
567 assert_check_retval $?
568
569 output=${output#*: }
570
571 case "${output}" in
572 *,*)
573 local rssi=${output%,*}
574 local ber=${output#*,}
575
576 print "${!argument}"
577 return ${EXIT_OK}
578 ;;
579 *)
580 log ERROR "Unknown format for AT+CSQ: ${device}: ${output}"
581 ;;
582 esac
583
584 return ${EXIT_ERROR}
585 }
586
587 modem_get_signal_quality() {
588 local device=${1}
589 assert isset device
590
591 local rssi
592 rssi=$(__modem_get_signal_quality ${device} rssi)
593 assert_check_retval $?
594
595 isset rssi || return ${EXIT_ERROR}
596
597 # 99 indicates an unknown signal strength.
598 [ ${rssi} -eq 99 ] && return ${EXIT_UNKNOWN}
599
600 local dbm=$(( ${rssi} * 2 ))
601 dbm=$(( ${dbm} - 113 ))
602
603 print "%d" "${dbm}"
604 return ${EXIT_OK}
605 }
606
607 modem_get_bit_error_rate() {
608 local device=${1}
609 assert isset device
610
611 local ber
612 ber=$(__modem_get_signal_quality ${device} ber)
613 assert_check_retval $?
614
615 isset ber || return ${EXIT_ERROR}
616
617 # 99 indicates that the bit error rate could not be detected or
618 # is unknown for some other reason.
619 [ ${ber} -eq 99 ] && return ${EXIT_UNKNOWN}
620
621 print "%d" "${ber}"
622 return ${EXIT_OK}
623 }
624
625 # USDD stuff
626
627 modem_ussd_send_command() {
628 local device="${1}"
629 assert isset device
630
631 local command="${2}"
632 assert isset command
633 shift 2
634
635 local cleartext="false"
636 local timeout="20"
637
638 while [ $# -gt 0 ]; do
639 case "${1}" in
640 --cleartext)
641 cleartext="true"
642 ;;
643 --timeout=*)
644 timeout="$(cli_get_val "${1}")"
645 ;;
646 *)
647 warning "Unrecognized argument: ${1}"
648 ;;
649 esac
650 shift
651 done
652
653 local encoded_command="${command}"
654 if ! enabled cleartext; then
655 encoded_command="$(modem_ussd_encode "${command}")"
656 fi
657
658 log INFO "Sending USSD command '${command}' on ${device}"
659
660 local at_command="AT+CUSD=1,\"${encoded_command}\",${encoding}"
661
662 # Send the AT command and parse the output.
663 modem_chat --answer="nothing" --timeout="${timeout}" \
664 "${device}" "${at_command}" | __modem_ussd_parse_output
665
666 local ret=${PIPESTATUS[1]}
667 return ${ret}
668 }
669
670 __modem_ussd_parse_output() {
671 local line
672 while read -r line; do
673 # Find the expected answer.
674 [ "${line:0:7}" = "+CUSD: " ] || continue
675
676 # Strip +CUSD:
677 line="${line:7}"
678
679 local response_type
680 local response
681 local response_encoding
682
683 line="${line//,/ }"
684
685 local section=0 arg
686 while read -r arg; do
687 case "${section}" in
688 0)
689 response_type="${arg}"
690 ;;
691 1)
692 response="${arg}"
693 ;;
694 2)
695 response_encoding="${arg}"
696 ;;
697 *)
698 break
699 ;;
700 esac
701 section=$(( ${section} + 1 ))
702 done <<< "$(args ${line})"
703
704 log DEBUG "USSD response type: ${response_type}"
705 log DEBUG "USSD response encoding: ${response_encoding}"
706 log DEBUG "USSD encoded response: ${response}"
707
708 # If we got anything else than a response (type 0),
709 # we don't know how to handle that.
710 if [ "${response_type}" -ne "0" ]; then
711 return ${EXIT_ERROR}
712 fi
713
714 # Decode the string if needed.
715 case "${response_encoding}" in
716 15)
717 response="$(modem_ussd_decode "${response}")"
718 ;;
719 esac
720 log DEBUG "USSD response: ${response}"
721
722 print "${response}"
723 return ${EXIT_OK}
724 done
725
726 return ${EXIT_ERROR}
727 }
728
729 modem_ussd_encode() {
730 local string="${1}"
731 assert isset string
732
733 local output buffer char
734 while read -r char; do
735 char="$(char2bin "${char}")"
736 char="$(string_reverse "${char:1:7}")"
737
738 buffer="${buffer}${char}"
739 done <<< "$(string_split "${string}")"
740
741 local pos=0 len=8
742 while [ ${pos} -lt ${#buffer} ]; do
743 char="$(string_reverse "${buffer:${pos}:${len}}")"
744 pos=$(( ${pos} + ${len} ))
745
746 char="$(bin2hex "${char}")"
747 output="${output}${char}"
748 done
749
750 # Make everything uppercase.
751 output="${output^^}"
752
753 print "${output}"
754 }
755
756 modem_ussd_decode() {
757 local string="${1}"
758 assert isset string
759
760 local buffer1 buffer2
761 local output char
762
763 local pos=0 len=2
764 while [ ${pos} -lt ${#string} ]; do
765 char="${string:${pos}:${len}}"
766 pos=$(( ${pos} + ${len} ))
767
768 char="$(hex2bin "${char}")"
769 char="$(string_reverse "${char}")"
770 buffer1="${buffer1}${char}"
771 done
772
773 # Reset pointers.
774 pos=0
775 len=7
776
777 while [ ${pos} -lt ${#buffer1} ]; do
778 char="${buffer1:${pos}:${len}}"
779 pos=$(( ${pos} + ${len} ))
780
781 buffer2="${buffer2}0${char}"
782 done
783 buffer2="${buffer2:1}"
784
785 # Reset pointers again.
786 pos=0
787 len=8
788
789 while [ ${pos} -lt ${#buffer2} ]; do
790 char="${buffer2:${pos}:${len}}"
791 pos=$(( ${pos} + ${len} ))
792
793 char="$(string_reverse "${char}")"
794 char="$(bin2char "${char}")"
795 output="${output}${char}"
796 done
797
798 print "${output}"
799 return ${EXIT_OK}
800 }