[Darwin] MTRDevice should change state to reachable immediately upon receiving subscription reports (#28996)
* [Darwin] MTRDevice should change state to reachable immediately upon receiving subscription reports
* Fix unit tests
* Address review comment on _handleEventReport: needing to handle _state behavior change
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Clarify _handleEventReport comments
* Clarify further _handleEventReport comments
---------
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.h b/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.h
index dc2dda3..12488e6 100644
--- a/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.h
+++ b/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.h
@@ -56,13 +56,16 @@
typedef void (^SubscriptionEstablishedHandler)(void);
typedef void (^OnDoneHandler)(void);
typedef void (^UnsolicitedMessageFromPublisherHandler)(void);
+typedef void (^ReportBeginHandler)(void);
+typedef void (^ReportEndHandler)(void);
class MTRBaseSubscriptionCallback : public chip::app::ClusterStateCache::Callback {
public:
MTRBaseSubscriptionCallback(DataReportCallback attributeReportCallback, DataReportCallback eventReportCallback,
ErrorCallback errorCallback, MTRDeviceResubscriptionScheduledHandler _Nullable resubscriptionCallback,
SubscriptionEstablishedHandler _Nullable subscriptionEstablishedHandler, OnDoneHandler _Nullable onDoneHandler,
- UnsolicitedMessageFromPublisherHandler _Nullable unsolicitedMessageFromPublisherHandler = NULL)
+ UnsolicitedMessageFromPublisherHandler _Nullable unsolicitedMessageFromPublisherHandler = nil,
+ ReportBeginHandler _Nullable reportBeginHandler = nil, ReportEndHandler _Nullable reportEndHandler = nil)
: mAttributeReportCallback(attributeReportCallback)
, mEventReportCallback(eventReportCallback)
, mErrorCallback(errorCallback)
@@ -71,6 +74,8 @@
, mBufferedReadAdapter(*this)
, mOnDoneHandler(onDoneHandler)
, mUnsolicitedMessageFromPublisherHandler(unsolicitedMessageFromPublisherHandler)
+ , mReportBeginHandler(reportBeginHandler)
+ , mReportEndHandler(reportEndHandler)
{
}
@@ -137,6 +142,8 @@
MTRDeviceResubscriptionScheduledHandler _Nullable mResubscriptionCallback = nil;
SubscriptionEstablishedHandler _Nullable mSubscriptionEstablishedHandler = nil;
UnsolicitedMessageFromPublisherHandler _Nullable mUnsolicitedMessageFromPublisherHandler = nil;
+ ReportBeginHandler _Nullable mReportBeginHandler = nil;
+ ReportEndHandler _Nullable mReportEndHandler = nil;
chip::app::BufferedReadCallback mBufferedReadAdapter;
// Our lifetime management is a little complicated. On errors that don't
diff --git a/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.mm b/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.mm
index 7e07720..24301ce 100644
--- a/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.mm
+++ b/src/darwin/Framework/CHIP/MTRBaseSubscriptionCallback.mm
@@ -27,6 +27,9 @@
{
mAttributeReports = [NSMutableArray new];
mEventReports = [NSMutableArray new];
+ if (mReportBeginHandler) {
+ mReportBeginHandler();
+ }
}
// Reports attribute and event data if any exists
@@ -48,7 +51,13 @@
}
}
-void MTRBaseSubscriptionCallback::OnReportEnd() { ReportData(); }
+void MTRBaseSubscriptionCallback::OnReportEnd()
+{
+ ReportData();
+ if (mReportEndHandler) {
+ mReportEndHandler();
+ }
+}
void MTRBaseSubscriptionCallback::OnError(CHIP_ERROR aError)
{
diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm
index 3b9f0bd..7fbff23 100644
--- a/src/darwin/Framework/CHIP/MTRDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRDevice.mm
@@ -91,6 +91,8 @@
using namespace chip::app;
using namespace chip::Protocols::InteractionModel;
+typedef void (^FirstReportHandler)(void);
+
namespace {
class SubscriptionCallback final : public MTRBaseSubscriptionCallback {
@@ -98,9 +100,11 @@
SubscriptionCallback(DataReportCallback attributeReportCallback, DataReportCallback eventReportCallback,
ErrorCallback errorCallback, MTRDeviceResubscriptionScheduledHandler resubscriptionCallback,
SubscriptionEstablishedHandler subscriptionEstablishedHandler, OnDoneHandler onDoneHandler,
- UnsolicitedMessageFromPublisherHandler unsolicitedMessageFromPublisherHandler)
+ UnsolicitedMessageFromPublisherHandler unsolicitedMessageFromPublisherHandler, ReportBeginHandler reportBeginHandler,
+ ReportEndHandler reportEndHandler)
: MTRBaseSubscriptionCallback(attributeReportCallback, eventReportCallback, errorCallback, resubscriptionCallback,
- subscriptionEstablishedHandler, onDoneHandler, unsolicitedMessageFromPublisherHandler)
+ subscriptionEstablishedHandler, onDoneHandler, unsolicitedMessageFromPublisherHandler, reportBeginHandler,
+ reportEndHandler)
{
}
@@ -278,7 +282,6 @@
// Return YES if there's a valid delegate AND subscription is expected to report value
- (BOOL)_subscriptionAbleToReport
{
- // TODO: include period from when first report comes in until establish callback
return (_weakDelegate.strongObject) && (_state == MTRDeviceStateReachable);
}
@@ -290,9 +293,11 @@
_state = state;
if (lastState != state) {
if (state != MTRDeviceStateReachable) {
- MTR_LOG_INFO("%@ Set estimated start time to nil due to state change", self);
+ MTR_LOG_INFO("%@ State change %lu => %lu, set estimated start time to nil", self, lastState, state);
_estimatedStartTime = nil;
_estimatedStartTimeFromGeneralDiagnosticsUpTime = nil;
+ } else {
+ MTR_LOG_INFO("%@ State change %lu => %lu", self, lastState, state);
}
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate) {
@@ -411,6 +416,20 @@
os_unfair_lock_unlock(&self->_lock);
}
+- (void)_handleReportBegin
+{
+ os_unfair_lock_lock(&self->_lock);
+ [self _changeState:MTRDeviceStateReachable];
+ os_unfair_lock_unlock(&self->_lock);
+}
+
+- (void)_handleReportEnd
+{
+ os_unfair_lock_lock(&self->_lock);
+ _estimatedStartTimeFromGeneralDiagnosticsUpTime = nil;
+ os_unfair_lock_unlock(&self->_lock);
+}
+
// assume lock is held
- (void)_reportAttributes:(NSArray<NSDictionary<NSString *, id> *> *)attributes
{
@@ -442,11 +461,33 @@
NSDate * oldEstimatedStartTime = _estimatedStartTime;
for (NSDictionary<NSString *, id> * eventDict in eventReport) {
// Whenever a StartUp event is received, reset the estimated start time
+ // New subscription case
+ // - Starts Unreachable
+ // - Start CASE and send subscription request
+ // - Receive priming report ReportBegin
+ // - Optionally receive UpTime attribute - update time and save start time estimate
+ // - Optionally receive StartUp event
+ // - Set estimated system time from event receipt time, or saved UpTime estimate if exists
+ // - ReportEnd handler clears the saved start time estimate based on UpTime
+ // Subscription dropped from client point of view case
+ // - Starts Unreachable
+ // - Resubscribe happens after some time, and then same as the above
+ // Server resuming subscription after reboot case
+ // - Starts Reachable
+ // - Receive priming report ReportBegin
+ // - Optionally receive UpTime attribute - update time and save value
+ // - Optionally receive StartUp event
+ // - Set estimated system time from event receipt time, or saved UpTime estimate if exists
+ // - ReportEnd handler clears the saved start time estimate based on UpTime
+ // Server resuming subscription after timeout case
+ // - Starts Reachable
+ // - Receive priming report ReportBegin
+ // - Optionally receive UpTime attribute - update time and save value
+ // - ReportEnd handler clears the saved start time estimate based on UpTime
MTREventPath * eventPath = eventDict[MTREventPathKey];
BOOL isStartUpEvent = (eventPath.cluster.unsignedLongValue == MTRClusterIDTypeBasicInformationID)
&& (eventPath.event.unsignedLongValue == MTREventIDTypeClusterBasicInformationEventStartUpID);
- if (isStartUpEvent && (_state == MTRDeviceStateReachable)) {
- // StartUp event received when server resumes subscription
+ if (isStartUpEvent) {
if (_estimatedStartTimeFromGeneralDiagnosticsUpTime) {
// If UpTime was received, make use of it as mark of system start time
MTR_LOG_INFO("%@ StartUp event: set estimated start time forward to %@", self,
@@ -610,6 +651,18 @@
// OnUnsolicitedMessageFromPublisher
[self _handleUnsolicitedMessageFromPublisher];
});
+ },
+ ^(void) {
+ MTR_LOG_DEFAULT("%@ got report begin", self);
+ dispatch_async(self.queue, ^{
+ [self _handleReportBegin];
+ });
+ },
+ ^(void) {
+ MTR_LOG_DEFAULT("%@ got report end", self);
+ dispatch_async(self.queue, ^{
+ [self _handleReportEnd];
+ });
});
// Set up a cluster state cache. We just want this for the logic it has for
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index 0d9b693..95f4aab 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -118,19 +118,19 @@
typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray<NSDictionary<NSString *, id> *> *);
@interface MTRDeviceTestDelegate : NSObject <MTRDeviceDelegate>
-@property (nonatomic) dispatch_block_t onSubscriptionEstablished;
+@property (nonatomic) dispatch_block_t onReachable;
+@property (nonatomic, nullable) dispatch_block_t onNotReachable;
@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onAttributeDataReceived;
@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onEventDataReceived;
-@property (nonatomic, nullable) dispatch_block_t onSubscriptionDropped;
@end
@implementation MTRDeviceTestDelegate
- (void)device:(MTRDevice *)device stateChanged:(MTRDeviceState)state
{
if (state == MTRDeviceStateReachable) {
- self.onSubscriptionEstablished();
- } else if (state == MTRDeviceStateUnknown && self.onSubscriptionDropped != nil) {
- self.onSubscriptionDropped();
+ self.onReachable();
+ } else if (state != MTRDeviceStateReachable && self.onNotReachable != nil) {
+ self.onNotReachable();
}
}
@@ -1423,10 +1423,12 @@
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
dispatch_queue_t queue = dispatch_get_main_queue();
+ // Given reachable state becomes true before underlying OnSubscriptionEstablished callback, this expectation is necessary but
+ // not sufficient as a mark to the end of reports
XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"];
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
- delegate.onSubscriptionEstablished = ^() {
+ delegate.onReachable = ^() {
[subscriptionExpectation fulfill];
};
@@ -1435,6 +1437,10 @@
attributeReportsReceived += data.count;
};
+ // This is dependent on current implementation that priming reports send attributes and events in that order, and also that
+ // events in this test would fit in one report. So receiving events would mean all attributes and events have been received, and
+ // can satisfy the test below.
+ XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];
__block unsigned eventReportsReceived = 0;
delegate.onEventDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * eventReport) {
eventReportsReceived += eventReport.count;
@@ -1451,6 +1457,7 @@
XCTAssertNotNil(eventDict[MTREventTimestampDateKey]);
}
}
+ [gotReportsExpectation fulfill];
};
[device setDelegate:delegate queue:queue];
@@ -1481,7 +1488,7 @@
[device readAttributeWithEndpointID:@(1) clusterID:@(MTRClusterIDTypeLevelControlID) attributeID:@(4) params:nil];
[device readAttributeWithEndpointID:@(1) clusterID:@(MTRClusterIDTypeLevelControlID) attributeID:@(4) params:nil];
- [self waitForExpectations:@[ subscriptionExpectation ] timeout:60];
+ [self waitForExpectations:@[ subscriptionExpectation, gotReportsExpectation ] timeout:60];
XCTAssertNotEqual(attributeReportsReceived, 0);
XCTAssertNotEqual(eventReportsReceived, 0);
@@ -1489,16 +1496,6 @@
attributeReportsReceived = 0;
eventReportsReceived = 0;
- XCTestExpectation * resubscriptionExpectation = [self expectationWithDescription:@"Resubscription has happened"];
- delegate.onSubscriptionEstablished = ^() {
- [resubscriptionExpectation fulfill];
- };
-
- XCTestExpectation * subscriptionDroppedExpectation = [self expectationWithDescription:@"Subscription has dropped"];
- delegate.onSubscriptionDropped = ^() {
- [subscriptionDroppedExpectation fulfill];
- };
-
// Before resubscribe, first test write failure and expected value effects
NSNumber * testEndpointID = @(1);
NSNumber * testClusterID = @(8);
@@ -1547,11 +1544,25 @@
[device readAttributeWithEndpointID:testEndpointID clusterID:testClusterID attributeID:testAttributeID params:nil];
[self waitForExpectations:@[ attributeReportErrorExpectation ] timeout:10];
+ // Resubscription test setup
+ XCTestExpectation * subscriptionDroppedExpectation = [self expectationWithDescription:@"Subscription has dropped"];
+ delegate.onNotReachable = ^() {
+ [subscriptionDroppedExpectation fulfill];
+ };
+ XCTestExpectation * resubscriptionExpectation = [self expectationWithDescription:@"Resubscription has happened"];
+ delegate.onReachable = ^() {
+ [resubscriptionExpectation fulfill];
+ };
+
// reset the onAttributeDataReceived to validate the following resubscribe test
delegate.onAttributeDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * data) {
attributeReportsReceived += data.count;
};
+ delegate.onEventDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * eventReport) {
+ eventReportsReceived += eventReport.count;
+ };
+
// Now trigger another subscription which will cause ours to drop; we should re-subscribe after that.
MTRBaseDevice * baseDevice = GetConnectedDevice();
__auto_type params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
@@ -1580,9 +1591,9 @@
// Now make sure we ignore later tests. Ideally we would just unsubscribe
// or remove the delegate, but there's no good way to do that.
- delegate.onSubscriptionEstablished = ^() {
+ delegate.onReachable = ^() {
};
- delegate.onSubscriptionDropped = nil;
+ delegate.onNotReachable = nil;
delegate.onAttributeDataReceived = nil;
delegate.onEventDataReceived = nil;