blob: c6ef366925302f84d3cca8598575351830058927 [file] [log] [blame]
/*
*
* Copyright (c) 2021 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MTRBaseDevice_Internal.h"
#import "MTRDeviceController_Internal.h"
#import "MTRError_Internal.h"
#import "zap-generated/MTRBaseClusters.h"
#include <app/data-model/NullObject.h>
#include <messaging/ExchangeMgr.h>
#include <platform/CHIPDeviceLayer.h>
#include <transport/SessionHandle.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Bridge that allows invoking a given MTRActionBlock on the Matter queue, after
* communication with the device in question has been established, as far as we
* know.
*/
class MTRCallbackBridgeBase {
};
typedef void (^MTRResponseHandler)(id _Nullable value, NSError * _Nullable error);
typedef void (*MTRErrorCallback)(void * context, CHIP_ERROR error);
/**
* The bridge will pass itself as the last argument to the action block.
*
* The action block must do one of three things:
*
* 1) Return an error.
* 2) Call the "successCb" callback, with the bridge passed to the block as the
* context, possibly asynchronously.
* 3) Call the "failureCb" callback, with the bridge passed to the block as the
* context, possibly asynchronously.
*
* For an MTRCallbackBridge that has keepAlive set to true, the success/failure
* callbacks may be called multiple times. If keepAlive is false, there must be
* no calls after the first one.
*/
template <typename SuccessCallback>
using MTRActionBlockT = CHIP_ERROR (^)(chip::Messaging::ExchangeManager & exchangeManager, const chip::SessionHandle & session,
SuccessCallback successCb, MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge);
template <typename SuccessCallback>
using MTRLocalActionBlockT = CHIP_ERROR (^)(SuccessCallback successCb, MTRErrorCallback failureCb);
template <class T> class MTRCallbackBridge : public MTRCallbackBridgeBase {
public:
using MTRActionBlock = MTRActionBlockT<T>;
using MTRLocalActionBlock = MTRLocalActionBlockT<T>;
using SuccessCallbackType = T;
/**
* Construct a callback bridge, which can then have DispatcLocalAction() called
* on it.
*/
MTRCallbackBridge(dispatch_queue_t queue, MTRResponseHandler handler, T OnSuccessFn)
: mQueue(queue)
, mHandler(handler)
, mSuccess(OnSuccessFn)
, mFailure(OnFailureFn)
{
}
/**
* Construct a callback bridge, which can then have DispatchAction() called
* on it.
*/
MTRCallbackBridge(dispatch_queue_t queue, MTRResponseHandler handler, MTRActionBlock _Nonnull action, T OnSuccessFn)
: mQueue(queue)
, mHandler(handler)
, mAction(action)
, mSuccess(OnSuccessFn)
, mFailure(OnFailureFn)
{
}
/**
* Run the given MTRActionBlock on the Matter thread, after getting a CASE
* session (possibly pre-existing) to the given node ID on the fabric
* represented by the given MTRDeviceController. On success, convert the
* success value to whatever type it needs to be to call the callback type
* we're templated over. Once this function has been called, on a callback
* bridge allocated with `new`, the bridge object must not be accessed by
* the caller. The action block will handle deleting the bridge.
*/
void DispatchAction(chip::NodeId nodeID, MTRDeviceController * controller) && { ActionWithNodeID(nodeID, controller); }
/**
* Run the given MTRActionBlock on the Matter thread after getting a secure
* session corresponding to the given MTRBaseDevice. On success, convert
* the success value to whatever type it needs to be to call the callback
* type we're templated over. Once this function has been called, on a callback
* bridge allocated with `new`, the bridge object must not be accessed by
* the caller. The action block will handle deleting the bridge.
*/
void DispatchAction(MTRBaseDevice * device) &&
{
if (device.isPASEDevice) {
ActionWithPASEDevice(device);
} else {
ActionWithNodeID(device.nodeID, device.deviceController);
}
}
/**
* Try to run the given MTRLocalActionBlock on the Matter thread, if we have
* a device and it's attached to a running controller, then handle
* converting the value produced by the success callback to the right type
* so it can be passed to a callback of the type we're templated over.
*
* Does not attempt to establish any sessions to devices. Must not be used
* with any action blocks that need a session.
*/
void DispatchLocalAction(MTRBaseDevice * _Nullable device, MTRLocalActionBlock _Nonnull action)
{
if (!device) {
OnFailureFn(this, CHIP_ERROR_INCORRECT_STATE);
return;
}
LogRequestStart();
[device.deviceController
asyncDispatchToMatterQueue:^(chip::Controller::DeviceCommissioner *) {
CHIP_ERROR err = action(mSuccess, mFailure);
if (err != CHIP_NO_ERROR) {
NSLog(@"Failure performing action. C++-mangled success callback type: '%s', error: %s", typeid(T).name(),
chip::ErrorStr(err));
// Take the normal async error-reporting codepath. This will also
// handle cleaning us up properly.
OnFailureFn(this, err);
}
}
errorHandler:^(NSError * error) {
DispatchFailure(this, error);
}];
}
void ActionWithPASEDevice(MTRBaseDevice * device)
{
LogRequestStart();
BOOL ok = [device.deviceController
getSessionForCommissioneeDevice:device.nodeID
completion:^(chip::Messaging::ExchangeManager * exchangeManager,
const chip::Optional<chip::SessionHandle> & session, NSError * error) {
MaybeDoAction(exchangeManager, session, error);
}];
if (ok == NO) {
OnFailureFn(this, CHIP_ERROR_INCORRECT_STATE);
}
}
void ActionWithNodeID(chip::NodeId nodeID, MTRDeviceController * controller)
{
LogRequestStart();
BOOL ok = [controller getSessionForNode:nodeID
completion:^(chip::Messaging::ExchangeManager * exchangeManager,
const chip::Optional<chip::SessionHandle> & session, NSError * error) {
MaybeDoAction(exchangeManager, session, error);
}];
if (ok == NO) {
OnFailureFn(this, CHIP_ERROR_INCORRECT_STATE);
}
}
void LogRequestStart()
{
mRequestTime = [NSDate date];
// Generate a unique cookie to track this operation
mCookie = [NSString stringWithFormat:@"Response Time: %s+%u", typeid(T).name(), arc4random()];
ChipLogDetail(Controller, "%s", mCookie.UTF8String);
}
void MaybeDoAction(chip::Messaging::ExchangeManager * _Nullable exchangeManager,
const chip::Optional<chip::SessionHandle> & session, NSError * _Nullable error)
{
// Make sure we don't hold on to our action longer than we have to.
auto action = mAction;
mAction = nil;
if (error != nil) {
DispatchFailure(this, error);
return;
}
CHIP_ERROR err = action(*exchangeManager, session.Value(), mSuccess, mFailure, this);
if (err != CHIP_NO_ERROR) {
NSLog(@"Failure performing action. C++-mangled success callback type: '%s', error: %s", typeid(T).name(),
chip::ErrorStr(err));
// Take the normal async error-reporting codepath. This will also
// handle cleaning us up properly.
OnFailureFn(this, err);
}
}
virtual ~MTRCallbackBridge() {};
static void OnFailureFn(void * context, CHIP_ERROR error) { DispatchFailure(context, [MTRError errorForCHIPErrorCode:error]); }
static void DispatchSuccess(void * context, id _Nullable value) { DispatchCallbackResult(context, nil, value); }
static void DispatchFailure(void * context, NSError * error) { DispatchCallbackResult(context, error, nil); }
protected:
// OnDone and KeepAliveOnCallback really only make sense for subscription
// bridges, but we put them here to avoid many copies of this code in
// generated bits.
void OnDone()
{
if (!mQueue) {
delete this;
return;
}
// Delete ourselves async, so that any error/data reports we
// queued up before getting OnDone have a chance to run.
auto * self = this;
dispatch_async(mQueue, ^{
delete self;
});
}
void KeepAliveOnCallback() { mKeepAlive = true; }
dispatch_queue_t mQueue;
private:
static void DispatchCallbackResult(void * context, NSError * _Nullable error, id _Nullable value)
{
MTRCallbackBridge * callbackBridge = static_cast<MTRCallbackBridge *>(context);
if (!callbackBridge) {
return;
}
if (!callbackBridge->mQueue) {
if (!callbackBridge->mKeepAlive) {
delete callbackBridge;
}
return;
}
dispatch_async(callbackBridge->mQueue, ^{
ChipLogDetail(Controller, "%s %f seconds", callbackBridge->mCookie.UTF8String,
-[callbackBridge->mRequestTime timeIntervalSinceNow]);
callbackBridge->mHandler(value, error);
if (!callbackBridge->mKeepAlive) {
delete callbackBridge;
}
});
}
MTRResponseHandler mHandler;
MTRActionBlock _Nullable mAction;
bool mKeepAlive = false;
T mSuccess;
MTRErrorCallback mFailure;
// Measure the time it took for the callback to trigger
NSDate * mRequestTime;
NSString * mCookie;
};
NS_ASSUME_NONNULL_END