Release commit
This commit is contained in:
commit
e8c1bd4127
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/en python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
# LoRa PHYdecoder - parse LoRa PHY decoded by gr-lora
|
||||
# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) at @PentHertz
|
||||
|
||||
import binascii
|
||||
from layers.loraphy import *
|
||||
import argparse
|
||||
|
||||
def decodePHY(pkt):
|
||||
decoded = LoRa(pkt[UDP].load)
|
||||
print (repr(decoded))
|
||||
|
||||
def filterpkt(pkt, port):
|
||||
if pkt.haslayer(UDP):
|
||||
if pkt[UDP].dport == port:
|
||||
return True
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Monitor and decode MAC PHY packets.')
|
||||
parser.add_argument('-p', '--port', dest='port', default=40868,
|
||||
help='TAP PORT to listen on (default: UDP 40868)')
|
||||
parser.add_argument('-i', '--iface', dest='iface', default='lo',
|
||||
help='Interface to monitor (default: local)')
|
||||
|
||||
args = parser.parse_args()
|
||||
iface = args.iface
|
||||
port = int(args.port)
|
||||
|
||||
sniff(prn=decodePHY,
|
||||
lfilter=lambda pkt:filterpkt(pkt, port),
|
||||
iface=iface)
|
|
@ -0,0 +1,131 @@
|
|||
# LoRa Craft
|
||||
|
||||
LoRa Craft is a small set of tools to receive signal with SDR, decode et craft LoRaWAN packets on top of a **gr-lora** GNU Radio module.
|
||||
|
||||
This repository will be completed with others tools soon, depending on needs during assessments :)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Python 2 or 3
|
||||
* Scapy
|
||||
* GNU Radio 3.8
|
||||
* gr-lora from [rpp0](https://github.com/rpp0): [link here](https://github.com/rpp0/gr-lora)
|
||||
* A Software-Defined Radio equipment (USRP, bladeRF, RTL-SDR dongle, etc.)
|
||||
|
||||
## Receive signal and decode its data
|
||||
|
||||
### Receive
|
||||
|
||||
To receive a signal, an example of a GRC schema is available in folder `grc_examples/usrp_LoRa_decode_to_UDP.grc` for USRP, as shown as follows:
|
||||
|
||||
![alt text](https://github.com/FlUxIuS/LoRaCraft/blob/master/img/completeschema.png "Schema to receive LoRa signal")
|
||||
|
||||
The channel frequency, as well as the spread facto and the bandwidth must be set correctly to valid values with the help of the FFT and waterfall sinks:
|
||||
|
||||
![alt text](https://github.com/FlUxIuS/LoRaCraft/blob/master/img/frequencydet_zoomout_sf12bw125.png "Waterfall and FFT sinks")
|
||||
|
||||
Note: Multiple frequencies can be used by targets. This would implies to include multiple receivers in GRC.
|
||||
|
||||
For more information on how to detect LoRa signal, please take a look at the following post: TODO.
|
||||
|
||||
### Decode
|
||||
|
||||
Once the receiver is running with the SDR equipment, we use the script `LoRa_PHYDecode.py`:
|
||||
|
||||
```bash
|
||||
$ python LoRa_PHYDecode.py -h 1 ↵
|
||||
usage: LoRa_PHYDecode.py [-h] [-p PORT] [-i IFACE]
|
||||
|
||||
Monitor and decode MAC PHY packets.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p PORT, --port PORT TAP PORT to listen on (default: UDP 40868)
|
||||
-i IFACE, --iface IFACE
|
||||
Interface to monitor (default: local)
|
||||
```
|
||||
|
||||
By default, the script can be run as follows to decode received LoRa frames:
|
||||
|
||||
```bash
|
||||
$ sudo python LoRa_PHYDecode.py
|
||||
<LoRa Preamble=0x1 PHDR=0x631e PHDR_CRC=0x0 MType=Unconfirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=0 |>] FCnt=0 FPort=2 DataPayload='i\x06D\x94\x97\x08\xce!\xd9' MIC=0x4b516899 CRC=0x96e1 |>
|
||||
...
|
||||
<LoRa Preamble=0x1 PHDR=0x631e PHDR_CRC=0x0 MType=Unconfirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=0 |>] FCnt=0 FPort=2 DataPayload='penthertz' MIC=0x20a5fcba CRC=0xcdc |>
|
||||
<LoRa Preamble=0x0 PHDR=0xd30c PHDR_CRC=0x0 MType=Confirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=1 |>] FCnt=0 FOpts_up=[<MACCommand_up CID=LinkCheckReq LinkCheck=[''] |>] FOpts_down=[<MACCommand_down CID=222 |>] FPort=92 DataPayload='' MIC=0x31c753f |>
|
||||
<LoRa Preamble=0x0 PHDR=0xd30c PHDR_CRC=0x0 MType=Confirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=1 |>] FCnt=0 FOpts_up=[<MACCommand_up CID=LinkCheckReq LinkCheck=[''] |>] FOpts_down=[<MACCommand_down CID=222 |>] FPort=92 DataPayload='' MIC=0x31c753f |
|
||||
```
|
||||
|
||||
## Generate packets
|
||||
|
||||
To generate packets, you can instanciate a Scapy packet as follows:
|
||||
|
||||
```python
|
||||
>>> from layers.loraphy import *
|
||||
>>> pkt = LoRa()
|
||||
>>> pkt
|
||||
<LoRa Join_Request_Field=[''] |>
|
||||
```
|
||||
|
||||
And start to fill it.
|
||||
|
||||
After crafting your packet, you can use [python-loranode](https://github.com/rpp0/python-loranode) as follows:
|
||||
|
||||
```python
|
||||
>>> from binascii
|
||||
>>> from loranode import RN2483Controller
|
||||
>>> to_send = binascii.hexlify(str(pkt))[3:]
|
||||
>>> c = RN2483Controller("/dev/ttyACM0") # Choose the correct /dev device here
|
||||
>>> c.set_sf(7) # choose your spreading factor here
|
||||
>>> c.set_bw(150) # choose the bandwidth here
|
||||
>>> c.set_cr("4/8") # Set 4/8 coding for example
|
||||
>>> c.send_p2p(to_send)
|
||||
```
|
||||
|
||||
Note that you should skip the first three bytes (Preambule, PHDR, PHDR_CRC), before sending it with `send_p2p` method.
|
||||
|
||||
## LoRa crypto helpers
|
||||
|
||||
Few helpers have been implemented to calculate MIC field, encrypt and decrypt packet:
|
||||
|
||||
* `JoinAcceptPayload_decrypt`: decrypt Join-accept payloads;
|
||||
* `JoinAcceptPayload_encrypt`: encrypt Join-accept payloads;
|
||||
* `getPHY_CMAC`: compute MIC field of a packet using a provided key;
|
||||
* `checkMIC`: check MIC of a packet against a provided key.
|
||||
|
||||
As an example, to check if the key `000102030405060708090A0B0C0D0E0F` is used to compute MIC on the following Join-request, we can write a little script as follows:
|
||||
|
||||
```python
|
||||
>>> from layers.loraphy import *
|
||||
>>> from lutil.crypto import *
|
||||
>>> key = "000102030405060708090A0B0C0D0E0F"
|
||||
>>> p = '000000006c6f7665636166656d656565746f6f00696953024c49'
|
||||
>>> pkt = LoRa(binascii.unhexlify(p))
|
||||
>>> pkt
|
||||
<LoRa Preamble=0x0 PHDR=0x0 PHDR_CRC=0x0 MType=Join-request RFU=0 Major=0 Join_Request_Field=[<Join_Request AppEUI='lovecafe' DevEUI='meeetoo' DevNonce=26985 |>] MIC=0x53024c49 |>
|
||||
>>> checkMIC(binascii.unhexlify(key), str(pkt))
|
||||
True
|
||||
```
|
||||
|
||||
To check if `000102030405060708090A0B0C0D0E0F` key is used to encrypt a Join-accept message, we can combine `JoinAcceptPayload_decrypt` and `checkMIC` as follows:
|
||||
|
||||
```python
|
||||
>>> pkt = "000000200836e287a9805cb7ee9e5fff7c9ee97a"
|
||||
>>> ja = JoinAcceptPayload_decrypt(binascii.unhexlify(key), binascii.unhexlify(pkt))
|
||||
>>> ja
|
||||
'ghi#\x01\x00\xb2\\C\x03\x00\x00{\x06O\x8a'
|
||||
>>> Join_Accept(ja)
|
||||
<Join_Accept JoinAppNonce=0x6fe14a NetID=0x10203 DevAddr=0x68e8cb1 OptNeg=0 RX1DRoffset=0x0 RX2_Data_rate=0x0 RxDelay=0x0 |<Padding load='\xbejsu' |>>
|
||||
>>> p = "\x00\x00\x00\x20"+ja # adding headers
|
||||
>>> checkMIC(key.decode("hex"), p)
|
||||
>>> True
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
* More helpers for other types of payloads
|
||||
* Implement helpers to transmit signal with dongles more easily
|
||||
* Transmit packets with SDR
|
||||
* Support gr-lora from Bastille: [link here](https://github.com/BastilleResearch/gr-lora)
|
||||
|
||||
Feel free to contribute if you have cool scripts/tools to share :)!
|
|
@ -0,0 +1,257 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0
|
||||
#
|
||||
# GNU Radio Python Flow Graph
|
||||
# Title: LoRa Receiver to UDP socket with USRP
|
||||
# Author: FlUxIuS
|
||||
# Copyright: PentHertz
|
||||
# Description: Example of a schema that receive LoRa frames and write to an UDP socket to be decoded with the Scapy Layer
|
||||
# GNU Radio version: 3.8.0.0
|
||||
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
if __name__ == '__main__':
|
||||
import ctypes
|
||||
import sys
|
||||
if sys.platform.startswith('linux'):
|
||||
try:
|
||||
x11 = ctypes.cdll.LoadLibrary('libX11.so')
|
||||
x11.XInitThreads()
|
||||
except:
|
||||
print("Warning: failed to XInitThreads()")
|
||||
|
||||
from PyQt5 import Qt
|
||||
from gnuradio import eng_notation
|
||||
from gnuradio import qtgui
|
||||
from gnuradio.filter import firdes
|
||||
import sip
|
||||
from gnuradio import gr
|
||||
import sys
|
||||
import signal
|
||||
from argparse import ArgumentParser
|
||||
from gnuradio.eng_arg import eng_float, intx
|
||||
from gnuradio import uhd
|
||||
import time
|
||||
from gnuradio.qtgui import Range, RangeWidget
|
||||
import lora
|
||||
from gnuradio import qtgui
|
||||
|
||||
class LoRa_usrp_receive_to_UDP(gr.top_block, Qt.QWidget):
|
||||
|
||||
def __init__(self):
|
||||
gr.top_block.__init__(self, "LoRa Receiver to UDP socket with USRP")
|
||||
Qt.QWidget.__init__(self)
|
||||
self.setWindowTitle("LoRa Receiver to UDP socket with USRP")
|
||||
qtgui.util.check_set_qss()
|
||||
try:
|
||||
self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc'))
|
||||
except:
|
||||
pass
|
||||
self.top_scroll_layout = Qt.QVBoxLayout()
|
||||
self.setLayout(self.top_scroll_layout)
|
||||
self.top_scroll = Qt.QScrollArea()
|
||||
self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame)
|
||||
self.top_scroll_layout.addWidget(self.top_scroll)
|
||||
self.top_scroll.setWidgetResizable(True)
|
||||
self.top_widget = Qt.QWidget()
|
||||
self.top_scroll.setWidget(self.top_widget)
|
||||
self.top_layout = Qt.QVBoxLayout(self.top_widget)
|
||||
self.top_grid_layout = Qt.QGridLayout()
|
||||
self.top_layout.addLayout(self.top_grid_layout)
|
||||
|
||||
self.settings = Qt.QSettings("GNU Radio", "LoRa_usrp_receive_to_UDP")
|
||||
|
||||
try:
|
||||
if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
|
||||
self.restoreGeometry(self.settings.value("geometry").toByteArray())
|
||||
else:
|
||||
self.restoreGeometry(self.settings.value("geometry"))
|
||||
except:
|
||||
pass
|
||||
|
||||
##################################################
|
||||
# Variables
|
||||
##################################################
|
||||
self.samp_rate = samp_rate = 1e6
|
||||
self.freq_slider = freq_slider = 868e6
|
||||
self.channel_freq = channel_freq = 868.2e6
|
||||
|
||||
##################################################
|
||||
# Blocks
|
||||
##################################################
|
||||
self._freq_slider_range = Range(867e6, 869e6, 1, 868e6, 200)
|
||||
self._freq_slider_win = RangeWidget(self._freq_slider_range, self.set_freq_slider, 'Frequency', "counter_slider", float)
|
||||
self.top_grid_layout.addWidget(self._freq_slider_win)
|
||||
self._channel_freq_tool_bar = Qt.QToolBar(self)
|
||||
self._channel_freq_tool_bar.addWidget(Qt.QLabel('Channel frequency' + ": "))
|
||||
self._channel_freq_line_edit = Qt.QLineEdit(str(self.channel_freq))
|
||||
self._channel_freq_tool_bar.addWidget(self._channel_freq_line_edit)
|
||||
self._channel_freq_line_edit.returnPressed.connect(
|
||||
lambda: self.set_channel_freq(eng_notation.str_to_num(str(self._channel_freq_line_edit.text()))))
|
||||
self.top_grid_layout.addWidget(self._channel_freq_tool_bar)
|
||||
self.uhd_usrp_source_0 = uhd.usrp_source(
|
||||
",".join(("", "")),
|
||||
uhd.stream_args(
|
||||
cpu_format="fc32",
|
||||
args='',
|
||||
channels=[],
|
||||
),
|
||||
)
|
||||
self.uhd_usrp_source_0.set_center_freq(freq_slider, 0)
|
||||
self.uhd_usrp_source_0.set_gain(0, 0)
|
||||
self.uhd_usrp_source_0.set_antenna('RX2', 0)
|
||||
self.uhd_usrp_source_0.set_bandwidth(samp_rate, 0)
|
||||
self.uhd_usrp_source_0.set_samp_rate(samp_rate)
|
||||
self.uhd_usrp_source_0.set_time_unknown_pps(uhd.time_spec())
|
||||
self.qtgui_waterfall_sink_x_0 = qtgui.waterfall_sink_c(
|
||||
1024, #size
|
||||
firdes.WIN_HAMMING, #wintype
|
||||
channel_freq, #fc
|
||||
samp_rate, #bw
|
||||
"", #name
|
||||
1 #number of inputs
|
||||
)
|
||||
self.qtgui_waterfall_sink_x_0.set_update_time(0.10)
|
||||
self.qtgui_waterfall_sink_x_0.enable_grid(True)
|
||||
self.qtgui_waterfall_sink_x_0.enable_axis_labels(True)
|
||||
|
||||
|
||||
|
||||
labels = ['', '', '', '', '',
|
||||
'', '', '', '', '']
|
||||
colors = [0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0]
|
||||
alphas = [1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0]
|
||||
|
||||
for i in range(1):
|
||||
if len(labels[i]) == 0:
|
||||
self.qtgui_waterfall_sink_x_0.set_line_label(i, "Data {0}".format(i))
|
||||
else:
|
||||
self.qtgui_waterfall_sink_x_0.set_line_label(i, labels[i])
|
||||
self.qtgui_waterfall_sink_x_0.set_color_map(i, colors[i])
|
||||
self.qtgui_waterfall_sink_x_0.set_line_alpha(i, alphas[i])
|
||||
|
||||
self.qtgui_waterfall_sink_x_0.set_intensity_range(-140, 10)
|
||||
|
||||
self._qtgui_waterfall_sink_x_0_win = sip.wrapinstance(self.qtgui_waterfall_sink_x_0.pyqwidget(), Qt.QWidget)
|
||||
self.top_grid_layout.addWidget(self._qtgui_waterfall_sink_x_0_win)
|
||||
self.qtgui_freq_sink_x_0 = qtgui.freq_sink_c(
|
||||
1024, #size
|
||||
firdes.WIN_BLACKMAN_hARRIS, #wintype
|
||||
freq_slider, #fc
|
||||
samp_rate, #bw
|
||||
"", #name
|
||||
1
|
||||
)
|
||||
self.qtgui_freq_sink_x_0.set_update_time(0.10)
|
||||
self.qtgui_freq_sink_x_0.set_y_axis(-140, 10)
|
||||
self.qtgui_freq_sink_x_0.set_y_label('Relative Gain', 'dB')
|
||||
self.qtgui_freq_sink_x_0.set_trigger_mode(qtgui.TRIG_MODE_FREE, 0.0, 0, "")
|
||||
self.qtgui_freq_sink_x_0.enable_autoscale(False)
|
||||
self.qtgui_freq_sink_x_0.enable_grid(False)
|
||||
self.qtgui_freq_sink_x_0.set_fft_average(1.0)
|
||||
self.qtgui_freq_sink_x_0.enable_axis_labels(True)
|
||||
self.qtgui_freq_sink_x_0.enable_control_panel(True)
|
||||
|
||||
|
||||
|
||||
labels = ['', '', '', '', '',
|
||||
'', '', '', '', '']
|
||||
widths = [1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1]
|
||||
colors = ["blue", "red", "green", "black", "cyan",
|
||||
"magenta", "yellow", "dark red", "dark green", "dark blue"]
|
||||
alphas = [1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0]
|
||||
|
||||
for i in range(1):
|
||||
if len(labels[i]) == 0:
|
||||
self.qtgui_freq_sink_x_0.set_line_label(i, "Data {0}".format(i))
|
||||
else:
|
||||
self.qtgui_freq_sink_x_0.set_line_label(i, labels[i])
|
||||
self.qtgui_freq_sink_x_0.set_line_width(i, widths[i])
|
||||
self.qtgui_freq_sink_x_0.set_line_color(i, colors[i])
|
||||
self.qtgui_freq_sink_x_0.set_line_alpha(i, alphas[i])
|
||||
|
||||
self._qtgui_freq_sink_x_0_win = sip.wrapinstance(self.qtgui_freq_sink_x_0.pyqwidget(), Qt.QWidget)
|
||||
self.top_grid_layout.addWidget(self._qtgui_freq_sink_x_0_win)
|
||||
self.lora_message_socket_sink_0 = lora.message_socket_sink('127.0.0.1', 40868, 0)
|
||||
self.lora_lora_receiver_0 = lora.lora_receiver(samp_rate, 868e6, [channel_freq], 125000, 7, False, 4, True, False, False, 1, False, False)
|
||||
|
||||
|
||||
|
||||
##################################################
|
||||
# Connections
|
||||
##################################################
|
||||
self.msg_connect((self.lora_lora_receiver_0, 'frames'), (self.lora_message_socket_sink_0, 'in'))
|
||||
self.connect((self.uhd_usrp_source_0, 0), (self.lora_lora_receiver_0, 0))
|
||||
self.connect((self.uhd_usrp_source_0, 0), (self.qtgui_freq_sink_x_0, 0))
|
||||
self.connect((self.uhd_usrp_source_0, 0), (self.qtgui_waterfall_sink_x_0, 0))
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.settings = Qt.QSettings("GNU Radio", "LoRa_usrp_receive_to_UDP")
|
||||
self.settings.setValue("geometry", self.saveGeometry())
|
||||
event.accept()
|
||||
|
||||
def get_samp_rate(self):
|
||||
return self.samp_rate
|
||||
|
||||
def set_samp_rate(self, samp_rate):
|
||||
self.samp_rate = samp_rate
|
||||
self.qtgui_freq_sink_x_0.set_frequency_range(self.freq_slider, self.samp_rate)
|
||||
self.qtgui_waterfall_sink_x_0.set_frequency_range(self.channel_freq, self.samp_rate)
|
||||
self.uhd_usrp_source_0.set_samp_rate(self.samp_rate)
|
||||
self.uhd_usrp_source_0.set_bandwidth(self.samp_rate, 0)
|
||||
|
||||
def get_freq_slider(self):
|
||||
return self.freq_slider
|
||||
|
||||
def set_freq_slider(self, freq_slider):
|
||||
self.freq_slider = freq_slider
|
||||
self.qtgui_freq_sink_x_0.set_frequency_range(self.freq_slider, self.samp_rate)
|
||||
self.uhd_usrp_source_0.set_center_freq(self.freq_slider, 0)
|
||||
|
||||
def get_channel_freq(self):
|
||||
return self.channel_freq
|
||||
|
||||
def set_channel_freq(self, channel_freq):
|
||||
self.channel_freq = channel_freq
|
||||
Qt.QMetaObject.invokeMethod(self._channel_freq_line_edit, "setText", Qt.Q_ARG("QString", eng_notation.num_to_str(self.channel_freq)))
|
||||
self.qtgui_waterfall_sink_x_0.set_frequency_range(self.channel_freq, self.samp_rate)
|
||||
|
||||
|
||||
|
||||
def main(top_block_cls=LoRa_usrp_receive_to_UDP, options=None):
|
||||
|
||||
if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
|
||||
style = gr.prefs().get_string('qtgui', 'style', 'raster')
|
||||
Qt.QApplication.setGraphicsSystem(style)
|
||||
qapp = Qt.QApplication(sys.argv)
|
||||
|
||||
tb = top_block_cls()
|
||||
tb.start()
|
||||
tb.show()
|
||||
|
||||
def sig_handler(sig=None, frame=None):
|
||||
Qt.QApplication.quit()
|
||||
|
||||
signal.signal(signal.SIGINT, sig_handler)
|
||||
signal.signal(signal.SIGTERM, sig_handler)
|
||||
|
||||
timer = Qt.QTimer()
|
||||
timer.start(500)
|
||||
timer.timeout.connect(lambda: None)
|
||||
|
||||
def quitting():
|
||||
tb.stop()
|
||||
tb.wait()
|
||||
qapp.aboutToQuit.connect(quitting)
|
||||
qapp.exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Binary file not shown.
|
@ -0,0 +1,648 @@
|
|||
options:
|
||||
parameters:
|
||||
author: FlUxIuS
|
||||
category: '[GRC Hier Blocks]'
|
||||
cmake_opt: ''
|
||||
comment: ''
|
||||
copyright: PentHertz
|
||||
description: Example of a schema that receive LoRa frames and write to an UDP
|
||||
socket to be decoded with the Scapy Layer
|
||||
gen_cmake: 'On'
|
||||
gen_linking: dynamic
|
||||
generate_options: qt_gui
|
||||
hier_block_src_path: '.:'
|
||||
id: LoRa_usrp_receive_to_UDP
|
||||
max_nouts: '0'
|
||||
output_language: python
|
||||
placement: (0,0)
|
||||
qt_qss_theme: ''
|
||||
realtime_scheduling: ''
|
||||
run: 'True'
|
||||
run_command: '{python} -u {filename}'
|
||||
run_options: prompt
|
||||
sizing_mode: fixed
|
||||
thread_safe_setters: ''
|
||||
title: LoRa Receiver to UDP socket with USRP
|
||||
window_size: ''
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [8, 8]
|
||||
rotation: 0
|
||||
state: enabled
|
||||
|
||||
blocks:
|
||||
- name: channel_freq
|
||||
id: variable_qtgui_entry
|
||||
parameters:
|
||||
comment: ''
|
||||
gui_hint: ''
|
||||
label: Channel frequency
|
||||
type: real
|
||||
value: 868.2e6
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [0, 284.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
- name: freq_slider
|
||||
id: variable_qtgui_range
|
||||
parameters:
|
||||
comment: ''
|
||||
gui_hint: ''
|
||||
label: Frequency
|
||||
min_len: '200'
|
||||
orient: Qt.Horizontal
|
||||
rangeType: float
|
||||
start: 867e6
|
||||
step: '1'
|
||||
stop: 869e6
|
||||
value: 868e6
|
||||
widget: counter_slider
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [8, 140.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
- name: samp_rate
|
||||
id: variable
|
||||
parameters:
|
||||
comment: ''
|
||||
value: 1e6
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [192, 12.0]
|
||||
rotation: 0
|
||||
state: enabled
|
||||
- name: lora_lora_receiver_0
|
||||
id: lora_lora_receiver
|
||||
parameters:
|
||||
affinity: ''
|
||||
alias: ''
|
||||
bandwidth: '125000'
|
||||
center_freq: 868e6
|
||||
channel_list: channel_freq
|
||||
comment: ''
|
||||
conj: 'False'
|
||||
cr: '4'
|
||||
crc: 'True'
|
||||
decimation: '1'
|
||||
disable_channelization: 'False'
|
||||
disable_drift_correction: 'False'
|
||||
implicit: 'False'
|
||||
maxoutbuf: '0'
|
||||
minoutbuf: '0'
|
||||
reduced_rate: 'False'
|
||||
samp_rate: samp_rate
|
||||
sf: '7'
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [648, 108.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
- name: lora_message_socket_sink_0
|
||||
id: lora_message_socket_sink
|
||||
parameters:
|
||||
affinity: ''
|
||||
alias: ''
|
||||
comment: ''
|
||||
ip: 127.0.0.1
|
||||
layer: '0'
|
||||
port: '40868'
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [904, 140.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
- name: qtgui_freq_sink_x_0
|
||||
id: qtgui_freq_sink_x
|
||||
parameters:
|
||||
affinity: ''
|
||||
alias: ''
|
||||
alpha1: '1.0'
|
||||
alpha10: '1.0'
|
||||
alpha2: '1.0'
|
||||
alpha3: '1.0'
|
||||
alpha4: '1.0'
|
||||
alpha5: '1.0'
|
||||
alpha6: '1.0'
|
||||
alpha7: '1.0'
|
||||
alpha8: '1.0'
|
||||
alpha9: '1.0'
|
||||
autoscale: 'False'
|
||||
average: '1.0'
|
||||
axislabels: 'True'
|
||||
bw: samp_rate
|
||||
color1: '"blue"'
|
||||
color10: '"dark blue"'
|
||||
color2: '"red"'
|
||||
color3: '"green"'
|
||||
color4: '"black"'
|
||||
color5: '"cyan"'
|
||||
color6: '"magenta"'
|
||||
color7: '"yellow"'
|
||||
color8: '"dark red"'
|
||||
color9: '"dark green"'
|
||||
comment: ''
|
||||
ctrlpanel: 'True'
|
||||
fc: freq_slider
|
||||
fftsize: '1024'
|
||||
freqhalf: 'True'
|
||||
grid: 'False'
|
||||
gui_hint: ''
|
||||
label: Relative Gain
|
||||
label1: ''
|
||||
label10: ''''''
|
||||
label2: ''''''
|
||||
label3: ''''''
|
||||
label4: ''''''
|
||||
label5: ''''''
|
||||
label6: ''''''
|
||||
label7: ''''''
|
||||
label8: ''''''
|
||||
label9: ''''''
|
||||
legend: 'True'
|
||||
maxoutbuf: '0'
|
||||
minoutbuf: '0'
|
||||
name: '""'
|
||||
nconnections: '1'
|
||||
showports: 'False'
|
||||
tr_chan: '0'
|
||||
tr_level: '0.0'
|
||||
tr_mode: qtgui.TRIG_MODE_FREE
|
||||
tr_tag: '""'
|
||||
type: complex
|
||||
units: dB
|
||||
update_time: '0.10'
|
||||
width1: '1'
|
||||
width10: '1'
|
||||
width2: '1'
|
||||
width3: '1'
|
||||
width4: '1'
|
||||
width5: '1'
|
||||
width6: '1'
|
||||
width7: '1'
|
||||
width8: '1'
|
||||
width9: '1'
|
||||
wintype: firdes.WIN_BLACKMAN_hARRIS
|
||||
ymax: '10'
|
||||
ymin: '-140'
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [648, 288.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
- name: qtgui_waterfall_sink_x_0
|
||||
id: qtgui_waterfall_sink_x
|
||||
parameters:
|
||||
affinity: ''
|
||||
alias: ''
|
||||
alpha1: '1.0'
|
||||
alpha10: '1.0'
|
||||
alpha2: '1.0'
|
||||
alpha3: '1.0'
|
||||
alpha4: '1.0'
|
||||
alpha5: '1.0'
|
||||
alpha6: '1.0'
|
||||
alpha7: '1.0'
|
||||
alpha8: '1.0'
|
||||
alpha9: '1.0'
|
||||
axislabels: 'True'
|
||||
bw: samp_rate
|
||||
color1: '0'
|
||||
color10: '0'
|
||||
color2: '0'
|
||||
color3: '0'
|
||||
color4: '0'
|
||||
color5: '0'
|
||||
color6: '0'
|
||||
color7: '0'
|
||||
color8: '0'
|
||||
color9: '0'
|
||||
comment: ''
|
||||
fc: channel_freq
|
||||
fftsize: '1024'
|
||||
freqhalf: 'True'
|
||||
grid: 'True'
|
||||
gui_hint: ''
|
||||
int_max: '10'
|
||||
int_min: '-140'
|
||||
label1: ''
|
||||
label10: ''
|
||||
label2: ''
|
||||
label3: ''
|
||||
label4: ''
|
||||
label5: ''
|
||||
label6: ''
|
||||
label7: ''
|
||||
label8: ''
|
||||
label9: ''
|
||||
legend: 'True'
|
||||
maxoutbuf: '0'
|
||||
minoutbuf: '0'
|
||||
name: '""'
|
||||
nconnections: '1'
|
||||
showports: 'False'
|
||||
type: complex
|
||||
update_time: '0.10'
|
||||
wintype: firdes.WIN_HAMMING
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [648, 440.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
- name: uhd_usrp_source_0
|
||||
id: uhd_usrp_source
|
||||
parameters:
|
||||
affinity: ''
|
||||
alias: ''
|
||||
ant0: RX2
|
||||
ant1: RX2
|
||||
ant10: RX2
|
||||
ant11: RX2
|
||||
ant12: RX2
|
||||
ant13: RX2
|
||||
ant14: RX2
|
||||
ant15: RX2
|
||||
ant16: RX2
|
||||
ant17: RX2
|
||||
ant18: RX2
|
||||
ant19: RX2
|
||||
ant2: RX2
|
||||
ant20: RX2
|
||||
ant21: RX2
|
||||
ant22: RX2
|
||||
ant23: RX2
|
||||
ant24: RX2
|
||||
ant25: RX2
|
||||
ant26: RX2
|
||||
ant27: RX2
|
||||
ant28: RX2
|
||||
ant29: RX2
|
||||
ant3: RX2
|
||||
ant30: RX2
|
||||
ant31: RX2
|
||||
ant4: RX2
|
||||
ant5: RX2
|
||||
ant6: RX2
|
||||
ant7: RX2
|
||||
ant8: RX2
|
||||
ant9: RX2
|
||||
bw0: samp_rate
|
||||
bw1: '0'
|
||||
bw10: '0'
|
||||
bw11: '0'
|
||||
bw12: '0'
|
||||
bw13: '0'
|
||||
bw14: '0'
|
||||
bw15: '0'
|
||||
bw16: '0'
|
||||
bw17: '0'
|
||||
bw18: '0'
|
||||
bw19: '0'
|
||||
bw2: '0'
|
||||
bw20: '0'
|
||||
bw21: '0'
|
||||
bw22: '0'
|
||||
bw23: '0'
|
||||
bw24: '0'
|
||||
bw25: '0'
|
||||
bw26: '0'
|
||||
bw27: '0'
|
||||
bw28: '0'
|
||||
bw29: '0'
|
||||
bw3: '0'
|
||||
bw30: '0'
|
||||
bw31: '0'
|
||||
bw4: '0'
|
||||
bw5: '0'
|
||||
bw6: '0'
|
||||
bw7: '0'
|
||||
bw8: '0'
|
||||
bw9: '0'
|
||||
center_freq0: freq_slider
|
||||
center_freq1: '0'
|
||||
center_freq10: '0'
|
||||
center_freq11: '0'
|
||||
center_freq12: '0'
|
||||
center_freq13: '0'
|
||||
center_freq14: '0'
|
||||
center_freq15: '0'
|
||||
center_freq16: '0'
|
||||
center_freq17: '0'
|
||||
center_freq18: '0'
|
||||
center_freq19: '0'
|
||||
center_freq2: '0'
|
||||
center_freq20: '0'
|
||||
center_freq21: '0'
|
||||
center_freq22: '0'
|
||||
center_freq23: '0'
|
||||
center_freq24: '0'
|
||||
center_freq25: '0'
|
||||
center_freq26: '0'
|
||||
center_freq27: '0'
|
||||
center_freq28: '0'
|
||||
center_freq29: '0'
|
||||
center_freq3: '0'
|
||||
center_freq30: '0'
|
||||
center_freq31: '0'
|
||||
center_freq4: '0'
|
||||
center_freq5: '0'
|
||||
center_freq6: '0'
|
||||
center_freq7: '0'
|
||||
center_freq8: '0'
|
||||
center_freq9: '0'
|
||||
clock_rate: 0e0
|
||||
clock_source0: ''
|
||||
clock_source1: ''
|
||||
clock_source2: ''
|
||||
clock_source3: ''
|
||||
clock_source4: ''
|
||||
clock_source5: ''
|
||||
clock_source6: ''
|
||||
clock_source7: ''
|
||||
comment: ''
|
||||
dc_offs_enb0: '""'
|
||||
dc_offs_enb1: '""'
|
||||
dc_offs_enb10: '""'
|
||||
dc_offs_enb11: '""'
|
||||
dc_offs_enb12: '""'
|
||||
dc_offs_enb13: '""'
|
||||
dc_offs_enb14: '""'
|
||||
dc_offs_enb15: '""'
|
||||
dc_offs_enb16: '""'
|
||||
dc_offs_enb17: '""'
|
||||
dc_offs_enb18: '""'
|
||||
dc_offs_enb19: '""'
|
||||
dc_offs_enb2: '""'
|
||||
dc_offs_enb20: '""'
|
||||
dc_offs_enb21: '""'
|
||||
dc_offs_enb22: '""'
|
||||
dc_offs_enb23: '""'
|
||||
dc_offs_enb24: '""'
|
||||
dc_offs_enb25: '""'
|
||||
dc_offs_enb26: '""'
|
||||
dc_offs_enb27: '""'
|
||||
dc_offs_enb28: '""'
|
||||
dc_offs_enb29: '""'
|
||||
dc_offs_enb3: '""'
|
||||
dc_offs_enb30: '""'
|
||||
dc_offs_enb31: '""'
|
||||
dc_offs_enb4: '""'
|
||||
dc_offs_enb5: '""'
|
||||
dc_offs_enb6: '""'
|
||||
dc_offs_enb7: '""'
|
||||
dc_offs_enb8: '""'
|
||||
dc_offs_enb9: '""'
|
||||
dev_addr: '""'
|
||||
dev_args: '""'
|
||||
gain0: '0'
|
||||
gain1: '0'
|
||||
gain10: '0'
|
||||
gain11: '0'
|
||||
gain12: '0'
|
||||
gain13: '0'
|
||||
gain14: '0'
|
||||
gain15: '0'
|
||||
gain16: '0'
|
||||
gain17: '0'
|
||||
gain18: '0'
|
||||
gain19: '0'
|
||||
gain2: '0'
|
||||
gain20: '0'
|
||||
gain21: '0'
|
||||
gain22: '0'
|
||||
gain23: '0'
|
||||
gain24: '0'
|
||||
gain25: '0'
|
||||
gain26: '0'
|
||||
gain27: '0'
|
||||
gain28: '0'
|
||||
gain29: '0'
|
||||
gain3: '0'
|
||||
gain30: '0'
|
||||
gain31: '0'
|
||||
gain4: '0'
|
||||
gain5: '0'
|
||||
gain6: '0'
|
||||
gain7: '0'
|
||||
gain8: '0'
|
||||
gain9: '0'
|
||||
iq_imbal_enb0: '""'
|
||||
iq_imbal_enb1: '""'
|
||||
iq_imbal_enb10: '""'
|
||||
iq_imbal_enb11: '""'
|
||||
iq_imbal_enb12: '""'
|
||||
iq_imbal_enb13: '""'
|
||||
iq_imbal_enb14: '""'
|
||||
iq_imbal_enb15: '""'
|
||||
iq_imbal_enb16: '""'
|
||||
iq_imbal_enb17: '""'
|
||||
iq_imbal_enb18: '""'
|
||||
iq_imbal_enb19: '""'
|
||||
iq_imbal_enb2: '""'
|
||||
iq_imbal_enb20: '""'
|
||||
iq_imbal_enb21: '""'
|
||||
iq_imbal_enb22: '""'
|
||||
iq_imbal_enb23: '""'
|
||||
iq_imbal_enb24: '""'
|
||||
iq_imbal_enb25: '""'
|
||||
iq_imbal_enb26: '""'
|
||||
iq_imbal_enb27: '""'
|
||||
iq_imbal_enb28: '""'
|
||||
iq_imbal_enb29: '""'
|
||||
iq_imbal_enb3: '""'
|
||||
iq_imbal_enb30: '""'
|
||||
iq_imbal_enb31: '""'
|
||||
iq_imbal_enb4: '""'
|
||||
iq_imbal_enb5: '""'
|
||||
iq_imbal_enb6: '""'
|
||||
iq_imbal_enb7: '""'
|
||||
iq_imbal_enb8: '""'
|
||||
iq_imbal_enb9: '""'
|
||||
lo_export0: 'False'
|
||||
lo_export1: 'False'
|
||||
lo_export10: 'False'
|
||||
lo_export11: 'False'
|
||||
lo_export12: 'False'
|
||||
lo_export13: 'False'
|
||||
lo_export14: 'False'
|
||||
lo_export15: 'False'
|
||||
lo_export16: 'False'
|
||||
lo_export17: 'False'
|
||||
lo_export18: 'False'
|
||||
lo_export19: 'False'
|
||||
lo_export2: 'False'
|
||||
lo_export20: 'False'
|
||||
lo_export21: 'False'
|
||||
lo_export22: 'False'
|
||||
lo_export23: 'False'
|
||||
lo_export24: 'False'
|
||||
lo_export25: 'False'
|
||||
lo_export26: 'False'
|
||||
lo_export27: 'False'
|
||||
lo_export28: 'False'
|
||||
lo_export29: 'False'
|
||||
lo_export3: 'False'
|
||||
lo_export30: 'False'
|
||||
lo_export31: 'False'
|
||||
lo_export4: 'False'
|
||||
lo_export5: 'False'
|
||||
lo_export6: 'False'
|
||||
lo_export7: 'False'
|
||||
lo_export8: 'False'
|
||||
lo_export9: 'False'
|
||||
lo_source0: internal
|
||||
lo_source1: internal
|
||||
lo_source10: internal
|
||||
lo_source11: internal
|
||||
lo_source12: internal
|
||||
lo_source13: internal
|
||||
lo_source14: internal
|
||||
lo_source15: internal
|
||||
lo_source16: internal
|
||||
lo_source17: internal
|
||||
lo_source18: internal
|
||||
lo_source19: internal
|
||||
lo_source2: internal
|
||||
lo_source20: internal
|
||||
lo_source21: internal
|
||||
lo_source22: internal
|
||||
lo_source23: internal
|
||||
lo_source24: internal
|
||||
lo_source25: internal
|
||||
lo_source26: internal
|
||||
lo_source27: internal
|
||||
lo_source28: internal
|
||||
lo_source29: internal
|
||||
lo_source3: internal
|
||||
lo_source30: internal
|
||||
lo_source31: internal
|
||||
lo_source4: internal
|
||||
lo_source5: internal
|
||||
lo_source6: internal
|
||||
lo_source7: internal
|
||||
lo_source8: internal
|
||||
lo_source9: internal
|
||||
maxoutbuf: '0'
|
||||
minoutbuf: '0'
|
||||
nchan: '1'
|
||||
norm_gain0: 'False'
|
||||
norm_gain1: 'False'
|
||||
norm_gain10: 'False'
|
||||
norm_gain11: 'False'
|
||||
norm_gain12: 'False'
|
||||
norm_gain13: 'False'
|
||||
norm_gain14: 'False'
|
||||
norm_gain15: 'False'
|
||||
norm_gain16: 'False'
|
||||
norm_gain17: 'False'
|
||||
norm_gain18: 'False'
|
||||
norm_gain19: 'False'
|
||||
norm_gain2: 'False'
|
||||
norm_gain20: 'False'
|
||||
norm_gain21: 'False'
|
||||
norm_gain22: 'False'
|
||||
norm_gain23: 'False'
|
||||
norm_gain24: 'False'
|
||||
norm_gain25: 'False'
|
||||
norm_gain26: 'False'
|
||||
norm_gain27: 'False'
|
||||
norm_gain28: 'False'
|
||||
norm_gain29: 'False'
|
||||
norm_gain3: 'False'
|
||||
norm_gain30: 'False'
|
||||
norm_gain31: 'False'
|
||||
norm_gain4: 'False'
|
||||
norm_gain5: 'False'
|
||||
norm_gain6: 'False'
|
||||
norm_gain7: 'False'
|
||||
norm_gain8: 'False'
|
||||
norm_gain9: 'False'
|
||||
num_mboards: '1'
|
||||
otw: ''
|
||||
rx_agc0: Default
|
||||
rx_agc1: Default
|
||||
rx_agc10: Default
|
||||
rx_agc11: Default
|
||||
rx_agc12: Default
|
||||
rx_agc13: Default
|
||||
rx_agc14: Default
|
||||
rx_agc15: Default
|
||||
rx_agc16: Default
|
||||
rx_agc17: Default
|
||||
rx_agc18: Default
|
||||
rx_agc19: Default
|
||||
rx_agc2: Default
|
||||
rx_agc20: Default
|
||||
rx_agc21: Default
|
||||
rx_agc22: Default
|
||||
rx_agc23: Default
|
||||
rx_agc24: Default
|
||||
rx_agc25: Default
|
||||
rx_agc26: Default
|
||||
rx_agc27: Default
|
||||
rx_agc28: Default
|
||||
rx_agc29: Default
|
||||
rx_agc3: Default
|
||||
rx_agc30: Default
|
||||
rx_agc31: Default
|
||||
rx_agc4: Default
|
||||
rx_agc5: Default
|
||||
rx_agc6: Default
|
||||
rx_agc7: Default
|
||||
rx_agc8: Default
|
||||
rx_agc9: Default
|
||||
samp_rate: samp_rate
|
||||
sd_spec0: ''
|
||||
sd_spec1: ''
|
||||
sd_spec2: ''
|
||||
sd_spec3: ''
|
||||
sd_spec4: ''
|
||||
sd_spec5: ''
|
||||
sd_spec6: ''
|
||||
sd_spec7: ''
|
||||
show_lo_controls: 'False'
|
||||
stream_args: ''
|
||||
stream_chans: '[]'
|
||||
sync: sync
|
||||
time_source0: ''
|
||||
time_source1: ''
|
||||
time_source2: ''
|
||||
time_source3: ''
|
||||
time_source4: ''
|
||||
time_source5: ''
|
||||
time_source6: ''
|
||||
time_source7: ''
|
||||
type: fc32
|
||||
states:
|
||||
bus_sink: false
|
||||
bus_source: false
|
||||
bus_structure: null
|
||||
coordinate: [272, 228.0]
|
||||
rotation: 0
|
||||
state: true
|
||||
|
||||
connections:
|
||||
- [lora_lora_receiver_0, frames, lora_message_socket_sink_0, in]
|
||||
- [uhd_usrp_source_0, '0', lora_lora_receiver_0, '0']
|
||||
- [uhd_usrp_source_0, '0', qtgui_freq_sink_x_0, '0']
|
||||
- [uhd_usrp_source_0, '0', qtgui_waterfall_sink_x_0, '0']
|
||||
|
||||
metadata:
|
||||
file_format: 1
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 408 KiB |
|
@ -0,0 +1,657 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# LoRa Scapy layers
|
||||
# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) at @PentHertz
|
||||
|
||||
from scapy.all import *
|
||||
|
||||
class FCtrl_DownLink(Packet):
|
||||
name = "FCtrl_DownLink"
|
||||
fields_desc = [BitField("ADR", 0, 1),
|
||||
BitField("ADRACKReq", 0, 1),
|
||||
BitField("ACK", 0, 1),
|
||||
BitField("FPending", 0, 1),
|
||||
BitFieldLenField("FOptsLen", 0, 4)]
|
||||
|
||||
def extract_padding(self, p):
|
||||
return "", p
|
||||
|
||||
|
||||
class FCtrl_UpLink(Packet):
|
||||
name = "FCtrl_UpLink"
|
||||
fields_desc = [BitField("ADR", 0, 1),
|
||||
BitField("ADRACKReq", 0, 1),
|
||||
BitField("ACK", 0, 1),
|
||||
BitField("ClassB", 0, 1),
|
||||
BitFieldLenField("FOptsLen", 0, 4)]
|
||||
|
||||
def extract_padding(self, p):
|
||||
return "", p
|
||||
|
||||
|
||||
class DevAddrElem(Packet):
|
||||
name = "DevAddrElem"
|
||||
fields_desc = [XByteField("NwkID", 0x0),
|
||||
LEX3BytesField("NwkAddr", b"\x00"*3)]
|
||||
|
||||
def extract_padding(self, p):
|
||||
return "", p
|
||||
|
||||
|
||||
CIDs_up = {0x01: "ResetInd",
|
||||
0x02: "LinkCheckReq",
|
||||
0x03: "LinkADRReq",
|
||||
0x04: "DutyCycleReq",
|
||||
0x05: "RXParamSetupReq",
|
||||
0x06: "DevStatusReq",
|
||||
0x07: "NewChannelReq",
|
||||
0x08: "RXTimingSetupReq",
|
||||
0x09: "TxParamSetupReq", # LoRa 1.1 specs from here
|
||||
0x0A: "DlChannelReq",
|
||||
0x0B: "RekeyInd",
|
||||
0x0C: "ADRParamSetupReq",
|
||||
0x0D: "DeviceTimeReq",
|
||||
0x0E: "ForceRejoinReq",
|
||||
0x0F: "RejoinParamSetupReq"} # end of LoRa 1.1 specs
|
||||
|
||||
|
||||
CIDs_down = {0x01: "ResetConf",
|
||||
0x02: "LinkCheckAns",
|
||||
0x03: "LinkADRAns",
|
||||
0x04: "DutyCycleAns",
|
||||
0x05: "RXParamSetupAns",
|
||||
0x06: "DevStatusAns",
|
||||
0x07: "NewChannelAns",
|
||||
0x08: "RXTimingSetupAns",
|
||||
0x09: "TxParamSetupAns", # LoRa 1.1 specs from here
|
||||
0x0A: "DlChannelAns",
|
||||
0x0B: "RekeyConf",
|
||||
0x0C: "ADRParamSetupAns",
|
||||
0x0D: "DeviceTimeAns",
|
||||
0x0F: "RejoinParamSetupAns"} # end of LoRa 1.1 specs
|
||||
|
||||
|
||||
class ResetInd(Packet):
|
||||
name = "ResetInd"
|
||||
fields_desc = [ByteField("Dev_version", 0)]
|
||||
|
||||
|
||||
class ResetConf(Packet):
|
||||
name = "ResetConf"
|
||||
fields_desc = [ByteField("Serv_version", 0)]
|
||||
|
||||
|
||||
class LinkCheckReq(Packet):
|
||||
name = "LinkCheckReq"
|
||||
fields_desc = []
|
||||
|
||||
|
||||
class LinkCheckAns(Packet):
|
||||
name = "LinkCheckAns"
|
||||
fields_desc = [ByteField("Margin", 0),
|
||||
ByteField("GwCnt", 0)]
|
||||
|
||||
|
||||
class DataRate_TXPower(Packet):
|
||||
name = "DataRate_TXPower"
|
||||
fields_desc = [XBitField("DataRate", 0, 4),
|
||||
XBitField("TXPower", 0, 4)]
|
||||
|
||||
|
||||
class Redundancy(Packet):
|
||||
name = "Redundancy"
|
||||
fields_desc = [XBitField("RFU", 0, 1),
|
||||
XBitField("ChMaskCntl", 0, 3),
|
||||
XBitField("NbTrans", 0, 4)]
|
||||
|
||||
|
||||
class LinkADRReq(Packet):
|
||||
name = "LinkADRReq"
|
||||
fields_desc = [DataRate_TXPower,
|
||||
XShortField("ChMask", 0),
|
||||
Redundancy]
|
||||
|
||||
|
||||
class LinkADRAns_Status(Packet):
|
||||
name = "LinkADRAns_Status"
|
||||
fields_desc = [BitField("RFU", 0, 5),
|
||||
BitField("PowerACK", 0, 1),
|
||||
BitField("ChannelMaskACK", 0, 1)]
|
||||
|
||||
|
||||
class LinkADRAns(Packet):
|
||||
name = "LinkADRAns"
|
||||
fields_desc = [LinkADRAns_Status]
|
||||
|
||||
|
||||
class DutyCyclePL(Packet):
|
||||
name = "DutyCyclePL"
|
||||
fields_desc = [BitField("MaxDCycle", 0, 4)]
|
||||
|
||||
|
||||
class DutyCycleReq(Packet):
|
||||
name = "DutyCycleReq"
|
||||
fields_desc = [DutyCyclePL]
|
||||
|
||||
|
||||
class DutyCycleAns(Packet):
|
||||
name = "DutyCycleAns"
|
||||
fields_desc = []
|
||||
|
||||
|
||||
class DLsettings(Packet):
|
||||
name = "DLsettings"
|
||||
fields_desc = [BitField("RFU", 0, 1),
|
||||
BitField("RX1DRoffset", 0, 3),
|
||||
BitField("RX2DataRate", 0, 4)]
|
||||
|
||||
|
||||
class RXParamSetupReq(Packet):
|
||||
name = "RXParamSetupReq"
|
||||
fields_desc = [DLsettings,
|
||||
X3BytesField("Frequency", 0)]
|
||||
|
||||
|
||||
class RXParamSetupAns_Status(Packet):
|
||||
name = "RXParamSetupAns_Status"
|
||||
fields_desc = [XBitField("RFU", 0, 5),
|
||||
BitField("RX1DRoffsetACK", 0, 1),
|
||||
BitField("RX2DatarateACK", 0, 1),
|
||||
BitField("ChannelACK", 0, 1)]
|
||||
|
||||
|
||||
class RXParamSetupAns(Packet):
|
||||
name = "RXParamSetupAns"
|
||||
fields_desc = [RXParamSetupAns_Status]
|
||||
|
||||
Battery_state = {0: "End-device connected to external source",
|
||||
255: "Battery level unknown"}
|
||||
|
||||
|
||||
class DevStatusReq(Packet):
|
||||
name = "DevStatusReq"
|
||||
fields_desc = [ByteEnumField("Battery", 0, Battery_state),
|
||||
ByteField("Margin", 0)]
|
||||
|
||||
|
||||
class DevStatusAns_Status(Packet):
|
||||
name = "DevStatusAns_Status"
|
||||
fields_desc = [XBitField("RFU", 0, 2),
|
||||
XBitField("Margin", 0, 6)]
|
||||
|
||||
|
||||
class DevStatusAns(Packet):
|
||||
name = "DevStatusAns"
|
||||
fields_desc = [DevStatusAns_Status]
|
||||
|
||||
|
||||
class DrRange(Packet):
|
||||
name = "DrRange"
|
||||
fields_desc = [XBitField("MaxDR", 0, 4),
|
||||
XBitField("MinDR", 0, 4)]
|
||||
|
||||
|
||||
class NewChannelReq(Packet):
|
||||
name = "NewChannelReq"
|
||||
fields_desc = [ByteField("ChIndex", 0),
|
||||
X3BytesField("Freq", 0),
|
||||
DrRange]
|
||||
|
||||
|
||||
class NewChannelAns_Status(Packet):
|
||||
name = "NewChannelAns_Status"
|
||||
fields_desc = [XBitField("RFU", 0, 6),
|
||||
BitField("Dataraterangeok", 0, 1),
|
||||
BitField("Channelfrequencyok", 0, 1)]
|
||||
|
||||
|
||||
class NewChannelAns(Packet):
|
||||
name = "NewChannelAns"
|
||||
fields_desc = [NewChannelAns_Status]
|
||||
|
||||
|
||||
class RXTimingSetupReq_Settings(Packet):
|
||||
name = "RXTimingSetupReq_Settings"
|
||||
fields_desc = [XBitField("RFU", 0, 4),
|
||||
XBitField("Del", 0, 4)]
|
||||
|
||||
|
||||
class RXTimingSetupReq(Packet):
|
||||
name = "RXTimingSetupReq"
|
||||
fields_desc = [RXTimingSetupReq_Settings]
|
||||
|
||||
|
||||
class RXTimingSetupAns(Packet):
|
||||
name = "RXTimingSetupAns"
|
||||
fields_desc = []
|
||||
|
||||
|
||||
# Specific commands for LoRa 1.1 here
|
||||
|
||||
|
||||
MaxEIRPs = {0: "8 dbm",
|
||||
1: "10 dbm",
|
||||
2: "12 dbm",
|
||||
3: "13 dbm",
|
||||
4: "14 dbm",
|
||||
5: "16 dbm",
|
||||
6: "18 dbm",
|
||||
7: "20 dbm",
|
||||
8: "21 dbm",
|
||||
9: "24 dbm",
|
||||
10: "26 dbm",
|
||||
11: "27 dbm",
|
||||
12: "29 dbm",
|
||||
13: "30 dbm",
|
||||
14: "33 dbm",
|
||||
15: "36 dbm"}
|
||||
|
||||
|
||||
DwellTimes = {0: "No limit",
|
||||
1: "400 ms"}
|
||||
|
||||
|
||||
class EIRP_DwellTime(Packet):
|
||||
name = "EIRP_DwellTime"
|
||||
fields_desc = [BitField("RFU", 0b0, 2),
|
||||
BitEnumField("DownlinkDwellTime", 0b0, 1, DwellTimes),
|
||||
BitEnumField("UplinkDwellTime", 0b0, 1, DwellTimes),
|
||||
BitEnumField("MaxEIRP", 0b0000, 4, MaxEIRPs)]
|
||||
|
||||
|
||||
class TxParamSetupReq(Packet):
|
||||
name = "TxParamSetupReq"
|
||||
fields_desc = [EIRP_DwellTime]
|
||||
|
||||
|
||||
class TxParamSetupAns(Packet):
|
||||
name = "TxParamSetupAns"
|
||||
fields_desc = []
|
||||
|
||||
|
||||
class DlChannelReq(Packet):
|
||||
name = "DlChannelReq"
|
||||
fields_desc = [ByteField("ChIndex", 0),
|
||||
X3BytesField("Freq", 0)]
|
||||
|
||||
|
||||
class DlChannelAns(Packet):
|
||||
name = "DlChannelAns"
|
||||
fields_desc = [ByteField("Status", 0)]
|
||||
|
||||
|
||||
class DevLoraWANversion(Packet):
|
||||
name = "DevLoraWANversion"
|
||||
fields_desc = [BitField("RFU", 0b0000, 4),
|
||||
BitField("Minor", 0b0001, 4)]
|
||||
|
||||
|
||||
class RekeyInd(Packet):
|
||||
name = "RekeyInd"
|
||||
fields_desc = [PacketListField("LoRaWANversion", b"",
|
||||
DevLoraWANversion, length_from=lambda pkt:1)]
|
||||
|
||||
|
||||
class RekeyConf(Packet):
|
||||
name = "RekeyConf"
|
||||
fields_desc = [ByteField("ServerVersion", 0)]
|
||||
|
||||
|
||||
class ADRparam(Packet):
|
||||
name = "ADRparam"
|
||||
fields_desc = [BitField("Limit_exp", 0b0000, 4),
|
||||
BitField("Delay_exp", 0b0000, 4)]
|
||||
|
||||
|
||||
class ADRParamSetupReq(Packet):
|
||||
name = "ADRParamSetupReq"
|
||||
fields_desc = [ADRparam]
|
||||
|
||||
|
||||
class ADRParamSetupAns(Packet):
|
||||
name = "ADRParamSetupReq"
|
||||
fields_desc = []
|
||||
|
||||
|
||||
class DeviceTimeReq(Packet):
|
||||
name = "DeviceTimeReq"
|
||||
fields_desc = []
|
||||
|
||||
|
||||
class DeviceTimeAns(Packet):
|
||||
name = "DeviceTimeAns"
|
||||
fields_desc = [IntField("SecondsSinceEpoch", 0),
|
||||
ByteField("FracSecond", 0x00)]
|
||||
|
||||
|
||||
class ForceRejoinReq(Packet):
|
||||
name ="ForceRejoinReq"
|
||||
fields_desc = [BitField("RFU", 0, 2),
|
||||
BitField("Period", 0, 3),
|
||||
BitField("Max_Retries", 0, 3),
|
||||
BitField("RFU", 0, 1),
|
||||
BitField("RejoinType", 0, 3),
|
||||
BitField("DR", 0, 4)]
|
||||
|
||||
|
||||
class RejoinParamSetupReq(Packet):
|
||||
name = "RejoinParamSetupReq"
|
||||
fields_desc = [BitField("MaxTimeN", 0, 4),
|
||||
BitField("MaxCountN", 0, 4)]
|
||||
|
||||
|
||||
class RejoinParamSetupAns(Packet):
|
||||
name = "RejoinParamSetupAns"
|
||||
fields_desc = [BitField("RFU", 0, 7),
|
||||
BitField("TimeOK", 0, 1)]
|
||||
|
||||
|
||||
# End of specific 1.1 commands
|
||||
|
||||
|
||||
class MACCommand_up(Packet):
|
||||
name = "MACCommand_up"
|
||||
fields_desc = [ByteEnumField("CID", 0, CIDs_up),
|
||||
ConditionalField(PacketListField("Reset", b"",
|
||||
ResetInd,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x01)),
|
||||
ConditionalField(PacketListField("LinkCheck", b"",
|
||||
LinkCheckReq,
|
||||
length_from=lambda pkt:0),
|
||||
lambda pkt:(pkt.CID == 0x02)),
|
||||
ConditionalField(PacketListField("LinkADR", b"",
|
||||
LinkADRReq,
|
||||
length_from=lambda pkt:4),
|
||||
lambda pkt:(pkt.CID == 0x03)),
|
||||
ConditionalField(PacketListField("DutyCycle", b"",
|
||||
DutyCycleReq,
|
||||
length_from=lambda pkt:4),
|
||||
lambda pkt:(pkt.CID == 0x04)),
|
||||
ConditionalField(PacketListField("RXParamSetup", b"",
|
||||
RXParamSetupReq,
|
||||
length_from=lambda pkt:4),
|
||||
lambda pkt:(pkt.CID == 0x05)),
|
||||
ConditionalField(PacketListField("DevStatus", b"",
|
||||
DevStatusReq,
|
||||
length_from=lambda pkt:2),
|
||||
lambda pkt:(pkt.CID == 0x06)),
|
||||
ConditionalField(PacketListField("NewChannel", b"",
|
||||
NewChannelReq,
|
||||
length_from=lambda pkt:5),
|
||||
lambda pkt:(pkt.CID == 0x07)),
|
||||
ConditionalField(PacketListField("RXTimingSetup", b"",
|
||||
RXTimingSetupReq,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x08)),
|
||||
ConditionalField(PacketListField("TxParamSetup", b"", # specific to 1.1 from here
|
||||
TxParamSetupReq,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x09)),
|
||||
ConditionalField(PacketListField("DlChannel", b"",
|
||||
DlChannelReq,
|
||||
length_from=lambda pkt:4),
|
||||
lambda pkt:(pkt.CID == 0x0A)),
|
||||
ConditionalField(PacketListField("Rekey", b"",
|
||||
RekeyInd,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x0B)),
|
||||
ConditionalField(PacketListField("ADRParamSetup", b"",
|
||||
ADRParamSetupReq,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x0C)),
|
||||
ConditionalField(PacketListField("DeviceTime", b"",
|
||||
DeviceTimeReq,
|
||||
length_from=lambda pkt:0),
|
||||
lambda pkt:(pkt.CID == 0x0D)),
|
||||
ConditionalField(PacketListField("ForceRejoin", b"",
|
||||
ForceRejoinReq,
|
||||
length_from=lambda pkt:2),
|
||||
lambda pkt:(pkt.CID == 0x0E)),
|
||||
ConditionalField(PacketListField("RejoinParamSetup", b"",
|
||||
RejoinParamSetupReq,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x0F))]
|
||||
|
||||
def extract_padding(self, p):
|
||||
return "", p
|
||||
|
||||
|
||||
class MACCommand_down(Packet):
|
||||
name = "MACCommand_down"
|
||||
fields_desc = [ByteEnumField("CID", 0, CIDs_up),
|
||||
ConditionalField(PacketListField("Reset", b"",
|
||||
ResetConf,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x01)),
|
||||
ConditionalField(PacketListField("LinkCheck", b"",
|
||||
LinkCheckAns,
|
||||
length_from=lambda pkt:2),
|
||||
lambda pkt:(pkt.CID == 0x02)),
|
||||
ConditionalField(PacketListField("LinkADR", b"",
|
||||
LinkADRAns,
|
||||
length_from=lambda pkt:0),
|
||||
lambda pkt:(pkt.CID == 0x03)),
|
||||
ConditionalField(PacketListField("DutyCycle", b"",
|
||||
DutyCycleAns,
|
||||
length_from=lambda pkt:4),
|
||||
lambda pkt:(pkt.CID == 0x04)),
|
||||
ConditionalField(PacketListField("RXParamSetup", b"",
|
||||
RXParamSetupAns,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x05)),
|
||||
ConditionalField(PacketListField("DevStatusAns", b"",
|
||||
RXParamSetupAns,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x06)),
|
||||
ConditionalField(PacketListField("NewChannel", b"",
|
||||
NewChannelAns,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x07)),
|
||||
ConditionalField(PacketListField("RXTimingSetup", b"",
|
||||
RXTimingSetupAns,
|
||||
length_from=lambda pkt:0),
|
||||
lambda pkt:(pkt.CID == 0x08)),
|
||||
ConditionalField(PacketListField("TxParamSetup", b"",
|
||||
TxParamSetupAns,
|
||||
length_from=lambda pkt:0),
|
||||
lambda pkt:(pkt.CID == 0x09)),
|
||||
ConditionalField(PacketListField("DlChannel", b"",
|
||||
DlChannelAns,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x0A)),
|
||||
ConditionalField(PacketListField("Rekey", b"",
|
||||
RekeyConf,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x0B)),
|
||||
ConditionalField(PacketListField("ADRParamSetup", b"",
|
||||
ADRParamSetupAns,
|
||||
length_from=lambda pkt:0),
|
||||
lambda pkt:(pkt.CID == 0x0C)),
|
||||
ConditionalField(PacketListField("DeviceTime", b"",
|
||||
DeviceTimeAns,
|
||||
length_from=lambda pkt:5),
|
||||
lambda pkt:(pkt.CID == 0x0D)),
|
||||
ConditionalField(PacketListField("RejoinParamSetup", b"",
|
||||
RejoinParamSetupAns,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.CID == 0x0F))]
|
||||
|
||||
def extract_padding(self, p):
|
||||
return "", p
|
||||
|
||||
class FOpts(Packet):
|
||||
name = "FOpts"
|
||||
fields_desc = [ConditionalField(PacketListField("FOpts_up", b"",
|
||||
MACCommand_up, # piggybacked MAC Command for uplink
|
||||
length_from=lambda pkt:pkt.FCtrl[0].FOptsLen),
|
||||
lambda pkt:(pkt.FCtrl[0].FOptsLen > 0
|
||||
and pkt.MType & 0b1 == 0
|
||||
and pkt.MType >= 0b010)),
|
||||
ConditionalField(PacketListField("FOpts_down", b"",
|
||||
MACCommand_down, # piggybacked MAC Command for downlink
|
||||
length_from=lambda pkt:pkt.FCtrl[0].FOptsLen),
|
||||
lambda pkt:(pkt.FCtrl[0].FOptsLen > 0
|
||||
and pkt.MType & 0b1 == 1
|
||||
and pkt.MType <= 0b101))]
|
||||
|
||||
def FOptsShow(pkt):
|
||||
try:
|
||||
if pkt.FCtrl[0].FOptsLen > 0 and pkt.MType & 0b1 == 0 and pkt.MType >= 0b010:
|
||||
return True
|
||||
elif pkt.FCtrl[0].FOptsLen > 0 and pkt.MType & 0b1 == 1 and pkt.MType <= 0b101:
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
class FHDR(Packet):
|
||||
name = "FHDR"
|
||||
fields_desc = [ConditionalField(PacketListField("DevAddr", b"", DevAddrElem,
|
||||
length_from=lambda pkt:4),
|
||||
lambda pkt:(pkt.MType >= 0b010
|
||||
and pkt.MType <= 0b101)),
|
||||
ConditionalField(PacketListField("FCtrl", b"",
|
||||
FCtrl_DownLink,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.MType & 0b1 == 1
|
||||
and pkt.MType <= 0b101)),
|
||||
ConditionalField(PacketListField("FCtrl", b"",
|
||||
FCtrl_UpLink,
|
||||
length_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.MType & 0b1 == 0
|
||||
and pkt.MType >= 0b010)),
|
||||
ConditionalField(LEShortField("FCnt", 0),
|
||||
lambda pkt:(pkt.MType >= 0b010
|
||||
and pkt.MType <= 0b101)),
|
||||
ConditionalField(PacketListField("FOpts_up", b"",
|
||||
MACCommand_up,
|
||||
length_from=lambda pkt:pkt.FCtrl[0].FOptsLen),
|
||||
lambda pkt:FOptsShow(pkt)),
|
||||
ConditionalField(PacketListField("FOpts_down", b"",
|
||||
MACCommand_down,
|
||||
length_from=lambda pkt:pkt.FCtrl[0].FOptsLen),
|
||||
lambda pkt:FOptsShow(pkt))]
|
||||
|
||||
|
||||
FPorts = {0: "NwkSKey"} # anything else is AppSKey
|
||||
|
||||
|
||||
JoinReqTypes = {0xFF: "Join-request",
|
||||
0x00: "Rejoin-request type 0",
|
||||
0x01: "Rejoin-request type 1",
|
||||
0x02: "Rejoin-request type 2"}
|
||||
|
||||
|
||||
class Join_Request(Packet):
|
||||
name = "Join_Request"
|
||||
fields_desc = [StrFixedLenField("AppEUI", b"\x00" * 8, 8),
|
||||
StrFixedLenField("DevEUI", b"\00" * 8, 8),
|
||||
LEShortField("DevNonce", 0x0000)]
|
||||
|
||||
|
||||
class DLsettings(Packet):
|
||||
name = "DLsettings"
|
||||
fields_desc = [BitField("OptNeg", 0, 1),
|
||||
XBitField("RX1DRoffset", 0, 3),
|
||||
XBitField("RX2_Data_rate", 0, 4)]
|
||||
|
||||
|
||||
class Join_Accept(Packet):
|
||||
name = "Join_Accept"
|
||||
dcflist = False
|
||||
fields_desc = [LEX3BytesField("JoinAppNonce", 0),
|
||||
LEX3BytesField("NetID", 0),
|
||||
XLEIntField("DevAddr", 0),
|
||||
DLsettings,
|
||||
XByteField("RxDelay", 0),
|
||||
ConditionalField(StrFixedLenField("CFList", b"\x00" * 16 , 16),
|
||||
lambda pkt:(Join_Accept.dcflist is True))]
|
||||
|
||||
def extract_padding(self, p):
|
||||
return "", p
|
||||
|
||||
def __init__(self, packet=""): # CFlist calculated with on rest packet len
|
||||
if len(packet) > 18:
|
||||
Join_Accept.dcflist = True
|
||||
return super(Join_Accept, self).__init__(packet)
|
||||
|
||||
|
||||
RejoinType = {0: "NetID+DevEUI",
|
||||
1: "JoinEUI+DevEUI",
|
||||
2: "NetID+DevEUI"}
|
||||
|
||||
|
||||
def RejoinReq(Packet): # LoRa 1.1 specs
|
||||
name = "RejoinReq"
|
||||
fields_desc = [ByteField("Type", 0),
|
||||
X3BytesField("NetID", 0),
|
||||
StrFixedLenField("DevEUI", b"\x00" * 8),
|
||||
XShortField("RJcount0", 0)]
|
||||
|
||||
|
||||
class FRMPayload(Packet):
|
||||
name = "FRMPayload"
|
||||
fields_desc = [ConditionalField(StrField("DataPayload", 0, remain=4), # Downlink
|
||||
lambda pkt:(pkt.MType == 0b101
|
||||
or pkt.MType == 0b011)),
|
||||
ConditionalField(StrField("DataPayload", 0, remain=6), # Uplink
|
||||
lambda pkt:(pkt.MType == 0b100
|
||||
or pkt.MType == 0b010)),
|
||||
ConditionalField(PacketListField("Join_Request_Field", b"",
|
||||
Join_Request,
|
||||
length_from=lambda pkt:18),
|
||||
lambda pkt:(pkt.MType == 0b000)),
|
||||
ConditionalField(PacketListField("Join_Accept_Field", b"",
|
||||
Join_Accept,
|
||||
count_from=lambda pkt:1),
|
||||
lambda pkt:(pkt.MType == 0b001
|
||||
and LoRa.encrypted is False)),
|
||||
ConditionalField(StrField("Join_Accept_Encrypted", 0),
|
||||
lambda pkt:(pkt.MType == 0b001 and LoRa.encrypted is True))]
|
||||
|
||||
|
||||
class MACPayload(Packet):
|
||||
name = "MACPayload"
|
||||
fields_desc = [FHDR,
|
||||
ConditionalField(ByteEnumField("FPort", 0, FPorts),
|
||||
lambda pkt:(pkt.MType >= 0b010 and pkt.MType <= 0b101)),
|
||||
FRMPayload]
|
||||
|
||||
|
||||
MTypes = {0b000: "Join-request",
|
||||
0b001: "Join-accept",
|
||||
0b010: "Unconfirmed Data Up",
|
||||
0b011: "Unconfirmed Data Down",
|
||||
0b100: "Confirmed Data Up",
|
||||
0b101: "Confirmed Data Down",
|
||||
0b110: "Rejoin-request", # Only in LoRa 1.1 specs
|
||||
0b111: "Proprietary"}
|
||||
|
||||
|
||||
class MHDR(Packet): # same for 1.0 and 1.1
|
||||
name = "MHDR"
|
||||
fields_desc = [BitEnumField("MType", 0b000, 3, MTypes),
|
||||
BitField("RFU", 0b000, 3),
|
||||
BitField("Major", 0b00, 2)]
|
||||
|
||||
|
||||
class PHYPayload(Packet):
|
||||
name = "PHYPayload"
|
||||
fields_desc = [MHDR,
|
||||
MACPayload,
|
||||
ConditionalField(XIntField("MIC", 0),
|
||||
lambda pkt:(pkt.MType != 0b001
|
||||
or LoRa.encrypted is False))]
|
||||
|
||||
|
||||
class LoRa(Packet): # default frame (unclear specs => taken from https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5677147/)
|
||||
name = "LoRa"
|
||||
version = "1.1" # default version to parse
|
||||
encrypted = True
|
||||
fields_desc = [XBitField("Preamble", 0, 4),
|
||||
XBitField("PHDR", 0, 16),
|
||||
XBitField("PHDR_CRC", 0, 4),
|
||||
PHYPayload,
|
||||
ConditionalField(XShortField("CRC", 0),
|
||||
lambda pkt:(pkt.MType & 0b1 == 0))]
|
|
@ -0,0 +1,64 @@
|
|||
# LoRa Cryto utils
|
||||
# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) at @PentHertz
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import CMAC
|
||||
import binascii
|
||||
|
||||
def JoinAcceptPayload_decrypt(key, hexpkt):
|
||||
"""
|
||||
Decrypt Join Accept payloads
|
||||
In(1): String 128 bits key
|
||||
In(2): String packet
|
||||
Out: String decrypted Join accept packet
|
||||
"""
|
||||
payload = hexpkt[4:]
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
return cipher.encrypt(payload) # logic right? :D
|
||||
|
||||
def JoinAcceptPayload_encrypt(key, hexpkt):
|
||||
"""
|
||||
Encrypts Join Accept Payload
|
||||
In(1): String 128 bits key
|
||||
In(2): String packet
|
||||
Out: String encrypted Join accept payload
|
||||
"""
|
||||
payload = hexpkt[4:]
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
return cipher.decrypt(payload)
|
||||
|
||||
def getPHY_CMAC(key, hexpkt, direction=1):
|
||||
"""
|
||||
Compute MIC with AES CMAC
|
||||
In(1): String 128 bits key for CMAC
|
||||
In(2): hexstring of the packet
|
||||
In(3): Direction (1: network, 0: end device)
|
||||
Out: Hexdigest of computed MIC
|
||||
"""
|
||||
lowoff = -4
|
||||
if direction == 0:
|
||||
lowoff = -6 # skip the CRC
|
||||
payload = hexpkt[3:lowoff]
|
||||
cobj = CMAC.new(key, ciphermod=AES)
|
||||
toret = cobj.update(payload).hexdigest()
|
||||
return toret[:8]
|
||||
|
||||
def checkMIC(key, hexpkt, direction=1):
|
||||
"""
|
||||
Check MIC in the packet
|
||||
In(1): String key for CMAC
|
||||
In(2): String of the packet
|
||||
In(3): Direction (1: network, 0: end device)
|
||||
Out: True if key is correct, False otherwise
|
||||
"""
|
||||
mic = hexpkt[-4:]
|
||||
if direction == 0:
|
||||
mic = hexpkt[-6:-2] # skip the CRC
|
||||
try:
|
||||
binascii.unhexlify(mic)
|
||||
except:
|
||||
mic = binascii.hexlify(mic)
|
||||
cmic = getPHY_CMAC(key, hexpkt)
|
||||
return (mic == cmic)
|
||||
|
||||
#TODO: more helpers
|
Loading…
Reference in New Issue