Маршрутизаторы 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, то на экран будет бесконечно сыпаться техническая информация о текущем статусе модема вот какого, малоинформативного вида:
^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
Вывод будет следующим:
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» }, что даст следующий вывод:
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-соединения.
/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 (см. выше зачем).
Теперь можно собирать данные.
Добавим задание в планировщик, которое будет выполнять скрипт раз в час.
/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 следующего содержания:
<?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:
CREATE TABLE cellstat(
id INTEGER PRIMARY KEY,
date INTEGER,
serial VARCHAR,
imsi VARCHAR,
sig INTEGER,
iccid VARCHAR,
lac INTEGER,
cell INTEGER
);
Скрипт выбирает последнюю запись с переданным серийным номером маршрутизатора и если новые данные совпадают со старыми, то обновляет время записи, дабы не раздувать базу, иначе добавляет новую запись.
Имея данные о базовых станций и уровне сигнала, можно получить координаты устройства через API геолокации Яндекса или Google. К стати, в моём случае Яндекс отрабатывал лучше, да и отечественный производитель опять же, что улучшает карму. И это вполне себе работает в местах более-менее близких от населённых пунктов. Где-нибудь по среди тайги, в гефтегазоносных местах нашей необъятной Родины, конечно же — нет, но как правило координаты этих мест обычно сообщаются при заезде и ничего не мешает не получать, а отсылать данные о БСках и уровне сигнала, дабы внести свой вклад в гоолокацию, за что + в карму, опять-таки.