Matter refactor reading of multiple attributes to reduce memory pressure (#21675)

This commit is contained in:
s-hadinger 2024-06-22 13:07:49 +02:00 committed by GitHub
parent 0ea245805b
commit c96a48b9e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 3916 additions and 3966 deletions

View File

@ -19,10 +19,12 @@ All notable changes to this project will be documented in this file.
- SerialBridge command ``SSerialSend9`` replaced by ``SSerialMode``
- SML replace vars in descriptor and line (#21622)
- NeoPool using temperature as only frequently changing value for NPTeleperiod (#21628)
- NeoPool make compiler setting available by user_config_override.h
- NeoPool make compiler setting available by `user_config_override.h` (#21645)
- ESP32 MI32 improve parser (#21648)
- ESP8266 platform update from 2024.01.01 to 2024.06.00 (#21668)
- ESP8266 Framework (Arduino Core) from v2.7.6 to v2.7.7 (#21668)
- Matter refactor reading of multiple attributes to reduce memory pressure
### Fixed
- Matter interverted attributes 0xFFF9 and 0xFFFB (#21636)

View File

@ -458,9 +458,9 @@ module matter (scope: global, strings: weak) {
IM_Status, class(be_class_Matter_IM_Status)
IM_InvokeResponse, class(be_class_Matter_IM_InvokeResponse)
IM_WriteResponse, class(be_class_Matter_IM_WriteResponse)
IM_ReportData, class(be_class_Matter_IM_ReportData)
IM_ReportDataSubscribed, class(be_class_Matter_IM_ReportDataSubscribed)
IM_SubscribeResponse, class(be_class_Matter_IM_SubscribeResponse)
IM_ReportData_Pull, class(be_class_Matter_IM_ReportData_Pull)
IM_ReportDataSubscribed_Pull, class(be_class_Matter_IM_ReportDataSubscribed_Pull)
IM_SubscribeResponse_Pull, class(be_class_Matter_IM_SubscribeResponse_Pull)
IM_SubscribedHeartbeat, class(be_class_Matter_IM_SubscribedHeartbeat)
IM_Subscription, class(be_class_Matter_IM_Subscription)
IM_Subscription_Shop, class(be_class_Matter_IM_Subscription_Shop)

View File

@ -75,14 +75,16 @@ class Matter_IM
return self.process_status_response(msg, val)
elif opcode == 0x02 # Read Request
# self.send_ack_now(msg) # to improve latency, we don't automatically Ack on invoke request
return self.process_read_request(msg, val)
return self.process_read_request_pull(msg, val)
elif opcode == 0x03 # Subscribe Request
self.send_ack_now(msg)
return self.subscribe_request(msg, val)
elif opcode == 0x04 # Subscribe Response
return self.subscribe_response(msg, val)
# return self.subscribe_response(msg, val)
return false # not implemented for Matter device
elif opcode == 0x05 # Report Data
return self.report_data(msg, val)
# return self.report_data(msg, val)
return false # not implemented for Matter device
elif opcode == 0x06 # Write Request
self.send_ack_now(msg)
return self.process_write_request(msg, val)
@ -92,7 +94,8 @@ class Matter_IM
# self.send_ack_now(msg) # to improve latency, we don't automatically Ack on invoke request
return self.process_invoke_request(msg, val)
elif opcode == 0x09 # Invoke Response
return self.process_invoke_response(msg, val)
# return self.process_invoke_response(msg, val)
return false # not implemented for Matter device
elif opcode == 0x0A # Timed Request
return self.process_timed_request(msg, val)
end
@ -222,210 +225,83 @@ class Matter_IM
end
#############################################################
# Inner code shared between read_attributes and subscribe_request
# read_single_attribute_to_bytes
#
# query: `ReadRequestMessage` or `SubscribeRequestMessage`
def _inner_process_read_request(session, query, msg, no_log)
# Takes a concrete context (endpoint/cluster/attribute)
# and a plugin reference, return either the bytes object
# or an array of bytes() if the response does not fit in
# a single packet
#
# `pi` is the plugin object
# if `pi` is nil, just report the status for ctx.status
# `ctx` is the context with endpoint/cluster/attribute, `cts.status` is non-nil for direct request and contains the error message to show
# `session` is the current session
# `force_log` is false, then don't log normal values - typically used to not log wildcard requests
#
# return `true` if
def read_single_attribute_to_bytes(pi, ctx, session, force_log)
var TLV = matter.TLV
var attr_name
if tasmota.loglevel(3)
attr_name = matter.get_attribute_name(ctx.cluster, ctx.attribute)
attr_name = attr_name ? " (" + attr_name + ")" : ""
end
### Inner function to be iterated upon
# ret is the ReportDataMessage list to send back
# ctx is the context with endpoint/cluster/attribute
# direct is true if error is reported, false if error is silently ignored
#
# if `pi` is nil, just report the status for ctx.status
#
# should return true if answered, false if passing to next handler
def read_single_attribute(ret, pi, ctx, direct)
var TLV = matter.TLV
var attr_name
if tasmota.loglevel(3)
attr_name = matter.get_attribute_name(ctx.cluster, ctx.attribute)
attr_name = attr_name ? " (" + attr_name + ")" : ""
# Special case to report unsupported item, if pi==nil
var direct = (ctx.status != nil) # memorize if the request is 'direct', ctx.status may be changed later
var res
var ret_raw_or_list # contains either a bytes() buffer to append, or a list of bytes(), or nil
if (pi != nil)
res = pi.read_attribute(session, ctx, self.tlv_solo)
end
# dispatch depending on the result of the `read_attribute` method
if res != nil # we got an actual value
# get the value with anonymous tag before it is tagged, for logging
var res_str = ""
if tasmota.loglevel(3) && force_log
res_str = res.to_str_val()
end
# Special case to report unsupported item, if pi==nil
ctx.status = nil # reset status, just in case
var res = (pi != nil) ? pi.read_attribute(session, ctx, self.tlv_solo) : nil
var found = true # stop expansion since we have a value
var a1_raw_or_list # contains either a bytes() buffer to append, or a list of bytes(), or nil
if res != nil
var res_str = ""
if !no_log
res_str = res.to_str_val() # get the value with anonymous tag before it is tagged, for logging
end
# check if too big to encode as a single packet, only for list and array
if (res.is_list || res.is_array) && (res.encode_len() > matter.IM_ReportData_Pull.MAX_MESSAGE)
ret_raw_or_list = [] # we return a list of block
var a1_raw = bytes(48)
var empty_list = TLV.Matter_TLV_array()
self.attributedata2raw(a1_raw, ctx, empty_list, false)
ret_raw_or_list.push(a1_raw)
# check if too big to encode as a single packet
if (res.is_list || res.is_array) && res.encode_len() > matter.IM_ReportData.MAX_MESSAGE
# log(f"MTR: >>>>>> long response", 3)
a1_raw_or_list = [] # we return a list of block
var a1_raw = bytes(48)
var empty_list = TLV.Matter_TLV_array()
self.attributedata2raw(a1_raw, ctx, empty_list, false)
a1_raw_or_list.push(a1_raw)
# log(f"MTR: >>>>>> long response global DELETE {a1_raw.tohex()}", 3)
for elt:res.val
a1_raw = bytes(48)
# var list_item = TLV.Matter_TLV_array()
# list_item.val.push(elt)
self.attributedata2raw(a1_raw, ctx, elt, true #- add ListIndex:null -#)
# log(f"MTR: >>>>>> long response global ADD {a1_raw.tohex()}", 3)
a1_raw_or_list.push(a1_raw)
end
# log(f"MTR: >>>>>> long response global {a1_raw_or_list}", 3)
else
# normal encoding
# encode directly raw bytes()
a1_raw_or_list = bytes(48) # pre-reserve 48 bytes
self.attributedata2raw(a1_raw_or_list, ctx, res)
end
if tasmota.loglevel(3) && !no_log
log(f"MTR: >Read_Attr ({session.local_session_id:6i}) {ctx}{attr_name} - {res_str}", 3)
end
elif ctx.status != nil
if direct # we report an error only if a concrete direct read, not with wildcards
# encode directly raw bytes()
a1_raw_or_list = bytes(48) # pre-reserve 48 bytes
self.attributestatus2raw(a1_raw_or_list, ctx, ctx.status)
if tasmota.loglevel(3)
log(format("MTR: >Read_Attr (%6i) %s%s - STATUS: 0x%02X %s", session.local_session_id, str(ctx), attr_name, ctx.status, ctx.status == matter.UNSUPPORTED_ATTRIBUTE ? "UNSUPPORTED_ATTRIBUTE" : ""), 3)
end
for elt : res.val
a1_raw = bytes(48)
# var list_item = TLV.Matter_TLV_array()
# list_item.val.push(elt)
self.attributedata2raw(a1_raw, ctx, elt, true #- add ListIndex:null -#)
ret_raw_or_list.push(a1_raw)
end
else
if tasmota.loglevel(3) && !no_log
log(format("MTR: >Read_Attr (%6i) %s%s - IGNORED", session.local_session_id, str(ctx), attr_name), 3)
end
# ignore if content is nil and status is undefined
if direct
found = false
end
# normal encoding
# encode directly raw bytes()
ret_raw_or_list = bytes(48) # pre-reserve 48 bytes
self.attributedata2raw(ret_raw_or_list, ctx, res)
end
# a1_raw_or_list if either nil, bytes(), of list(bytes())
var idx = isinstance(a1_raw_or_list, list) ? 0 : nil # index in list, or nil if non-list
while a1_raw_or_list != nil
var elt = (idx == nil) ? a1_raw_or_list : a1_raw_or_list[idx] # dereference
if size(ret.attribute_reports) == 0
ret.attribute_reports.push(elt) # push raw binary instead of a TLV
else # already blocks present, see if we can add to the latest, or need to create a new block
var last_block = ret.attribute_reports[-1]
if size(last_block) + size(elt) <= matter.IM_ReportData.MAX_MESSAGE
# add to last block
last_block .. elt
else
ret.attribute_reports.push(elt) # push raw binary instead of a TLV
end
end
if idx == nil
a1_raw_or_list = nil # stop loop
else
idx += 1
if idx >= size(a1_raw_or_list)
a1_raw_or_list = nil # stop loop
end
end
if tasmota.loglevel(3) && force_log
log(f"MTR: >Read_Attr ({session.local_session_id:6i}) {ctx}{attr_name} - {res_str}", 3)
end
# below, we didn't have a response from `read_attribute`, check if ctx.status contains some information
elif ctx.status != nil
if direct # we report an error only if a concrete direct read, not with wildcards
# encode directly raw bytes()
ret_raw_or_list = bytes(48) # pre-reserve 48 bytes
self.attributestatus2raw(ret_raw_or_list, ctx, ctx.status)
# check if we still have enough room in last block
# if a1_raw_or_list # do we have bytes to add, and it's not zero size
# if size(ret.attribute_reports) == 0
# ret.attribute_reports.push(a1_raw_or_list) # push raw binary instead of a TLV
# else # already blocks present, see if we can add to the latest, or need to create a new block
# var last_block = ret.attribute_reports[-1]
# if size(last_block) + size(a1_raw_or_list) <= matter.IM_ReportData.MAX_MESSAGE
# # add to last block
# last_block .. a1_raw_or_list
# else
# ret.attribute_reports.push(a1_raw_or_list) # push raw binary instead of a TLV
# end
# end
# end
return found # return true if we had a match
end
var endpoints = self.device.get_active_endpoints()
# structure is `ReadRequestMessage` 10.6.2 p.558
var ctx = matter.Path()
ctx.msg = msg
var node_id = (msg != nil) ? msg.get_node_id() : nil
# prepare the response
var ret = matter.ReportDataMessage()
# ret.suppress_response = true
ret.attribute_reports = []
for q:query.attributes_requests
# need to do expansion here
ctx.endpoint = q.endpoint
ctx.cluster = q.cluster
ctx.attribute = q.attribute
ctx.fabric_filtered = query.fabric_filtered
ctx.status = matter.UNSUPPORTED_ATTRIBUTE #default error if returned `nil`
# expand endpoint
if ctx.endpoint == nil || ctx.cluster == nil || ctx.attribute == nil
# we need expansion, log first
if tasmota.loglevel(3)
if ctx.cluster != nil && ctx.attribute != nil
var attr_name = matter.get_attribute_name(ctx.cluster, ctx.attribute)
log(format("MTR: >Read_Attr (%6i) %s", session.local_session_id, str(ctx) + (attr_name ? " (" + attr_name + ")" : "")), 3)
else
log(format("MTR: >Read_Attr (%6i) %s", session.local_session_id, str(ctx)), 3)
end
end
end
# implement concrete expansion
self.device.process_attribute_expansion(ctx,
/ pi, ctx, direct -> read_single_attribute(ret, pi, ctx, direct)
)
end
# tasmota.log(f">>>: event_1")
var event_requests = query.event_requests
var event_filters = query.event_filters
var event_no_min = nil # do we have a filter for minimum event_no (int64 or nil)
if event_requests # if not `nil` and not empty list
# read event minimum
if event_filters
for filter: event_filters # filter is an instance of `EventFilterIB`
tasmota.log(f"MTR: EventFilter {filter=} {node_id=}", 3)
var filter_node = int64.toint64(filter.node) # nil or int64
if filter_node # there is a filter on node-id
if filter.node.tobytes() != node_id # the node id doesn't match
tasmota.log(f"MTR: node_id filter {filter_node.tobytes().tohex()} doesn't match {node_id.tohex()}")
continue
end
# specified minimum value
var new_event_no_min = int64.toint64(filter.event_min)
if (event_no_min != nil) || (event_no_min < new_event_no_min)
event_no_min = new_event_no_min
end
end
log(format("MTR: >Read_Attr (%6i) %s%s - STATUS: 0x%02X %s", session.local_session_id, str(ctx), attr_name, ctx.status, ctx.status == matter.UNSUPPORTED_ATTRIBUTE ? "UNSUPPORTED_ATTRIBUTE" : ""), 3)
end
end
# event_no_min is either `nil` or has an `int64` value
ret.event_reports = []
for q: event_requests
# need to do expansion here
ctx.endpoint = q.endpoint
ctx.cluster = q.cluster
ctx.attribute = q.event
#TODO
tasmota.log(f"MTR: >Read_Event({session.local_session_id:%6i}) {ctx}", 3)
end
end
# tasmota.log("MTR: ReportDataMessage=" + str(ret), 3)
# tasmota.log("MTR: ReportDataMessageTLV=" + str(ret.to_TLV()), 3)
return ret
return ret_raw_or_list
end
#############################################################
@ -713,20 +589,75 @@ class Matter_IM
end
#############################################################
# process IM 0x02 Read Request
# process IM 0x02 Read Request (Pull Mode)
#
# val is the TLV structure
#
# This version lazily reads attributes when building the response packets
#
# returns `true` if processed, `false` if silently ignored,
# or raises an exception
def process_read_request(msg, val)
matter.profiler.log("read_request_start")
def process_read_request_pull(msg, val)
matter.profiler.log("read_request_start_pull")
var query = matter.ReadRequestMessage().from_TLV(val)
var generator_or_arr = self.process_read_or_subscribe_request_pull(query, msg)
self.send_report_data_pull(msg, generator_or_arr) # pack into a response structure that will read attributes when expansion is triggered
return true
end
#############################################################
# process_read_or_subscribe_request_pull
#
# This version lazily reads attributes when building the response packets
#
# returns `true` if processed, `false` if silently ignored,
# or raises an exception
def process_read_or_subscribe_request_pull(query, msg)
if query.attributes_requests != nil
var ret = self._inner_process_read_request(msg.session, query, msg)
self.send_report_data(msg, ret)
var generator_or_arr # single path generator (common case) or array of generators
var node_id = (msg != nil) ? msg.get_node_id() : nil
# structure is `ReadRequestMessage` 10.6.2 p.558
if size(query.attributes_requests) > 1
generator_or_arr = []
end
for q : query.attributes_requests
var gen = matter.PathGenerator(self.device)
gen.start(q.endpoint, q.cluster, q.attribute, query.fabric_filtered)
if size(query.attributes_requests) > 1
generator_or_arr.push(gen)
else
generator_or_arr = gen
end
if tasmota.loglevel(3)
# log read request if it contains expansion (wildcard), single reads are logged at concrete time
if q.endpoint == nil || q.cluster == nil || q.attribute == nil
# we need expansion, log first
var ctx = matter.Path()
ctx.endpoint = q.endpoint
ctx.cluster = q.cluster
ctx.attribute = q.attribute
ctx.fabric_filtered = query.fabric_filtered
var ctx_str = str(ctx)
if q.cluster != nil && q.attribute != nil
var attr_name = matter.get_attribute_name(q.cluster, q.attribute)
log(format("MTR: >Read_Attr (%6i) %s", msg.session.local_session_id, ctx_str + (attr_name ? " (" + attr_name + ")" : "")), 3)
else
log(format("MTR: >Read_Attr (%6i) %s", msg.session.local_session_id, ctx_str), 3)
end
end
end
end
return generator_or_arr
end
return true # always consider succesful even if empty
return nil
end
#############################################################
@ -755,13 +686,13 @@ class Matter_IM
if res != nil
# check if the payload is a complex structure and too long to fit in a single response packet
if (res.is_list || res.is_array) && (res.encode_len() > matter.IM_ReportData.MAX_MESSAGE)
if (res.is_list || res.is_array) && (res.encode_len() > matter.IM_ReportData_Pull.MAX_MESSAGE)
# revert to standard
# the attribute will be read again, but it's hard to avoid it
res = nil # indicated to GC that we don't need it again
log(f"MTR: Response to big, revert to non-solo", 3)
var val = matter.TLV.parse(msg.raw, msg.app_payload_idx)
return self.process_read_request(msg, val)
return self.process_read_request_pull(msg, val)
end
# encode directly raw bytes()
raw = bytes(48) # pre-reserve 48 bytes
@ -850,6 +781,7 @@ class Matter_IM
var query = matter.SubscribeRequestMessage().from_TLV(val)
if !query.keep_subscriptions
# log(f"MTR: remove all subscriptions for session {msg.session}", 3)
self.subs_shop.remove_by_session(msg.session) # if `keep_subscriptions`, kill all subscriptions from current session
end
@ -858,26 +790,27 @@ class Matter_IM
var sub = self.subs_shop.new_subscription(msg.session, query)
# expand a string with all attributes requested
var attr_req = []
var ctx = matter.Path()
ctx.msg = msg
for q:query.attributes_requests
ctx.endpoint = q.endpoint
ctx.cluster = q.cluster
ctx.attribute = q.attribute
attr_req.push(str(ctx))
if tasmota.loglevel(3)
var attr_req = []
var ctx = matter.Path()
ctx.msg = msg
for q:query.attributes_requests
ctx.endpoint = q.endpoint
ctx.cluster = q.cluster
ctx.attribute = q.attribute
attr_req.push(str(ctx))
end
log(format("MTR: >Subscribe (%6i) %s (min=%i, max=%i, keep=%i) sub=%i fabric_filtered=%s attr_req=%s event_req=%s",
msg.session.local_session_id, attr_req.concat(" "), sub.min_interval, sub.max_interval, query.keep_subscriptions ? 1 : 0,
sub.subscription_id, query.fabric_filtered,
query.attributes_requests != nil ? size(query.attributes_requests) : "-",
query.event_requests != nil ? size(query.event_requests) : "-"), 3)
end
log(format("MTR: >Subscribe (%6i) %s (min=%i, max=%i, keep=%i) sub=%i fabric_filtered=%s attr_req=%s event_req=%s",
msg.session.local_session_id, attr_req.concat(" "), sub.min_interval, sub.max_interval, query.keep_subscriptions ? 1 : 0,
sub.subscription_id, query.fabric_filtered,
query.attributes_requests != nil ? size(query.attributes_requests) : "-",
query.event_requests != nil ? size(query.event_requests) : "-"), 3)
var ret = self._inner_process_read_request(msg.session, query, msg, !self.device.debug #-log only if debug enabled-#)
# ret is of type `Matter_ReportDataMessage`
ret.subscription_id = sub.subscription_id # enrich with subscription id TODO
self.send_subscribe_response(msg, ret, sub)
var generator_or_arr = self.process_read_or_subscribe_request_pull(query, msg)
self.send_subscribe_response_pull(msg, generator_or_arr, sub) # pack into a response structure that will read attributes when expansion is triggered
return true
end
#############################################################
@ -1030,19 +963,53 @@ class Matter_IM
#############################################################
# process IM 0x04 Subscribe Response
#
def subscribe_response(msg, val)
var query = matter.SubscribeResponseMessage().from_TLV(val)
# log("MTR: received SubscribeResponsetMessage=" + str(query), 4)
return false
end
# def subscribe_response(msg, val)
# var query = matter.SubscribeResponseMessage().from_TLV(val)
# # log("MTR: received SubscribeResponsetMessage=" + str(query), 4)
# return false
# end
#############################################################
# process IM 0x05 ReportData
#
def report_data(msg, val)
var query = matter.ReportDataMessage().from_TLV(val)
# log("MTR: received ReportDataMessage=" + str(query), 4)
return false
# def report_data(msg, val)
# var query = matter.ReportDataMessage().from_TLV(val)
# # log("MTR: received ReportDataMessage=" + str(query), 4)
# return false
# end
#############################################################
# write_single_attribute_status_to_bytes
#
# Takes a concrete context (endpoint/cluster/attribute)
# and a status, and completes the WriteResponseMessage (in ret)
#
# `ret` is the array WriteResponseMessage.write_responses
# `ctx` is the context with endpoint/cluster/attribute, `cts.status` is non-nil for direct request and contains the error message to show
# `write_data` the data written, only for logging
#
def write_single_attribute_status_to_bytes(ret, ctx, write_data)
var TLV = matter.TLV
var attr_name = matter.get_attribute_name(ctx.cluster, ctx.attribute)
attr_name = attr_name ? " (" + attr_name + ")" : ""
# output only if there is a status
if ctx.status != nil
var a1 = matter.AttributeStatusIB()
a1.path = matter.AttributePathIB()
a1.status = matter.StatusIB()
a1.path.endpoint = ctx.endpoint
a1.path.cluster = ctx.cluster
a1.path.attribute = ctx.attribute
a1.status.status = ctx.status
ret.write_responses.push(a1)
log(format("MTR: >Write_Attr%s%s - %s STATUS: 0x%02X %s", str(ctx), attr_name, write_data, ctx.status, ctx.status == matter.SUCCESS ? "SUCCESS" : ""), (ctx.endpoint != 0) ? 2 : 3)
elif tasmota.loglevel(3)
log(format("MTR: >Write_Attr%s%s - IGNORED", str(ctx), attr_name), 3)
# ignore if content is nil and status is undefined
end
end
#############################################################
@ -1051,93 +1018,59 @@ class Matter_IM
def process_write_request(msg, val)
var query = matter.WriteRequestMessage().from_TLV(val)
# log("MTR: received WriteRequestMessage=" + str(query), 3)
var ctx_log = matter.Path() # pre-allocate object for logging
var suppress_response = query.suppress_response
# var timed_request = query.timed_request # TODO not supported
# var more_chunked_messages = query.more_chunked_messages # TODO not supported
var endpoints = self.device.get_active_endpoints()
### Inner function to be iterated upon
# ret is the ReportDataMessage list to send back
# ctx is the context with endpoint/cluster/attribute
# val is the TLV object containing the value
# direct is true if error is reported, false if error is silently ignored
#
# if `pi` is nil, just report the status for ctx.status
#
# should return true if answered, false if failed
def write_single_attribute(ret, pi, ctx, write_data, direct)
var attr_name = matter.get_attribute_name(ctx.cluster, ctx.attribute)
attr_name = attr_name ? " (" + attr_name + ")" : ""
# log(format("MTR: Read Attribute " + str(ctx) + (attr_name ? " (" + attr_name + ")" : ""), 2)
# Special case to report unsupported item, if pi==nil
ctx.status = matter.UNSUPPORTED_WRITE
var res = (pi != nil) ? pi.write_attribute(msg.session, ctx, write_data) : nil
if res ctx.status = matter.SUCCESS end # if the cb returns true, the request was processed
if ctx.status != nil
if direct
var a1 = matter.AttributeStatusIB()
a1.path = matter.AttributePathIB()
a1.status = matter.StatusIB()
a1.path.endpoint = ctx.endpoint
a1.path.cluster = ctx.cluster
a1.path.attribute = ctx.attribute
a1.status.status = ctx.status
ret.write_responses.push(a1)
log(format("MTR: Write_Attr %s%s - STATUS: 0x%02X %s", str(ctx), attr_name, ctx.status, ctx.status == matter.SUCCESS ? "SUCCESS" : ""), (ctx.endpoint != 0) ? 2 : 3)
return true
end
else
log(format("MTR: Write_Attr %s%s - IGNORED", str(ctx), attr_name), 3)
# ignore if content is nil and status is undefined
end
end
# structure is `ReadRequestMessage` 10.6.2 p.558
# log("MTR: IM:write_request processing start", 4)
var ctx = matter.Path()
ctx.msg = msg
if query.write_requests != nil
# prepare the response
var ret = matter.WriteResponseMessage()
# ret.suppress_response = true
ret.write_responses = []
var generator = matter.PathGenerator(self.device)
for q:query.write_requests # q is AttributeDataIB
var path = q.path
var write_path = q.path
var write_data = q.data
# need to do expansion here
ctx.endpoint = path.endpoint
ctx.cluster = path.cluster
ctx.attribute = path.attribute
ctx.status = matter.UNSUPPORTED_ATTRIBUTE #default error if returned `nil`
ctx_log.copy(write_path) # copy endpoint/cluster/attribute in ctx_log for pretty logging
# return an error if the expansion is illegal
if ctx.cluster == nil || ctx.attribute == nil
# force INVALID_ACTION reporting
ctx.status = matter.INVALID_ACTION
write_single_attribute(ret, nil, ctx, nil, true)
if write_path.cluster == nil || write_path.attribute == nil
ctx_log.status = matter.INVALID_ACTION
self.write_single_attribute_status_to_bytes(ret, ctx_log, nil)
continue
end
# expand endpoint
if tasmota.loglevel(3) && (ctx.endpoint == nil)
# we need expansion, log first
var attr_name = matter.get_attribute_name(ctx.cluster, ctx.attribute)
log("MTR: Write_Attr " + str(ctx) + (attr_name ? " (" + attr_name + ")" : ""), 3)
# expansion is only allowed on endpoint number, log if it happens
if (write_path.endpoint == nil) && tasmota.loglevel(3)
var attr_name = matter.get_attribute_name(write_path.cluster, write_path.attribute)
log("MTR: Write_Attr " + str(ctx_log) + (attr_name ? " (" + attr_name + ")" : ""), 3)
end
# implement concrete expansion
self.device.process_attribute_expansion(ctx,
/ pi, ctx, direct -> write_single_attribute(ret, pi, ctx, write_data, direct)
)
end
generator.start(write_path.endpoint, write_path.cluster, write_path.attribute)
var direct = generator.is_direct()
var ctx
while (ctx := generator.next())
ctx.msg = msg # enrich with message
if ctx.status != nil # no match, return error because it was direct
ctx.status = nil # remove status to silence output
self.write_single_attribute_status_to_bytes(ret, ctx, write_data)
# log("MTR: ReportWriteMessage=" + str(ret), 4)
# log("MTR: ReportWriteMessageTLV=" + str(ret.to_TLV()), 3)
else # ctx.status is nil, it exists
var pi = generator.get_pi()
ctx.status = matter.UNSUPPORTED_WRITE
# ctx.status = matter.UNSUPPORTED_WRITE
var res = (pi != nil) ? pi.write_attribute(msg.session, ctx, write_data) : nil
if (res) ctx.status = matter.SUCCESS end # if the cb returns true, the request was processed
self.write_single_attribute_status_to_bytes(ret, ctx, write_data)
end
end
end
# send the reponse that may need to be chunked if too large to fit in a single UDP message
if !suppress_response
@ -1159,11 +1092,11 @@ class Matter_IM
#############################################################
# process IM 0x09 Invoke Response
#
def process_invoke_response(msg, val)
var query = matter.InvokeResponseMessage().from_TLV(val)
# log("MTR: received InvokeResponseMessage=" + str(query), 4)
return false
end
# def process_invoke_response(msg, val)
# var query = matter.InvokeResponseMessage().from_TLV(val)
# # log("MTR: received InvokeResponseMessage=" + str(query), 4)
# return false
# end
#############################################################
# process IM 0x0A Timed Request
@ -1202,11 +1135,9 @@ class Matter_IM
log(format("MTR: <Sub_Data (%6i) sub=%i", session.local_session_id, sub.subscription_id), 3)
sub.is_keep_alive = false # sending an actual data update
var ret = self._inner_process_read_request(session, fake_read, nil #-no msg-#)
ret.suppress_response = false
ret.subscription_id = sub.subscription_id
var generator_or_arr = self.process_read_or_subscribe_request_pull(fake_read, nil #-no msg-#)
var report_data_msg = matter.IM_ReportDataSubscribed(session._message_handler, session, ret, sub)
var report_data_msg = matter.IM_ReportDataSubscribed_Pull(session._message_handler, session, generator_or_arr, sub)
self.send_queue.push(report_data_msg) # push message to queue
self.send_enqueued(session._message_handler) # and send queued messages now
end
@ -1217,15 +1148,10 @@ class Matter_IM
def send_subscribe_heartbeat(sub)
var session = sub.session
log(format("MTR: <Sub_Alive (%6i) sub=%i", session.local_session_id, sub.subscription_id), 3)
log(f"MTR: <Sub_Alive ({session.local_session_id:6i}) sub={sub.subscription_id}", 3)
sub.is_keep_alive = true # sending keep-alive
# prepare the response
var ret = matter.ReportDataMessage()
ret.suppress_response = true
ret.subscription_id = sub.subscription_id
var report_data_msg = matter.IM_SubscribedHeartbeat(session._message_handler, session, ret, sub)
var report_data_msg = matter.IM_SubscribedHeartbeat(session._message_handler, session, sub)
self.send_queue.push(report_data_msg) # push message to queue
self.send_enqueued(session._message_handler) # and send queued messages now
end
@ -1252,17 +1178,17 @@ class Matter_IM
end
#############################################################
# send_report_data
# send_report_data_pull
#
def send_report_data(msg, data)
self.send_queue.push(matter.IM_ReportData(msg, data))
def send_report_data_pull(msg, ctx_generator_or_arr)
self.send_queue.push(matter.IM_ReportData_Pull(msg, ctx_generator_or_arr))
end
#############################################################
# send_report_data
# send_subscribe_response_pull
#
def send_subscribe_response(msg, data, sub)
self.send_queue.push(matter.IM_SubscribeResponse(msg, data, sub))
def send_subscribe_response_pull(msg, data, sub)
self.send_queue.push(matter.IM_SubscribeResponse_Pull(msg, data, sub))
end
#############################################################

View File

@ -23,10 +23,10 @@ import matter
#@ solidify:Matter_IM_Status,weak
#@ solidify:Matter_IM_InvokeResponse,weak
#@ solidify:Matter_IM_WriteResponse,weak
#@ solidify:Matter_IM_ReportData,weak
#@ solidify:Matter_IM_ReportDataSubscribed,weak
#@ solidify:Matter_IM_ReportData_Pull,weak
#@ solidify:Matter_IM_ReportDataSubscribed_Pull,weak
#@ solidify:Matter_IM_SubscribedHeartbeat,weak
#@ solidify:Matter_IM_SubscribeResponse,weak
#@ solidify:Matter_IM_SubscribeResponse_Pull,weak
#################################################################################
# Matter_IM_Message
@ -48,7 +48,7 @@ class Matter_IM_Message
end
def reset(msg, opcode, reliable)
self.resp = msg.build_response(opcode, reliable)
self.resp = (msg != nil) ? msg.build_response(opcode, reliable) : nil # is nil for spontaneous reports
self.ready = true # by default send immediately
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
self.last_counter = 0 # avoid `nil` value
@ -159,53 +159,91 @@ end
matter.IM_WriteResponse = Matter_IM_WriteResponse
#################################################################################
# Matter_IM_ReportData
# Matter_IM_ReportData_Pull
#
# Report Data for a Read Request
#
# This version pull the attributes in lazy mode, only when response is computed
#################################################################################
class Matter_IM_ReportData : Matter_IM_Message
class Matter_IM_ReportData_Pull : Matter_IM_Message
static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
# the maximum MTU is 1280, which leaves 80 bytes for the rest of the message
# section 4.4.4 (p. 114)
# note: `self.data` (bytes or nil) is containing any remaining responses that could not fit in previous packets
var generator_or_arr # a PathGenerator or an array of PathGenerator
var subscription_id # if not `nil`, subscription_id in response
var suppress_response # if not `nil`, suppress_response attribute
def init(msg, data)
def init(msg, ctx_generator_or_arr)
super(self).init(msg, 0x05 #-Report Data-#, true)
self.data = data
self.generator_or_arr = ctx_generator_or_arr
end
def set_subscription_id(subscription_id)
self.subscription_id = subscription_id
end
def set_suppress_response(suppress_response)
self.suppress_response = suppress_response
end
# default responder for data
def send_im(responder)
# log(format("MTR: IM_ReportData send_im exch=%i ready=%i", self.resp.exchange_id, self.ready ? 1 : 0), 3)
# log(format(">>>: Matter_IM_ReportData_Pull send_im exch=%i ready=%i", self.resp.exchange_id, self.ready ? 1 : 0), 3)
if !self.ready return false end
var resp = self.resp # response frame object
var data = self.data # TLV data of the response (if any)
var was_chunked = data.more_chunked_messages # is this following a chunked packet?
var resp = self.resp # response frame object
var data = (self.data != nil) ? self.data : bytes() # bytes() object of the TLV encoded response
self.data = nil # we remove the data that was saved for next packet
# the message were grouped by right-sized binaries upfront, we just need to send one block at time
var elements = 1 # number of elements added
var not_full = true # marker used to exit imbricated loops
# log(format("MTR: exch=%i elements=%i msg_sz=%i total=%i", self.get_exchangeid(), elements, msg_sz, sz_attribute_reports), 3)
var next_elemnts
if data.attribute_reports != nil
next_elemnts = data.attribute_reports[elements .. ]
data.attribute_reports = data.attribute_reports[0 .. elements - 1]
data.more_chunked_messages = (size(next_elemnts) > 0)
else
data.more_chunked_messages = false
end
if was_chunked
# log(format("MTR: .Read_Attr next_chunk exch=%i", self.get_exchangeid()), 4)
end
if data.more_chunked_messages
if !was_chunked
# log(format("MTR: .Read_Attr first_chunk exch=%i", self.get_exchangeid()), 4)
while not_full && (self.generator_or_arr != nil)
# get the current generator (first element of list or single object)
var current_generator = isinstance(self.generator_or_arr, list) ? self.generator_or_arr[0] : self.generator_or_arr
# log(f">>>: ReportData_Pull send_im start {current_generator.path_in_endpoint}/{current_generator.path_in_cluster}/{current_generator.path_in_attribute}",3)
var ctx
while not_full && (ctx := current_generator.next()) # 'not_full' must be first to avoid removing an item when we don't want
# log(f">>>: ReportData_Pull {ctx=}", 3)
var debug = responder.device.debug
var force_log = current_generator.is_direct() || debug
var elt_bytes = responder.im.read_single_attribute_to_bytes(current_generator.get_pi(), ctx, resp.session, force_log) # TODO adapt no_log
if (elt_bytes == nil) continue end # silently ignored, iterate to next
# check if we overflow
if (size(data) + size(elt_bytes) > self.MAX_MESSAGE)
self.data = elt_bytes # save response for later
not_full = false
else
data.append(elt_bytes) # append response since we have enough room
end
end
# log("MTR: sending TLV" + str(data), 4)
# if we are here, then we exhausted the current generator, and we need to move to the next one
if not_full
# log(f">>>: ReportData_Pull remove current generator",3)
if isinstance(self.generator_or_arr, list)
self.generator_or_arr.remove(0) # remove first element
if size(self.generator_or_arr) == 0
self.generator_or_arr = nil # empty array so we put nil
end
else
self.generator_or_arr = nil # there was a single entry, so replace with nil
end
end
end
# prepare the response
var ret = matter.ReportDataMessage()
ret.subscription_id = self.subscription_id
ret.suppress_response = self.suppress_response
# ret.suppress_response = true
ret.attribute_reports = [data]
ret.more_chunked_messages = (self.data != nil) # we got more data to send
# print(">>>>> send elements before encode")
var raw_tlv = self.data.to_TLV()
var raw_tlv = ret.to_TLV()
# print(">>>>> send elements before encode 2")
var encoded_tlv = raw_tlv.tlv2raw(bytes(self.MAX_MESSAGE)) # takes time
# print(">>>>> send elements before encode 3")
@ -217,37 +255,49 @@ class Matter_IM_ReportData : Matter_IM_Message
responder.send_response_frame(resp)
self.last_counter = resp.message_counter
if next_elemnts != nil && size(next_elemnts) > 0
data.attribute_reports = next_elemnts
# log(format("MTR: to_be_sent_later size=%i exch=%i", size(data.attribute_reports), resp.exchange_id), 4)
if ret.more_chunked_messages # we have more to send
self.ready = false # wait for Status Report before continuing sending
# keep alive
else
# log(f">>>: ReportData_Pull finished",3)
self.finish = true # finished, remove
end
end
end
matter.IM_ReportData = Matter_IM_ReportData
matter.IM_ReportData_Pull = Matter_IM_ReportData_Pull
#################################################################################
# Matter_IM_ReportDataSubscribed
# Matter_IM_ReportDataSubscribed_Pull
#
# Main difference is that we are the spontaneous initiator
#################################################################################
class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
class Matter_IM_ReportDataSubscribed_Pull : Matter_IM_ReportData_Pull
# inherited from Matter_IM_Message
# static var MSG_TIMEOUT = 5000 # 5s
# var expiration # expiration time for the reporting
# var resp # response Frame object
# var ready # bool: ready to send (true) or wait (false)
# var finish # if true, the message is removed from the queue
# var data # TLV data of the response (if any)
# var last_counter # counter value of last sent packet (to match ack)
# inherited from Matter_IM_ReportData_Pull
# static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
# var generator_or_arr # a PathGenerator or an array of PathGenerator
# var subscription_id # if not `nil`, subscription_id in response
var sub # subscription object
var report_data_phase # true during reportdata
def init(message_handler, session, data, sub)
def init(message_handler, session, ctx_generator_or_arr, sub)
super(self).init(nil, ctx_generator_or_arr) # send msg=nil to avoid creating a reponse
# we need to initiate a new virtual response, because it's a spontaneous message
self.resp = matter.Frame.initiate_response(message_handler, session, 0x05 #-Report Data-#, true)
self.data = data
self.ready = true # by default send immediately
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
#
self.sub = sub
self.report_data_phase = true
self.set_subscription_id(sub.subscription_id)
self.set_suppress_response(false)
end
def reached_timeout()
@ -256,7 +306,7 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
# ack received, confirm the heartbeat
def ack_received(msg)
# log(format("MTR: IM_ReportDataSubscribed ack_received sub=%i", self.sub.subscription_id), 3)
# log(format("MTR: IM_ReportDataSubscribed_Pull ack_received sub=%i", self.sub.subscription_id), 3)
super(self).ack_received(msg)
if !self.report_data_phase
# 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
# we received an ACK error, remove subscription
def status_error_received(msg)
# log(format("MTR: IM_ReportDataSubscribed status_error_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
# log(format("MTR: IM_ReportDataSubscribed_Pull status_error_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
self.sub.remove_self()
end
# ack received for previous message, proceed to next (if any)
# return true if we manage the ack ourselves, false if it needs to be done upper
def status_ok_received(msg)
# log(format("MTR: IM_ReportDataSubscribed status_ok_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
# log(format("MTR: IM_ReportDataSubscribed_Pull status_ok_received sub=%i exch=%i", self.sub.subscription_id, self.resp.exchange_id), 3)
if self.report_data_phase
return super(self).status_ok_received(msg)
else
@ -291,10 +341,11 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
# returns true if transaction is complete (remove object from queue)
# default responder for data
def send_im(responder)
# 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)
# 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)
# 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)
if !self.ready return false end
if size(self.data.attribute_reports) > 0 # do we have still attributes to send
if (self.generator_or_arr != nil) # do we have still attributes to send
if self.report_data_phase
super(self).send_im(responder)
# log(format("MTR: ReportDataSubscribed::send_im called super finish=%i", self.finish), 3)
@ -327,7 +378,7 @@ class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
end
end
end
matter.IM_ReportDataSubscribed = Matter_IM_ReportDataSubscribed
matter.IM_ReportDataSubscribed_Pull = Matter_IM_ReportDataSubscribed_Pull
#################################################################################
# Matter_IM_SubscribedHeartbeat
@ -336,16 +387,17 @@ matter.IM_ReportDataSubscribed = Matter_IM_ReportDataSubscribed
#
# Main difference is that we are the spontaneous initiator
#################################################################################
class Matter_IM_SubscribedHeartbeat : Matter_IM_ReportData
class Matter_IM_SubscribedHeartbeat : Matter_IM_ReportData_Pull
var sub # subscription object
def init(message_handler, session, data, sub)
def init(message_handler, session, sub)
super(self).init(nil, nil #-no ctx_generator_or_arr-#) # send msg=nil to avoid creating a reponse
# we need to initiate a new virtual response, because it's a spontaneous message
self.resp = matter.Frame.initiate_response(message_handler, session, 0x05 #-Report Data-#, true)
self.data = data
self.ready = true # by default send immediately
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
#
self.sub = sub
self.set_subscription_id(sub.subscription_id)
self.set_suppress_response(true)
end
def reached_timeout()
@ -386,18 +438,22 @@ end
matter.IM_SubscribedHeartbeat = Matter_IM_SubscribedHeartbeat
#################################################################################
# Matter_IM_SubscribeResponse
# Matter_IM_SubscribeResponse_Pull
#
# Report Data for a Read Request
# Report Data for a Read Request - pull (lazy) mode
#################################################################################
class Matter_IM_SubscribeResponse : Matter_IM_ReportData
class Matter_IM_SubscribeResponse_Pull : Matter_IM_ReportData_Pull
# inherited
# static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
# var generator_or_arr # a PathGenerator or an array of PathGenerator
var sub # subscription object
var report_data_phase # true during reportdata
def init(msg, data, sub)
super(self).init(msg, data)
def init(msg, ctx_generator_or_arr, sub)
super(self).init(msg, ctx_generator_or_arr)
self.sub = sub
self.report_data_phase = true
self.set_subscription_id(sub.subscription_id)
end
# default responder for data
@ -414,6 +470,7 @@ class Matter_IM_SubscribeResponse : Matter_IM_ReportData
self.ready = false # wait for Status Report before continuing sending
else
# send the final SubscribeReponse
var resp = self.resp
var sr = matter.SubscribeResponseMessage()
@ -440,6 +497,6 @@ class Matter_IM_SubscribeResponse : Matter_IM_ReportData
end
return super(self).status_ok_received(msg)
end
end
matter.IM_SubscribeResponse = Matter_IM_SubscribeResponse
matter.IM_SubscribeResponse_Pull = Matter_IM_SubscribeResponse_Pull

View File

@ -27,7 +27,6 @@ import matter
# INPUT: Takes a context:
# - plugin
# - path (abstract or concrete)
# - session
#
# OUTPUT:
# - returns a concrete Path
@ -35,10 +34,12 @@ import matter
#################################################################################
class Matter_PathGenerator
var device # reference of device main object
var path_in # input path (abstract or concrete)
var session # session object in which the request was made
var path_in_endpoint # input endpoint filter (nil or int)
var path_in_cluster # input cluster filter (nil or int)
var path_in_attribute # input attribute filter (nil or int)
var path_in_fabric_filtered # input flag for fabric filtered reads (not implemented yet)
# current status
var pi # plugin object, `nil` waiting for value, `false` exhausted values
var pi # plugin object, `nil` waiting for value, `false` exhausted values, `true` if we responded a direct unmatched and it is the last one
var cluster # current cluster number, `nil` waiting for value, `false` exhausted values
var attribute # current attribute number, `nil` waiting for value, `false` exhausted values
# cache
@ -49,49 +50,103 @@ class Matter_PathGenerator
var attribute_found # did we find a valid attribute?
# reused at each output
var path_concrete # placeholder for output concrete path
var path_concrete # placeholder for output concrete path, contains 'matter.Path()' instance - WARNING it can be modified once provided
#################################################################################
# simple constructor
#
def init(device)
self.device = device
end
#################################################################################
# start generator
def start(path_in, session)
#
# `in_endpoint`: endpoint number filter (int or nil for all)
# `in_cluster`: cluster number filter (int or nil for all)
# `in_attribute`: attribute number filter (int or nil for all)
# `in_fabric_filtered`: is the filter fabric-filtered (nil or false or true) - currently stored but ignored
def start(in_endpoint, in_cluster, in_attribute, in_fabric_filtered)
# log(f">>>: PathGenerator start ep:{in_endpoint} cluster:{in_cluster} attribute:{in_attribute}", 3)
self.path_concrete = matter.Path()
self.reset()
self.path_in = path_in
self.session = session
self.path_in_endpoint = in_endpoint
self.path_in_cluster = in_cluster
self.path_in_attribute = in_attribute
self.path_in_fabric_filtered = bool(in_fabric_filtered) # defaults to `false` if `nil`
self.pi = nil # ready to start
#
self.endpoint_found = false
self.cluster_found = false
self.attribute_found = false
end
#################################################################################
# reset and free memory
#
def reset()
var n = nil
self.path_in = n
self.session = n
self.path_concrete.reset()
#
self.pi = n # pre-load first plugin
self.pi = false # mark as inactive
self.cluster = n
self.attribute = n
self.clusters = n
self.clusters = n
end
def get_pi()
return self.pi
################################################################################
# is_direct
#
# Returns true if the original path is concrete, i.e. no expansion.
# If not, errors while reading expanded attributes should not return an error
def is_direct()
return (self.path_in_endpoint != nil) && (self.path_in_cluster != nil) && (self.path_in_attribute != nil)
end
################################################################################
# default_status_error
#
# Get the default error if the read or write fails.
# This error is only reported if `direct` is true
def default_status_error()
if self.is_direct()
if (!self.endpoint_found) return matter.UNSUPPORTED_ENDPOINT end
if (!self.cluster_found) return matter.UNSUPPORTED_CLUSTER end
if (!self.attribute_found) return matter.UNSUPPORTED_ATTRIBUTE end
return matter.UNREPORTABLE_ATTRIBUTE
end
return nil
end
################################################################################
# finished
#
# Returns `true` if we have exhausted the generator
def finished()
return (self.pi != false)
end
################################################################################
# finished
#
# Returns the endpoint object for the last context returned, or `nil` if not found or exhausted
def get_pi()
return ((self.pi == false) || (self.pi == true)) ? nil : self.pi
end
################################################################################
# next
#
# Generate next concrete path
# Returns:
# - a path object (that is valid until next call)
# - if 'direct' (concrete path), ctx.status contains the appropriate error code if the path value is not supported
# - `nil` if no more objects
def next()
if (self.path_in == nil) return nil end
if (self.pi == true) || (self.pi != nil && self.is_direct()) # if we already answered a succesful or missing context for direct request, abort on second call
self.reset()
return nil
end
while (self.pi != false) # loop until we exhausted endpoints
# PRE: self.pi is not `false`
@ -118,8 +173,27 @@ class Matter_PathGenerator
path_concrete.endpoint = self.pi.get_endpoint()
path_concrete.cluster = self.cluster
path_concrete.attribute = self.attribute
path_concrete.fabric_filtered = self.path_in_fabric_filtered
path_concrete.status = nil
# log(f">>>: PathGenerator next path_concrete:{path_concrete}", 3)
return path_concrete
end
# special case, if it was 'direct' and we are here, then we didn't find a match
# return the concrete path ans prepare status
if self.is_direct()
var path_concrete = self.path_concrete
path_concrete.reset()
path_concrete.endpoint = self.path_in_endpoint
path_concrete.cluster = self.path_in_cluster
path_concrete.attribute = self.path_in_attribute
path_concrete.fabric_filtered = self.path_in_fabric_filtered
path_concrete.status = self.default_status_error()
self.pi = true # next call will trigger Generator exhausted
# log(f">>>: PathGenerator next path_concrete:{path_concrete} direct", 3)
return path_concrete
end
# we exhausted all endpoints - finish and clean
self.reset()
return nil
@ -131,7 +205,7 @@ class Matter_PathGenerator
if (self.pi == false) return false end # exhausted all possible values
var plugins = self.device.plugins # shortcut
var ep_filter = self.path_in.endpoint
var ep_filter = self.path_in_endpoint
# cluster and attribute are now undefined
self.cluster = nil
self.attribute = nil
@ -164,7 +238,7 @@ class Matter_PathGenerator
if (self.cluster == false) return false end # exhausted all possible values
var clusters = self.clusters
var cl_filter = self.path_in.cluster
var cl_filter = self.path_in_cluster
# attribute is now undefined
self.attribute = nil
var idx = -1
@ -193,7 +267,7 @@ class Matter_PathGenerator
if (self.attribute == false) return false end # exhausted all possible values
var attributes = self.pi.get_attribute_list(self.cluster)
var attr_filter = self.path_in.attribute
var attr_filter = self.path_in_attribute
var idx = -1
if (self.attribute != nil)
idx = attributes.find(self.attribute) # find index in current list
@ -222,11 +296,7 @@ matter.PathGenerator = Matter_PathGenerator
var gen = matter.PathGenerator(matter_device)
def gen_path_dump(endpoint, cluster, attribute)
var path = matter.Path()
path.endpoint = endpoint
path.cluster = cluster
path.attribute = attribute
gen.start(path)
gen.start(endpoint, cluster, attribute)
var cp
while (cp := gen.next())
print(cp)
@ -242,32 +312,4 @@ gen_path_dump(nil, nil, 0xFFFB)
gen_path_dump(4, 5, 5)
gen_path_dump(4, 5, 6)
var gen = matter.PathGenerator(matter_device)
var path = matter.Path()
path.endpoint = nil
gen.start(path)
# print(gen._next_endpoint())
# print(gen._next_cluster())
# print(gen._next_attribute())
var gen = matter.PathGenerator(matter_device)
var path = matter.Path()
path.endpoint = 4
path.cluster = 5
path.attribute = 1
gen.start(path)
var cp
while (cp := gen.next())
print(cp)
end
-#

View File

@ -518,8 +518,6 @@ class Matter_Device
#############################################################
# Proceed to attribute expansion (used for Attribute Read/Write/Subscribe)
#
# Called only when expansion is needed, so we don't need to report any error since they are ignored
#
# calls `cb(pi, ctx, direct)` for each attribute expanded.
# `pi`: plugin instance targeted by the attribute (via endpoint). Note: nothing is sent if the attribute is not declared in supported attributes in plugin.
# `ctx`: context object with `endpoint`, `cluster`, `attribute` (no `command`)
@ -530,68 +528,44 @@ class Matter_Device
var endpoint = ctx.endpoint
var cluster = ctx.cluster
var attribute = ctx.attribute
var endpoint_found = false # did any endpoint match
var cluster_found = false
var attribute_found = false
var direct = (ctx.endpoint != nil) && (ctx.cluster != nil) && (ctx.attribute != nil) # true if the target is a precise attribute, false if it results from an expansion and error are ignored
# log(f"MTR: process_attribute_expansion {str(ctx))}", 4)
# build the generator for all endpoint/cluster/attributes candidates
var path_generator = matter.PathGenerator(self)
path_generator.start(ctx, nil) # TODO add session if we think it's needed later
path_generator.start(endpoint, cluster, attribute)
var direct = path_generator.is_direct()
var concrete_path
while ((concrete_path := path_generator.next()) != nil)
var finished = cb(path_generator.get_pi(), concrete_path, direct) # call the callback with the plugin and the context
if direct && finished return end
end
# we didn't have any successful match, report an error if direct (non-expansion request)
if direct
# since it's a direct request, ctx has already the correct endpoint/cluster/attribute
if !path_generator.endpoint_found ctx.status = matter.UNSUPPORTED_ENDPOINT
elif !path_generator.cluster_found ctx.status = matter.UNSUPPORTED_CLUSTER
elif !path_generator.attribute_found ctx.status = matter.UNSUPPORTED_ATTRIBUTE
else ctx.status = matter.UNREPORTABLE_ATTRIBUTE
end
cb(nil, ctx, true)
var finished = cb(path_generator.get_pi(), concrete_path) # call the callback with the plugin and the context
end
end
#############################################################
# Optimized version for a single endpoint/cluster/attribute
#
# Retrieve the plugin for a read
# Retrieve the plugin for a read, or nil if not found
# In case of error, ctx.status is updated accordingly
def resolve_attribute_read_solo(ctx)
var endpoint = ctx.endpoint
# var endpoint_found = false # did any endpoint match
var cluster = ctx.cluster
# var cluster_found = false
var attribute = ctx.attribute
# var attribute_found = false
# all 3 elements must be non-nil
if endpoint == nil || cluster == nil || attribute == nil return nil end
if (endpoint == nil) || (cluster == nil) || (attribute == nil) return nil end
# look for plugin
var pi = self.find_plugin_by_endpoint(endpoint)
if pi == nil # endpoint not found
if (pi == nil)
ctx.status = matter.UNSUPPORTED_ENDPOINT
return nil
end
# check cluster
if !pi.contains_cluster(cluster)
ctx.status = matter.UNSUPPORTED_CLUSTER
return nil
end
# attribute list
if !pi.contains_attribute(cluster, attribute)
ctx.status = matter.UNSUPPORTED_ATTRIBUTE
return nil
else
if !pi.contains_cluster(cluster)
ctx.status = matter.UNSUPPORTED_CLUSTER
return nil
elif !pi.contains_attribute(cluster, attribute)
ctx.status = matter.UNSUPPORTED_ATTRIBUTE
return nil
end
end
# all good

File diff suppressed because it is too large Load Diff

View File

@ -837,7 +837,7 @@ be_local_closure(class_Matter_Device_event_fabrics_saved, /* name */
extern const bclass be_class_Matter_Device;
be_local_closure(class_Matter_Device_process_attribute_expansion, /* name */
be_nested_proto(
16, /* nstack */
12, /* nstack */
3, /* argc */
2, /* varg */
0, /* has upvals */
@ -845,100 +845,48 @@ be_local_closure(class_Matter_Device_process_attribute_expansion, /* name */
0, /* has sup protos */
&be_class_Matter_Device,
1, /* has constants */
( &(const bvalue[16]) { /* constants */
( &(const bvalue[ 9]) { /* constants */
/* K0 */ be_nested_str_weak(endpoint),
/* K1 */ be_nested_str_weak(cluster),
/* K2 */ be_nested_str_weak(attribute),
/* K3 */ be_nested_str_weak(matter),
/* K4 */ be_nested_str_weak(PathGenerator),
/* K5 */ be_nested_str_weak(start),
/* K6 */ be_nested_str_weak(next),
/* K7 */ be_nested_str_weak(get_pi),
/* K8 */ be_nested_str_weak(endpoint_found),
/* K9 */ be_nested_str_weak(status),
/* K10 */ be_nested_str_weak(UNSUPPORTED_ENDPOINT),
/* K11 */ be_nested_str_weak(cluster_found),
/* K12 */ be_nested_str_weak(UNSUPPORTED_CLUSTER),
/* K13 */ be_nested_str_weak(attribute_found),
/* K14 */ be_nested_str_weak(UNSUPPORTED_ATTRIBUTE),
/* K15 */ be_nested_str_weak(UNREPORTABLE_ATTRIBUTE),
/* K6 */ be_nested_str_weak(is_direct),
/* K7 */ be_nested_str_weak(next),
/* K8 */ be_nested_str_weak(get_pi),
}),
be_str_weak(process_attribute_expansion),
&be_const_str_solidified,
( &(const binstruction[73]) { /* code */
( &(const binstruction[28]) { /* code */
0x880C0300, // 0000 GETMBR R3 R1 K0
0x88100301, // 0001 GETMBR R4 R1 K1
0x88140302, // 0002 GETMBR R5 R1 K2
0x50180000, // 0003 LDBOOL R6 0 0
0x501C0000, // 0004 LDBOOL R7 0 0
0x50200000, // 0005 LDBOOL R8 0 0
0x88240300, // 0006 GETMBR R9 R1 K0
0x4C280000, // 0007 LDNIL R10
0x2024120A, // 0008 NE R9 R9 R10
0x78260007, // 0009 JMPF R9 #0012
0x88240301, // 000A GETMBR R9 R1 K1
0x4C280000, // 000B LDNIL R10
0x2024120A, // 000C NE R9 R9 R10
0x78260003, // 000D JMPF R9 #0012
0x88240302, // 000E GETMBR R9 R1 K2
0x4C280000, // 000F LDNIL R10
0x2024120A, // 0010 NE R9 R9 R10
0x74260000, // 0011 JMPT R9 #0013
0x50240001, // 0012 LDBOOL R9 0 1
0x50240200, // 0013 LDBOOL R9 1 0
0xB82A0600, // 0014 GETNGBL R10 K3
0x8C281504, // 0015 GETMET R10 R10 K4
0x5C300000, // 0016 MOVE R12 R0
0x7C280400, // 0017 CALL R10 2
0x8C2C1505, // 0018 GETMET R11 R10 K5
0x5C340200, // 0019 MOVE R13 R1
0x4C380000, // 001A LDNIL R14
0x7C2C0600, // 001B CALL R11 3
0x4C2C0000, // 001C LDNIL R11
0x8C301506, // 001D GETMET R12 R10 K6
0x7C300200, // 001E CALL R12 1
0x5C2C1800, // 001F MOVE R11 R12
0x4C340000, // 0020 LDNIL R13
0x2030180D, // 0021 NE R12 R12 R13
0x78320009, // 0022 JMPF R12 #002D
0x5C300400, // 0023 MOVE R12 R2
0x8C341507, // 0024 GETMET R13 R10 K7
0x7C340200, // 0025 CALL R13 1
0x5C381600, // 0026 MOVE R14 R11
0x5C3C1200, // 0027 MOVE R15 R9
0x7C300600, // 0028 CALL R12 3
0x78260001, // 0029 JMPF R9 #002C
0x78320000, // 002A JMPF R12 #002C
0x80001A00, // 002B RET 0
0x7001FFEF, // 002C JMP #001D
0x78260019, // 002D JMPF R9 #0048
0x88301508, // 002E GETMBR R12 R10 K8
0x74320003, // 002F JMPT R12 #0034
0xB8320600, // 0030 GETNGBL R12 K3
0x8830190A, // 0031 GETMBR R12 R12 K10
0x9006120C, // 0032 SETMBR R1 K9 R12
0x7002000E, // 0033 JMP #0043
0x8830150B, // 0034 GETMBR R12 R10 K11
0x74320003, // 0035 JMPT R12 #003A
0xB8320600, // 0036 GETNGBL R12 K3
0x8830190C, // 0037 GETMBR R12 R12 K12
0x9006120C, // 0038 SETMBR R1 K9 R12
0x70020008, // 0039 JMP #0043
0x8830150D, // 003A GETMBR R12 R10 K13
0x74320003, // 003B JMPT R12 #0040
0xB8320600, // 003C GETNGBL R12 K3
0x8830190E, // 003D GETMBR R12 R12 K14
0x9006120C, // 003E SETMBR R1 K9 R12
0x70020002, // 003F JMP #0043
0xB8320600, // 0040 GETNGBL R12 K3
0x8830190F, // 0041 GETMBR R12 R12 K15
0x9006120C, // 0042 SETMBR R1 K9 R12
0x5C300400, // 0043 MOVE R12 R2
0x4C340000, // 0044 LDNIL R13
0x5C380200, // 0045 MOVE R14 R1
0x503C0200, // 0046 LDBOOL R15 1 0
0x7C300600, // 0047 CALL R12 3
0x80000000, // 0048 RET 0
0xB81A0600, // 0003 GETNGBL R6 K3
0x8C180D04, // 0004 GETMET R6 R6 K4
0x5C200000, // 0005 MOVE R8 R0
0x7C180400, // 0006 CALL R6 2
0x8C1C0D05, // 0007 GETMET R7 R6 K5
0x5C240600, // 0008 MOVE R9 R3
0x5C280800, // 0009 MOVE R10 R4
0x5C2C0A00, // 000A MOVE R11 R5
0x7C1C0800, // 000B CALL R7 4
0x8C1C0D06, // 000C GETMET R7 R6 K6
0x7C1C0200, // 000D CALL R7 1
0x4C200000, // 000E LDNIL R8
0x8C240D07, // 000F GETMET R9 R6 K7
0x7C240200, // 0010 CALL R9 1
0x5C201200, // 0011 MOVE R8 R9
0x4C280000, // 0012 LDNIL R10
0x2024120A, // 0013 NE R9 R9 R10
0x78260005, // 0014 JMPF R9 #001B
0x5C240400, // 0015 MOVE R9 R2
0x8C280D08, // 0016 GETMET R10 R6 K8
0x7C280200, // 0017 CALL R10 1
0x5C2C1000, // 0018 MOVE R11 R8
0x7C240400, // 0019 CALL R9 2
0x7001FFF3, // 001A JMP #000F
0x80000000, // 001B RET 0
})
)
);
@ -2093,7 +2041,7 @@ be_local_closure(class_Matter_Device_resolve_attribute_read_solo, /* name */
}),
be_str_weak(resolve_attribute_read_solo),
&be_const_str_solidified,
( &(const binstruction[45]) { /* code */
( &(const binstruction[47]) { /* code */
0x88080300, // 0000 GETMBR R2 R1 K0
0x880C0301, // 0001 GETMBR R3 R1 K1
0x88100302, // 0002 GETMBR R4 R1 K2
@ -2113,32 +2061,34 @@ be_local_closure(class_Matter_Device_resolve_attribute_read_solo, /* name */
0x7C140400, // 0010 CALL R5 2
0x4C180000, // 0011 LDNIL R6
0x1C180A06, // 0012 EQ R6 R5 R6
0x781A0004, // 0013 JMPF R6 #0019
0x781A0005, // 0013 JMPF R6 #001A
0xB81A0A00, // 0014 GETNGBL R6 K5
0x88180D06, // 0015 GETMBR R6 R6 K6
0x90060806, // 0016 SETMBR R1 K4 R6
0x4C180000, // 0017 LDNIL R6
0x80040C00, // 0018 RET 1 R6
0x8C180B07, // 0019 GETMET R6 R5 K7
0x5C200600, // 001A MOVE R8 R3
0x7C180400, // 001B CALL R6 2
0x741A0004, // 001C JMPT R6 #0022
0xB81A0A00, // 001D GETNGBL R6 K5
0x88180D08, // 001E GETMBR R6 R6 K8
0x90060806, // 001F SETMBR R1 K4 R6
0x4C180000, // 0020 LDNIL R6
0x80040C00, // 0021 RET 1 R6
0x8C180B09, // 0022 GETMET R6 R5 K9
0x5C200600, // 0023 MOVE R8 R3
0x5C240800, // 0024 MOVE R9 R4
0x7C180600, // 0025 CALL R6 3
0x741A0004, // 0026 JMPT R6 #002C
0xB81A0A00, // 0027 GETNGBL R6 K5
0x88180D0A, // 0028 GETMBR R6 R6 K10
0x90060806, // 0029 SETMBR R1 K4 R6
0x4C180000, // 002A LDNIL R6
0x80040C00, // 002B RET 1 R6
0x80040A00, // 002C RET 1 R5
0x70020013, // 0019 JMP #002E
0x8C180B07, // 001A GETMET R6 R5 K7
0x5C200600, // 001B MOVE R8 R3
0x7C180400, // 001C CALL R6 2
0x741A0005, // 001D JMPT R6 #0024
0xB81A0A00, // 001E GETNGBL R6 K5
0x88180D08, // 001F GETMBR R6 R6 K8
0x90060806, // 0020 SETMBR R1 K4 R6
0x4C180000, // 0021 LDNIL R6
0x80040C00, // 0022 RET 1 R6
0x70020009, // 0023 JMP #002E
0x8C180B09, // 0024 GETMET R6 R5 K9
0x5C200600, // 0025 MOVE R8 R3
0x5C240800, // 0026 MOVE R9 R4
0x7C180600, // 0027 CALL R6 3
0x741A0004, // 0028 JMPT R6 #002E
0xB81A0A00, // 0029 GETNGBL R6 K5
0x88180D0A, // 002A GETMBR R6 R6 K10
0x90060806, // 002B SETMBR R1 K4 R6
0x4C180000, // 002C LDNIL R6
0x80040C00, // 002D RET 1 R6
0x80040A00, // 002E RET 1 R5
})
)
);
@ -6163,25 +6113,25 @@ be_local_class(Matter_Device,
{ be_const_key_weak(http_rain, -1), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Rain) },
{ be_const_key_weak(http_pressure, 32), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Pressure) },
{ be_const_key_weak(v_light2, -1), be_const_class(be_class_Matter_Plugin_Virt_Light2) },
{ be_const_key_weak(temperature, -1), be_const_class(be_class_Matter_Plugin_Sensor_Temp) },
{ be_const_key_weak(pressure, -1), be_const_class(be_class_Matter_Plugin_Sensor_Pressure) },
{ be_const_key_weak(relay, -1), be_const_class(be_class_Matter_Plugin_OnOff) },
{ be_const_key_weak(v_illuminance, 15), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Illuminance) },
{ be_const_key_weak(contact, 1), be_const_class(be_class_Matter_Plugin_Sensor_Contact) },
{ be_const_key_weak(http_relay, 12), be_const_class(be_class_Matter_Plugin_Bridge_OnOff) },
{ be_const_key_weak(temperature, 27), be_const_class(be_class_Matter_Plugin_Sensor_Temp) },
{ be_const_key_weak(waterleak, -1), be_const_class(be_class_Matter_Plugin_Sensor_Waterleak) },
{ be_const_key_weak(v_fan, -1), be_const_class(be_class_Matter_Plugin_Virt_Fan) },
{ be_const_key_weak(http_occupancy, 6), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Occupancy) },
{ be_const_key_weak(v_airquality, -1), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Air_Quality) },
{ be_const_key_weak(shutter_X2Btilt, -1), be_const_class(be_class_Matter_Plugin_ShutterTilt) },
{ be_const_key_weak(fan, -1), be_const_class(be_class_Matter_Plugin_Fan) },
{ be_const_key_weak(light1, -1), be_const_class(be_class_Matter_Plugin_Light1) },
{ be_const_key_weak(root, -1), be_const_class(be_class_Matter_Plugin_Root) },
{ be_const_key_weak(illuminance, -1), be_const_class(be_class_Matter_Plugin_Sensor_Illuminance) },
{ be_const_key_weak(v_temp, 20), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Temp) },
{ be_const_key_weak(v_temp, 41), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Temp) },
{ be_const_key_weak(v_light1, -1), be_const_class(be_class_Matter_Plugin_Virt_Light1) },
{ be_const_key_weak(v_waterleak, -1), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Waterleak) },
{ be_const_key_weak(http_relay, -1), be_const_class(be_class_Matter_Plugin_Bridge_OnOff) },
{ be_const_key_weak(v_relay, -1), be_const_class(be_class_Matter_Plugin_Virt_OnOff) },
{ be_const_key_weak(pressure, -1), be_const_class(be_class_Matter_Plugin_Sensor_Pressure) },
{ be_const_key_weak(light2, 27), be_const_class(be_class_Matter_Plugin_Light2) },
{ be_const_key_weak(v_waterleak, -1), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Waterleak) },
{ be_const_key_weak(light2, 29), be_const_class(be_class_Matter_Plugin_Light2) },
{ be_const_key_weak(http_light1, -1), be_const_class(be_class_Matter_Plugin_Bridge_Light1) },
{ be_const_key_weak(v_flow, -1), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Flow) },
{ be_const_key_weak(onoff, -1), be_const_class(be_class_Matter_Plugin_Sensor_OnOff) },
@ -6192,14 +6142,14 @@ be_local_class(Matter_Device,
{ be_const_key_weak(v_rain, 43), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Rain) },
{ be_const_key_weak(http_light0, -1), be_const_class(be_class_Matter_Plugin_Bridge_Light0) },
{ be_const_key_weak(http_waterleak, 45), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Waterleak) },
{ be_const_key_weak(shutter_X2Btilt, -1), be_const_class(be_class_Matter_Plugin_ShutterTilt) },
{ be_const_key_weak(v_airquality, -1), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Air_Quality) },
{ be_const_key_weak(v_light3, -1), be_const_class(be_class_Matter_Plugin_Virt_Light3) },
{ be_const_key_weak(airquality, 35), be_const_class(be_class_Matter_Plugin_Sensor_Air_Quality) },
{ be_const_key_weak(http_flow, -1), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Flow) },
{ be_const_key_weak(humidity, -1), be_const_class(be_class_Matter_Plugin_Sensor_Humidity) },
{ be_const_key_weak(http_temperature, 41), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Temp) },
{ be_const_key_weak(http_temperature, 20), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Temp) },
{ be_const_key_weak(http_light3, -1), be_const_class(be_class_Matter_Plugin_Bridge_Light3) },
{ be_const_key_weak(v_humidity, 29), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Humidity) },
{ be_const_key_weak(v_humidity, 12), be_const_class(be_class_Matter_Plugin_Virt_Sensor_Humidity) },
{ be_const_key_weak(http_airquality, -1), be_const_class(be_class_Matter_Plugin_Bridge_Sensor_Air_Quality) },
{ be_const_key_weak(aggregator, 11), be_const_class(be_class_Matter_Plugin_Aggregator) },
{ be_const_key_weak(rain, -1), be_const_class(be_class_Matter_Plugin_Sensor_Rain) },