В предыдущей части про передачу в GNU Radio был задан вопрос о том, можно ли декодировать протокол LoRa (передача данных для устройств с низким энергопотреблением) с помощью SDR. Мне эта тема показалась интересной, тем более что и сам сигнал у LoRa довольно-таки необычный — так называемая Chirp Spread Spectrum modulation, или «модуляция чирпами».
Как это работает, продолжение под катом.
Кстати, дословный перевод Chirp modulation звучал бы как «модуляция чириканием», но это звучит уж совсем сюрреалистично, так что лучше оставлю простое слово «чирп».
Модуляция LoRa
Как было сказано выше, при передаче LoRa используется способ модуляции при помощи «чирпов», кстати запатентованный компанией Semtech. Если кто хочет подробностей реализации с формулами, можно почитать PDF на сайте semtech или здесь, ну а если совсем грубо, то один «чирп» — это одно изменение частоты, такими изменениями и кодируется битовый поток, как показано на картинке выше. Параметрами сигнала в LoRa являются SF (spreading factor — по сути, длительность одного «чирпа») и bandwidth — ширина полосы передачи. Параметр SF задается предопределенными значениями SF7 — SF12, где 7 самый быстрый, а 12 — самый медленный режим (для примера можно посмотреть картинку с иллюстрацией разных скоростей «чирпования» с researchgate).
Очевидно, чем меньше длина «чирпа» и чем шире полоса, тем больше можно получить скорость передачи. Все это связано примерно такой таблицей:
С точки зрения дальности и помехозащищенности выгодно передавать медленно и печально, но при этом мы во-первых, теряем в скорости, во-вторых, проигрываем во времени, а по правилам LoRa, устройство может передавать не более 1% времени, чтобы не мешать другим устройствам. Так что выбор оптимальной скорости передачи для малопотребляющих устройств задача тоже не простая.
С общим принципом надеюсь, ясно, теперь перейдем к SDR и декодированию.
Железо
Для тестирования я использовал LoRa Click RN2483 и Arduino M0, просто потому что они были в наличии.
Это достаточно удобный форм-фактор для прототипирования, т.к. позволяет легко заменить плату одну на другую без пайки (в этом формате, называемом MikroBUS доступно много разной периферии).
Код в черновом варианте, не претендующий на production, добавлен под спойлер. В качестве теста передается значение «1234».
// RN2483 Modem and LoRa Click test TX. Tested with Arduino M0 int reset = A2; int rts = 9; // CS int cts = 3; // INT // the setup routine runs once when you press reset: void setup() { Serial1.begin(57600); // Serial port to radio // output LED pin pinMode(LED_BUILTIN, OUTPUT); pinMode(cts, INPUT); pinMode(rts, OUTPUT); digitalWrite(rts, HIGH); // Reset rn2483 pinMode(reset, OUTPUT); digitalWrite(reset, LOW); delay(100); digitalWrite(reset, HIGH); delay(100); sendCommand("sys get verrn"); sendCommand("sys get hweuirn"); sendCommand("mac pausern"); sendCommand("radio set mod lorarn"); sendCommand("radio set pwr -3rn"); // the transceiver output power, from -3 to 15 sendCommand("radio set sf sf8rn"); // sf7..sf12, sf7 the fastest spreading factor but gives the shortest range // sendCommand("mac set dr 0rn"); // data rate: 0-4, 4 faster sendCommand("radio set freq 869100000rn"); // sendCommand("radio set afcbw 41.7rn"); sendCommand("radio set rxbw 125rn"); // sendCommand("radio set prlen 8rn"); sendCommand("radio set crc onrn"); // sendCommand("radio set iqi offrn"); sendCommand("radio set cr 4/8rn"); // sendCommand("radio set wdt 60000rn"); // disable for continuous reception // sendCommand("radio set sync 12rn"); sendCommand("radio set bw 125rn"); } void sendCommand(const char *cmd) { Serial1.print(cmd); String incoming = Serial1.readString(); // SerialUSB.print(cmd); // SerialUSB.println(incoming); } // the loop routine runs over and over again forever: void loop() { char data[64] = {0}; // hexadecimal value representing the data to be transmitted, from 0 to 255 bytes for LoRa modulation and from 0 to 64 bytes for FSK modulation. sprintf(data, "radio tx 1234rn"); sendCommand(data); if (msg_num > 10000) msg_num=0; digitalWrite(LED_BUILTIN, 1); delay(400); digitalWrite(LED_BUILTIN, 0); delay(600); }
Кстати, максимальная заявленная дальность передачи для RN2483 составляет до 15км, на практике, при наличии одноэтажной застройки сигнал пропадает уже за 1км, и может быть не более 100м в городских «муравейниках».
Запускаем модем, и приступаем к декодированию.
Декодирование
В самом GNU Radio поддержки LoRa нет, так что придется использовать сторонние компоненты. Их нашлось всего два, и к сожалению, оба автора не проявили никакой фантазии в названии, и назвали их совершенно одинаково — gr-lora (https://github.com/rpp0/gr-lora и https://github.com/BastilleResearch/gr-lora соответственно).
rpp0/gr-lora
Скачать исходники декодера можно с github, сборка стандартна и затруднений не вызывает:
git clone https://github.com/rpp0/gr-lora.git cd gr-lora mkdir build cd build cmake .. make && sudo make install && sudo ldconfig
После установки в GNU Radio появляются дополнительные блоки, из которых несложно собрать декодер.
Блоки в GNU Radio стандартизированы, так что можно использовать любой приемник, например RTL-SDR. Я использовал SDRPlay. Для вывода данных в консоль использовалась простая программа на Python.
import socket UDP_IP = "127.0.0.1" UDP_PORT = 40868 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((UDP_IP, UDP_PORT)) sock.settimeout(0.5) while True: try: data, addr = sock.recvfrom(128) # buffer size is 1024 bytes print("Msg:", ' '.join('{:02x}'.format(x) for x in data)) except socket.timeout: pass
В качестве параметров декодера необходимо указать ширину полосы передачи, spreading factor, центральную частоту SDR и частоту приема.
Результат работы показан на рисунке.
Как можно видеть, в строке есть блоки заголовка и окончания передачи, а в середине мы видим наши данные «1234».
BastilleResearch/gr-lora
Этот модуль примечателен тем, что может работать не только на прием, но и на передачу. Установка примерно такая же: компонент нужно собрать из исходников.
git clone git://github.com/BastilleResearch/gr-lora.git cd gr-lora mkdir build cd build cmake .. make && sudo make install && sudo ldconfig
Connection Graph для данного декодера показан на рисунке.
Как можно видеть, блоков тут побольше. Rotator и Polyphase Resampler выделяют нужную частоту и обрезают лишнее, Demodulator преобразует «чирпы» в бинарный код (на выходе получается последовательность вроде «17 00 3e 00 38 00 2f 00 01 00 39 00 2c 00 30 00 c6 00 18 00 7e 00 d5 00 85 00 e9 00 d8 00 67 00 c4 00»), а Decoder формирует окончательный пакет.
К сожалению, нормально оно так и не заработало. Декодирование определенно работает, во время работы модема данные появляются, но принимаемые сообщения не имеют ничего общего с передаваемыми.
Причину я так и не понял, то ли где-то ошибся в настройках, то ли этот декодер совместим только со своим же кодером. Желающие могут проверить самостоятельно с помощью Channel Model.
LoRaWAN
Как можно видеть, выше рассматривался нижний, физический уровень передачи. В более высокоуровневом протоколе LoRaWAN поверх помещается еще один логический уровень — с шифрованием, ключами и прочими сервисами. Желающие посмотреть как устроено кодирование, могут попробовать онлайн-декодер здесь.
Заключение
Как можно видеть, декодирование LoRa средствами SDR вполне возможно. Конечно, реальный gateway делать на базе SDR вряд ли целесообразно — его чувствительность будет хуже чувствительности «настоящих» модемов, которые специально рассчитаны на прием слабых сигналов, и имеют более узкополосные фильтры и LNA. Но для тестирования или исследования это может быть вполне интересно.
Для тех кто захочет повторить эксперименты, файлы GNU Radio под спойлером.
Mon Jun 3 09:39:45 2019 options author window_size category [GRC Hier Blocks] comment description _enabled True _coordinate (8, 8) _rotation 0 generate_options wx_gui hier_block_src_path .: id top_block max_nouts 0 qt_qss_theme realtime_scheduling run_command {python} -u {filename} run_options prompt run True thread_safe_setters title variable comment _enabled True _coordinate (760, 12) _rotation 0 id bw value 125000.0 variable comment _enabled 1 _coordinate (936, 12) _rotation 0 id code_rate value 4 variable comment _enabled 1 _coordinate (848, 12) _rotation 0 id header value True variable comment _enabled 1 _coordinate (680, 12) _rotation 0 id ldr value True variable comment _enabled True _coordinate (400, 12) _rotation 0 id offset value -100000.0 variable comment _enabled True _coordinate (8, 76) _rotation 0 id samp_rate value 1000000 variable comment _enabled 1 _coordinate (544, 12) _rotation 0 id spreading_factor value 8 blocks_rotator_cc alias comment affinity _enabled 1 _coordinate (416, 252) _rotation 0 id blocks_rotator_cc_0 maxoutbuf 0 minoutbuf 0 phase_inc (2 * math.pi * offset) / samp_rate blocks_socket_pdu alias comment affinity _enabled 1 _coordinate (944, 452) _rotation 0 host 127.0.0.1 id blocks_socket_pdu_0 mtu 10000 maxoutbuf 0 minoutbuf 0 port 40868 tcp_no_delay False type "UDP_CLIENT" import alias comment _enabled True _coordinate (296, 12) _rotation 0 id import_0 import import math lora_decode alias code_rate code_rate comment affinity _enabled 1 header header _coordinate (648, 452) _rotation 0 id lora_decode_0 low_data_rate ldr maxoutbuf 0 minoutbuf 0 spreading_factor spreading_factor lora_demod alias comment affinity _enabled 1 fft_factor 2 beta 25.0 _coordinate (384, 452) _rotation 0 id lora_demod_0 low_data_rate ldr maxoutbuf 0 minoutbuf 0 spreading_factor spreading_factor pfb_arb_resampler_xxx alias comment affinity _enabled 1 _coordinate (656, 292) _rotation 0 id pfb_arb_resampler_xxx_0 maxoutbuf 0 minoutbuf 0 nfilts 32 rrate bw/samp_rate samp_delay 0 atten 100 taps type ccf sdrplay_rsp2_source agc_enabled False antenna 'A' bw 400 alias comment affinity dc_offset_mode True debug_enabled False device_serial '0' _enabled True _coordinate (144, 196) _rotation 0 id sdrplay_rsp2_source_0 if_atten_db 30 ifType 0 iq_balance_mode True lna_atten_step 3 lo_mode 1 maxoutbuf 0 minoutbuf 0 rf_freq 869.0e6 sample_rate samp_rate wxgui_fftsink2 avg_alpha 0 average False baseband_freq 0 alias comment affinity _enabled True fft_size 1024 freqvar None _coordinate (1000, 116) _rotation 0 grid_pos id wxgui_fftsink2_0 notebook peak_hold False ref_level 0 ref_scale 2.0 fft_rate 15 samp_rate samp_rate title FFT Plot type complex win_size win None y_divs 10 y_per_div 10 blocks_rotator_cc_0 pfb_arb_resampler_xxx_0 0 0 blocks_rotator_cc_0 wxgui_fftsink2_0 0 0 lora_decode_0 blocks_socket_pdu_0 out pdus lora_demod_0 lora_decode_0 out in pfb_arb_resampler_xxx_0 lora_demod_0 0 0 sdrplay_rsp2_source_0 blocks_rotator_cc_0 0 0
Mon Jun 3 09:39:45 2019 options author window_size category [GRC Hier Blocks] comment description _enabled True _coordinate (8, 8) _rotation 0 generate_options wx_gui hier_block_src_path .: id top_block max_nouts 0 qt_qss_theme realtime_scheduling run_command {python} -u {filename} run_options prompt run True thread_safe_setters title variable comment _enabled True _coordinate (184, 12) _rotation 0 id samp_rate value 1000000 lora_lora_receiver bandwidth 125000 alias crc True center_freq 869e6 channel_list [869.1e6] cr 4 comment conj False affinity decimation 1 disable_channelization False disable_drift_correction False _enabled True _coordinate (456, 332) _rotation 0 id lora_lora_receiver_0 implicit False maxoutbuf 0 minoutbuf 0 reduced_rate False samp_rate 1e6 sf 8 lora_message_socket_sink alias comment affinity _enabled True _coordinate (696, 364) _rotation 0 id lora_message_socket_sink_0 ip 127.0.0.1 layer 1 port 40868 sdrplay_rsp2_source agc_enabled False antenna 'A' bw 400 alias comment affinity dc_offset_mode True debug_enabled False device_serial '0' _enabled True _coordinate (72, 148) _rotation 0 id sdrplay_rsp2_source_0 if_atten_db 30 ifType 0 iq_balance_mode True lna_atten_step 3 lo_mode 1 maxoutbuf 0 minoutbuf 0 rf_freq 869.0e6 sample_rate samp_rate wxgui_fftsink2 avg_alpha 0 average True baseband_freq 0 alias comment affinity _enabled True fft_size 1024 freqvar None _coordinate (688, 108) _rotation 0 grid_pos id wxgui_fftsink2_0 notebook peak_hold True ref_level 0 ref_scale 2.0 fft_rate 15 samp_rate samp_rate title FFT Plot type complex win_size win None y_divs 10 y_per_div 10 lora_lora_receiver_0 lora_message_socket_sink_0 frames in sdrplay_rsp2_source_0 lora_lora_receiver_0 0 0 sdrplay_rsp2_source_0 wxgui_fftsink2_0 0 0
Всем удачных экспериментов.
Источник