Skip to content

Commit f5ece32

Browse files
Tryton77nordicjm
authored andcommitted
scripts: esb_sniffer: Plugins for Wireshark
Added wireshark extcap interface and dissector for ESB Added scripts to handle communication with the DK JIRA: NCSDK-34713 Signed-off-by: Michał Strządała <[email protected]>
1 parent 704a99e commit f5ece32

File tree

11 files changed

+838
-1
lines changed

11 files changed

+838
-1
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@
715715
/scripts/generate_psa_key_attributes.py @nrfconnect/ncs-aurora
716716
/scripts/tests/ @nrfconnect/ncs-pluto @fundakol
717717
/scripts/vale/ @francescoser
718+
/scripts/esb_sniffer/ @nrfconnect/ncs-si-muffin
718719

719720
/scripts/docker/*.rst @nrfconnect/ncs-doc-leads
720721
/scripts/hid_configurator/*.rst @nrfconnect/ncs-si-bluebagel-doc
@@ -723,6 +724,7 @@
723724
/scripts/partition_manager/*.rst @nrfconnect/ncs-aurora-doc
724725
/scripts/shell/ble_console/**/*.rst @nrfconnect/ncs-doc-leads
725726
/scripts/west_commands/sbom/*.rst @nrfconnect/ncs-co-doc @nrfconnect/ncs-doc-leads
727+
/scripts/esb_sniffer/*.rst @nrfconnect/ncs-si-muffin-doc
726728
/scripts/runners/nrf_common_next.py @nrfconnect/ncs-ci
727729
/scripts/runners/nrfutil_next.py @nrfconnect/ncs-ci
728730

doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ See the changelog for each library in the :doc:`nrfxlib documentation <nrfxlib:R
525525
Scripts
526526
=======
527527

528-
|no_changes_yet_note|
528+
* Added the :ref:`esb_sniffer_scripts` scripts for the :ref:`esb_monitor` sample.
529529

530530
Integrations
531531
============

doc/nrf/scripts.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ Here you can find documentation for these scripts.
1919
../../scripts/partition_manager/*
2020
../../scripts/west_commands/sbom/*
2121
../../scripts/memfault/*
22+
../../scripts/esb_sniffer/*

doc/nrf/shortcuts.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,5 @@
274274
.. |matter_cluster_editor_preview| replace:: The Matter Cluster Editor app is currently in the preview stage.
275275

276276
.. |ISE| replace:: IronSide SE
277+
278+
.. |ESB| replace:: Enhanced ShockBurst

scripts/esb_sniffer/README.rst

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
.. _esb_sniffer_scripts:
2+
3+
|ESB| Sniffer
4+
#############
5+
6+
.. contents::
7+
:local:
8+
:depth: 2
9+
10+
The Python scripts introduced in this document are used for the :ref:`esb_monitor` sample configured as |ESB| Sniffer.
11+
12+
Overview
13+
********
14+
15+
There are two separate scripts you can use with the DK configured as an |ESB| sniffer:
16+
17+
* ``main.py`` provides integration with the Wireshark extcap interface and the UART shell for real-time updates of sniffer parameters such as:
18+
19+
* Bitrate
20+
* Channel
21+
* Radio addresses
22+
* Pipe prefixes
23+
* Enabled pipes
24+
25+
* ``capture_to_pcap.py`` is a simple CLI utility to read packets from the DK and save them into pcap formatted file for further analysis.
26+
27+
Requirements
28+
************
29+
30+
The script source files are located in the :file:`scripts/esb_sniffer` directory.
31+
Complete the following steps to install scripts requirements:
32+
33+
1. Install the Python requirements:
34+
35+
.. code-block:: console
36+
37+
pip3 install -r nrf/scripts/esb_sniffer/requirements.txt
38+
39+
#. Install `Wireshark`_.
40+
41+
Set up Wireshark
42+
****************
43+
44+
Complete the following steps to set up Wireshark:
45+
46+
1. Enter `nrf/scripts/esb_sniffer` directory.
47+
48+
#. Add a custom plugins to Wireshark:
49+
50+
.. tabs::
51+
52+
.. group-tab:: Linux
53+
54+
.. code-block:: console
55+
56+
mkdir -p $HOME/.local/lib/wireshark/{extcap,plugins}
57+
cp esb_dissector.lua $HOME/.local/lib/wireshark/plugins
58+
cp extcap/esb_extcap.py $HOME/.local/lib/wireshark/extcap
59+
60+
.. group-tab:: Windows
61+
62+
Copy the :file:`esb_dissector.lua` file into the :file:`%APPDATA%\\Wireshark\\plugins` directory.
63+
64+
#. Enable the dissector for |ESB|:
65+
66+
a. Open Wireshark.
67+
#. Go to :guilabel:`Edit` -> :guilabel:`Preferences` -> :guilabel:`Protocols` -> :guilabel:`DLT_USER` -> :guilabel:`Edit`.
68+
#. Click the :guilabel:`Create new entry` icon (bottom left).
69+
#. Select ``DLT=147`` for **DLT** column and ``esb`` for **Payload dissector** column.
70+
#. Click :guilabel:`Ok`.
71+
#. Restart Wireshark.
72+
73+
After completing these steps, a new |ESB| sniffer interface appears in Wireshark.
74+
75+
main.py
76+
*******
77+
78+
This script works on Linux only.
79+
80+
Complete the following steps to use this script:
81+
82+
1. Start the script:
83+
84+
.. code-block:: console
85+
86+
python3 main.py
87+
88+
#. Start Wireshark and select the |ESB| sniffer interface.
89+
#. Observe the packets being received in Wireshark in real time.
90+
#. Type ``q`` or ``quit`` to stop the application.
91+
92+
capture_to_pcap.py
93+
******************
94+
95+
This script is not designed to work with a live Wireshark capture.
96+
You can capture packets into a file and open it in Wireshark later.
97+
98+
Complete the following steps to use this script:
99+
100+
1. Start the script with the output filename as an argument:
101+
102+
.. code-block:: console
103+
104+
python3 capture_to_pcap.py output.pcap
105+
106+
#. Type ``q`` or ``quit`` to stop the application.
107+
108+
Dependencies
109+
************
110+
111+
The scripts use the ``pynrfjprog`` and ``pyserial`` libraries to communicate with the DK, and `Wireshark`_ as tool for visualizing |ESB| packets.

scripts/esb_sniffer/Sniffer.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#
2+
# Copyright (c) 2025 Nordic Semiconductor ASA
3+
#
4+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
5+
#
6+
7+
from pynrfjprog.Parameters import CoProcessor
8+
from pynrfjprog.APIError import APIError
9+
from pynrfjprog.LowLevel import API
10+
from time import sleep
11+
from enum import Enum
12+
import logging
13+
import struct
14+
15+
class RttCommands(Enum):
16+
SNIFFER_START = 1
17+
SNIFFER_STOP = 2
18+
SNIFFER_STATUS = 3
19+
20+
class Sniffer():
21+
'''API to communicate with the DK.'''
22+
def __init__(self, packet_len: int=45, rtt_channels: dict={"data_down":1, "comm_down":2, "comm_up":1}, read_at_once: int=2000, swd_freq_khz: int=8000, log_lvl=logging.INFO):
23+
self.jlink = None
24+
self.remaining_data = None
25+
self.packet_len = packet_len
26+
self.rtt_ch = rtt_channels
27+
self.read_at_once = read_at_once
28+
self.swd_freq_khz = swd_freq_khz
29+
self.logger = logging.getLogger("ESBSniffer")
30+
self.logger.setLevel(log_lvl)
31+
logging.basicConfig()
32+
33+
def __get_packet_length(self) -> int:
34+
'''Get packet length from the DK.'''
35+
timeout = 2
36+
try:
37+
# wait for device to settle packet_length in rtt buffer,
38+
# because rrt_read is no blocking and can read 0 instead
39+
sleep(0.4)
40+
ret = self.jlink.rtt_read(self.rtt_ch["comm_down"], 4, encoding=None)
41+
while (len(ret) == 0 and timeout > 0):
42+
sleep(0.2)
43+
timeout -= 0.2
44+
ret = self.jlink.rtt_read(self.rtt_ch["comm_down"], 4, encoding=None)
45+
except APIError as err:
46+
self.logger.error("Failed to get packet length from sniffer: err = %d, using default %d", err.err_code, self.packet_len)
47+
return -1
48+
49+
if len(ret) == 4:
50+
length = int.from_bytes(ret, byteorder='big', signed=False)
51+
else:
52+
self.logger.error("Failed to get packet length from sniffer, using default %d", self.packet_len)
53+
return -1
54+
55+
if length != 0:
56+
self.packet_len = length
57+
58+
return 0
59+
60+
def connect(self) -> int:
61+
'''Connect to the DK.'''
62+
timeout = 5
63+
64+
if self.jlink is not None:
65+
return 0
66+
67+
try:
68+
self.jlink = API()
69+
self.jlink.open()
70+
self.jlink.connect_to_emu_without_snr(jlink_speed_khz=self.swd_freq_khz)
71+
72+
if self.jlink.read_device_family() == "NRF53":
73+
self.jlink.select_coprocessor(CoProcessor.CP_NETWORK)
74+
75+
self.jlink.sys_reset()
76+
self.jlink.go()
77+
self.jlink.rtt_start()
78+
except APIError as err:
79+
self.logger.error("Failed to connect to device: err = %d", err.err_code)
80+
self.jlink = None
81+
return -1
82+
83+
while (not self.jlink.rtt_is_control_block_found() and timeout > 0):
84+
sleep(1)
85+
timeout -= 1
86+
87+
if timeout == 0:
88+
self.logger.error("Failed to find rtt control block")
89+
self.jlink = None
90+
return -1
91+
92+
93+
self.__get_packet_length()
94+
self.logger.info("Connected to device")
95+
return 0
96+
97+
def disconnect(self):
98+
'''Disconnect from the DK.'''
99+
try:
100+
self.jlink.close()
101+
except APIError:
102+
self.logger.error("JLink connection lost")
103+
104+
self.jlink = None
105+
self.logger.info("Disconnected from device")
106+
107+
def __gen_pcap_packets(self, buff: bytearray) -> list:
108+
'''Parse data from DK into pcap formatted packets.'''
109+
if len(buff) < self.packet_len:
110+
return b''
111+
112+
packets = []
113+
114+
# Since rtt_read(self.packet_len) can read fragment of the packet,
115+
# ensure that we will parse it correctly
116+
if self.remaining_data is not None:
117+
buff = self.remaining_data + buff
118+
self.remaining_data = None
119+
120+
if len(buff) % self.packet_len != 0:
121+
start = int(len(buff)/self.packet_len) * self.packet_len
122+
self.remaining_data = buff[start:]
123+
124+
for i in range(0, int(len(buff)/self.packet_len)):
125+
pkt = buff[i*self.packet_len:(i+1)*self.packet_len]
126+
if pkt != b'':
127+
timestamp_ms = int.from_bytes(pkt[0:4], byteorder='big', signed=False)
128+
timestamp_us = int.from_bytes(pkt[4:8], byteorder='big', signed=False)
129+
130+
sec = int(timestamp_ms / 1000)
131+
usec = int(timestamp_us+((timestamp_ms % 1000) * 1e3))
132+
data_length = pkt[8]
133+
pkt_len = 5 + data_length
134+
135+
pkt_header = struct.pack('<IIII', sec, usec, pkt_len, pkt_len)
136+
packets.append(pkt_header + pkt[8:8+pkt_len])
137+
138+
return packets
139+
140+
def __send_command(self, command: RttCommands) -> int:
141+
'''Send command to the DK through RTT.'''
142+
bin_command = struct.pack("B", command.value)
143+
144+
try:
145+
self.jlink.rtt_write(self.rtt_ch["comm_up"], bin_command, None)
146+
147+
timeout = 2
148+
ret = self.jlink.rtt_read(self.rtt_ch["comm_down"], 4, encoding=None)
149+
while (len(ret) == 0 and timeout > 0):
150+
sleep(0.2)
151+
timeout -= 0.2
152+
ret = self.jlink.rtt_read(self.rtt_ch["comm_down"], 4, encoding=None)
153+
154+
if len(ret) == 4:
155+
ret = int.from_bytes(ret[0:4], byteorder='big', signed=False)
156+
return ret
157+
else:
158+
self.logger.error("Connection timeout")
159+
return -1
160+
except APIError as err:
161+
self.logger.error("Jlink error while sending command, %d", err.err_code)
162+
return -1
163+
164+
def start(self) -> int:
165+
'''Send start command to the DK.'''
166+
if self.jlink is None:
167+
raise IOError
168+
169+
ret = self.__send_command(RttCommands.SNIFFER_START)
170+
171+
if ret < 0:
172+
self.logger.error("Failed to start sniffer")
173+
return -1
174+
175+
self.logger.info("Sniffer started")
176+
return 0
177+
178+
def stop(self) -> int:
179+
'''Send stop command to the DK.'''
180+
if self.jlink is None:
181+
raise IOError
182+
183+
ret = self.__send_command(RttCommands.SNIFFER_STOP)
184+
185+
if ret < 0:
186+
self.logger.error("Failed to stop sniffer, please restart device to clear receive buffer")
187+
return -1
188+
189+
self.logger.info("Sniffer stopped")
190+
return 0
191+
192+
def get_status(self) -> str:
193+
'''Send status command to the DK.'''
194+
if self.jlink is None:
195+
raise IOError
196+
197+
ret = self.__send_command(RttCommands.SNIFFER_STATUS)
198+
199+
if ret < 0:
200+
self.logger.error("Failed to check sniffer status")
201+
return -1
202+
203+
if ret == 0:
204+
return "Stopped"
205+
else:
206+
return "Running"
207+
208+
def read(self) -> bytearray:
209+
'''Read data from the DK.'''
210+
if self.jlink is None:
211+
raise IOError
212+
213+
try:
214+
buff = self.jlink.rtt_read(self.rtt_ch["data_down"], self.read_at_once*self.packet_len, encoding=None)
215+
except APIError as err:
216+
self.logger.error("JLink connection error while reading packets: err = %d", err.err_code)
217+
raise IOError
218+
219+
packets = self.__gen_pcap_packets(buff)
220+
221+
return bytearray(b''.join([bytes(i) for i in packets]))
222+
223+
def _get_com_port(self) -> 'str|None':
224+
'''Find serial ports of the connected DK.'''
225+
if self.jlink is None:
226+
raise IOError
227+
228+
try:
229+
sn = self.jlink.read_connected_emu_snr()
230+
family = self.jlink.read_device_family()
231+
ports = self.jlink.enum_emu_com_ports(sn)
232+
if len(ports) == 0:
233+
self.logger.info("Device does not expose serial ports")
234+
return None
235+
236+
if family in ("NRF54L", "NRF54H"):
237+
return ports[0].path
238+
elif family == "NRF53":
239+
return ports[1].path
240+
elif family == "NRF52":
241+
return ports[0].path
242+
else:
243+
self.logger.error("Unsupported device family!")
244+
return None
245+
246+
except APIError:
247+
self.logger.error("Failed to read available com ports")
248+
return None

0 commit comments

Comments
 (0)