Skip to content

Commit

Permalink
Update CommandHandler to support batch commands
Browse files Browse the repository at this point in the history
  • Loading branch information
tehampson committed Nov 22, 2023
1 parent 90f36a4 commit 4fe0bdc
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 50 deletions.
119 changes: 103 additions & 16 deletions src/app/CommandHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <app/RequiredPrivilege.h>
#include <app/util/MatterCallbacks.h>
#include <credentials/GroupDataProvider.h>
#include <lib/core/CHIPConfig.h>
#include <lib/core/TLVData.h>
#include <lib/core/TLVUtilities.h>
#include <lib/support/TypeTraits.h>
Expand Down Expand Up @@ -97,6 +98,54 @@ void CommandHandler::OnInvokeCommandRequest(Messaging::ExchangeContext * ec, con
mGoneAsync = true;
}

CHIP_ERROR CommandHandler::ValidateCommands(TLV::TLVReader & invokeRequestsReader)
{
CHIP_ERROR err = CHIP_NO_ERROR;
size_t commandCount = 0;
bool commandRefExpected = false;

TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */);
VerifyOrReturnError(commandCount <= CHIP_CONFIG_MAX_PATHS_PER_INVOKE, CHIP_ERROR_INVALID_ARGUMENT);
commandRefExpected = commandCount > 1;

while (CHIP_NO_ERROR == (err = invokeRequestsReader.Next()))
{
VerifyOrReturnError(TLV::AnonymousTag() == invokeRequestsReader.GetTag(), CHIP_ERROR_INVALID_ARGUMENT);
CommandDataIB::Parser commandData;
ReturnErrorOnFailure(commandData.Init(invokeRequestsReader));

CommandPathIB::Parser commandPath;
ConcreteCommandPath concretePath(0, 0, 0);

ReturnErrorOnFailure(commandData.GetPath(&commandPath));
ReturnErrorOnFailure(commandPath.GetConcreteCommandPath(concretePath));

Optional<uint16_t> commandRef;
uint16_t ref;
err = commandData.GetRef(&ref);
VerifyOrReturnError(err == CHIP_NO_ERROR || err == CHIP_END_OF_TLV, err);
if (err == CHIP_END_OF_TLV && commandRefExpected)
{
return CHIP_ERROR_INVALID_ARGUMENT;
}
if (err == CHIP_NO_ERROR)
{
commandRef = MakeOptional(ref);
}

VerifyOrReturnError(!mCommandRefLookupTable.IsRequestedPathAndRefUnique(concretePath, commandRef),
CHIP_ERROR_INVALID_ARGUMENT);
ReturnErrorOnFailure(mCommandRefLookupTable.Add(concretePath, commandRef));
}

// if we have exhausted this container
if (CHIP_END_OF_TLV == err)
{
err = CHIP_NO_ERROR;
}
return err;
}

Status CommandHandler::ProcessInvokeRequest(System::PacketBufferHandle && payload, bool isTimedInvoke)
{
CHIP_ERROR err = CHIP_NO_ERROR;
Expand All @@ -116,13 +165,9 @@ Status CommandHandler::ProcessInvokeRequest(System::PacketBufferHandle && payloa
VerifyOrReturnError(mTimedRequest == isTimedInvoke, Status::UnsupportedAccess);
invokeRequests.GetReader(&invokeRequestsReader);

{
// We don't support handling multiple commands but the protocol is ready to support it in the future, reject all of them and
// IM Engine will send a status response.
size_t commandCount = 0;
TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */);
VerifyOrReturnError(commandCount == 1, Status::InvalidAction);
}
VerifyOrReturnError(ValidateCommands(invokeRequestsReader) == CHIP_NO_ERROR, Status::InvalidAction);
// Reset the reader
invokeRequests.GetReader(&invokeRequestsReader);

while (CHIP_NO_ERROR == (err = invokeRequestsReader.Next()))
{
Expand Down Expand Up @@ -500,15 +545,43 @@ CHIP_ERROR CommandHandler::AddClusterSpecificFailure(const ConcreteCommandPath &
return AddStatusInternal(aCommandPath, StatusIB(Status::Failure, aClusterStatus));
}

CHIP_ERROR CommandHandler::PrepareCommand(const ConcreteCommandPath & aRequestCommandPath, const ConcreteCommandPath & aCommandPath,
bool aStartDataStruct)
{
auto * commandRefTableEntry = mCommandRefLookupTable.Find(aRequestCommandPath);
return PrepareCommand(commandRefTableEntry, aCommandPath, aStartDataStruct);
}

CHIP_ERROR CommandHandler::PrepareCommand(const ConcreteCommandPath & aCommandPath, bool aStartDataStruct)
{
// Legacy code is calling the deprecated version of PrepareCommand. If we are in a case where
// there was a single command in the request, we can just assume this response is triggered by
// the single command.
auto * commandRefTableEntry = mCommandRefLookupTable.GetCommandRefTableEntryIfSingleRequest();

// At this point application supports Batch Invoke Commands since there does not exist a single command in table,
// but application is calling the depricated PrepareCommand. We have no way to determine the associated CommandRef
// to put into the InvokeResponse.
VerifyOrDieWithMsg(commandRefTableEntry, DataManagement,
"Seemingly device supports batch commands, but is calling the deprecated PrepareCommand API");
return PrepareCommand(commandRefTableEntry, aCommandPath, aStartDataStruct);
}

CHIP_ERROR CommandHandler::PrepareCommand(const CommandRefLookupTable::CommandRefTableEntry * apCommandRefTableEntry,
const ConcreteCommandPath & aCommandPath, bool aStartDataStruct)
{
VerifyOrReturnError(apCommandRefTableEntry, CHIP_ERROR_INCORRECT_STATE);
ReturnErrorOnFailure(AllocateBuffer());

mInvokeResponseBuilder.Checkpoint(mBackupWriter);
mBackupState = mState;
//
// We must not be in the middle of preparing a command, or having prepared or sent one.
//
VerifyOrReturnError(mState == State::Idle, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(mState == State::Idle || mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE);

mRefForResponse = apCommandRefTableEntry->ref;

MoveToState(State::Preparing);
InvokeResponseIBs::Builder & invokeResponses = mInvokeResponseBuilder.GetInvokeResponses();
InvokeResponseIB::Builder & invokeResponse = invokeResponses.CreateInvokeResponse();
Expand Down Expand Up @@ -536,10 +609,14 @@ CHIP_ERROR CommandHandler::FinishCommand(bool aStartDataStruct)
{
ReturnErrorOnFailure(commandData.GetWriter()->EndContainer(mDataElementContainerType));
}

if (mRefForResponse.HasValue())
{
ReturnErrorOnFailure(commandData.Ref(mRefForResponse.Value()));
}

ReturnErrorOnFailure(commandData.EndOfCommandDataIB());
ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().EndOfInvokeResponseIB());
ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().EndOfInvokeResponses());
ReturnErrorOnFailure(mInvokeResponseBuilder.EndOfInvokeResponseMessage());
MoveToState(State::AddedCommand);
return CHIP_NO_ERROR;
}
Expand All @@ -550,7 +627,12 @@ CHIP_ERROR CommandHandler::PrepareStatus(const ConcreteCommandPath & aCommandPat
//
// We must not be in the middle of preparing a command, or having prepared or sent one.
//
VerifyOrReturnError(mState == State::Idle, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(mState == State::Idle || mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE);

auto * commandRefTableEntry = mCommandRefLookupTable.Find(aCommandPath);
VerifyOrReturnError(commandRefTableEntry, CHIP_ERROR_INCORRECT_STATE);
mRefForResponse = commandRefTableEntry->ref;

MoveToState(State::Preparing);
InvokeResponseIBs::Builder & invokeResponses = mInvokeResponseBuilder.GetInvokeResponses();
InvokeResponseIB::Builder & invokeResponse = invokeResponses.CreateInvokeResponse();
Expand All @@ -567,10 +649,15 @@ CHIP_ERROR CommandHandler::PrepareStatus(const ConcreteCommandPath & aCommandPat
CHIP_ERROR CommandHandler::FinishStatus()
{
VerifyOrReturnError(mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE);

CommandStatusIB::Builder & commandStatus = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus();
if (mRefForResponse.HasValue())
{
ReturnErrorOnFailure(commandStatus.Ref(mRefForResponse.Value()));
}

ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus().EndOfCommandStatusIB());
ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().EndOfInvokeResponseIB());
ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().EndOfInvokeResponses());
ReturnErrorOnFailure(mInvokeResponseBuilder.EndOfInvokeResponseMessage());
MoveToState(State::AddedCommand);
return CHIP_NO_ERROR;
}
Expand All @@ -579,9 +666,7 @@ CHIP_ERROR CommandHandler::RollbackResponse()
{
VerifyOrReturnError(mState == State::Preparing || mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE);
mInvokeResponseBuilder.Rollback(mBackupWriter);
// Note: We only support one command per request, so we reset the state to Idle here, need to review the states when adding
// supports of having multiple requests in the same transaction.
MoveToState(State::Idle);
MoveToState(mBackupState);
return CHIP_NO_ERROR;
}

Expand Down Expand Up @@ -635,6 +720,8 @@ CommandHandler::Handle::Handle(CommandHandler * handle)
CHIP_ERROR CommandHandler::Finalize(System::PacketBufferHandle & commandPacket)
{
VerifyOrReturnError(mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE);
ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().EndOfInvokeResponses());
ReturnErrorOnFailure(mInvokeResponseBuilder.EndOfInvokeResponseMessage());
return mCommandMessageWriter.Finalize(&commandPacket);
}

Expand Down
19 changes: 17 additions & 2 deletions src/app/CommandHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

#pragma once

#include "CommandRefLookupTable.h"

#include <app/ConcreteCommandPath.h>
#include <app/data-model/Encode.h>
#include <lib/core/CHIPCore.h>
Expand Down Expand Up @@ -190,7 +192,10 @@ class CommandHandler : public Messaging::ExchangeDelegate
CHIP_ERROR AddClusterSpecificFailure(const ConcreteCommandPath & aCommandPath, ClusterStatus aClusterStatus);

Protocols::InteractionModel::Status ProcessInvokeRequest(System::PacketBufferHandle && payload, bool isTimedInvoke);
CHIP_ERROR PrepareCommand(const ConcreteCommandPath & aCommandPath, bool aStartDataStruct = true);
CHIP_ERROR PrepareCommand(const ConcreteCommandPath & aRequestCommandPath, const ConcreteCommandPath & aCommandPath,
bool aStartDataStruct = true);
[[deprecated("PrepareCommand now needs the requested command path. Without it device cannot support batch invoke")]] CHIP_ERROR
PrepareCommand(const ConcreteCommandPath & aCommandPath, bool aStartDataStruct = true);
CHIP_ERROR FinishCommand(bool aEndDataStruct = true);
CHIP_ERROR PrepareStatus(const ConcreteCommandPath & aCommandPath);
CHIP_ERROR FinishStatus();
Expand Down Expand Up @@ -363,6 +368,11 @@ class CommandHandler : public Messaging::ExchangeDelegate
*/
CHIP_ERROR AllocateBuffer();

CHIP_ERROR ValidateCommands(TLV::TLVReader & invokeRequestsReader);

CHIP_ERROR PrepareCommand(const CommandRefLookupTable::CommandRefTableEntry * apCommandRefTableEntry,
const ConcreteCommandPath & aCommandPath, bool aStartDataStruct);

CHIP_ERROR Finalize(System::PacketBufferHandle & commandPacket);

/**
Expand Down Expand Up @@ -398,7 +408,7 @@ class CommandHandler : public Messaging::ExchangeDelegate
CHIP_ERROR TryAddResponseData(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData)
{
ConcreteCommandPath path = { aRequestCommandPath.mEndpointId, aRequestCommandPath.mClusterId, CommandData::GetCommandId() };
ReturnErrorOnFailure(PrepareCommand(path, false));
ReturnErrorOnFailure(PrepareCommand(aRequestCommandPath, path, false));
TLV::TLVWriter * writer = GetCommandDataIBTLVWriter();
VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE);
ReturnErrorOnFailure(DataModel::Encode(*writer, TLV::ContextTag(CommandDataIB::Tag::kFields), aData));
Expand Down Expand Up @@ -428,8 +438,13 @@ class CommandHandler : public Messaging::ExchangeDelegate

State mState = State::Idle;
bool mGroupRequest = false;
Optional<uint16_t> mRefForResponse;

CommandRefLookupTable mCommandRefLookupTable;
chip::System::PacketBufferTLVWriter mCommandMessageWriter;
TLV::TLVWriter mBackupWriter;
State mBackupState;

bool mBufferAllocated = false;
// If mGoneAsync is true, we have finished out initial processing of the
// incoming invoke. After this point, our session could go away at any
Expand Down
106 changes: 106 additions & 0 deletions src/app/CommandRefLookupTable.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
*
* Copyright (c) 2023 Project CHIP Authors
* All rights reserved.
*
* 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.
*/

#pragma once

#include <app/ConcreteCommandPath.h>
#include <lib/core/CHIPError.h>
#include <lib/core/Optional.h>

namespace chip {
namespace app {

/**
* @class CommandRefLookupTable
*
* @brief Allows looking up CommandRef using the requested ConcreteCommandPath.
*
* While there are faster implementations, right now batch commands are capped at a low number due to
* message size constrains. All commands need to be contained within a single InvokeRequest. In
* practice this is less than 60 commands.
*
*/
class CommandRefLookupTable
{
public:
struct CommandRefTableEntry
{
ConcreteCommandPath requestPath = ConcreteCommandPath(0, 0, 0);
Optional<uint16_t> ref;
};

/**
* IsRequestedPathAndRefUnique() is used to determine if requestPath and ref (if it has value)
* are unique and do NOT already exists in the table. This is to help validate incoming requests.
*/
bool IsRequestedPathAndRefUnique(const ConcreteCommandPath & requestPath, const Optional<uint16_t> & ref)
{
for (int i = 0; i < mCount; i++)
{
if (mTable[i].requestPath == requestPath)
{
return false;
}
if (mTable[i].ref.HasValue() && mTable[i].ref == ref)
{
return false;
}
}
return true;
}

const CommandRefTableEntry * Find(const ConcreteCommandPath & requestPath)
{
for (int i = 0; i < mCount; i++)
{
if (mTable[i].requestPath == requestPath)
{
return &mTable[i];
}
}
return nullptr;
}

CommandRefTableEntry * GetCommandRefTableEntryIfSingleRequest()
{
if (mCount == 1)
{
return &mTable[0];
}
return nullptr;
}

CHIP_ERROR Add(const ConcreteCommandPath & requestPath, const Optional<uint16_t> & ref)
{
if (mCount > CHIP_CONFIG_MAX_PATHS_PER_INVOKE)
{
return CHIP_ERROR_NO_MEMORY;
}

mTable[mCount] = CommandRefTableEntry{ requestPath, ref };
mCount++;
return CHIP_NO_ERROR;
}

private:
uint16_t mCount = 0;
CommandRefTableEntry mTable[CHIP_CONFIG_MAX_PATHS_PER_INVOKE];
};

} // namespace app
} // namespace chip
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ void Instance::OnFinished(Status status, CharSpan debugText, ThreadScanResponseI
uint8_t extendedAddressBuffer[Thread::kSizeExtendedPanId];

SuccessOrExit(err = commandHandle->PrepareCommand(
ConcreteCommandPath(mPath.mEndpointId, NetworkCommissioning::Id, Commands::ScanNetworksResponse::Id)));
mPath, ConcreteCommandPath(mPath.mEndpointId, NetworkCommissioning::Id, Commands::ScanNetworksResponse::Id)));
VerifyOrExit((writer = commandHandle->GetCommandDataIBTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE);

SuccessOrExit(err = writer->Put(TLV::ContextTag(Commands::ScanNetworksResponse::Fields::kNetworkingStatus), status));
Expand Down Expand Up @@ -928,7 +928,7 @@ void Instance::OnFinished(Status status, CharSpan debugText, WiFiScanResponseIte
size_t networksEncoded = 0;

SuccessOrExit(err = commandHandle->PrepareCommand(
ConcreteCommandPath(mPath.mEndpointId, NetworkCommissioning::Id, Commands::ScanNetworksResponse::Id)));
mPath, ConcreteCommandPath(mPath.mEndpointId, NetworkCommissioning::Id, Commands::ScanNetworksResponse::Id)));
VerifyOrExit((writer = commandHandle->GetCommandDataIBTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE);

SuccessOrExit(err = writer->Put(TLV::ContextTag(Commands::ScanNetworksResponse::Fields::kNetworkingStatus), status));
Expand Down

0 comments on commit 4fe0bdc

Please sign in to comment.