blob: 72806bc1e00bc8a0a44cfdae8cda1bd9df12b8bd [file]
/*
*
* Copyright (c) 2026 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/BufferedReadCallback.h>
#include <app/CASESessionManager.h>
#include <app/CommandSender.h>
#include <app/ReadClient.h>
#include <app/clusters/av-analysis-server/AvAnalysisCameraClient.h>
#include <clusters/CameraAvStreamManagement/Commands.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/TypeTraits.h>
#include <transport/SessionHolder.h>
namespace chip {
namespace app {
namespace Clusters {
/**
* Production AvAnalysisCameraClient: reaches the camera over a CASE session obtained from the
* CASESessionManager and sends the CameraAVStreamManagement commands with a CommandSender.
*
* One interaction is in flight at a time; a request made while another is pending fails with
* CHIP_ERROR_BUSY.
*
* The application creates one instance at startup and hands it to the cluster:
* client.Init(Server::GetInstance().GetCASESessionManager());
* cluster.SetCameraClient(&client);
*/
class DefaultAvAnalysisCameraClient : public AvAnalysisCameraClient, public CommandSender::Callback, public ReadClient::Callback
{
public:
/**
* What the client knows about the camera when building requests toward it.
*/
struct CameraProfile
{
static constexpr uint16_t kDefaultMinFrameRate = 15;
static constexpr uint16_t kDefaultMaxFrameRate = 30;
static constexpr uint16_t kDefaultMinWidth = 640;
static constexpr uint16_t kDefaultMinHeight = 480;
static constexpr uint16_t kDefaultMaxWidth = 1920;
static constexpr uint16_t kDefaultMaxHeight = 1080;
static constexpr uint32_t kDefaultMinBitRateBps = 500000;
static constexpr uint32_t kDefaultMaxBitRateBps = 2000000;
// Discovered per request, invalid until the Descriptor read finds the AVSM endpoint
EndpointId avsmEndpoint = kInvalidEndpointId;
bool hasWatermark = false;
bool hasOSD = false;
uint16_t minFrameRate = kDefaultMinFrameRate;
uint16_t maxFrameRate = kDefaultMaxFrameRate;
uint16_t minWidth = kDefaultMinWidth;
uint16_t minHeight = kDefaultMinHeight;
uint16_t maxWidth = kDefaultMaxWidth;
uint16_t maxHeight = kDefaultMaxHeight;
uint32_t minBitRateBps = kDefaultMinBitRateBps;
uint32_t maxBitRateBps = kDefaultMaxBitRateBps;
};
DefaultAvAnalysisCameraClient() :
mOnConnectedCallback(OnDeviceConnected, this), mOnConnectionFailureCallback(OnDeviceConnectionFailure, this)
{}
/**
* @param aCASESessionManager Used to reach the camera node; must outlive this instance.
*
* Everything camera-specific — the CameraAVStreamManagement endpoint, its features, and its
* stream constraints — is discovered from the camera itself per request.
*/
CHIP_ERROR Init(CASESessionManager * aCASESessionManager);
// AvAnalysisCameraClient
CHIP_ERROR RequestVideoStreamAllocation(const ScopedNodeId & aCameraNode,
AvAnalysisCameraClient::Callback & aCallback) override;
CHIP_ERROR RequestVideoStreamDeallocation(const ScopedNodeId & aCameraNode, uint16_t aVideoStreamId,
AvAnalysisCameraClient::Callback & aCallback) override;
void Cancel() override;
// CommandSender::Callback
void OnResponse(CommandSender * apCommandSender, const ConcreteCommandPath & aPath, const StatusIB & aStatusIB,
TLV::TLVReader * apData) override;
void OnError(const CommandSender * apCommandSender, CHIP_ERROR aError) override;
void OnDone(CommandSender * apCommandSender) override;
// ReadClient::Callback (profile discovery reads)
void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override;
void OnError(CHIP_ERROR aError) override;
void OnDone(ReadClient * apReadClient) override;
protected:
/**
* Starts CASE session establishment toward the camera; the connected/connection-failure callbacks
* continue the pending request. The default body is the expected behavior; virtual only so unit
* tests can intercept the network boundary.
*/
virtual void EstablishSession(const ScopedNodeId & aCameraNode)
{
mCASESessionManager->FindOrEstablishSession(aCameraNode, &mOnConnectedCallback, &mOnConnectionFailureCallback);
}
/**
* Begins profile discovery for the pending request, once the camera session is up.
*/
virtual void StartProfileDiscovery();
/**
* Completion of StartProfileDiscovery: on success the pending command is sent on the held session,
* otherwise the request fails.
*/
void OnProfileDiscoveryComplete(CHIP_ERROR aError);
/**
* One camera request, from the first CASE attempt to the camera's answer.
*
* Reaching the camera is a multi-step asynchronous sequence, so the phase says which step is
* outstanding and therefore which callback is legitimate; the rest is the state that lives
* exactly as long as the request does. Exactly one request exists at a time.
*
* kIdle -> kConnecting -> kDiscoveringEndpoint -> kDiscoveringCapabilities -> kInvoking -> kIdle
* (a deallocation skips the capabilities read)
*/
class Request
{
public:
enum class Phase : uint8_t
{
kIdle, // No request in flight
kConnecting, // Awaiting a CASE session with the camera
kDiscoveringEndpoint, // Wildcard Descriptor ServerList read locating the AVSM endpoint
kDiscoveringCapabilities, // Targeted read of the AVSM capability attributes
kInvoking, // Command sent, awaiting the camera's answer
};
enum class CommandType : uint8_t
{
kVideoStreamAllocate,
kVideoStreamDeallocate,
};
Phase GetPhase() const { return mPhase; }
bool InPhase(Phase aPhase) const { return mPhase == aPhase; }
bool InFlight() const { return mPhase != Phase::kIdle; }
void Advance(Phase aPhase) { mPhase = aPhase; }
CommandType GetCommandType() const { return mCommandType; }
uint16_t VideoStreamId() const { return mVideoStreamId; }
CameraProfile & Profile() { return mProfile; }
const CameraProfile & Profile() const { return mProfile; }
/**
* Starts a request in kConnecting. The callback is delivered exactly once, by TakeCallback().
*/
void Begin(CommandType aCommandType, uint16_t aVideoStreamId, AvAnalysisCameraClient::Callback & aCallback)
{
Reset();
mPhase = Phase::kConnecting;
mCommandType = aCommandType;
mVideoStreamId = aVideoStreamId;
mCallback = &aCallback;
}
/**
* Returns the callback owed the outcome, or nullptr when it was already delivered (or the
* request was cancelled). Delivering is therefore exactly-once by construction.
*/
AvAnalysisCameraClient::Callback * TakeCallback()
{
auto * callback = mCallback;
mCallback = nullptr;
return callback;
}
// Session held from kConnecting until the request ends
void HoldSession(const SessionHandle & aSession, Messaging::ExchangeManager & aExchangeMgr)
{
mSessionHolder.Grab(aSession);
mExchangeMgr = &aExchangeMgr;
}
bool HasSession() const { return mSessionHolder && mExchangeMgr != nullptr; }
Optional<SessionHandle> Session() const { return mSessionHolder.Get(); }
Messaging::ExchangeManager & ExchangeManager() const { return *mExchangeMgr; }
// The sender this request invoked with, if any. A CommandSender callback that does not
// carry this pointer belongs to an interaction this request has already finished with.
void SetInvokedSender(CommandSender * aSender) { mInvokedSender = aSender; }
bool WasInvokedBy(const CommandSender * aSender) const { return mInvokedSender == aSender; }
// Discovery outcome; the error only annotates the log, the flag fails an allocation early
void SetDiscoveryError(CHIP_ERROR aError) { mDiscoveryError = aError; }
CHIP_ERROR DiscoveryError() const { return mDiscoveryError; }
void SetAnalysisUsageSupported(bool aSupported) { mAnalysisUsageSupported = aSupported; }
bool AnalysisUsageSupported() const { return mAnalysisUsageSupported; }
/**
* Which capability attributes the camera has reported. All of them are Fixed and mandatory
* for a camera that serves video (VideoSensorParams and RateDistortionTradeOffPoints under
* the AVSM Video feature, SupportedStreamUsages unconditionally), so a missing one means a
* lost report or a non-conformant camera, not an attribute the camera opted out of. An
* allocation built on the built-in defaults instead would send constraints we invented, and
* without the FeatureMap it would omit WatermarkEnabled/OSDEnabled where the camera
* requires them, which the camera must reject.
*/
enum class Capability : uint8_t
{
kFeatureMap = 0x01,
kVideoSensorParams = 0x02,
kRateDistortionTradeOffPoints = 0x04,
kSupportedStreamUsages = 0x08,
};
static constexpr uint8_t kAllCapabilities = 0x0F;
void MarkCapabilityReported(Capability aCapability)
{
mCapabilitiesSeen = static_cast<uint8_t>(mCapabilitiesSeen | to_underlying(aCapability));
}
bool AllCapabilitiesReported() const { return mCapabilitiesSeen == kAllCapabilities; }
uint8_t MissingCapabilities() const { return static_cast<uint8_t>(kAllCapabilities & ~mCapabilitiesSeen); }
void Reset() { *this = Request{}; }
private:
Phase mPhase = Phase::kIdle;
CommandType mCommandType = CommandType::kVideoStreamAllocate;
uint16_t mVideoStreamId = 0;
AvAnalysisCameraClient::Callback * mCallback = nullptr;
CameraProfile mProfile;
SessionHolder mSessionHolder;
Messaging::ExchangeManager * mExchangeMgr = nullptr;
CommandSender * mInvokedSender = nullptr;
CHIP_ERROR mDiscoveryError = CHIP_NO_ERROR;
bool mAnalysisUsageSupported = true;
uint8_t mCapabilitiesSeen = 0;
};
/**
* The request in flight
*/
Request & CurrentRequest() { return mRequest; }
/**
* Profile being assembled for the pending request.
*/
const CameraProfile & CurrentProfile() const { return mRequest.Profile(); }
/**
* Reconciles the profile's bounds once every capability report is in, so the result does not
* depend on the order they arrived in.
*
* Which side yields depends on where the bound came from. The resolution and frame-rate maximums
* are the camera's own VideoSensorParams, a hard ceiling, so a minimum above them yields - asking
* for more than the sensor can produce would be rejected. Nothing publishes a maximum bit rate,
* so ours is only a preference and it rises to meet a camera-reported minimum instead.
*/
static void NormalizeProfile(CameraProfile & aProfile);
/**
* Builds the VideoStreamAllocate request (StreamUsage Analysis) from a camera profile.
*/
static CameraAvStreamManagement::Commands::VideoStreamAllocate::Type BuildAllocateRequest(const CameraProfile & aProfile);
/**
* Discovery report decoders, dispatched from OnAttributeData by discovery phase
*/
void HandleServerListReport(const ConcreteDataAttributePath & aPath, TLV::TLVReader & aData);
void HandleCapabilityReport(const ConcreteDataAttributePath & aPath, TLV::TLVReader & aData);
private:
CHIP_ERROR StartRequest(Request::CommandType aCommandType, const ScopedNodeId & aCameraNode, uint16_t aVideoStreamId,
AvAnalysisCameraClient::Callback & aCallback);
CHIP_ERROR SendPendingCommand();
CHIP_ERROR AddPendingCommandData();
CHIP_ERROR SendDiscoveryRead(AttributePathParams * aPaths, size_t aPathCount);
void StartCapabilitiesRead();
void ResetReadClient();
void CompleteEndpointDiscovery();
void CompleteCapabilityDiscovery();
void FinishRequest(Protocols::InteractionModel::Status aStatus, uint16_t aStreamId);
static void OnDeviceConnected(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle);
static void OnDeviceConnectionFailure(void * context, const ScopedNodeId & peerId, CHIP_ERROR error);
CASESessionManager * mCASESessionManager = nullptr;
Request mRequest;
// mReadCallback is declared before mReadClient on purpose: the ReadClient holds the callback by
// reference, and members are destroyed in reverse declaration order, so the callback outlives it.
Platform::UniquePtr<BufferedReadCallback> mReadCallback;
Platform::UniquePtr<ReadClient> mReadClient;
Platform::UniquePtr<CommandSender> mCommandSender;
chip::Callback::Callback<chip::OnDeviceConnected> mOnConnectedCallback;
chip::Callback::Callback<chip::OnDeviceConnectionFailure> mOnConnectionFailureCallback;
};
} // namespace Clusters
} // namespace app
} // namespace chip