blob: e4c8d30eac0e103b622eda4b83c6aded608dd7df [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 <cstddef>
#include <cstdint>
#include <app/data-model/Decode.h>
#include <clusters/AvAnalysis/Enums.h>
#include <clusters/AvAnalysis/Structs.h>
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/core/ScopedNodeId.h>
#include <lib/core/TLV.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/ScopedMemoryBuffer.h>
namespace chip {
namespace app {
namespace Clusters {
namespace AvAnalysis {
/**
* Backing storage for one entry of the AnalysisStreams attribute (AnalysisStreamStruct).
*/
struct AnalysisStreamEntry
{
// The AnalysisStreamStruct fields, encoded into the AnalysisStreams attribute
uint16_t analysisStreamID = 0;
DataModel::Nullable<EndpointId> webRTCEndpointID;
DataModel::Nullable<EndpointId> pushAVEndpointID;
AnalysisStreamStateEnum state = AnalysisStreamStateEnum::kPendingInitiation;
// Internal associations the server keeps for the stream, never encoded
ScopedNodeId cameraNode;
uint16_t videoStreamID = 0;
DataModel::Nullable<uint16_t> webRTCSessionID;
Structs::AnalysisStreamStruct::Type ToEncodableStruct() const
{
Structs::AnalysisStreamStruct::Type encodable;
encodable.analysisStreamID = analysisStreamID;
// Exactly one of the endpoint fields is supported (O.b); PushAV is not implemented
encodable.webRTCEndpointID = MakeOptional(webRTCEndpointID);
encodable.analysisStreamState = state;
return encodable;
}
};
/**
* Fixed-capacity table backing the AnalysisStreams attribute. Capacity is allocated once at Init()
* and bounded by MaxAnalysisStreamCount; no reallocation happens afterwards.
*/
class AnalysisStreamTable
{
public:
AnalysisStreamTable() = default;
/**
* Allocates storage for at most aCapacity entries. Must be called exactly once before any other use.
*/
CHIP_ERROR Init(uint8_t aCapacity)
{
VerifyOrReturnError(mEntries.Get() == nullptr, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(aCapacity > 0, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(mEntries.Calloc(aCapacity), CHIP_ERROR_NO_MEMORY);
mCapacity = aCapacity;
return CHIP_NO_ERROR;
}
uint8_t Capacity() const { return mCapacity; }
uint8_t Count() const { return mCount; }
bool IsFull() const { return mCount == mCapacity; }
/**
* Adds an entry in PendingInitiation state for a camera stream, generating a new AnalysisStreamID
* Returns the new entry, or nullptr if the table is full or not initialized.
*/
AnalysisStreamEntry * Add(uint16_t aVideoStreamId, const ScopedNodeId & aCameraNode)
{
VerifyOrReturnValue(mEntries.Get() != nullptr && !IsFull(), nullptr);
AnalysisStreamEntry & entry = mEntries[mCount];
entry = AnalysisStreamEntry{};
entry.analysisStreamID = GenerateAnalysisStreamId();
entry.videoStreamID = aVideoStreamId;
entry.cameraNode = aCameraNode;
mCount++;
return &entry;
}
/**
* Finds the entry backed by the given camera stream, if any.
*/
AnalysisStreamEntry * FindByCameraStream(const ScopedNodeId & aCameraNode, uint16_t aVideoStreamId)
{
for (uint8_t i = 0; i < mCount; i++)
{
if (mEntries[i].cameraNode == aCameraNode && mEntries[i].videoStreamID == aVideoStreamId)
{
return &mEntries[i];
}
}
return nullptr;
}
AnalysisStreamEntry * Find(uint16_t aAnalysisStreamId)
{
for (uint8_t i = 0; i < mCount; i++)
{
if (mEntries[i].analysisStreamID == aAnalysisStreamId)
{
return &mEntries[i];
}
}
return nullptr;
}
/**
* Removes the entry with the given id, if present. Returns true if an entry was removed.
*/
bool Remove(uint16_t aAnalysisStreamId)
{
for (uint8_t i = 0; i < mCount; i++)
{
if (mEntries[i].analysisStreamID == aAnalysisStreamId)
{
// Keep entries dense; ordering of the AnalysisStreams list is not specified.
mEntries[i] = mEntries[mCount - 1];
mCount--;
return true;
}
}
return false;
}
const AnalysisStreamEntry & operator[](uint8_t aIndex) const { return mEntries[aIndex]; }
AnalysisStreamEntry & operator[](uint8_t aIndex) { return mEntries[aIndex]; }
// Iteration over the in-use entries
AnalysisStreamEntry * begin() { return mEntries.Get(); }
AnalysisStreamEntry * end() { return mEntries.Get() + mCount; }
const AnalysisStreamEntry * begin() const { return mEntries.Get(); }
const AnalysisStreamEntry * end() const { return mEntries.Get() + mCount; }
// Worst-case TLV size of one persisted entry (analysis id + camera node id + fabric + video id).
static constexpr size_t kEntrySerializedSize =
TLV::EstimateStructOverhead(sizeof(uint16_t), sizeof(NodeId), sizeof(FabricIndex), sizeof(uint16_t));
// TLV overhead of the enclosing structure written by Encode(): the outer container, the
// next-AnalysisStreamID field and the entry array container.
static constexpr size_t kArraySerializedOverhead = 16;
/**
* Writes the next-AnalysisStreamID counter and the in-use entries as one anonymous TLV structure.
*/
CHIP_ERROR Encode(TLV::TLVWriter & aWriter) const
{
TLV::TLVType outerType;
ReturnErrorOnFailure(aWriter.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerType));
ReturnErrorOnFailure(aWriter.Put(TLV::ContextTag(kPersistedTagNextStreamId), mNextAnalysisStreamId));
TLV::TLVType arrayType;
ReturnErrorOnFailure(aWriter.StartContainer(TLV::ContextTag(kPersistedTagEntries), TLV::kTLVType_Array, arrayType));
for (const auto & entry : *this)
{
TLV::TLVType entryType;
ReturnErrorOnFailure(aWriter.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, entryType));
ReturnErrorOnFailure(aWriter.Put(TLV::ContextTag(kPersistedTagStreamId), entry.analysisStreamID));
ReturnErrorOnFailure(aWriter.Put(TLV::ContextTag(kPersistedTagCameraNodeId), entry.cameraNode.GetNodeId()));
ReturnErrorOnFailure(aWriter.Put(TLV::ContextTag(kPersistedTagCameraFabric), entry.cameraNode.GetFabricIndex()));
ReturnErrorOnFailure(aWriter.Put(TLV::ContextTag(kPersistedTagVideoStreamId), entry.videoStreamID));
ReturnErrorOnFailure(aWriter.EndContainer(entryType));
}
ReturnErrorOnFailure(aWriter.EndContainer(arrayType));
return aWriter.EndContainer(outerType);
}
/**
* Replaces the table contents and the next-AnalysisStreamID counter from TLV written by Encode().
* Restored entries restart in PendingInitiation state. On failure the table is left empty, never
* half-loaded.
*/
CHIP_ERROR Decode(TLV::TLVReader & aReader)
{
CHIP_ERROR err = DecodeContents(aReader);
if (err != CHIP_NO_ERROR)
{
mCount = 0;
mNextAnalysisStreamId = 0;
}
return err;
}
private:
/**
* Parses the next-AnalysisStreamID counter and the entry array, populating the table as it goes.
* The caller rolls back on failure.
*/
CHIP_ERROR DecodeContents(TLV::TLVReader & aReader)
{
ReturnErrorOnFailure(aReader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()));
TLV::TLVType outerType;
ReturnErrorOnFailure(aReader.EnterContainer(outerType));
ReturnErrorOnFailure(aReader.Next(TLV::ContextTag(kPersistedTagNextStreamId)));
ReturnErrorOnFailure(aReader.Get(mNextAnalysisStreamId));
VerifyOrReturnError(mNextAnalysisStreamId <= kMaxAnalysisStreamId, CHIP_ERROR_INVALID_ARGUMENT);
ReturnErrorOnFailure(aReader.Next(TLV::kTLVType_Array, TLV::ContextTag(kPersistedTagEntries)));
TLV::TLVType arrayType;
ReturnErrorOnFailure(aReader.EnterContainer(arrayType));
mCount = 0;
CHIP_ERROR err;
while ((err = aReader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag())) == CHIP_NO_ERROR)
{
TLV::TLVType entryType;
ReturnErrorOnFailure(aReader.EnterContainer(entryType));
uint16_t streamId = 0;
NodeId cameraNodeId = kUndefinedNodeId;
FabricIndex cameraFabric = kUndefinedFabricIndex;
uint16_t videoStreamId = 0;
ReturnErrorOnFailure(aReader.Next(TLV::ContextTag(kPersistedTagStreamId)));
ReturnErrorOnFailure(aReader.Get(streamId));
VerifyOrReturnError(streamId <= kMaxAnalysisStreamId, CHIP_ERROR_INVALID_ARGUMENT);
ReturnErrorOnFailure(aReader.Next(TLV::ContextTag(kPersistedTagCameraNodeId)));
ReturnErrorOnFailure(aReader.Get(cameraNodeId));
ReturnErrorOnFailure(aReader.Next(TLV::ContextTag(kPersistedTagCameraFabric)));
ReturnErrorOnFailure(aReader.Get(cameraFabric));
ReturnErrorOnFailure(aReader.Next(TLV::ContextTag(kPersistedTagVideoStreamId)));
ReturnErrorOnFailure(aReader.Get(videoStreamId));
ReturnErrorOnFailure(aReader.ExitContainer(entryType));
VerifyOrReturnError(mEntries.Get() != nullptr && !IsFull(), CHIP_ERROR_NO_MEMORY);
VerifyOrReturnError(Find(streamId) == nullptr, CHIP_ERROR_INVALID_ARGUMENT);
AnalysisStreamEntry & entry = mEntries[mCount];
entry = AnalysisStreamEntry{};
entry.analysisStreamID = streamId;
entry.videoStreamID = videoStreamId;
entry.cameraNode = ScopedNodeId(cameraNodeId, cameraFabric);
mCount++;
}
VerifyOrReturnError(err == CHIP_ERROR_END_OF_TLV, err);
ReturnErrorOnFailure(aReader.ExitContainer(arrayType));
ReturnErrorOnFailure(aReader.ExitContainer(outerType));
return CHIP_NO_ERROR;
}
// Context tags of the persisted blob written by Encode()
static constexpr uint8_t kPersistedTagNextStreamId = 0;
static constexpr uint8_t kPersistedTagEntries = 1;
// Context tags of one persisted entry
static constexpr uint8_t kPersistedTagStreamId = 0;
static constexpr uint8_t kPersistedTagCameraNodeId = 1;
static constexpr uint8_t kPersistedTagCameraFabric = 2;
static constexpr uint8_t kPersistedTagVideoStreamId = 3;
uint16_t GenerateAnalysisStreamId()
{
uint16_t id;
do
{
id = mNextAnalysisStreamId;
mNextAnalysisStreamId = (id >= kMaxAnalysisStreamId) ? 0 : static_cast<uint16_t>(id + 1);
} while (Find(id) != nullptr);
return id;
}
static constexpr uint16_t kMaxAnalysisStreamId = 65534;
Platform::ScopedMemoryBuffer<AnalysisStreamEntry> mEntries;
uint8_t mCapacity = 0;
uint8_t mCount = 0;
uint16_t mNextAnalysisStreamId = 0;
};
} // namespace AvAnalysis
} // namespace Clusters
} // namespace app
} // namespace chip