Маршрутизаторы MikroTik RB951G-2HND обладают портом USB, к которому можно подключить 3G/4G-модем для подключения к Интернет.
С USB-модемом, как и с любым другим, можно работать при помощи AT-команд. GSM-модемы, как правило, поддерживают команды получения данных о сети.
Эта заметка о нюансах работы с AT-командами на маршрутизаторах MikroTik.
На MikroTik, доступ к модему осуществляется командой /system serial-terminal
Собираем инфу для работы
Если в настройках PPP-соеденения нажать «Advanced Mode», то можно увидеть поля Data Channel и Info Channel. Это каналы модема отвечающие за, соответственно, передаваемые данные и информацию о статусе. Если подключиться к каналу информации при помощи команды /system serial-terminal usb1 channel=3, то на экран будет бесконечно сыпаться техническая информация о текущем статусе модема вот какого, малоинформативного вида:
1 2 3 4 |
^DSFLOWRPT:00001D5C,00000362,000002A0,00000000000A83FC,0000000000050247,00000000,00000000 ^DSFLOWRPT:00001D5E,00000308,00000278,00000000000A8A0C,0000000000050737,00000000,00000000 ^DSFLOWRPT:00001D60,00000256,00000180,00000000000A8EB9,0000000000050A37,00000000,00000000 ^DSFLOWRPT:00001D62,00000000,0000004E,00000000000A8EB9,0000000000050AD4,00000000,00000000 |
Выполним команду /interface ppp-client info ppp-out1
Вывод будет следующим:
1 2 3 4 5 6 7 8 9 10 11 |
modem-status: ready pin-status: no password required functionality: full manufacturer: model: E3131 revision: 21.158.13.03.143 serial-number: XXXXXXXXXXXXXXX current-operator: Beeline (cellid XXXX) access-technology: 3G signal-strengh: -83 dBm frame-error-rate: n/a |
Команда опять же интерактивна и передать данные в скрипт не позволяет.
Не сдаёмся и выполним /interface ppp-client info ppp-out1 do={ :global modemsn $»serial-number»;}
Команда запустилась в бесконечном цикле и создала глобальную переменную $modemsn, в которую положила значение, идущее после serial-number:. После завершения команды по Ctrl-C переменная остаётся.
Также можно заметить, что команда позволяет передать пользовательские данные через параметр user-command, правда разметка вывода несколько поедет, но это не беда.
Открываем справочник AT-команд и ищем те, которые нас интересуют, а именно:
- AT+CIMI — информация об IMSI номере SIM карты
- AT+CREG? — тип регистрации сети
- AT^ICCID? — информация о серийном номере SIM карты
Чтобы команда AT+CREG выдавала инфу о базовых станциях, предварительно нужно выполнить AT+CREG=2. Выполняем /interface ppp-client info ppp-out1 user-command=»AT+CIMI\r\nAT+CREG\?\r\nAT^ICCID\?» do={ :global imsi $»manufacturer»; :global cell $»model»; :global iccid $»revision»; :global signal $»signal-strengh» }, что даст следующий вывод:
1 2 3 4 5 6 7 8 9 10 11 |
pin-status: no password required functionality: full manufacturer: 25099XXXXXXXXXX model: +CREG: 2,1,"XXXX","XXXX" revision: ^ICCID: XXXXXXXXXXXXXXXXXXXX serial-number: current-operator: Beeline (cellid XXXX) access-technology: 3G signal-strengh: -85 dBm frame-error-rate: n/a user-command: AT+CIMI |
а в соответствующие переменные упадут нужные данные, различной степени сырости.
Теперь, вооружившись полученными знаниями, напишем скрипты, которые будут получать данные за нас.
Скрипты
Т.к. у разных модемов, номер канала данных может различаться, то нам нужен скрипт, который будет выполняться при старте маршрутизатора и производить инициализацию ppp-соединения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/system scheduler add interval=20s name=start-up on-event=start-up policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive start-date=jan/01/1970 start-time=00:00:00 /system script add name=start-up owner=admin policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive source=":global modemsn\ \n:global d\ \n:if ([/system scheduler get start-up interval] != \"00:00:00\") do={/system scheduler set start-up start-time=startup interval=\"00:00:00\"} \ \n:delay 20\ \n:foreach i in=[/interface ppp-client find] do={\ \n if ([/interface ppp-client get \$i value-name=disabled] = true) do={\ \n /interface ppp-client set \$i modem-init=\"AT+CREG=2\"\ \n :set \$chcnt ([/port get [/port find where name~[/interface ppp-client get \$i value-name=port]] value-name=channels]-1)\ \n while (\$chcnt!=0) do={\ \n\t/interface ppp-client set \$i info-channel=\$chcnt\ \n\t:set \$d \$i\ \n\t:local j [execute {/interface ppp-client info \$d do={ :global modemsn \$\"serial-number\";}}]\ \n\t:delay 5\ \n\t:do { /system script job remove \$j } on-error={}\ \n\tif ([:len \$modemsn]!=0) do={\ \n\t /interface ppp-client set \$i info-channel=\$chcnt\ \n\t :set \$chcnt 0\ \n\t }\ \n }\ \n /interface ppp-client set \$i disabled=no\ \n }\ \n}\ \n:set \$d\ \n:set \$modemsn" |
По непонятной мне причине, при первом старте маршрутизатора после сброса, правило планировщика, помеченного как start-up, не срабатывает, хотя при последующих запусках отрабатывает как положено. В связи с этим, добавляем в шедулер правило c интервалом 20 сек., которое вызовет скрипт, устанавливающий этому правилу интервал в start-up, в результате, при первом старте скрипт выполнится по таймеру, а во все последующие по событию start-up. Попутно, скрипт проходит все соединения и для каждого получает количество каналов модема, по очереди, от 1 до MAX_CHANNELS, ставит в качестве канала информации в ppp-соединении, пытается получить данные и если данные получены, то оставляет текущий канал и разрешает это соединение. В качестве строки инициализации устанавливает AT+CREG=2 (см. выше зачем).
Теперь можно собирать данные.
Добавим задание в планировщик, которое будет выполнять скрипт раз в час.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/system scheduler add interval=1h name=netinfo on-event=netinfo policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon start-date=dec/22/2016 start-time=07:09:46 /system script add name=netinfo owner=admin policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source=":global imsi\ \n:global cell\ \n:global iccid\ \n:global signal\ \n:global lac\ \n\ \n:local serial [/system routerboard get value-name=\"serial-number\"]\ \n:local osver [:pick [/system resource get value-name=version] 0 [:find [/system resource get value-name=version] \" \"]]\ \n:local url \"myserver_ip:port/cells.php\"\ \n:local user \"username\"\ \n:local pass \"password\"\ \n\ \n:local j [execute {/interface ppp-client info ppp-out1 user-command=\"AT+CIMI\\r\\nAT+CREG\\\?\\r\\nAT^ICCID\\\?\" do={ :global imsi \$\"manufacturer\"; :global\ \_cell \$\"model\"; :global iccid \$\"revision\"; :global signal \$\"signal-strengh\" }}]\ \n:delay 10\ \n\ \n:do { /system script job remove \$j } on-error={}\ \n\ \nset \$signal [pick \$signal 0 [:find \$signal \" \" -1]]\ \nset \$iccid [pick \$iccid ([:find \$iccid \" \" -1]+1) [:len \$iccid]]\ \nset \$cell [:pick \$cell ([:find \$cell \"\\\"\"]+1) [:len \$cell]]\ \n\ \n:set \$lac [:pick \$cell 0 [find \$cell \"\\\"\"]]\ \n:set \$cell [pick \$cell ([:find \$cell \",\"]+2) ([:len \$cell]-1)]\ \n\ \n/tool fetch mode=https keep-result=no user=\$user password=\$pass [:put (\"https://\$url\\\?serial=\$serial&imsi=\$imsi&signal=\$signal&iccid=\$iccid&lac=\$lac&\ cell=\$cell\")]" |
Скрипт создает фоновое задание с командой /interface ppp-client info …, которая, как мы помним, является интерактивной, ждёт 10 секунд, завершает задание, парсит полученные данные и выполняет запрос к серверу, в котором передаёт параметры.
Сервер
Ставим web-сервер, поддержку cgi и СУБД. В моём случае это nginx, PHP и SQLite. Также рекомендую поднять на сервере SLL, хотя бы самоподписанный — 2017 год как-никак, безопасность и всё такое…
На web-сервере кладём файл cellstat.php следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php $imsi=$_GET['imsi']; $signal=$_GET['signal']; $iccid=$_GET['iccid']; $lac=hexdec($_GET['lac']); $cell=hexdec($_GET['cell']); $dbname="cells.sqlite"; try { $db=new SQLite3($dbname,SQLITE3_OPEN_READWRITE); } catch (Exception $exception) { header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); exit(); } if(!empty($imsi) && !empty($signal) && !empty($iccid) && !empty($lac) && !empty($cell)) //Additing signal strength and BS info to DB { $time=time(); $results=$db->query("SELECT * FROM cellstat WHERE id=(SELECT MAX(id) FROM cellstat WHERE serial='$serial')"); //Select last data from DB $cellstat=$results->fetchArray(); if($cellstat['imsi']==$imsi && $cellstat['iccid']==$iccid && $cellstat['lac']==$lac && $cellstat['cell']==$cell && $cellstat['sig']==$signal) //If new data equals old update record uptade date { $results=$db->query("UPDATE cellstat SET date=$time WHERE id=".$cellstat['id']); } else $results=$db->query("INSERT INTO cellstat VALUES(NULL,$time,'$serial','$imsi',$signal,'$iccid',$lac,$cell)"); $results->finalize(); $db->close(); header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); exit(); } ?> |
Находясь в папке со скриптом, выполним sqlite cellstat.sqlite и создадим таблицу, в которую будут складываться данные и не забудем назначить права на запись пользователю www:
1 2 3 4 5 6 7 8 9 10 |
CREATE TABLE cellstat( id INTEGER PRIMARY KEY, date INTEGER, serial VARCHAR, imsi VARCHAR, sig INTEGER, iccid VARCHAR, lac INTEGER, cell INTEGER ); |
Скрипт выбирает последнюю запись с переданным серийным номером маршрутизатора и если новые данные совпадают со старыми, то обновляет время записи, дабы не раздувать базу, иначе добавляет новую запись.
Имея данные о базовых станций и уровне сигнала, можно получить координаты устройства через API геолокации Яндекса или Google. К стати, в моём случае Яндекс отрабатывал лучше, да и отечественный производитель опять же, что улучшает карму. И это вполне себе работает в местах более-менее близких от населённых пунктов. Где-нибудь по среди тайги, в гефтегазоносных местах нашей необъятной Родины, конечно же — нет, но как правило координаты этих мест обычно сообщаются при заезде и ничего не мешает не получать, а отсылать данные о БСках и уровне сигнала, дабы внести свой вклад в гоолокацию, за что + в карму, опять-таки.