nixos-configuration/modules/wireshark/extcap/SnifferAPI/SnifferCollector.py
2025-11-13 00:53:25 +01:00

283 lines
12 KiB
Python

# Copyright (c) Nordic Semiconductor ASA
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form, except as embedded into a Nordic
# Semiconductor ASA integrated circuit in a product or a software update for
# such product, must reproduce the above copyright notice, this list of
# conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# 3. Neither the name of Nordic Semiconductor ASA nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# 4. This software, with or without modification, must only be used with a
# Nordic Semiconductor ASA integrated circuit.
#
# 5. Any software provided in binary form under this license must not be reverse
# engineered, decompiled, modified and/or disassembled.
#
# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from . import Packet, Exceptions, CaptureFiles, Devices, Notifications
import time, sys, threading, subprocess, os, logging, copy
from serial import SerialException
from .Types import *
STATE_INITIALIZING = 0
STATE_SCANNING = 1
STATE_FOLLOWING = 2
class SnifferCollector(Notifications.Notifier):
def __init__(self, portnum=None, baudrate=None, *args, **kwargs):
Notifications.Notifier.__init__(self, *args, **kwargs)
self._portnum = portnum
self._fwversion = "Unknown version"
self._setState(STATE_INITIALIZING)
self._captureHandler = CaptureFiles.CaptureFileHandler(capture_file_path=kwargs.get("capture_file_path", None))
self._exit = False
self._connectionAccessAddress = None
self._packetListLock = threading.RLock()
with self._packetListLock:
self._packets = []
self._packetReader = Packet.PacketReader(self._portnum, baudrate=baudrate,
callbacks=[("*", self.passOnNotification)])
self._devices = Devices.DeviceList(callbacks=[("*", self.passOnNotification)])
self._missedPackets = 0
self._packetsInLastConnection = None
self._connectEventPacketCounterValue = None
self._inConnection = False
self._currentConnectRequest = None
self._nProcessedPackets = 0
self._switchingBaudRate = False
self._attemptedBaudRates = []
self._last_time = None
self._last_timestamp = 0
self._boardId = self._makeBoardId()
def __del__(self):
self._doExit()
def _setup(self):
self._packetReader.setup()
def _makeBoardId(self):
try:
if sys.platform == 'win32':
boardId = int(self._packetReader.portnum.split("COM")[1])
logging.info("board ID: %d" % boardId)
elif sys.platform == 'linux':
boardId = int(self._packetReader.portnum.split("ttyACM")[1])
logging.info("board ID: %d" % boardId)
else:
# Generate a random boardID
raise IndexError()
except (IndexError, AttributeError):
import random
random.seed()
boardId = random.randint(0,255)
logging.info("board ID (random): %d" % boardId)
return boardId
@property
def state(self):
return self._state
def _setState(self, newState):
self._state = newState
self.notify("STATE_CHANGE", newState)
def _switchBaudRate(self, newBaudRate):
if newBaudRate in self._packetReader.uart.ser.BAUDRATES:
self._packetReader.sendSwitchBaudRate(newBaudRate)
self._switchingBaudRate = True
self._proposedBaudRate = newBaudRate
self._attemptedBaudRates.append(newBaudRate)
def _addDevice(self, device):
self._devices.appendOrUpdate(device)
def _processBLEPacket(self, packet):
packet.boardId = self._boardId
if packet.protover >= PROTOVER_V3:
if self._last_time is None:
# Timestamp from Host
packet.time = time.time()
else:
# Timestamp using reference and packet timestamp diff
if packet.timestamp < self._last_timestamp:
time_diff = (1 << 32) - (self._last_timestamp - packet.timestamp)
else:
time_diff = (packet.timestamp - self._last_timestamp)
packet.time = self._last_time + (time_diff / 1_000_000)
self._last_time = packet.time
self._last_timestamp = packet.timestamp
else:
# Timestamp from Host
packet.time = time.time()
self._appendPacket(packet)
self.notify("NEW_BLE_PACKET", {"packet": packet})
self._captureHandler.writePacket(packet)
self._nProcessedPackets += 1
if packet.OK:
try:
if packet.blePacket.type == PACKET_TYPE_ADVERTISING:
if self.state == STATE_FOLLOWING and packet.blePacket.advType == 5:
self._connectionAccessAddress = packet.blePacket.accessAddress
if self.state == STATE_FOLLOWING and packet.blePacket.advType == 4:
newDevice = Devices.Device(address=packet.blePacket.advAddress, name=packet.blePacket.name, RSSI=packet.RSSI)
self._devices.appendOrUpdate(newDevice)
if self.state == STATE_SCANNING:
if (packet.blePacket.advType in [0, 1, 2, 4, 6, 7] and
packet.blePacket.advAddress != None and
packet.crcOK and
not packet.direction
):
newDevice = Devices.Device(address=packet.blePacket.advAddress, name=packet.blePacket.name, RSSI=packet.RSSI)
self._devices.appendOrUpdate(newDevice)
except Exception as e:
logging.exception("packet processing error %s" % str(e))
self.notify("PACKET_PROCESSING_ERROR", {"errorString": str(e)})
def _continuouslyPipe(self):
while not self._exit:
try:
packet = self._packetReader.getPacket(timeout=12)
if packet == None or not packet.valid:
raise Exceptions.InvalidPacketException("")
except Exceptions.SnifferTimeout as e:
logging.info(str(e))
packet = None
except (SerialException, ValueError):
logging.exception("UART read error")
logging.error("Lost contact with sniffer hardware.")
self._doExit()
except Exceptions.InvalidPacketException:
pass
else:
if packet.id == EVENT_PACKET_DATA_PDU or packet.id == EVENT_PACKET_ADV_PDU:
self._processBLEPacket(packet)
elif packet.id == EVENT_FOLLOW:
# This packet has no value for the user.
pass
elif packet.id == EVENT_CONNECT:
self._connectEventPacketCounterValue = packet.packetCounter
self._inConnection = True
# copy it because packets are eventually deleted
self._currentConnectRequest = copy.copy(self._findPacketByPacketCounter(self._connectEventPacketCounterValue-1))
elif packet.id == EVENT_DISCONNECT:
if self._inConnection:
self._packetsInLastConnection = packet.packetCounter - self._connectEventPacketCounterValue
self._inConnection = False
elif packet.id == SWITCH_BAUD_RATE_RESP and self._switchingBaudRate:
self._switchingBaudRate = False
if (packet.baudRate == self._proposedBaudRate):
self._packetReader.switchBaudRate(self._proposedBaudRate)
else:
self._switchBaudRate(packet.baudRate)
elif packet.id == PING_RESP:
if hasattr(packet, 'version'):
versions = { 1116: '3.1.0',
1115: '3.0.0',
1114: '2.0.0',
1113: '2.0.0-beta-3',
1112: '2.0.0-beta-1' }
self._fwversion = versions.get(packet.version, 'SVN rev: %d' % packet.version)
logging.info("Firmware version %s" % self._fwversion)
elif packet.id == RESP_VERSION:
self._fwversion = packet.version
logging.info("Firmware version %s" % self._fwversion)
elif packet.id == RESP_TIMESTAMP:
# Use current time as timestamp reference
self._last_time = time.time()
self._last_timestamp = packet.timestamp
lt = time.localtime(self._last_time)
usecs = int((self._last_time - int(self._last_time)) * 1_000_000)
logging.info(f'Firmware timestamp {self._last_timestamp} reference: '
f'{time.strftime("%b %d %Y %X", lt)}.{usecs} {time.strftime("%Z", lt)}')
else:
logging.info("Unknown packet ID")
def _findPacketByPacketCounter(self, packetCounterValue):
with self._packetListLock:
for i in range(-1, -1-len(self._packets), -1):
# iterate backwards through packets
if self._packets[i].packetCounter == packetCounterValue:
return self._packets[i]
return None
def _startScanning(self, findScanRsp = False, findAux = False, scanCoded = False):
logging.info("starting scan")
if self.state == STATE_FOLLOWING:
logging.info("Stopped sniffing device")
self._setState(STATE_SCANNING)
self._packetReader.sendScan(findScanRsp, findAux, scanCoded)
self._packetReader.sendTK([0])
def _doExit(self):
self._exit = True
self.notify("APP_EXIT")
self._packetReader.doExit()
# Clear method references to avoid uncollectable cyclic references
self.clearCallbacks()
self._devices.clearCallbacks()
def _startFollowing(self, device, followOnlyAdvertisements = False, followOnlyLegacy = False, followCoded = False):
self._devices.setFollowed(device)
logging.info("Sniffing device " + str(self._devices.index(device)) + ' - "'+device.name+'"')
self._packetReader.sendFollow(device.address, followOnlyAdvertisements, followOnlyLegacy, followCoded)
self._setState(STATE_FOLLOWING)
def _clearDevices(self):
self._devices.clear()
def _appendPacket(self, packet):
with self._packetListLock:
if len(self._packets) > 100000:
self._packets = self._packets[20000:]
self._packets.append(packet)
def _getPackets(self, number = -1):
with self._packetListLock:
returnList = self._packets[0:number]
self._packets = self._packets[number:]
return returnList
def _clearPackets(self):
with self._packetListLock:
del self._packets[:]