Skip to content

Commit c96a48b

Browse files
authored
Matter refactor reading of multiple attributes to reduce memory pressure (#21675)
1 parent 0ea2458 commit c96a48b

File tree

10 files changed

+3949
-3999
lines changed

10 files changed

+3949
-3999
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ All notable changes to this project will be documented in this file.
1919
- SerialBridge command ``SSerialSend9`` replaced by ``SSerialMode``
2020
- SML replace vars in descriptor and line (#21622)
2121
- NeoPool using temperature as only frequently changing value for NPTeleperiod (#21628)
22+
- NeoPool make compiler setting available by user_config_override.h
2223
- NeoPool make compiler setting available by `user_config_override.h` (#21645)
2324
- ESP32 MI32 improve parser (#21648)
2425
- ESP8266 platform update from 2024.01.01 to 2024.06.00 (#21668)
2526
- ESP8266 Framework (Arduino Core) from v2.7.6 to v2.7.7 (#21668)
27+
- Matter refactor reading of multiple attributes to reduce memory pressure
2628

2729
### Fixed
2830
- Matter interverted attributes 0xFFF9 and 0xFFFB (#21636)

lib/libesp32/berry_matter/src/be_matter_module.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,9 @@ module matter (scope: global, strings: weak) {
458458
IM_Status, class(be_class_Matter_IM_Status)
459459
IM_InvokeResponse, class(be_class_Matter_IM_InvokeResponse)
460460
IM_WriteResponse, class(be_class_Matter_IM_WriteResponse)
461-
IM_ReportData, class(be_class_Matter_IM_ReportData)
462-
IM_ReportDataSubscribed, class(be_class_Matter_IM_ReportDataSubscribed)
463-
IM_SubscribeResponse, class(be_class_Matter_IM_SubscribeResponse)
461+
IM_ReportData_Pull, class(be_class_Matter_IM_ReportData_Pull)
462+
IM_ReportDataSubscribed_Pull, class(be_class_Matter_IM_ReportDataSubscribed_Pull)
463+
IM_SubscribeResponse_Pull, class(be_class_Matter_IM_SubscribeResponse_Pull)
464464
IM_SubscribedHeartbeat, class(be_class_Matter_IM_SubscribedHeartbeat)
465465
IM_Subscription, class(be_class_Matter_IM_Subscription)
466466
IM_Subscription_Shop, class(be_class_Matter_IM_Subscription_Shop)

lib/libesp32/berry_matter/src/embedded/Matter_IM.be

Lines changed: 242 additions & 316 deletions
Large diffs are not rendered by default.

lib/libesp32/berry_matter/src/embedded/Matter_IM_Message.be

Lines changed: 119 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ import matter
2323
#@ solidify:Matter_IM_Status,weak
2424
#@ solidify:Matter_IM_InvokeResponse,weak
2525
#@ solidify:Matter_IM_WriteResponse,weak
26-
#@ solidify:Matter_IM_ReportData,weak
27-
#@ solidify:Matter_IM_ReportDataSubscribed,weak
26+
#@ solidify:Matter_IM_ReportData_Pull,weak
27+
#@ solidify:Matter_IM_ReportDataSubscribed_Pull,weak
2828
#@ solidify:Matter_IM_SubscribedHeartbeat,weak
29-
#@ solidify:Matter_IM_SubscribeResponse,weak
29+
#@ solidify:Matter_IM_SubscribeResponse_Pull,weak
3030

3131
#################################################################################
3232
# Matter_IM_Message
@@ -48,7 +48,7 @@ class Matter_IM_Message
4848
end
4949

5050
def reset(msg, opcode, reliable)
51-
self.resp = msg.build_response(opcode, reliable)
51+
self.resp = (msg != nil) ? msg.build_response(opcode, reliable) : nil # is nil for spontaneous reports
5252
self.ready = true # by default send immediately
5353
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
5454
self.last_counter = 0 # avoid `nil` value
@@ -159,53 +159,91 @@ end
159159
matter.IM_WriteResponse = Matter_IM_WriteResponse
160160

161161
#################################################################################
162-
# Matter_IM_ReportData
162+
# Matter_IM_ReportData_Pull
163163
#
164164
# Report Data for a Read Request
165+
#
166+
# This version pull the attributes in lazy mode, only when response is computed
165167
#################################################################################
166-
class Matter_IM_ReportData : Matter_IM_Message
168+
class Matter_IM_ReportData_Pull : Matter_IM_Message
167169
static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
168170
# the maximum MTU is 1280, which leaves 80 bytes for the rest of the message
169171
# section 4.4.4 (p. 114)
172+
# note: `self.data` (bytes or nil) is containing any remaining responses that could not fit in previous packets
173+
var generator_or_arr # a PathGenerator or an array of PathGenerator
174+
var subscription_id # if not `nil`, subscription_id in response
175+
var suppress_response # if not `nil`, suppress_response attribute
170176

171-
def init(msg, data)
177+
def init(msg, ctx_generator_or_arr)
172178
super(self).init(msg, 0x05 #-Report Data-#, true)
173-
self.data = data
179+
self.generator_or_arr = ctx_generator_or_arr
180+
end
181+
182+
def set_subscription_id(subscription_id)
183+
self.subscription_id = subscription_id
184+
end
185+
186+
def set_suppress_response(suppress_response)
187+
self.suppress_response = suppress_response
174188
end
175189

176190
# default responder for data
177191
def send_im(responder)
178-
# log(format("MTR: IM_ReportData send_im exch=%i ready=%i", self.resp.exchange_id, self.ready ? 1 : 0), 3)
192+
# log(format(">>>: Matter_IM_ReportData_Pull send_im exch=%i ready=%i", self.resp.exchange_id, self.ready ? 1 : 0), 3)
179193
if !self.ready return false end
180-
var resp = self.resp # response frame object
181-
var data = self.data # TLV data of the response (if any)
182-
var was_chunked = data.more_chunked_messages # is this following a chunked packet?
183-
184-
# the message were grouped by right-sized binaries upfront, we just need to send one block at time
185-
var elements = 1 # number of elements added
186-
187-
# log(format("MTR: exch=%i elements=%i msg_sz=%i total=%i", self.get_exchangeid(), elements, msg_sz, sz_attribute_reports), 3)
188-
var next_elemnts
189-
if data.attribute_reports != nil
190-
next_elemnts = data.attribute_reports[elements .. ]
191-
data.attribute_reports = data.attribute_reports[0 .. elements - 1]
192-
data.more_chunked_messages = (size(next_elemnts) > 0)
193-
else
194-
data.more_chunked_messages = false
195-
end
194+
var resp = self.resp # response frame object
195+
var data = (self.data != nil) ? self.data : bytes() # bytes() object of the TLV encoded response
196+
self.data = nil # we remove the data that was saved for next packet
197+
198+
var not_full = true # marker used to exit imbricated loops
199+
200+
201+
while not_full && (self.generator_or_arr != nil)
202+
# get the current generator (first element of list or single object)
203+
var current_generator = isinstance(self.generator_or_arr, list) ? self.generator_or_arr[0] : self.generator_or_arr
204+
# log(f">>>: ReportData_Pull send_im start {current_generator.path_in_endpoint}/{current_generator.path_in_cluster}/{current_generator.path_in_attribute}",3)
205+
206+
var ctx
207+
while not_full && (ctx := current_generator.next()) # 'not_full' must be first to avoid removing an item when we don't want
208+
# log(f">>>: ReportData_Pull {ctx=}", 3)
209+
var debug = responder.device.debug
210+
var force_log = current_generator.is_direct() || debug
211+
var elt_bytes = responder.im.read_single_attribute_to_bytes(current_generator.get_pi(), ctx, resp.session, force_log) # TODO adapt no_log
212+
if (elt_bytes == nil) continue end # silently ignored, iterate to next
213+
# check if we overflow
214+
if (size(data) + size(elt_bytes) > self.MAX_MESSAGE)
215+
self.data = elt_bytes # save response for later
216+
not_full = false
217+
else
218+
data.append(elt_bytes) # append response since we have enough room
219+
end
220+
end
196221

197-
if was_chunked
198-
# log(format("MTR: .Read_Attr next_chunk exch=%i", self.get_exchangeid()), 4)
199-
end
200-
if data.more_chunked_messages
201-
if !was_chunked
202-
# log(format("MTR: .Read_Attr first_chunk exch=%i", self.get_exchangeid()), 4)
222+
# if we are here, then we exhausted the current generator, and we need to move to the next one
223+
if not_full
224+
# log(f">>>: ReportData_Pull remove current generator",3)
225+
if isinstance(self.generator_or_arr, list)
226+
self.generator_or_arr.remove(0) # remove first element
227+
if size(self.generator_or_arr) == 0
228+
self.generator_or_arr = nil # empty array so we put nil
229+
end
230+
else
231+
self.generator_or_arr = nil # there was a single entry, so replace with nil
232+
end
203233
end
204-
# log("MTR: sending TLV" + str(data), 4)
234+
205235
end
206236

237+
# prepare the response
238+
var ret = matter.ReportDataMessage()
239+
ret.subscription_id = self.subscription_id
240+
ret.suppress_response = self.suppress_response
241+
# ret.suppress_response = true
242+
ret.attribute_reports = [data]
243+
ret.more_chunked_messages = (self.data != nil) # we got more data to send
244+
207245
# print(">>>>> send elements before encode")
208-
var raw_tlv = self.data.to_TLV()
246+
var raw_tlv = ret.to_TLV()
209247
# print(">>>>> send elements before encode 2")
210248
var encoded_tlv = raw_tlv.tlv2raw(bytes(self.MAX_MESSAGE)) # takes time
211249
# print(">>>>> send elements before encode 3")
@@ -217,37 +255,49 @@ class Matter_IM_ReportData : Matter_IM_Message
217255
responder.send_response_frame(resp)
218256
self.last_counter = resp.message_counter
219257

220-
if next_elemnts != nil && size(next_elemnts) > 0
221-
data.attribute_reports = next_elemnts
222-
# log(format("MTR: to_be_sent_later size=%i exch=%i", size(data.attribute_reports), resp.exchange_id), 4)
258+
if ret.more_chunked_messages # we have more to send
223259
self.ready = false # wait for Status Report before continuing sending
224260
# keep alive
225261
else
262+
# log(f">>>: ReportData_Pull finished",3)
226263
self.finish = true # finished, remove
227264
end
265+
228266
end
229267

230268
end
231-
matter.IM_ReportData = Matter_IM_ReportData
232-
269+
matter.IM_ReportData_Pull = Matter_IM_ReportData_Pull
233270

234271
#################################################################################
235-
# Matter_IM_ReportDataSubscribed
272+
# Matter_IM_ReportDataSubscribed_Pull
236273
#
237274
# Main difference is that we are the spontaneous initiator
238275
#################################################################################
239-
class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
276+
class Matter_IM_ReportDataSubscribed_Pull : Matter_IM_ReportData_Pull
277+
# inherited from Matter_IM_Message
278+
# static var MSG_TIMEOUT = 5000 # 5s
279+
# var expiration # expiration time for the reporting
280+
# var resp # response Frame object
281+
# var ready # bool: ready to send (true) or wait (false)
282+
# var finish # if true, the message is removed from the queue
283+
# var data # TLV data of the response (if any)
284+
# var last_counter # counter value of last sent packet (to match ack)
285+
# inherited from Matter_IM_ReportData_Pull
286+
# static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
287+
# var generator_or_arr # a PathGenerator or an array of PathGenerator
288+
# var subscription_id # if not `nil`, subscription_id in response
240289
var sub # subscription object
241290
var report_data_phase # true during reportdata
242291

243-
def init(message_handler, session, data, sub)
292+
def init(message_handler, session, ctx_generator_or_arr, sub)
293+
super(self).init(nil, ctx_generator_or_arr) # send msg=nil to avoid creating a reponse
294+
# we need to initiate a new virtual response, because it's a spontaneous message
244295
self.resp = matter.Frame.initiate_response(message_handler, session, 0x05 #-Report Data-#, true)
245-
self.data = data
246-
self.ready = true # by default send immediately
247-
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
248296
#
249297
self.sub = sub
250298
self.report_data_phase = true
299+
self.set_subscription_id(sub.subscription_id)
300+
self.set_suppress_response(false)
251301
end
252302

253303
def reached_timeout()
@@ -256,7 +306,7 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
256306

257307
# ack received, confirm the heartbeat
258308
def ack_received(msg)
259-
# log(format("MTR: IM_ReportDataSubscribed ack_received sub=%i", self.sub.subscription_id), 3)
309+
# log(format("MTR: IM_ReportDataSubscribed_Pull ack_received sub=%i", self.sub.subscription_id), 3)
260310
super(self).ack_received(msg)
261311
if !self.report_data_phase
262312
# if ack is received while all data is sent, means that it finished without error
@@ -271,14 +321,14 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
271321

272322
# we received an ACK error, remove subscription
273323
def status_error_received(msg)
274-
# log(format("MTR: IM_ReportDataSubscribed status_error_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
324+
# log(format("MTR: IM_ReportDataSubscribed_Pull status_error_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
275325
self.sub.remove_self()
276326
end
277327

278328
# ack received for previous message, proceed to next (if any)
279329
# return true if we manage the ack ourselves, false if it needs to be done upper
280330
def status_ok_received(msg)
281-
# log(format("MTR: IM_ReportDataSubscribed status_ok_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
331+
# log(format("MTR: IM_ReportDataSubscribed_Pull status_ok_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
282332
if self.report_data_phase
283333
return super(self).status_ok_received(msg)
284334
else
@@ -291,10 +341,11 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
291341
# returns true if transaction is complete (remove object from queue)
292342
# default responder for data
293343
def send_im(responder)
294-
# log(format("MTR: IM_ReportDataSubscribed send sub=%i exch=%i ready=%i", self.sub.subscription_id, self.resp.exchange_id, self.ready ? 1 : 0), 3)
344+
# log(format("MTR: IM_ReportDataSubscribed_Pull send sub=%i exch=%i ready=%i", self.sub.subscription_id, self.resp.exchange_id, self.ready ? 1 : 0), 3)
295345
# log(format("MTR: ReportDataSubscribed::send_im size(self.data.attribute_reports)=%i ready=%s report_data_phase=%s", size(self.data.attribute_reports), str(self.ready), str(self.report_data_phase)), 3)
296346
if !self.ready return false end
297-
if size(self.data.attribute_reports) > 0 # do we have still attributes to send
347+
348+
if (self.generator_or_arr != nil) # do we have still attributes to send
298349
if self.report_data_phase
299350
super(self).send_im(responder)
300351
# log(format("MTR: ReportDataSubscribed::send_im called super finish=%i", self.finish), 3)
@@ -327,7 +378,7 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
327378
end
328379
end
329380
end
330-
matter.IM_ReportDataSubscribed = Matter_IM_ReportDataSubscribed
381+
matter.IM_ReportDataSubscribed_Pull = Matter_IM_ReportDataSubscribed_Pull
331382

332383
#################################################################################
333384
# Matter_IM_SubscribedHeartbeat
@@ -336,16 +387,17 @@ matter.IM_ReportDataSubscribed = Matter_IM_ReportDataSubscribed
336387
#
337388
# Main difference is that we are the spontaneous initiator
338389
#################################################################################
339-
class Matter_IM_SubscribedHeartbeat : Matter_IM_ReportData
390+
class Matter_IM_SubscribedHeartbeat : Matter_IM_ReportData_Pull
340391
var sub # subscription object
341392

342-
def init(message_handler, session, data, sub)
393+
def init(message_handler, session, sub)
394+
super(self).init(nil, nil #-no ctx_generator_or_arr-#) # send msg=nil to avoid creating a reponse
395+
# we need to initiate a new virtual response, because it's a spontaneous message
343396
self.resp = matter.Frame.initiate_response(message_handler, session, 0x05 #-Report Data-#, true)
344-
self.data = data
345-
self.ready = true # by default send immediately
346-
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
347397
#
348398
self.sub = sub
399+
self.set_subscription_id(sub.subscription_id)
400+
self.set_suppress_response(true)
349401
end
350402

351403
def reached_timeout()
@@ -386,18 +438,22 @@ end
386438
matter.IM_SubscribedHeartbeat = Matter_IM_SubscribedHeartbeat
387439

388440
#################################################################################
389-
# Matter_IM_SubscribeResponse
441+
# Matter_IM_SubscribeResponse_Pull
390442
#
391-
# Report Data for a Read Request
443+
# Report Data for a Read Request - pull (lazy) mode
392444
#################################################################################
393-
class Matter_IM_SubscribeResponse : Matter_IM_ReportData
445+
class Matter_IM_SubscribeResponse_Pull : Matter_IM_ReportData_Pull
446+
# inherited
447+
# static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
448+
# var generator_or_arr # a PathGenerator or an array of PathGenerator
394449
var sub # subscription object
395450
var report_data_phase # true during reportdata
396451

397-
def init(msg, data, sub)
398-
super(self).init(msg, data)
452+
def init(msg, ctx_generator_or_arr, sub)
453+
super(self).init(msg, ctx_generator_or_arr)
399454
self.sub = sub
400455
self.report_data_phase = true
456+
self.set_subscription_id(sub.subscription_id)
401457
end
402458

403459
# default responder for data
@@ -414,6 +470,7 @@ class Matter_IM_SubscribeResponse : Matter_IM_ReportData
414470
self.ready = false # wait for Status Report before continuing sending
415471

416472
else
473+
417474
# send the final SubscribeReponse
418475
var resp = self.resp
419476
var sr = matter.SubscribeResponseMessage()
@@ -440,6 +497,6 @@ class Matter_IM_SubscribeResponse : Matter_IM_ReportData
440497
end
441498
return super(self).status_ok_received(msg)
442499
end
443-
500+
444501
end
445-
matter.IM_SubscribeResponse = Matter_IM_SubscribeResponse
502+
matter.IM_SubscribeResponse_Pull = Matter_IM_SubscribeResponse_Pull

0 commit comments

Comments
 (0)