From f165aeaf86896c99dae388cc5f50aa90f273c0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20V=C3=A1zquez=20Blanco?= Date: Fri, 11 Sep 2026 22:15:37 +0200 Subject: [PATCH] bluetooth_vsc_espressif: Initial VSC batch AI-Assisted: yes (Claude Opus 5) --- scapy/contrib/bluetooth_vsc_espressif.py | 271 +++++++++++++++++++++++ test/contrib/bluetooth_vsc_espressif.uts | 155 +++++++++++++ test/contrib/bluetooth_vsc_zephyr.uts | 10 +- 3 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 scapy/contrib/bluetooth_vsc_espressif.py create mode 100644 test/contrib/bluetooth_vsc_espressif.uts diff --git a/scapy/contrib/bluetooth_vsc_espressif.py b/scapy/contrib/bluetooth_vsc_espressif.py new file mode 100644 index 00000000000..60e8c01784d --- /dev/null +++ b/scapy/contrib/bluetooth_vsc_espressif.py @@ -0,0 +1,271 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# +# scapy.contrib.description = Espressif Bluetooth HCI Vendor-Specific Commands +# scapy.contrib.status = loads +# +# Information sources: +# - https://www.tarlogic.com/blog/esp32-hidden-hci-vendor-commands/ +# - esp_bt_vs.h in esp-idf (release/v6.0): https://github.com/espressif/esp-idf + + +from scapy.packet import Packet, bind_layers +from scapy.fields import ( + ByteEnumField, + ByteField, + FieldLenField, + LEMACField, + LEShortField, + SignedByteField, + StrField, + StrLenField, + XByteField, + XLEIntField, +) + +from scapy.layers.bluetooth import ( + HCI_Command_Hdr, + HCI_Event_Command_Complete, + HCI_Event_Vendor, +) + + +# Espressif vendor-specific event subcodes (event code 0xFF), from esp_bt_vs.h +_esp_vs_evt_subcodes = { + 0x03: "legacy_rem_auth", + 0x05: "afh_chg", + 0x06: "ch_classification", + 0x07: "ch_classification_reporting_mode", + 0xF0: "le_adv_lost", +} + + +############################################################################### +# Commands (OGF 0x3F) +############################################################################### + +class HCI_Cmd_VSC_Espressif_Common_Echo(Packet): + """ + ESP_BT_VS_COMMON_ECHO (OCF 0x081) + + The controller echoes the payload byte back in the command complete, so this + doubles as an Espressif-controller fingerprint that works over any transport. + """ + name = "Espressif Common Echo" + fields_desc = [XByteField("echo", 0)] + + +class HCI_Cmd_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl(Packet): + """ESP_BT_VS_RD_NEW_CONN_TX_PWR_LVL (OCF 0x192)""" + name = "Espressif Read New Connection TX Power Level" + fields_desc = [] + + +class HCI_Cmd_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl(Packet): + """ESP_BT_VS_RD_PAGE_TX_PWR_LVL (OCF 0x194)""" + name = "Espressif Read Page TX Power Level" + fields_desc = [] + + +class HCI_Cmd_VSC_Espressif_Rd_Pscan_Tx_Pwr_Lvl(Packet): + """ESP_BT_VS_RD_PSCAN_TX_PWR_LVL (OCF 0x196)""" + name = "Espressif Read Page Scan TX Power Level" + fields_desc = [] + + +class HCI_Cmd_VSC_Espressif_Rd_Inq_Tx_Pwr_Lvl(Packet): + """ESP_BT_VS_RD_INQ_TX_PWR_LVL (OCF 0x198)""" + name = "Espressif Read Inquiry TX Power Level" + fields_desc = [] + + +class HCI_Cmd_VSC_Espressif_Set_Mac(Packet): + """ + Set MAC address (OCF 0x032) - legacy ROM debug command. + + Sets the controller's public BD_ADDR (6-byte little-endian address). This is + one of the undocumented ESP32 ROM debug commands (CVE-2025-27840): it exists + only on the ORIGINAL ESP32 (not the C/S/H series) and only on ESP-IDF + releases *before* the fix (removed in v5.4.1 / v5.3.3 / v5.2.6 / v5.1.7 / + v5.0.9 and v6.0+, see advisory AR2025-004), where it answers 0x01 Unknown HCI + Command. + """ + name = "Espressif Set MAC Address" + fields_desc = [LEMACField("bd_addr", None)] + + +# RivieraWaves dbg-task access sizes for the ROM read/write-memory commands +# (_8_Bit / _16_Bit / _32_Bit are the literal bit widths). +_esp_mem_access_size = {8: "8-bit", 16: "16-bit", 32: "32-bit"} + + +class HCI_Cmd_VSC_Espressif_Rd_Mem(Packet): + """ + Read memory (OCF 0x001) - legacy ROM debug command. + + Reads ``length`` bytes (1..128) from ``start_addr`` using ``access_size``-bit + accesses (8/16/32). One of the undocumented ESP32 ROM debug commands + (CVE-2025-27840): original ESP32 only, and only on pre-fix ESP-IDF (removed in + v5.4.1 / v5.3.3 / v5.2.6 / v5.1.7 / v5.0.9 and v6.0+, advisory AR2025-004). The + command-complete returns status, then a length byte and that many data bytes + (see HCI_Cmd_Complete_VSC_Espressif_Rd_Mem). + """ + name = "Espressif Read Memory" + fields_desc = [XLEIntField("start_addr", 0), + ByteEnumField("access_size", 8, _esp_mem_access_size), + ByteField("length", 4)] + + +class HCI_Cmd_VSC_Espressif_Wr_Mem(Packet): + """ + Write memory (OCF 0x002) - legacy ROM debug command. + + Writes ``data`` to ``start_addr`` using ``access_size``-bit accesses; returns + status only. Same availability as Read Memory (pre-fix IDF, original ESP32). + """ + name = "Espressif Write Memory" + fields_desc = [XLEIntField("start_addr", 0), + ByteEnumField("access_size", 8, _esp_mem_access_size), + FieldLenField("length", None, length_of="data", fmt="B"), + StrLenField("data", b"", length_from=lambda p: p.length)] + + +class HCI_Cmd_Complete_VSC_Espressif_Common_Echo(Packet): + """COMMON_ECHO (0xFC81) command complete""" + name = "Espressif Common Echo complete" + fields_desc = [XByteField("echo", 0)] + + +class HCI_Cmd_Complete_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl(Packet): + """RD_NEW_CONN_TX_PWR_LVL (0xFD92) command complete""" + name = "Espressif Read New Connection TX Power Level complete" + fields_desc = [SignedByteField("tx_power_min", 0), + SignedByteField("tx_power_max", 0)] + + +class HCI_Cmd_Complete_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl(Packet): + """RD_PAGE_TX_PWR_LVL (0xFD94) command complete""" + name = "Espressif Read Page TX Power Level complete" + fields_desc = [SignedByteField("tx_power", 0)] + + +class HCI_Cmd_Complete_VSC_Espressif_Rd_Pscan_Tx_Pwr_Lvl(Packet): + """RD_PSCAN_TX_PWR_LVL (0xFD96) command complete""" + name = "Espressif Read Page Scan TX Power Level complete" + fields_desc = [SignedByteField("tx_power", 0)] + + +class HCI_Cmd_Complete_VSC_Espressif_Rd_Inq_Tx_Pwr_Lvl(Packet): + """RD_INQ_TX_PWR_LVL (0xFD98) command complete""" + name = "Espressif Read Inquiry TX Power Level complete" + fields_desc = [SignedByteField("tx_power", 0)] + + +class HCI_Cmd_Complete_VSC_Espressif_Rd_Mem(Packet): + """Read memory (0xFC01) command complete: a length byte then that many data + bytes (both after the standard status byte).""" + name = "Espressif Read Memory complete" + fields_desc = [FieldLenField("length", None, length_of="data", fmt="B"), + StrLenField("data", b"", length_from=lambda p: p.length)] + + +class HCI_Event_VSC_Espressif(HCI_Event_Vendor): + """ + Espressif vendor-specific event header (HCI event code 0xFF). + + Registered as an ``HCI_Event_Vendor`` handler, so it replaces the generic + vendor event whenever the body starts with a known Espressif subcode (see + ``check``). The first parameter byte is that subcode (see + ``_esp_vs_evt_subcodes``); the payload is dispatched accordingly. + """ + name = "Espressif Vendor-Specific Event" + match_subclass = True + fields_desc = [ByteEnumField("subcode", 0, _esp_vs_evt_subcodes)] + + @classmethod + def check(cls, body): + """ + Checks if the given 0xFF vendor-event body starts with a known + Espressif subcode. + """ + return len(body) >= 1 and body[0] in _esp_vs_evt_subcodes + + +class HCI_Event_VSC_Espressif_Legacy_Rem_Auth(Packet): + """ESP_BT_VS_LEGACY_REM_AUTH_EVT (subcode 0x03)""" + name = "Espressif Legacy Remote Auth" + fields_desc = [LEShortField("conhdl", 0)] + + +class HCI_Event_VSC_Espressif_Afh_Chg(Packet): + """ESP_BT_VS_AFH_CHG_EVT (subcode 0x05)""" + name = "Espressif AFH Change" + fields_desc = [StrField("data", b"")] + + +class HCI_Event_VSC_Espressif_Ch_Classification(Packet): + """ESP_BT_VS_CH_CLASSIFICATION_EVT (subcode 0x06)""" + name = "Espressif Channel Classification" + fields_desc = [StrField("data", b"")] + + +class HCI_Event_VSC_Espressif_Ch_Classification_Reporting_Mode(Packet): + """ESP_BT_VS_CH_CLASSIFICATION_REPORTING_MODE_EVT (subcode 0x07)""" + name = "Espressif Channel Classification Reporting Mode" + fields_desc = [StrField("data", b"")] + + +class HCI_Event_VSC_Espressif_LE_Adv_Lost(Packet): + """ + ESP_BT_VS_LE_ADV_LOST_EVT (subcode 0xF0) + """ + name = "Espressif LE Advertising Report Lost" + fields_desc = [XLEIntField("nb_lost", 0)] + + +# Commands: HCI_Command_Hdr -> HCI_Cmd_VSC_Espressif_* (ogf 0x3F, ocf) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Rd_Mem, + ogf=0x3F, ocf=0x001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Wr_Mem, + ogf=0x3F, ocf=0x002) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Set_Mac, + ogf=0x3F, ocf=0x032) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Common_Echo, + ogf=0x3F, ocf=0x081) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl, + ogf=0x3F, ocf=0x192) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl, + ogf=0x3F, ocf=0x194) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Rd_Pscan_Tx_Pwr_Lvl, + ogf=0x3F, ocf=0x196) +bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Espressif_Rd_Inq_Tx_Pwr_Lvl, + ogf=0x3F, ocf=0x198) + +# Command-complete returns: bound by full opcode (0xFC00 | OCF) +bind_layers(HCI_Event_Command_Complete, + HCI_Cmd_Complete_VSC_Espressif_Rd_Mem, opcode=0xFC01) +bind_layers(HCI_Event_Command_Complete, + HCI_Cmd_Complete_VSC_Espressif_Common_Echo, opcode=0xFC81) +bind_layers(HCI_Event_Command_Complete, + HCI_Cmd_Complete_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl, opcode=0xFD92) +bind_layers(HCI_Event_Command_Complete, + HCI_Cmd_Complete_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl, opcode=0xFD94) +bind_layers(HCI_Event_Command_Complete, + HCI_Cmd_Complete_VSC_Espressif_Rd_Pscan_Tx_Pwr_Lvl, opcode=0xFD96) +bind_layers(HCI_Event_Command_Complete, + HCI_Cmd_Complete_VSC_Espressif_Rd_Inq_Tx_Pwr_Lvl, opcode=0xFD98) + +# Events: the 0xFF vendor event is shared across vendors, so register the +# Espressif event header as a handler +HCI_Event_Vendor.register_handler(HCI_Event_VSC_Espressif) +bind_layers(HCI_Event_VSC_Espressif, + HCI_Event_VSC_Espressif_Legacy_Rem_Auth, subcode=0x03) +bind_layers(HCI_Event_VSC_Espressif, + HCI_Event_VSC_Espressif_Afh_Chg, subcode=0x05) +bind_layers(HCI_Event_VSC_Espressif, + HCI_Event_VSC_Espressif_Ch_Classification, subcode=0x06) +bind_layers(HCI_Event_VSC_Espressif, + HCI_Event_VSC_Espressif_Ch_Classification_Reporting_Mode, subcode=0x07) +bind_layers(HCI_Event_VSC_Espressif, + HCI_Event_VSC_Espressif_LE_Adv_Lost, subcode=0xF0) diff --git a/test/contrib/bluetooth_vsc_espressif.uts b/test/contrib/bluetooth_vsc_espressif.uts new file mode 100644 index 00000000000..b2c35c98957 --- /dev/null +++ b/test/contrib/bluetooth_vsc_espressif.uts @@ -0,0 +1,155 @@ +% Espressif Bluetooth Vendor-Specific Command (VSC) layer tests + ++ Load the Espressif VSC layer + += Import the module (opt-in layer) +from scapy.layers.bluetooth import * +load_contrib("bluetooth_vsc_espressif") +from scapy.contrib.bluetooth_vsc_espressif import * + + ++ Espressif VSC commands +# This contrib carries only the commands with a live consumer (COMMON_ECHO and +# the BR/EDR RD_*_TX_PWR_LVL reads); the full 26-command spec is preserved under +# vendors/espressif/scapy-full-spec/. + += Common Echo (OCF 0x081) build + dissect +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Common_Echo(echo=0x42) +assert cmd.ogf == 0x3f +assert cmd.ocf == 0x081 +assert cmd.opcode == 0xfc81 +r = raw(cmd) +assert r == b'\x81\xfc\x01\x42' +p = HCI_Command_Hdr(r) +assert HCI_Cmd_VSC_Espressif_Common_Echo in p +assert p[HCI_Cmd_VSC_Espressif_Common_Echo].echo == 0x42 + += Read New Connection TX Power Level (OCF 0x192) - no parameters +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl() +assert cmd.opcode == 0xfd92 +assert raw(cmd) == b'\x92\xfd\x00' + += Read Inquiry TX Power Level (OCF 0x198) - no parameters +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Rd_Inq_Tx_Pwr_Lvl() +assert cmd.opcode == 0xfd98 +assert raw(cmd) == b'\x98\xfd\x00' + += Read Page TX Power Level (OCF 0x194) - no parameters +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl() +assert cmd.opcode == 0xfd94 +assert raw(cmd) == b'\x94\xfd\x00' + += Read Page Scan TX Power Level (OCF 0x196) - no parameters +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Rd_Pscan_Tx_Pwr_Lvl() +assert cmd.opcode == 0xfd96 +assert raw(cmd) == b'\x96\xfd\x00' + += Set MAC Address (OCF 0x032) - legacy ROM command, 6-byte LE address +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Set_Mac(bd_addr="11:22:33:44:55:66") +assert cmd.opcode == 0xfc32 +r = raw(cmd) +assert r == b'\x32\xfc\x06\x66\x55\x44\x33\x22\x11' +p = HCI_Command_Hdr(r) +assert p[HCI_Cmd_VSC_Espressif_Set_Mac].bd_addr == "11:22:33:44:55:66" + +# OCF 0x001/0x002 (Read/Write Memory) collide with Zephyr's Read Version Info / +# Read Supported Commands. The opcodes are fully bound (a single loaded contrib +# auto-dissects normally), but auto-dissection is ambiguous when BOTH vendor +# contribs are loaded (first-loaded wins) -- e.g. scapy's test suite loads every +# contrib into one process. So these tests parse the colliding opcodes with the +# explicit class, making them independent of contrib load order. + += Read Memory (OCF 0x001) - u32 LE addr, access size, length; build + explicit parse +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Rd_Mem(start_addr=0x40000000, access_size=8, length=16) +assert cmd.opcode == 0xfc01 +r = raw(cmd) +assert r == b'\x01\xfc\x06\x00\x00\x00\x40\x08\x10' +inner = HCI_Cmd_VSC_Espressif_Rd_Mem(r[3:]) +assert inner.start_addr == 0x40000000 +assert inner.access_size == 8 +assert inner.length == 16 + += Read Memory command complete (opcode 0xFC01) - length byte + data after status +evt = HCI_Event_Command_Complete(number=1, opcode=0xfc01, status=0) / HCI_Cmd_Complete_VSC_Espressif_Rd_Mem(data=b'\xde\xad\xbe\xef') +cc = HCI_Event_Command_Complete(raw(evt)) +assert cc.opcode == 0xfc01 and cc.status == 0 +inner = HCI_Cmd_Complete_VSC_Espressif_Rd_Mem(bytes(cc.payload)) +assert inner.length == 4 +assert inner.data == b'\xde\xad\xbe\xef' + += Write Memory (OCF 0x002) - addr, access size, length-prefixed data; build + explicit parse +cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_Espressif_Wr_Mem(start_addr=0x3ffb0000, access_size=32, data=b'\x01\x02\x03\x04') +assert cmd.opcode == 0xfc02 +r = raw(cmd) +assert r == b'\x02\xfc\x0a\x00\x00\xfb\x3f\x20\x04\x01\x02\x03\x04' +inner = HCI_Cmd_VSC_Espressif_Wr_Mem(r[3:]) +assert inner.length == 4 +assert inner.data == b'\x01\x02\x03\x04' + + ++ Espressif VSC command-complete returns + += Common Echo command complete (opcode 0xFC81) +evt = HCI_Event_Command_Complete(number=1, opcode=0xfc81, status=0) / HCI_Cmd_Complete_VSC_Espressif_Common_Echo(echo=0x42) +r = raw(evt) +p = HCI_Event_Command_Complete(r) +assert HCI_Cmd_Complete_VSC_Espressif_Common_Echo in p +assert p.status == 0 +assert p[HCI_Cmd_Complete_VSC_Espressif_Common_Echo].echo == 0x42 + += Read New Connection TX Power Level command complete (opcode 0xFD92) +evt = HCI_Event_Command_Complete(number=1, opcode=0xfd92, status=0) / HCI_Cmd_Complete_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl(tx_power_min=0, tx_power_max=3) +p = HCI_Event_Command_Complete(raw(evt)) +assert p[HCI_Cmd_Complete_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl].tx_power_min == 0 +assert p[HCI_Cmd_Complete_VSC_Espressif_Rd_New_Conn_Tx_Pwr_Lvl].tx_power_max == 3 + += Read Page TX Power Level command complete (opcode 0xFD94) +evt = HCI_Event_Command_Complete(number=1, opcode=0xfd94, status=0) / HCI_Cmd_Complete_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl(tx_power=-12) +p = HCI_Event_Command_Complete(raw(evt)) +assert p[HCI_Cmd_Complete_VSC_Espressif_Rd_Page_Tx_Pwr_Lvl].tx_power == -12 + + ++ Espressif VSC events + += The event header is registered as an HCI_Event_Vendor handler +# The 0xFF event code is shared across vendors, so the Espressif header is +# registered instead of rebound, and its check() claims only its own bodies. +assert HCI_Event_VSC_Espressif in HCI_Event_Vendor.registered_handlers +assert issubclass(HCI_Event_VSC_Espressif, HCI_Event_Vendor) +assert HCI_Event_VSC_Espressif.check(b'\xf0\x00\x00\x00\x00') +assert not HCI_Event_VSC_Espressif.check(b'\x99\x00') +assert not HCI_Event_VSC_Espressif.check(b'') + += LE Adv Lost event (code 0xFF, subcode 0xF0) +evt = HCI_Event_Hdr(code=0xff) / HCI_Event_VSC_Espressif(subcode=0xf0) / HCI_Event_VSC_Espressif_LE_Adv_Lost(nb_lost=1234) +r = raw(evt) +assert r == b'\xff\x05\xf0\xd2\x04\x00\x00' +p = HCI_Event_Hdr(r) +assert HCI_Event_VSC_Espressif in p +assert p[HCI_Event_VSC_Espressif].subcode == 0xf0 +assert HCI_Event_VSC_Espressif_LE_Adv_Lost in p +assert p[HCI_Event_VSC_Espressif_LE_Adv_Lost].nb_lost == 1234 +# match_subclass keeps the handler reachable as the generic vendor event +assert HCI_Event_Vendor in p +assert p[HCI_Event_Vendor] is p[HCI_Event_VSC_Espressif] +assert raw(p) == r + += Legacy Remote Auth event (code 0xFF, subcode 0x03) +evt = HCI_Event_Hdr(code=0xff) / HCI_Event_VSC_Espressif(subcode=0x03) / HCI_Event_VSC_Espressif_Legacy_Rem_Auth(conhdl=7) +p = HCI_Event_Hdr(raw(evt)) +assert p[HCI_Event_VSC_Espressif_Legacy_Rem_Auth].conhdl == 7 + += AFH Change event (code 0xFF, subcode 0x05) raw payload +evt = HCI_Event_Hdr(code=0xff) / HCI_Event_VSC_Espressif(subcode=0x05) / HCI_Event_VSC_Espressif_Afh_Chg(data=b'\xaa\xbb') +p = HCI_Event_Hdr(raw(evt)) +assert p[HCI_Event_VSC_Espressif].subcode == 0x05 +assert p[HCI_Event_VSC_Espressif_Afh_Chg].data == b'\xaa\xbb' + += A vendor event with an unknown subcode still falls back to the generic event +# No registered check claims it, so the body stays in HCI_Event_Vendor.data +# and other vendor contribs remain free to claim it. +p = HCI_Event_Hdr(b'\xff\x02\x99\xaa') +assert type(p[HCI_Event_Vendor]) is HCI_Event_Vendor +assert HCI_Event_VSC_Espressif not in p +assert p[HCI_Event_Vendor].data == b'\x99\xaa' +assert raw(p) == b'\xff\x02\x99\xaa' diff --git a/test/contrib/bluetooth_vsc_zephyr.uts b/test/contrib/bluetooth_vsc_zephyr.uts index dea0917593c..06023f89b63 100644 --- a/test/contrib/bluetooth_vsc_zephyr.uts +++ b/test/contrib/bluetooth_vsc_zephyr.uts @@ -51,8 +51,14 @@ assert evt[HCI_Event_Command_Complete].status == 0 = Dissect the version info return (Nordic nRF52x, Zephyr v3.4 build 99) evt = HCI_Hdr(bytes.fromhex("040e100101fc00020002000003040063000000")) -vi = evt[HCI_Cmd_Complete_VSC_Zephyr_Read_Version_Info] -assert evt[HCI_Event_Command_Complete].status == 0 +cc = evt[HCI_Event_Command_Complete] +assert cc.opcode == 0xfc01 +assert cc.status == 0 +# opcode 0xFC01 command-complete is also Espressif Read Memory (bluetooth_vsc_espressif); +# a bare VSC opcode has no vendor tag, so when both contribs are loaded the first +# one loaded wins the shared binding. Parse the return parameters with the explicit +# class so this test is independent of contrib load order. +vi = HCI_Cmd_Complete_VSC_Zephyr_Read_Version_Info(bytes(cc.payload)) assert vi.hw_platform == 0x0002 # Nordic Semiconductor assert vi.hw_variant == 0x0002 # nRF52x assert vi.fw_variant == 0x00 # standard_controller