diff --git a/lib/libesp32/berry/tools/coc/coc b/lib/libesp32/berry/tools/coc/coc
index ffc833433..6a1e6fad1 100755
--- a/lib/libesp32/berry/tools/coc/coc
+++ b/lib/libesp32/berry/tools/coc/coc
@@ -16,7 +16,7 @@ class builder:
def __init__(self, input_folders, output_folder, macro_files):
self.output = output_folder
self.input = input_folders
- self.config = macro_files
+ self.config = list(macro_files)
self.macro = None
self.strmap = {}
self.strmap_weak = {}
@@ -24,8 +24,9 @@ class builder:
self.bytesmap = {}
self.macro = macro_table()
+ self.config.append("../../../tasmota/tasmota_defines_for_berry.h")
for path in self.config:
- self.macro.scan_file(path)
+ self.macro.scan_file(path)
for d in self.input:
self.scandir(d)
diff --git a/lib/libesp32/berry_matter/src/be_matter_module.c b/lib/libesp32/berry_matter/src/be_matter_module.c
index e4dee163f..65917083e 100644
--- a/lib/libesp32/berry_matter/src/be_matter_module.c
+++ b/lib/libesp32/berry_matter/src/be_matter_module.c
@@ -273,6 +273,11 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
#include "solidify/solidified_Matter_Plugin_9_Zigbee_Humidity.h"
#include "solidify/solidified_Matter_Plugin_9_Zigbee_Occupancy.h"
#include "solidify/solidified_Matter_Plugin_z_All.h"
+#if USE_MI_EXT_GUI
+#include "solidify/solidified_Matter_BTP.h"
+#include "solidify/solidified_Matter_zz_Device_BLE.h"
+// #warning ________________
+#endif //USE_MI_EXT_GUI
#include "solidify/solidified_Matter_zz_Device.h"
#include "be_fixed_matter.h"
@@ -436,6 +441,12 @@ module matter (scope: global, strings: weak) {
// Base38 for QR Code
Base38, class(be_class_Matter_Base38)
+ // optional BTP for BLE
+ BTP, class(be_class_Matter_BTP), USE_MI_EXT_GUI
+
+ // optional Matter BLE Device core class
+ Device_BLE, class(be_class_Matter_Device_BLE), USE_MI_EXT_GUI
+
// Matter Device core class
Device, class(be_class_Matter_Device)
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_BTP.be b/lib/libesp32/berry_matter/src/embedded/Matter_BTP.be
new file mode 100644
index 000000000..c9dcdec8a
--- /dev/null
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_BTP.be
@@ -0,0 +1,212 @@
+#
+# Matter_BTP.be - implements the Matter Bluetooth Transport Protocol (BTP)
+#
+# Copyright (C) 2026 Stephan Hadinger, Christian Baars & Theo Arends
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+import matter
+
+#@ solidify:Matter_BTP,weak
+#if USE_MI_EXT_GUI
+
+#################################################################################
+# Matter_BTP
+#
+# Pure protocol implementation of the Matter Bluetooth Transport Protocol (BTP).
+# Transport-agnostic: only requires the owning `device` to expose `ble_send(payload)`.
+#################################################################################
+class Matter_BTP
+
+ static MTU = 244
+ static VERSION = 4
+ static WINDOW_SIZE = 6
+ # control flag masks
+ static F_HANDSHAKE = 0x40
+ static F_MGMT = 0x20
+ static F_ACK = 0x08
+ static F_END = 0x04
+ static F_CONT = 0x02
+ static F_BEGIN = 0x01
+
+ var device
+ var flags # control flags byte
+ var management_opcode
+ var ack_number
+ var seq_tx
+ var seq_rx
+ var message_length
+ var segment_payload
+ var combined_payload
+ var send_queue
+ var can_send
+
+ def init(device)
+ self.flags = 0
+ self.send_queue = []
+ self.device = device
+ self.seq_tx = 0
+ self.can_send = true
+ end
+
+ # Serialize the frame to bytes
+ def serialize()
+ var frame = bytes()
+ frame.add(self.flags)
+ if (self.flags & self.F_MGMT) && self.management_opcode != nil
+ frame.add(self.management_opcode)
+ end
+ if (self.flags & self.F_HANDSHAKE)
+ if self.segment_payload != nil frame += self.segment_payload end
+ return frame
+ end
+ if (self.flags & self.F_ACK) && self.ack_number != nil
+ frame.add(self.ack_number & 0xff)
+ end
+ frame.add(self.seq_tx & 0xff)
+ if (self.flags & self.F_BEGIN) && self.message_length != nil
+ frame.add(self.message_length, 2)
+ end
+ if self.segment_payload != nil
+ frame += self.segment_payload
+ end
+ return frame
+ end
+
+ # Parse frame from bytes
+ def parse(data)
+ if size(data) < 1 return false end
+
+ var offset = 0
+ self.flags = data[offset]
+ offset += 1
+
+ # management message (handshake)
+ if (self.flags & self.F_MGMT)
+ self.management_opcode = data[offset]
+ self.segment_payload = data[2..]
+ return true
+ end
+
+ if (self.flags & self.F_ACK)
+ self.ack_number = data[offset]
+ offset += 1
+ self.can_send = true
+ end
+
+ self.seq_rx = data[offset]
+ if self.seq_rx > 1 && self.can_send == false
+ self.seq_rx = nil
+ end
+ offset += 1
+
+ if size(data) < 4
+ # pure ACK
+ self.message_length = 0
+ self.combined_payload = nil
+ self.continue_queue()
+ return false
+ end
+
+ if (self.flags & self.F_BEGIN)
+ self.message_length = data.get(offset, 2)
+ offset += 2
+ self.combined_payload = bytes(1024)
+ end
+
+ if offset < size(data)
+ self.combined_payload .. data[offset..]
+ end
+
+ if !(self.flags & self.F_END)
+ return false
+ end
+ return true
+ end
+
+ def continue_queue()
+ self.can_send = true
+ var p = self.get_packet()
+ if !p return end
+ self.can_send = false
+ tasmota.defer(/-> self.device.ble_send(p))
+ self.segment_payload = nil
+ end
+
+ def create_send_queue(payload)
+ var bytes_left = size(payload)
+ var pl_sz = self.MTU - 5
+ var offset = 0
+ var segment_type = 0
+ if bytes_left > pl_sz segment_type = 1 end
+ while bytes_left > 0
+ if bytes_left - pl_sz <= 0 && offset != 0 segment_type = 3 end
+ self.send_queue.push([payload[offset .. (offset + pl_sz - 1)], segment_type, bytes_left])
+ segment_type = 2
+ bytes_left -= pl_sz
+ offset += pl_sz
+ end
+ end
+
+ def get_packet()
+ self.seq_tx += 1
+ if size(self.send_queue) > 0
+ var el = self.send_queue.pop(0)
+ self.segment_payload = el[0]
+ if el[1] == 0 # single segment
+ self.flags = self.F_BEGIN | self.F_END
+ self.message_length = size(el[0])
+ elif el[1] == 1 # first segment
+ self.flags = self.F_BEGIN
+ self.message_length = el[2]
+ elif el[1] == 3 # last segment
+ self.flags = self.F_CONT | self.F_END
+ else # middle segment
+ self.flags = self.F_CONT
+ end
+ else
+ self.flags = 0
+ self.segment_payload = nil
+ end
+ # add ack
+ if self.seq_rx != nil
+ self.ack_number = self.seq_rx
+ self.flags |= self.F_ACK
+ self.seq_rx = nil
+ end
+ return self.serialize()
+ end
+
+ # Create handshake response frame
+ def create_handshake_response()
+ self.flags = 0x65
+ self.management_opcode = 0x6C
+ var payload = bytes()
+ payload.add(self.VERSION & 0x0F)
+ payload.add(self.MTU, 2)
+ payload.add(self.WINDOW_SIZE)
+ self.segment_payload = payload
+ end
+
+ def delete()
+ self.combined_payload = nil
+ self.message_length = 0
+ end
+end
+
+matter.BTP = Matter_BTP
+#else
+class Matter_BTP end # will be discarded
+#endif // USE_MI_EXT_GUI
\ No newline at end of file
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be
index 4f5f059d8..2ac001610 100644
--- a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be
@@ -109,7 +109,7 @@ class Matter_Plugin
# 0x0003: 0x00, # Identify: no optional features
0x0006: 0x01, # On/Off: Lighting feature (bit 0)
0x0008: 0x03, # Level Control: On/Off (bit 0) + Lighting (bit 1)
- 0x0031: 0x04, # Network Commissioning: Ethernet (for WiFi/Ethernet devices)
+ 0x0031: 0x05, # Eth + WiFi - the latter is needed for Bluetooth commissioning
0x0046: 0x00, # ICD Management: 0x00 = no optional features (base SIT mode, no CIP/UAT/LITS)
0x0062: 0x01, # Scenes Management: SceneNames (bit 0)
0x0102: 1 + 4, # Window Covering: Lift (bit 0) + PA_LF (bit 2)
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Root.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Root.be
index 0530bd85e..1966993d9 100644
--- a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Root.be
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Root.be
@@ -1268,6 +1268,32 @@ class Matter_Plugin_Root : Matter_Plugin
return true # OK
end
+#if USE_MI_EXT_GUI
+ elif cluster == 0x0031 # ========== Network Commissioning Cluster 11.9. ==========
+
+ if command == 0x0002 # ---------- AddOrUpdateWiFiNetwork ----------
+ var ssid = (val.findsubval(0)).asstring()
+ var pass = (val.findsubval(1)).asstring()
+ tasmota.cmd(f"wifitest1 {ssid}+{pass}")
+ tasmota.delay(1000)
+
+ var ncresp = TLV.Matter_TLV_struct()
+ ncresp.add_TLV(0, 0x04 #-TLV.U1-#, 0x00 #-matter.SUCCESS-#) # NetworkCommissioningStatusEnum
+ ncresp.add_TLV(1, 0x0D #-TLV.UTF2-#, tasmota.wifi().tostring()) # DebugText
+ ncresp.add_TLV(2, 0x04 #-TLV.U1-#, 1) # NetworkIndex
+ ctx.command = 0x05 # NetworkConfigResponse
+ return ncresp
+
+ elif command == 0x0006 # ---------- ConnectNetwork ----------
+ # Defer response - poll_deferred_connect_network() in Matter_BLE_Device handles it
+ import global
+ log("MTR: ConnectNetwork deferred, waiting for WiFi", 2)
+ global.matter_device.deferred_connect_network = ctx.msg
+ ctx.status = nil
+ return nil
+ end
+#endif
+
else
return super(self).invoke_request(session, val, ctx)
end
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device_BLE.be b/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device_BLE.be
new file mode 100644
index 000000000..342505ddf
--- /dev/null
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device_BLE.be
@@ -0,0 +1,443 @@
+#
+# Matter_zz_Device_BLE.be - generic Matter-over-BLE commissionee
+#
+# Copyright (C) 2026 Stephan Hadinger, Christian Baars & Theo Arends
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+
+import matter
+
+#@ solidify:Matter_Device_BLE,weak
+#if USE_MI_EXT_GUI
+#################################################################################
+# Matter_Device_BLE
+#
+# Generic Matter-over-BLE commissionee. Subclasses `matter.Device` and owns the
+# BLE bring-up (GATT service FFF6, characteristics C1/C2, advertising), BTP
+# wiring, msg multiplexing between BTP ("BLE" remote_ip marker) and UDP, the
+# GATT callback dispatcher, deferred ConnectNetwork polling and commissioning
+# finalization.
+#
+# Subclasses can override the following hooks for application-specific behavior:
+# - on_ble_init() - called once at the end of init()
+# - on_ble_ready() - called when the BLE stack signals ready (op 229)
+# - on_ble_disconnected() - called on BLE disconnect (op 228)
+# - on_commissioning_success() - called from check_final() on success
+# - on_commissioning_failure() - called from check_final() on failure
+# - heart_beat() - periodic feedback while commissioning is open
+#################################################################################
+class Matter_Device_BLE : Matter_Device
+ var current_func, next_func, btp, send_over_udp, check_if_commissioned
+ var ble_ready
+ var deferred_connect_network # holds msg frame for deferred ConnectNetwork response
+ var cbuf # shared BLE I/O buffer (bytes(-255))
+ # commissioning progress / error tracking
+ var saw_ble_peer # set when GATT op 227 fires (commissioner connected)
+ var saw_pase_start # set when PASE has begun (admin_fabric assigned)
+ var last_wifi_result # last wifitest1 string from a deferred ConnectNetwork
+
+ # failure reason constants - kept as plain strings for easy logging
+ static var REASON_SUCCESS = "success"
+ static var REASON_BLE_DROP = "ble_drop"
+ static var REASON_PASE_FAILED = "pase_failed"
+ static var REASON_WIFI_FAILED = "wifi_failed"
+ static var REASON_WIFI_TIMEOUT = "wifi_timeout"
+ static var REASON_WINDOW_TIMEOUT = "window_timeout"
+ static var REASON_ALREADY_COMMISSIONED = "already_commissioned"
+ static var REASON_UNKNOWN = "unknown"
+
+ def init()
+ import cb
+ import global
+ import BLE
+ if global.matter_device
+ tasmota.remove_driver(global.matter_device)
+ global.matter_device = self
+ tasmota.gc()
+ end
+ # minimal setup to inspect persisted state before bringing BLE up
+ self.plugins = []
+ self.plugins_persist = false
+ self.plugins_config_remotes = {}
+ self.next_ep = self.EP
+ self.ipv4only = false
+ self.commissioning = matter.Commissioning(self)
+ self.load_param()
+ self.sessions = matter.Session_Store(self)
+ self.sessions.load_fabrics()
+
+ # if device is already commissioned, do NOT bring BLE up.
+ # Let the application decide what to do (delete fabrics, restart, go operational, ...).
+ var fabric_count = self.sessions.count_active_fabrics()
+ if fabric_count > 0
+ var ctx = {'reason': self.REASON_ALREADY_COMMISSIONED, 'detail': '', 'fabric_count': fabric_count}
+ log(f"MTR: BLE commissioning skipped - device already commissioned (fabrics={fabric_count:i})", 2)
+ self.on_commissioning_failure(ctx)
+ return
+ end
+
+ self.cbuf = bytes(-255)
+ matter.profiler = matter.Profiler()
+ var cbp = cb.gen_cb(/e,o,u,h->self.cb(e,o,u,h))
+ BLE.conn_cb(cbp, self.cbuf)
+ self.current_func = /->self.init_C1()
+ BLE.set_svc("FFF6")
+ self.btp = matter.BTP(self)
+ self.tick = 0
+ self.message_handler = matter.MessageHandler(self)
+ self.events = matter.EventHandler(self)
+ self.autoconf_device()
+ self.commissioning.init_basic_commissioning()
+ tasmota.add_fast_loop(/-> BLE.loop())
+ tasmota.add_driver(self)
+ self.on_ble_init()
+ self.ble_ready = true
+ log(f"BLE: start MATTER commissionee, discriminator:{self.root_discriminator:i}", 1)
+ end
+
+ # ---------------------------------------------------------------------------
+ # Subclass hooks - empty defaults
+ # ---------------------------------------------------------------------------
+ # ctx for success/failure hooks is a map: { 'reason': , 'detail': , 'fabric_count': }
+ def on_ble_init() end
+ def on_ble_ready() end
+ def on_ble_disconnected() end
+ def on_commissioning_success(ctx) end
+ def on_commissioning_failure(ctx) end
+ def heart_beat() end
+
+ # ---------------------------------------------------------------------------
+ # Driver lifecycle
+ # ---------------------------------------------------------------------------
+ def start()
+ tasmota.when_network_up(def ()
+ self._start_udp(self.UDP_PORT)
+ self.commissioning.start_mdns_announce_hostnames()
+ self.send_over_udp = true
+ end)
+ end
+
+ def stop()
+ tasmota.remove_driver(self)
+ end
+
+ # Override: prevent flush_socket during WiFi stabilization after BLE commissioning
+ def network_down()
+ end
+
+ def every_second()
+ self.sessions.every_second()
+ self.message_handler.every_second()
+ self.events.every_second()
+ self.commissioning.every_second()
+ # latch PASE start as soon as commissioner kicks off SPAKE2+
+ if !self.saw_pase_start && self.commissioning && self.commissioning.commissioning_admin_fabric != nil
+ self.saw_pase_start = true
+ end
+ if self.check_if_commissioned == true
+ self.check_final()
+ end
+ self.poll_deferred_connect_network()
+ end
+
+ # Poll wifitest1 for deferred ConnectNetwork response
+ def poll_deferred_connect_network()
+ if self.deferred_connect_network == nil return end
+ var r = tasmota.cmd("wifitest1")["WiFiTest"]
+ if r == "Testing" return end
+
+ var msg = self.deferred_connect_network
+ self.deferred_connect_network = nil
+ self.last_wifi_result = r
+
+ import global
+ var TLV = matter.TLV
+ var im = self.message_handler.im
+
+ var ctx = matter.Path()
+ ctx.endpoint = 0
+ ctx.cluster = 0x0031
+ ctx.command = 0x07 # ConnectNetworkResponse
+
+ var cnresp = TLV.Matter_TLV_struct()
+ if r == "Successful"
+ cnresp.add_TLV(0, 0x04 #-TLV.U1-#, 0x00 #-SUCCESS-#)
+ cnresp.add_TLV(1, 0x0D #-TLV.UTF2-#, format("ip6=%s", tasmota.wifi()['ip6local']))
+ log("MTR: ConnectNetwork deferred -> Successful", 2)
+ global.matter_device.start()
+ try
+ self.commissioning._mdns_announce_hostname(false)
+ except .. as e, m
+ log(f"MTR: mDNS hostname FAILED: {e} {m}", 2)
+ end
+ for fabric : self.sessions.fabrics
+ if fabric.get_device_id() && fabric.get_fabric_id()
+ try
+ self.commissioning.mdns_announce_op_discovery(fabric)
+ except .. as e, m
+ log(f"MTR: mDNS op_discovery FAILED: {e} {m}", 2)
+ end
+ end
+ end
+ else
+ cnresp.add_TLV(0, 0x04 #-TLV.U1-#, 0x01 #-FAILURE-#)
+ cnresp.add_TLV(1, 0x0D #-TLV.UTF2-#, format("wifi connect failed: %s", r))
+ log(f"MTR: ConnectNetwork deferred -> Failed: {r}", 2)
+ end
+ cnresp.add_TLV(2, 0x02 #-TLV.I4-#, 0) # NetworkIndex
+
+ var raw = bytes(32)
+ im.invokeresponse2raw(raw, ctx, cnresp)
+
+ var ret = matter.InvokeResponseMessage()
+ ret.suppress_response = false
+ ret.invoke_responses = [raw]
+
+ im.send_queue.push(matter.IM_InvokeResponse(msg, ret))
+ im.send_enqueued(self.message_handler)
+ end
+
+ def every_50ms()
+ self.current_func()
+ self.tick += 1
+ self.message_handler.every_50ms()
+ end
+
+ def wait()
+ end
+
+ def then(func)
+ self.next_func = func
+ self.current_func = /->self.wait()
+ end
+
+ # ---------------------------------------------------------------------------
+ # Message multiplexing: BLE/BTP vs UDP
+ # ---------------------------------------------------------------------------
+ def received_ack(msg)
+ self.heart_beat()
+ if msg.remote_ip != "BLE" && self.udp_server
+ self.udp_server.received_ack(msg)
+ end
+ end
+
+ def msg_send(msg)
+ if msg.remote_ip == "BLE"
+ self.btp.create_send_queue(msg.raw)
+ self.heart_beat()
+ tasmota.delay(30)
+ if self.btp.can_send == false || self.ble_ready == false
+ return
+ end
+ var p = self.btp.get_packet()
+ self.btp.can_send = false
+ self.ble_send(p)
+ else
+ self.udp_server.send_UDP(msg)
+ end
+ end
+
+ def parse()
+ var msg = self.btp
+ var is_synced = msg.parse(self.cbuf[1..self.cbuf[0]])
+ if !(msg.flags & matter.BTP.F_HANDSHAKE) && msg.combined_payload
+ if (msg.flags & matter.BTP.F_END)
+ if size(msg.combined_payload) == 0 return end
+ if is_synced
+ log(f"BLE: <<< {self.cbuf[0]:i} bytes")
+ self.message_handler.msg_received(msg.combined_payload, "BLE", 0)
+ end
+ msg.delete()
+ end
+ end
+ end
+
+ def handshake_ack()
+ self.btp.create_handshake_response()
+ self.ble_send(self.btp.serialize())
+ end
+
+ def check_final()
+ if self.commissioning && self.commissioning.is_commissioning_open()
+ return
+ end
+ var fabric_count = self.sessions.count_active_fabrics()
+ if fabric_count > 0
+ log("MTR: Successfully finished BLE commissioning.", 2)
+ self.on_commissioning_success({'reason': self.REASON_SUCCESS, 'detail': '', 'fabric_count': fabric_count})
+ else
+ log("MTR: commissioning failure", 2)
+ self.on_commissioning_failure({'reason': self.REASON_UNKNOWN, 'detail': '', 'fabric_count': 0})
+ self.rearm()
+ end
+ self.check_if_commissioned = false
+ end
+
+ # Handle a BLE disconnect event - decide whether it is a normal completion
+ # (PASE/CASE done, fabrics stored) or a premature drop that needs recovery.
+ # Called from cb() op 228. Single source of truth for disconnect logic.
+ def handle_disconnect()
+ # always notify subclass first
+ self.on_ble_disconnected()
+
+ var fabric_count = self.sessions.count_active_fabrics()
+ var pase_open = self.commissioning && self.commissioning.is_commissioning_open()
+
+ if fabric_count > 0 && !pase_open
+ # clean disconnect after commissioning succeeded - defer to check_final
+ # via the every_second() tick so success is reported in one place
+ log("BLE: disconnect after successful commissioning", 2)
+ self.check_if_commissioned = true
+ return
+ end
+
+ # PREMATURE disconnect - PASE window may still be open so check_final()
+ # would early-return; classify and recover here directly.
+ log(f"BLE: PREMATURE disconnect (fabrics={fabric_count:i}, pase_open={pase_open}, saw_pase={self.saw_pase_start}, saw_peer={self.saw_ble_peer})", 2)
+
+ var ctx = {'reason': self.REASON_UNKNOWN, 'detail': '', 'fabric_count': fabric_count}
+ if self.last_wifi_result != nil && self.last_wifi_result != "Successful"
+ ctx['reason'] = self.REASON_WIFI_FAILED
+ ctx['detail'] = self.last_wifi_result
+ elif self.deferred_connect_network != nil
+ ctx['reason'] = self.REASON_WIFI_TIMEOUT
+ elif self.saw_pase_start
+ ctx['reason'] = self.REASON_PASE_FAILED
+ elif self.saw_ble_peer
+ ctx['reason'] = self.REASON_BLE_DROP
+ else
+ ctx['reason'] = self.REASON_WINDOW_TIMEOUT
+ end
+
+ log(f"MTR: commissioning failure reason={ctx['reason']} detail={ctx['detail']}", 2)
+ self.on_commissioning_failure(ctx)
+ self.check_if_commissioned = false
+ self.rearm()
+ end
+
+ # Return to a clean state where a new commissioner can connect.
+ # Idempotent. Safe to call from a subclass hook.
+ def rearm()
+ log("MTR: rearming BLE commissioning", 2)
+ self.deferred_connect_network = nil
+ self.last_wifi_result = nil
+ self.saw_ble_peer = false
+ self.saw_pase_start = false
+ self.btp = matter.BTP(self)
+ try
+ self.commissioning.stop_basic_commissioning()
+ except .. as e, m
+ log(f"MTR: rearm stop_basic_commissioning err: {e} {m}", 3)
+ end
+ self.commissioning.init_basic_commissioning()
+ self.ble_ready = true
+ self.current_func = /->self.init_C1()
+ end
+
+ def ble_send(payload)
+ import BLE
+ self.cbuf[0] = size(payload)
+ self.cbuf.setbytes(1, payload)
+ BLE.set_chr("18EE2EF5-263D-4559-959F-4F9C429F9D12")
+ BLE.run(211, true)
+ self.ble_ready = false
+ log(f"BLE: >>> {self.cbuf[0]:i} bytes")
+ self.then(/->self.wait())
+ tasmota.defer(/->self.heart_beat())
+ return true
+ end
+
+ # ---------------------------------------------------------------------------
+ # GATT event dispatcher
+ # ---------------------------------------------------------------------------
+ def cb(error, op, uuid, handle)
+ if op == 201
+ log(f"BLE: Handles created: {self.cbuf[1..self.cbuf[0]].tohex()}")
+ elif op == 222
+ self.parse()
+ elif op == 224 || op == 225
+ var kind = op == 224 ? "notification" : "indication"
+ log(f"BLE: Subscribed to {kind}")
+ self.next_func = /->self.handshake_ack()
+ elif op == 227
+ log(f"BLE: peer MAC: {self.cbuf[1..self.cbuf[0]].tohex()}")
+ self.saw_ble_peer = true
+ elif op == 228
+ log("BLE: Disconnected")
+ self.handle_disconnect()
+ elif op == 229
+ self.ble_ready = true
+ log("BLE: stack ready")
+ self.on_ble_ready()
+ return
+ end
+ if error == 0 && op != 229
+ self.current_func = self.next_func
+ end
+ end
+
+ # ---------------------------------------------------------------------------
+ # BLE GATT service setup
+ # ---------------------------------------------------------------------------
+ def init_C1()
+ import BLE
+ BLE.set_chr("18EE2EF5-263D-4559-959F-4F9C429F9D11")
+ self.cbuf.setbytes(0, bytes("0100"))
+ BLE.run(211, true, 8)
+ self.then(/->self.init_C2())
+ end
+
+ def init_C2()
+ import BLE
+ BLE.set_chr("18EE2EF5-263D-4559-959F-4F9C429F9D12")
+ self.cbuf.setbytes(0, bytes("0100"))
+ BLE.run(211, true, 32)
+ self.then(/->self.add_ScanResp())
+ end
+
+ def add_ADV()
+ import BLE
+ var descriptor = bytes("05025000A000") # connectable mode = 2, itvl_min = 50 ms, itvl_max = 100 ms
+ self.cbuf.setbytes(0, descriptor)
+ BLE.run(232)
+ var payload = bytes("0201060B16F6FF00")
+ payload.add(self.root_discriminator, 2)
+ payload.add(self.VENDOR_ID, 2)
+ payload.add(self.PRODUCT_ID, 2)
+ payload.add(0x00)
+ self.cbuf[0] = size(payload)
+ self.cbuf.setbytes(1, payload)
+ BLE.run(201)
+ self.then(/->self.wait())
+ log("BLE: advertising Matter accessory")
+ end
+
+ def add_ScanResp()
+ import BLE
+ var local_name = "Tasmota Matter"
+ var payload = bytes("0201060008") + bytes().fromstring(local_name)
+ payload[3] = size(local_name) + 1
+ self.cbuf[0] = size(payload)
+ self.cbuf.setbytes(1, payload)
+ BLE.run(202)
+ self.then(/->self.add_ADV())
+ end
+end
+
+matter.Device_BLE = Matter_Device_BLE
+#else
+class Matter_Device_BLE end # will be discarded
+#endif //USE_MI_EXT_GUI
\ No newline at end of file