| /* | |
| * 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. | |
| */ | |
| #include <app/clusters/audio-control-server/AudioControlCluster.h> | |
| #include <algorithm> | |
| #include <app/ConcreteAttributePath.h> | |
| #include <app/MessageDef/StatusIB.h> | |
| #include <app/data-model/Decode.h> | |
| #include <app/data-model/List.h> | |
| #include <app/persistence/AttributePersistence.h> | |
| #include <app/server-cluster/AttributeListBuilder.h> | |
| #include <clusters/AudioControl/Commands.h> | |
| #include <clusters/AudioControl/Metadata.h> | |
| #include <clusters/ScenesManagement/Structs.h> | |
| #include <lib/support/CodeUtils.h> | |
| #include <lib/support/Span.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| #include <optional> | |
| namespace chip::app::Clusters { | |
| using namespace AudioControl; | |
| using namespace AudioControl::Attributes; | |
| using namespace AudioControl::Commands; | |
| using chip::Protocols::InteractionModel::Status; | |
| namespace { | |
| // Adds stepSize to volume, capped at effectiveMax. The intermediate sum is computed in uint32_t | |
| // to avoid overflow; the cast back to uint16_t is safe because std::min bounds the result to | |
| // effectiveMax, which is already a uint16_t. | |
| uint16_t AddCappedAtMax(uint16_t volume, uint16_t stepSize, uint16_t effectiveMax) | |
| { | |
| return static_cast<uint16_t>(std::min<uint32_t>(static_cast<uint32_t>(volume) + static_cast<uint32_t>(stepSize), effectiveMax)); | |
| } | |
| // Restricts scene extension field sets to the AudioControl cluster and its scene-able | |
| // attributes. Per-attribute enablement (BEQ / optional bands) is enforced by the setters in | |
| // ApplyScene(); this only rejects clearly out-of-cluster IDs. | |
| class AudioControlSceneValidator : public scenes::AttributeValuePairValidator | |
| { | |
| public: | |
| CHIP_ERROR Validate(const app::ConcreteClusterPath & clusterPath, | |
| AttributeValuePairValidator::AttributeValuePairType & value) override | |
| { | |
| VerifyOrReturnError(clusterPath.mClusterId == AudioControl::Id, CHIP_ERROR_INVALID_ARGUMENT); | |
| switch (value.attributeID) | |
| { | |
| case SoftMuted::Id: | |
| case Volume::Id: | |
| case Bass::Id: | |
| case Mid::Id: | |
| case Treble::Id: | |
| return CHIP_NO_ERROR; | |
| default: | |
| return CHIP_ERROR_INVALID_ARGUMENT; | |
| } | |
| } | |
| }; | |
| AudioControlSceneValidator & GlobalSceneValidator() | |
| { | |
| static AudioControlSceneValidator sValidator; | |
| return sValidator; | |
| } | |
| } // namespace | |
| AudioControlCluster::AudioControlCluster(EndpointId endpointId, AudioControlDelegate & delegate, const Config & config) : | |
| DefaultServerCluster({ endpointId, AudioControl::Id }), scenes::DefaultSceneHandlerImpl(GlobalSceneValidator()), | |
| mDelegate(delegate), mFeatures(config.mFeatures), mOptionalAttributeSet(config.mOptionalAttributeSet), | |
| mMinDeviceVolume(delegate.GetMinDeviceVolume()), mMaxDeviceVolume(delegate.GetMaxDeviceVolume()), | |
| mMaxDeviceVolumeDB(delegate.GetMaxDeviceVolumeDB()), mMinCorrection(delegate.GetMinCorrection()), | |
| mMaxCorrection(delegate.GetMaxCorrection()) | |
| { | |
| VerifyOrDie(mMinDeviceVolume >= 1); | |
| VerifyOrDie(mMaxDeviceVolume >= mMinDeviceVolume); | |
| VerifyOrDie(config.mVolume == 0 || (config.mVolume >= mMinDeviceVolume && config.mVolume <= mMaxDeviceVolume)); | |
| if (mFeatures.Has(Feature::kBasicEqualizer)) | |
| { | |
| VerifyOrDie(mMinCorrection <= 0); | |
| VerifyOrDie(mMaxCorrection >= 0); | |
| // BEQ feature requires at least one EQ band to be enabled. | |
| VerifyOrDie(mOptionalAttributeSet.IsSet(Bass::Id) || mOptionalAttributeSet.IsSet(Mid::Id) || | |
| mOptionalAttributeSet.IsSet(Treble::Id)); | |
| } | |
| if (mFeatures.Has(Feature::kDecibel)) | |
| { | |
| VerifyOrDie(!mOptionalAttributeSet.IsSet(MaxDeviceVolumeDB::Id)); | |
| } | |
| VerifyOrDie(config.mSetVolumeUnmutePolicy < UnmutePolicyEnum::kUnknownEnumValue); | |
| VerifyOrDie(config.mIncreaseVolumeUnmutePolicy < UnmutePolicyEnum::kUnknownEnumValue); | |
| VerifyOrDie(config.mIncreaseVolumeUnmuteVolume < UnmuteVolumeEnum::kUnknownEnumValue); | |
| VerifyOrDie(config.mDecreaseVolumeUnmutePolicy < UnmutePolicyEnum::kUnknownEnumValue); | |
| mSoftMuted = config.mSoftMuted; | |
| mPhysicallyMuted = config.mPhysicallyMuted; | |
| mVolume = config.mVolume; | |
| mDefaultStepSize = config.mDefaultStepSize; | |
| mSetVolumeUnmutePolicy = config.mSetVolumeUnmutePolicy; | |
| mIncreaseVolumeUnmutePolicy = config.mIncreaseVolumeUnmutePolicy; | |
| mIncreaseVolumeUnmuteVolume = config.mIncreaseVolumeUnmuteVolume; | |
| mDecreaseVolumeUnmutePolicy = config.mDecreaseVolumeUnmutePolicy; | |
| mStartUpMuted = config.mStartUpMuted; | |
| mStartUpVolume = config.mStartUpVolume; | |
| mBass = config.mBass; | |
| mMid = config.mMid; | |
| mTreble = config.mTreble; | |
| mMaxUserVolume = mOptionalAttributeSet.IsSet(MaxUserVolume::Id) ? config.mMaxUserVolume : mMaxDeviceVolume; | |
| VerifyOrDie(mMaxUserVolume >= mMinDeviceVolume && mMaxUserVolume <= mMaxDeviceVolume); | |
| } | |
| CHIP_ERROR AudioControlCluster::Startup(ServerClusterContext & context) | |
| { | |
| ReturnErrorOnFailure(DefaultServerCluster::Startup(context)); | |
| AttributePersistence attributePersistence(context.attributeStorage); | |
| // MaxUserVolume must be restored before the volume clamp (EffectiveMaxVolume() depends on it). | |
| if (mOptionalAttributeSet.IsSet(MaxUserVolume::Id)) | |
| { | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::MaxUserVolume::Id), mMaxUserVolume, | |
| mMaxUserVolume); | |
| // Guard against a corrupted/stale KVS value so EffectiveMaxVolume() never returns something | |
| // below mMinDeviceVolume, which would violate std::clamp's precondition below. | |
| mMaxUserVolume = std::clamp(mMaxUserVolume, mMinDeviceVolume, mMaxDeviceVolume); | |
| } | |
| // Restore last-known Volume (always; serves as fallback when StartUpVolume is null or absent). | |
| attributePersistence.LoadNativeEndianValue(ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Volume::Id), | |
| mVolume, mVolume); | |
| // Restore behavioral configuration. | |
| // For enum attributes, capture the config default before loading so that a stale/corrupted | |
| // KVS entry that decodes to kUnknownEnumValue falls back to the config default rather than | |
| // leaving the enum in an invalid state that would break command handling. | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DefaultStepSize::Id), mDefaultStepSize, | |
| mDefaultStepSize); | |
| // Guard against a corrupted/stale KVS value: a step size of 0 would make IncreaseVolume/ | |
| // DecreaseVolume fail permanently whenever the command omits an explicit stepSize. | |
| mDefaultStepSize = std::max<uint16_t>(mDefaultStepSize, 1); | |
| const auto defaultSetVolumePolicy = mSetVolumeUnmutePolicy; | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SetVolumeUnmutePolicy::Id), mSetVolumeUnmutePolicy, | |
| defaultSetVolumePolicy); | |
| if (mSetVolumeUnmutePolicy >= UnmutePolicyEnum::kUnknownEnumValue) | |
| { | |
| mSetVolumeUnmutePolicy = defaultSetVolumePolicy; | |
| } | |
| const auto defaultIncreaseVolumePolicy = mIncreaseVolumeUnmutePolicy; | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmutePolicy::Id), | |
| mIncreaseVolumeUnmutePolicy, defaultIncreaseVolumePolicy); | |
| if (mIncreaseVolumeUnmutePolicy >= UnmutePolicyEnum::kUnknownEnumValue) | |
| { | |
| mIncreaseVolumeUnmutePolicy = defaultIncreaseVolumePolicy; | |
| } | |
| const auto defaultIncreaseVolumeVolume = mIncreaseVolumeUnmuteVolume; | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmuteVolume::Id), | |
| mIncreaseVolumeUnmuteVolume, defaultIncreaseVolumeVolume); | |
| if (mIncreaseVolumeUnmuteVolume >= UnmuteVolumeEnum::kUnknownEnumValue) | |
| { | |
| mIncreaseVolumeUnmuteVolume = defaultIncreaseVolumeVolume; | |
| } | |
| const auto defaultDecreaseVolumePolicy = mDecreaseVolumeUnmutePolicy; | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DecreaseVolumeUnmutePolicy::Id), | |
| mDecreaseVolumeUnmutePolicy, defaultDecreaseVolumePolicy); | |
| if (mDecreaseVolumeUnmutePolicy >= UnmutePolicyEnum::kUnknownEnumValue) | |
| { | |
| mDecreaseVolumeUnmutePolicy = defaultDecreaseVolumePolicy; | |
| } | |
| // SoftMuted is non-volatile per spec and must be restored on startup regardless of whether | |
| // StartUpMuted is supported. | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SoftMuted::Id), mSoftMuted, mSoftMuted); | |
| // Apply StartUpMuted: | |
| // absent -> mSoftMuted stays at the just-restored persisted value. | |
| // null -> retain the persisted SoftMuted state. | |
| // non-null -> override with the StartUpMuted value. | |
| if (mOptionalAttributeSet.IsSet(StartUpMuted::Id)) | |
| { | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpMuted::Id), mStartUpMuted, mStartUpMuted); | |
| if (!mStartUpMuted.IsNull()) | |
| { | |
| const bool startUpMuted = mStartUpMuted.Value(); | |
| if (mSoftMuted != startUpMuted) | |
| { | |
| mSoftMuted = startUpMuted; | |
| StoreSoftMuted(); | |
| } | |
| } | |
| } | |
| // Apply StartUpVolume: absent → mVolume stays at the persisted Volume (null-like behavior). | |
| const uint16_t persistedVolume = mVolume; | |
| if (mOptionalAttributeSet.IsSet(StartUpVolume::Id)) | |
| { | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpVolume::Id), mStartUpVolume, | |
| mStartUpVolume); | |
| if (!mStartUpVolume.IsNull()) | |
| { | |
| mVolume = mStartUpVolume.Value(); | |
| } | |
| } | |
| mVolume = std::clamp(mVolume, mMinDeviceVolume, EffectiveMaxVolume()); | |
| if (mVolume != persistedVolume) | |
| { | |
| StoreVolume(); | |
| } | |
| if (mFeatures.Has(Feature::kBasicEqualizer)) | |
| { | |
| if (mOptionalAttributeSet.IsSet(Bass::Id)) | |
| { | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Bass::Id), mBass, mBass); | |
| mBass = std::clamp(mBass, mMinCorrection, mMaxCorrection); | |
| } | |
| if (mOptionalAttributeSet.IsSet(Mid::Id)) | |
| { | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Mid::Id), mMid, mMid); | |
| mMid = std::clamp(mMid, mMinCorrection, mMaxCorrection); | |
| } | |
| if (mOptionalAttributeSet.IsSet(Treble::Id)) | |
| { | |
| attributePersistence.LoadNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Treble::Id), mTreble, mTreble); | |
| mTreble = std::clamp(mTreble, mMinCorrection, mMaxCorrection); | |
| } | |
| } | |
| mDelegate.OnStartup({ mVolume, mSoftMuted, mBass, mMid, mTreble }); | |
| return CHIP_NO_ERROR; | |
| } | |
| uint16_t AudioControlCluster::EffectiveMaxVolume() const | |
| { | |
| return mOptionalAttributeSet.IsSet(MaxUserVolume::Id) ? mMaxUserVolume : mMaxDeviceVolume; | |
| } | |
| void AudioControlCluster::StoreVolume() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Volume::Id), mVolume)); | |
| } | |
| void AudioControlCluster::StoreSoftMuted() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SoftMuted::Id), mSoftMuted)); | |
| } | |
| void AudioControlCluster::StoreStartUpMuted() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpMuted::Id), mStartUpMuted)); | |
| } | |
| void AudioControlCluster::StoreStartUpVolume() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpVolume::Id), mStartUpVolume)); | |
| } | |
| void AudioControlCluster::StoreDefaultStepSize() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DefaultStepSize::Id), mDefaultStepSize)); | |
| } | |
| void AudioControlCluster::StoreSetVolumeUnmutePolicy() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SetVolumeUnmutePolicy::Id), mSetVolumeUnmutePolicy)); | |
| } | |
| void AudioControlCluster::StoreIncreaseVolumeUnmutePolicy() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmutePolicy::Id), | |
| mIncreaseVolumeUnmutePolicy)); | |
| } | |
| void AudioControlCluster::StoreIncreaseVolumeUnmuteVolume() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmuteVolume::Id), | |
| mIncreaseVolumeUnmuteVolume)); | |
| } | |
| void AudioControlCluster::StoreDecreaseVolumeUnmutePolicy() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DecreaseVolumeUnmutePolicy::Id), | |
| mDecreaseVolumeUnmutePolicy)); | |
| } | |
| void AudioControlCluster::StoreMaxUserVolume() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::MaxUserVolume::Id), mMaxUserVolume)); | |
| } | |
| void AudioControlCluster::StoreBass() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Bass::Id), mBass)); | |
| } | |
| void AudioControlCluster::StoreMid() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Mid::Id), mMid)); | |
| } | |
| void AudioControlCluster::StoreTreble() | |
| { | |
| VerifyOrReturn(mContext != nullptr); | |
| AttributePersistence attributePersistence(mContext->attributeStorage); | |
| LogErrorOnFailure(attributePersistence.StoreNativeEndianValue( | |
| ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Treble::Id), mTreble)); | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request, | |
| AttributeValueEncoder & encoder) | |
| { | |
| switch (request.path.mAttributeId) | |
| { | |
| case ClusterRevision::Id: | |
| return encoder.Encode(AudioControl::kRevision); | |
| case FeatureMap::Id: | |
| return encoder.Encode(mFeatures); | |
| case SoftMuted::Id: | |
| return encoder.Encode(mSoftMuted); | |
| case PhysicallyMuted::Id: | |
| return encoder.Encode(mPhysicallyMuted); | |
| case Volume::Id: | |
| return encoder.Encode(mVolume); | |
| case MinDeviceVolume::Id: | |
| return encoder.Encode(mMinDeviceVolume); | |
| case MaxDeviceVolume::Id: | |
| return encoder.Encode(mMaxDeviceVolume); | |
| case MaxDeviceVolumeDB::Id: | |
| return encoder.Encode(mMaxDeviceVolumeDB); | |
| case MaxUserVolume::Id: | |
| return encoder.Encode(mMaxUserVolume); | |
| case DefaultStepSize::Id: | |
| return encoder.Encode(mDefaultStepSize); | |
| case SetVolumeUnmutePolicy::Id: | |
| return encoder.Encode(mSetVolumeUnmutePolicy); | |
| case IncreaseVolumeUnmutePolicy::Id: | |
| return encoder.Encode(mIncreaseVolumeUnmutePolicy); | |
| case IncreaseVolumeUnmuteVolume::Id: | |
| return encoder.Encode(mIncreaseVolumeUnmuteVolume); | |
| case DecreaseVolumeUnmutePolicy::Id: | |
| return encoder.Encode(mDecreaseVolumeUnmutePolicy); | |
| case StartUpMuted::Id: | |
| return encoder.Encode(mStartUpMuted); | |
| case StartUpVolume::Id: | |
| return encoder.Encode(mStartUpVolume); | |
| case Bass::Id: | |
| return encoder.Encode(mBass); | |
| case Mid::Id: | |
| return encoder.Encode(mMid); | |
| case Treble::Id: | |
| return encoder.Encode(mTreble); | |
| case MinCorrection::Id: | |
| return encoder.Encode(mMinCorrection); | |
| case MaxCorrection::Id: | |
| return encoder.Encode(mMaxCorrection); | |
| default: | |
| return Status::UnsupportedAttribute; | |
| } | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request, | |
| AttributeValueDecoder & decoder) | |
| { | |
| switch (request.path.mAttributeId) | |
| { | |
| case MaxUserVolume::Id: { | |
| uint16_t value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| return SetMaxUserVolume(value); | |
| } | |
| case DefaultStepSize::Id: { | |
| uint16_t value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| const uint16_t effectiveMax = EffectiveMaxVolume(); | |
| VerifyOrReturnError(value >= 1 && effectiveMax > mMinDeviceVolume && value <= effectiveMax - mMinDeviceVolume, | |
| Status::ConstraintError); | |
| if (SetAttributeValue(mDefaultStepSize, value, DefaultStepSize::Id)) | |
| { | |
| StoreDefaultStepSize(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case SetVolumeUnmutePolicy::Id: { | |
| UnmutePolicyEnum value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError); | |
| if (SetAttributeValue(mSetVolumeUnmutePolicy, value, SetVolumeUnmutePolicy::Id)) | |
| { | |
| StoreSetVolumeUnmutePolicy(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case IncreaseVolumeUnmutePolicy::Id: { | |
| UnmutePolicyEnum value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError); | |
| if (SetAttributeValue(mIncreaseVolumeUnmutePolicy, value, IncreaseVolumeUnmutePolicy::Id)) | |
| { | |
| StoreIncreaseVolumeUnmutePolicy(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case IncreaseVolumeUnmuteVolume::Id: { | |
| UnmuteVolumeEnum value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value < UnmuteVolumeEnum::kUnknownEnumValue, Status::ConstraintError); | |
| if (SetAttributeValue(mIncreaseVolumeUnmuteVolume, value, IncreaseVolumeUnmuteVolume::Id)) | |
| { | |
| StoreIncreaseVolumeUnmuteVolume(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case DecreaseVolumeUnmutePolicy::Id: { | |
| UnmutePolicyEnum value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError); | |
| if (SetAttributeValue(mDecreaseVolumeUnmutePolicy, value, DecreaseVolumeUnmutePolicy::Id)) | |
| { | |
| StoreDecreaseVolumeUnmutePolicy(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case StartUpMuted::Id: { | |
| DataModel::Nullable<bool> value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| if (SetAttributeValue(mStartUpMuted, value, StartUpMuted::Id)) | |
| { | |
| StoreStartUpMuted(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case StartUpVolume::Id: { | |
| DataModel::Nullable<uint16_t> value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| if (!value.IsNull()) | |
| { | |
| const uint16_t effectiveMax = EffectiveMaxVolume(); | |
| VerifyOrReturnError(value.Value() >= mMinDeviceVolume && value.Value() <= effectiveMax, Status::ConstraintError); | |
| } | |
| if (SetAttributeValue(mStartUpVolume, value, StartUpVolume::Id)) | |
| { | |
| StoreStartUpVolume(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case Bass::Id: { | |
| int16_t value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value >= mMinCorrection && value <= mMaxCorrection, Status::ConstraintError); | |
| // Equality check gates the delegate call below, not just the reported status: a same-value | |
| // write must not spuriously invoke HandleBassChanged. | |
| if (value != mBass) | |
| { | |
| Status s = mDelegate.HandleBassChanged(value); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| SetAttributeValue(mBass, value, Bass::Id); | |
| StoreBass(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case Mid::Id: { | |
| int16_t value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value >= mMinCorrection && value <= mMaxCorrection, Status::ConstraintError); | |
| // Equality check gates the delegate call below, not just the reported status: a same-value | |
| // write must not spuriously invoke HandleMidChanged. | |
| if (value != mMid) | |
| { | |
| Status s = mDelegate.HandleMidChanged(value); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| SetAttributeValue(mMid, value, Mid::Id); | |
| StoreMid(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| case Treble::Id: { | |
| int16_t value{}; | |
| ReturnErrorOnFailure(decoder.Decode(value)); | |
| VerifyOrReturnError(value >= mMinCorrection && value <= mMaxCorrection, Status::ConstraintError); | |
| // Equality check gates the delegate call below, not just the reported status: a same-value | |
| // write must not spuriously invoke HandleTrebleChanged. | |
| if (value != mTreble) | |
| { | |
| Status s = mDelegate.HandleTrebleChanged(value); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| SetAttributeValue(mTreble, value, Treble::Id); | |
| StoreTreble(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| default: | |
| return Status::UnsupportedAttribute; | |
| } | |
| } | |
| CHIP_ERROR AudioControlCluster::Attributes(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) | |
| { | |
| AttributeListBuilder listBuilder(builder); | |
| const AttributeListBuilder::OptionalAttributeEntry optionalAttributes[] = { | |
| { mOptionalAttributeSet.IsSet(PhysicallyMuted::Id), PhysicallyMuted::kMetadataEntry }, | |
| { !mFeatures.Has(Feature::kDecibel) && mOptionalAttributeSet.IsSet(MaxDeviceVolumeDB::Id), | |
| MaxDeviceVolumeDB::kMetadataEntry }, | |
| { mOptionalAttributeSet.IsSet(MaxUserVolume::Id), MaxUserVolume::kMetadataEntry }, | |
| { mOptionalAttributeSet.IsSet(StartUpMuted::Id), StartUpMuted::kMetadataEntry }, | |
| { mOptionalAttributeSet.IsSet(StartUpVolume::Id), StartUpVolume::kMetadataEntry }, | |
| { mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Bass::Id), Bass::kMetadataEntry }, | |
| { mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Mid::Id), Mid::kMetadataEntry }, | |
| { mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Treble::Id), Treble::kMetadataEntry }, | |
| { mFeatures.Has(Feature::kBasicEqualizer), MinCorrection::kMetadataEntry }, | |
| { mFeatures.Has(Feature::kBasicEqualizer), MaxCorrection::kMetadataEntry }, | |
| }; | |
| return listBuilder.Append(Span(kMandatoryMetadata), Span(optionalAttributes)); | |
| } | |
| CHIP_ERROR AudioControlCluster::AcceptedCommands(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) | |
| { | |
| static constexpr DataModel::AcceptedCommandEntry kCommands[] = { | |
| Mute::kMetadataEntry, Unmute::kMetadataEntry, ToggleMuted::kMetadataEntry, | |
| SetVolume::kMetadataEntry, IncreaseVolume::kMetadataEntry, DecreaseVolume::kMetadataEntry, | |
| }; | |
| return builder.ReferenceExisting(Span(kCommands)); | |
| } | |
| // Applies the UnmutePolicy table (spec §ref_UnmutePolicyTable). | |
| // Computes final (volume, softMuted), notifies the delegate, then updates cluster state. | |
| // Only called when at least one of SoftMuted or PhysicallyMuted is TRUE. | |
| Status AudioControlCluster::ApplyUnmutePolicy(UnmutePolicyEnum policy, uint16_t requestedVolume) | |
| { | |
| uint16_t newVolume = mVolume; | |
| bool newSoftMuted = mSoftMuted; | |
| switch (policy) | |
| { | |
| case UnmutePolicyEnum::kUnmuteOrChangeVolume: | |
| // Always set Volume and clear SoftMuted, regardless of PhysicallyMuted. | |
| newVolume = requestedVolume; | |
| newSoftMuted = false; | |
| break; | |
| case UnmutePolicyEnum::kUnmuteOrDoNotChangeVolume: | |
| if (mPhysicallyMuted) | |
| { | |
| // Cannot unmute a physically-muted device — leave state unchanged. | |
| return Status::InvalidInState; | |
| } | |
| // PhysicallyMuted is FALSE; SoftMuted must have been TRUE (we're in muted state). | |
| newVolume = requestedVolume; | |
| newSoftMuted = false; | |
| break; | |
| case UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume: | |
| // Change volume but leave SoftMuted unchanged. | |
| newVolume = requestedVolume; | |
| break; | |
| case UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume: | |
| // Leave all state unchanged. | |
| return Status::InvalidInState; | |
| default: | |
| // Not a valid UnmutePolicyEnum value — malformed command field, not a state conflict. | |
| return Status::ConstraintError; | |
| } | |
| Status s = mDelegate.HandleVolumeAndMuteChange(newVolume, newSoftMuted); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mVolume, newVolume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| if (SetAttributeValue(mSoftMuted, newSoftMuted, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return Status::Success; | |
| } | |
| std::optional<DataModel::ActionReturnStatus> AudioControlCluster::InvokeCommand(const DataModel::InvokeRequest & request, | |
| chip::TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| switch (request.path.mCommandId) | |
| { | |
| case Mute::Id: | |
| return HandleMute(); | |
| case Unmute::Id: | |
| return HandleUnmute(); | |
| case ToggleMuted::Id: | |
| return HandleToggleMuted(); | |
| case SetVolume::Id: | |
| return HandleSetVolume(input_arguments); | |
| case IncreaseVolume::Id: | |
| return HandleIncreaseVolume(input_arguments); | |
| case DecreaseVolume::Id: | |
| return HandleDecreaseVolume(input_arguments); | |
| default: | |
| return Status::UnsupportedCommand; | |
| } | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::HandleMute() | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, true); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mSoftMuted, true, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return Status::Success; | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::HandleUnmute() | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, false); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mSoftMuted, false, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return Status::Success; | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::HandleToggleMuted() | |
| { | |
| const bool newSoftMuted = !mSoftMuted; | |
| Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, newSoftMuted); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mSoftMuted, newSoftMuted, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return Status::Success; | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::HandleSetVolume(chip::TLV::TLVReader & input_arguments) | |
| { | |
| SetVolume::DecodableType req; | |
| ReturnErrorOnFailure(DataModel::Decode(input_arguments, req)); | |
| if (req.unmutePolicy.HasValue()) | |
| { | |
| VerifyOrReturnError(req.unmutePolicy.Value() < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError); | |
| } | |
| const uint16_t effectiveMax = EffectiveMaxVolume(); | |
| // Constraint: 0 OR [MinDeviceVolume, EffectiveMax] | |
| if (req.newVolume != 0 && (req.newVolume < mMinDeviceVolume || req.newVolume > effectiveMax)) | |
| { | |
| return Status::ConstraintError; | |
| } | |
| // NewVolume == 0: mute and succeed. | |
| if (req.newVolume == 0) | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, true); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mSoftMuted, true, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return Status::Success; | |
| } | |
| // Fast path: device is not muted. | |
| if (!mSoftMuted && !mPhysicallyMuted) | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(req.newVolume, false); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mVolume, req.newVolume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| return Status::Success; | |
| } | |
| return ApplyUnmutePolicy(req.unmutePolicy.ValueOr(mSetVolumeUnmutePolicy), req.newVolume); | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::HandleIncreaseVolume(chip::TLV::TLVReader & input_arguments) | |
| { | |
| IncreaseVolume::DecodableType req; | |
| ReturnErrorOnFailure(DataModel::Decode(input_arguments, req)); | |
| if (req.unmutePolicy.HasValue()) | |
| { | |
| VerifyOrReturnError(req.unmutePolicy.Value() < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError); | |
| } | |
| if (req.unmuteVolume.HasValue()) | |
| { | |
| VerifyOrReturnError(req.unmuteVolume.Value() < UnmuteVolumeEnum::kUnknownEnumValue, Status::ConstraintError); | |
| } | |
| const uint16_t effectiveMax = EffectiveMaxVolume(); | |
| const uint16_t stepSize = req.stepSize.ValueOr(mDefaultStepSize); | |
| // StepSize is clamped against the volume range below rather than rejected here, so a | |
| // previously-valid DefaultStepSize doesn't start failing IncreaseVolume calls after | |
| // MaxUserVolume shrinks. | |
| VerifyOrReturnError(stepSize >= 1, Status::ConstraintError); | |
| const UnmutePolicyEnum policy = req.unmutePolicy.ValueOr(mIncreaseVolumeUnmutePolicy); | |
| const UnmuteVolumeEnum unmuteVolume = req.unmuteVolume.ValueOr(mIncreaseVolumeUnmuteVolume); | |
| uint16_t requestedVolume; | |
| // Step 2.a: special case for soft-muted-only device with unmute policy | |
| if (mSoftMuted && !mPhysicallyMuted && | |
| (policy == UnmutePolicyEnum::kUnmuteOrChangeVolume || policy == UnmutePolicyEnum::kUnmuteOrDoNotChangeVolume)) | |
| { | |
| switch (unmuteVolume) | |
| { | |
| case UnmuteVolumeEnum::kMinDeviceVolume: | |
| requestedVolume = mMinDeviceVolume; | |
| break; | |
| case UnmuteVolumeEnum::kVolume: | |
| requestedVolume = mVolume; | |
| break; | |
| case UnmuteVolumeEnum::kVolumePlusStepSize: | |
| requestedVolume = AddCappedAtMax(mVolume, stepSize, effectiveMax); | |
| break; | |
| default: | |
| return Status::ConstraintError; | |
| } | |
| } | |
| else | |
| { | |
| // Step 2.b: Volume + StepSize, capped at EffectiveMax | |
| requestedVolume = AddCappedAtMax(mVolume, stepSize, effectiveMax); | |
| } | |
| // Step 3: fast path — device is not muted | |
| if (!mSoftMuted && !mPhysicallyMuted) | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(requestedVolume, false); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mVolume, requestedVolume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| return Status::Success; | |
| } | |
| // Step 4: apply UnmutePolicy table | |
| return ApplyUnmutePolicy(policy, requestedVolume); | |
| } | |
| DataModel::ActionReturnStatus AudioControlCluster::HandleDecreaseVolume(chip::TLV::TLVReader & input_arguments) | |
| { | |
| DecreaseVolume::DecodableType req; | |
| ReturnErrorOnFailure(DataModel::Decode(input_arguments, req)); | |
| if (req.unmutePolicy.HasValue()) | |
| { | |
| VerifyOrReturnError(req.unmutePolicy.Value() < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError); | |
| } | |
| const uint16_t stepSize = req.stepSize.ValueOr(mDefaultStepSize); | |
| // StepSize is clamped against the volume range below rather than rejected here, so a | |
| // previously-valid DefaultStepSize doesn't start failing DecreaseVolume calls after | |
| // MaxUserVolume shrinks. | |
| VerifyOrReturnError(stepSize >= 1, Status::ConstraintError); | |
| // RequestedVolume = Volume - StepSize (with underflow protection) | |
| const uint32_t requestedVolume32 = static_cast<uint32_t>(mVolume) > static_cast<uint32_t>(stepSize) | |
| ? static_cast<uint32_t>(mVolume) - static_cast<uint32_t>(stepSize) | |
| : 0; | |
| // Step 3: floor at MinDeviceVolume → mute and succeed | |
| if (requestedVolume32 < static_cast<uint32_t>(mMinDeviceVolume)) | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(mMinDeviceVolume, true); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mVolume, mMinDeviceVolume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| if (SetAttributeValue(mSoftMuted, true, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return Status::Success; | |
| } | |
| const uint16_t requestedVolume = static_cast<uint16_t>(requestedVolume32); | |
| // Step 4: fast path — device is not muted | |
| if (!mSoftMuted && !mPhysicallyMuted) | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(requestedVolume, false); | |
| VerifyOrReturnError(s == Status::Success, s); | |
| if (SetAttributeValue(mVolume, requestedVolume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| return Status::Success; | |
| } | |
| // Step 5: apply UnmutePolicy table | |
| return ApplyUnmutePolicy(req.unmutePolicy.ValueOr(mDecreaseVolumeUnmutePolicy), requestedVolume); | |
| } | |
| // Application-facing setters. SetSoftMuted/SetVolume/SetPhysicallyMuted reflect hardware-initiated | |
| // changes and do NOT call the delegate. SetMaxUserVolume is app-initiated and DOES call | |
| // HandleVolumeAndMuteChange when Volume must be clipped to the new ceiling. | |
| CHIP_ERROR AudioControlCluster::SetSoftMuted(bool softMuted) | |
| { | |
| if (SetAttributeValue(mSoftMuted, softMuted, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR AudioControlCluster::SetPhysicallyMuted(bool physicallyMuted) | |
| { | |
| VerifyOrReturnError(mOptionalAttributeSet.IsSet(PhysicallyMuted::Id), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); | |
| SetAttributeValue(mPhysicallyMuted, physicallyMuted, PhysicallyMuted::Id); | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR AudioControlCluster::SetVolume(uint16_t volume) | |
| { | |
| VerifyOrReturnError(volume >= mMinDeviceVolume && volume <= EffectiveMaxVolume(), CHIP_IM_GLOBAL_STATUS(ConstraintError)); | |
| if (SetAttributeValue(mVolume, volume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR AudioControlCluster::SetMaxUserVolume(uint16_t maxUserVolume) | |
| { | |
| VerifyOrReturnError(mOptionalAttributeSet.IsSet(MaxUserVolume::Id), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); | |
| VerifyOrReturnError(maxUserVolume >= mMinDeviceVolume && maxUserVolume <= mMaxDeviceVolume, | |
| CHIP_IM_GLOBAL_STATUS(ConstraintError)); | |
| // If Volume must be clipped, inform hardware before committing any state change | |
| // so that a delegate failure leaves the cluster in a consistent state. | |
| if (mVolume > maxUserVolume) | |
| { | |
| Status s = mDelegate.HandleVolumeAndMuteChange(maxUserVolume, mSoftMuted); | |
| ReturnErrorOnFailure(StatusIB(s).ToChipError()); | |
| } | |
| if (SetAttributeValue(mMaxUserVolume, maxUserVolume, MaxUserVolume::Id)) | |
| { | |
| StoreMaxUserVolume(); | |
| } | |
| if (mVolume > maxUserVolume) | |
| { | |
| SetAttributeValue(mVolume, maxUserVolume, Volume::Id); | |
| StoreVolume(); | |
| } | |
| if (mOptionalAttributeSet.IsSet(StartUpVolume::Id) && !mStartUpVolume.IsNull() && mStartUpVolume.Value() > maxUserVolume) | |
| { | |
| SetAttributeValue(mStartUpVolume, DataModel::MakeNullable<uint16_t>(maxUserVolume), StartUpVolume::Id); | |
| StoreStartUpVolume(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR AudioControlCluster::SetBass(int16_t bass) | |
| { | |
| VerifyOrReturnError(mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Bass::Id), | |
| CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); | |
| VerifyOrReturnError(bass >= mMinCorrection && bass <= mMaxCorrection, CHIP_IM_GLOBAL_STATUS(ConstraintError)); | |
| if (SetAttributeValue(mBass, bass, Bass::Id)) | |
| { | |
| StoreBass(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR AudioControlCluster::SetMid(int16_t mid) | |
| { | |
| VerifyOrReturnError(mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Mid::Id), | |
| CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); | |
| VerifyOrReturnError(mid >= mMinCorrection && mid <= mMaxCorrection, CHIP_IM_GLOBAL_STATUS(ConstraintError)); | |
| if (SetAttributeValue(mMid, mid, Mid::Id)) | |
| { | |
| StoreMid(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR AudioControlCluster::SetTreble(int16_t treble) | |
| { | |
| VerifyOrReturnError(mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Treble::Id), | |
| CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); | |
| VerifyOrReturnError(treble >= mMinCorrection && treble <= mMaxCorrection, CHIP_IM_GLOBAL_STATUS(ConstraintError)); | |
| if (SetAttributeValue(mTreble, treble, Treble::Id)) | |
| { | |
| StoreTreble(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| // --------------------------------------------------------------------------------------- | |
| // scenes::SceneHandler | |
| // --------------------------------------------------------------------------------------- | |
| bool AudioControlCluster::SupportsCluster(EndpointId endpoint, ClusterId cluster) | |
| { | |
| return (cluster == AudioControl::Id) && (endpoint == mPath.mEndpointId); | |
| } | |
| CHIP_ERROR AudioControlCluster::SerializeSave(EndpointId endpoint, ClusterId cluster, MutableByteSpan & serializedBytes) | |
| { | |
| using AttributeValuePair = ScenesManagement::Structs::AttributeValuePairStruct::Type; | |
| VerifyOrReturnError(SupportsCluster(endpoint, cluster), CHIP_ERROR_INVALID_ARGUMENT); | |
| // SoftMuted and Volume are mandatory scene-able attributes; Bass/Mid/Treble are added only | |
| // when the BasicEqualizer feature and the matching band attribute are enabled. | |
| AttributeValuePair pairs[5]; | |
| size_t count = 0; | |
| pairs[count].attributeID = SoftMuted::Id; | |
| pairs[count].valueUnsigned8.SetValue(static_cast<uint8_t>(mSoftMuted)); | |
| count++; | |
| pairs[count].attributeID = Volume::Id; | |
| pairs[count].valueUnsigned16.SetValue(mVolume); | |
| count++; | |
| if (mFeatures.Has(Feature::kBasicEqualizer)) | |
| { | |
| if (mOptionalAttributeSet.IsSet(Bass::Id)) | |
| { | |
| pairs[count].attributeID = Bass::Id; | |
| pairs[count].valueSigned16.SetValue(mBass); | |
| count++; | |
| } | |
| if (mOptionalAttributeSet.IsSet(Mid::Id)) | |
| { | |
| pairs[count].attributeID = Mid::Id; | |
| pairs[count].valueSigned16.SetValue(mMid); | |
| count++; | |
| } | |
| if (mOptionalAttributeSet.IsSet(Treble::Id)) | |
| { | |
| pairs[count].attributeID = Treble::Id; | |
| pairs[count].valueSigned16.SetValue(mTreble); | |
| count++; | |
| } | |
| } | |
| app::DataModel::List<AttributeValuePair> attributeValueList(pairs, count); | |
| return EncodeAttributeValueList(attributeValueList, serializedBytes); | |
| } | |
| CHIP_ERROR AudioControlCluster::ApplyScene(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes, | |
| scenes::TransitionTimeMs timeMs) | |
| { | |
| // Transition time is intentionally ignored: the stored values are applied immediately, as | |
| // there is no meaningful hardware ramp for mute or tone controls. | |
| (void) timeMs; | |
| VerifyOrReturnError(SupportsCluster(endpoint, cluster), CHIP_ERROR_INVALID_ARGUMENT); | |
| app::DataModel::DecodableList<ScenesManagement::Structs::AttributeValuePairStruct::DecodableType> attributeValueList; | |
| ReturnErrorOnFailure(DecodeAttributeValueList(serializedBytes, attributeValueList)); | |
| // Pass 1: decode each pair into an optional, so an extension field set carrying only a subset | |
| // of the attributes leaves the rest untouched. Values outside the current constraints are | |
| // saturated to the closest legal value (Scenes spec: a non-boolean non-nullable attribute | |
| // takes the valid value closest to the one provided), so a scene stays recallable after | |
| // MaxUserVolume or the correction range shrinks. Bands not enabled on this instance are | |
| // skipped - the attribute does not exist here, and the shared scene validator is stateless so | |
| // it cannot make that per-instance check at AddScene time. | |
| const bool equalizer = mFeatures.Has(Feature::kBasicEqualizer); | |
| std::optional<uint16_t> volume; | |
| std::optional<bool> softMuted; | |
| std::optional<int16_t> bass, mid, treble; | |
| auto pairIterator = attributeValueList.begin(); | |
| while (pairIterator.Next()) | |
| { | |
| const auto & pair = pairIterator.GetValue(); | |
| switch (pair.attributeID) | |
| { | |
| case SoftMuted::Id: | |
| VerifyOrReturnError(pair.valueUnsigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); | |
| softMuted = pair.valueUnsigned8.Value() != 0; | |
| break; | |
| case Volume::Id: | |
| VerifyOrReturnError(pair.valueUnsigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); | |
| volume = std::clamp(pair.valueUnsigned16.Value(), mMinDeviceVolume, EffectiveMaxVolume()); | |
| break; | |
| case Bass::Id: | |
| VerifyOrReturnError(pair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); | |
| if (equalizer && mOptionalAttributeSet.IsSet(Bass::Id)) | |
| { | |
| bass = std::clamp(pair.valueSigned16.Value(), mMinCorrection, mMaxCorrection); | |
| } | |
| break; | |
| case Mid::Id: | |
| VerifyOrReturnError(pair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); | |
| if (equalizer && mOptionalAttributeSet.IsSet(Mid::Id)) | |
| { | |
| mid = std::clamp(pair.valueSigned16.Value(), mMinCorrection, mMaxCorrection); | |
| } | |
| break; | |
| case Treble::Id: | |
| VerifyOrReturnError(pair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); | |
| if (equalizer && mOptionalAttributeSet.IsSet(Treble::Id)) | |
| { | |
| treble = std::clamp(pair.valueSigned16.Value(), mMinCorrection, mMaxCorrection); | |
| } | |
| break; | |
| default: | |
| // Per the Scenes Management cluster (AttributeValuePairStruct), a pair referencing an | |
| // attribute that is not implemented on the endpoint is ignored rather than failing | |
| // the recall. | |
| break; | |
| } | |
| } | |
| ReturnErrorOnFailure(pairIterator.GetStatus()); | |
| // Pass 2: notify the delegate before committing, matching the command handlers and | |
| // SetMaxUserVolume, so a rejection leaves the cluster unchanged. Volume and SoftMuted are one | |
| // atomic call (the absent side taken from current state), made only when the scene carries at | |
| // least one of them; the equalizer bands mirror the write path (guarded by an actual-change | |
| // check). The bands are committed after Volume/SoftMuted, so a band delegate that rejects | |
| // leaves the recall partially applied - logged below so that is diagnosable. | |
| if (volume.has_value() || softMuted.has_value()) | |
| { | |
| const uint16_t newVolume = volume.value_or(mVolume); | |
| const bool newSoftMuted = softMuted.value_or(mSoftMuted); | |
| Status s = mDelegate.HandleVolumeAndMuteChange(newVolume, newSoftMuted); | |
| VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError()); | |
| if (SetAttributeValue(mVolume, newVolume, Volume::Id)) | |
| { | |
| StoreVolume(); | |
| } | |
| if (SetAttributeValue(mSoftMuted, newSoftMuted, SoftMuted::Id)) | |
| { | |
| StoreSoftMuted(); | |
| } | |
| } | |
| if (bass.has_value() && *bass != mBass) | |
| { | |
| Status s = mDelegate.HandleBassChanged(*bass); | |
| VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError(), | |
| ChipLogError(Zcl, "AudioControl scene recall: Bass rejected; recall partially applied")); | |
| SetAttributeValue(mBass, *bass, Bass::Id); | |
| StoreBass(); | |
| } | |
| if (mid.has_value() && *mid != mMid) | |
| { | |
| Status s = mDelegate.HandleMidChanged(*mid); | |
| VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError(), | |
| ChipLogError(Zcl, "AudioControl scene recall: Mid rejected; recall partially applied")); | |
| SetAttributeValue(mMid, *mid, Mid::Id); | |
| StoreMid(); | |
| } | |
| if (treble.has_value() && *treble != mTreble) | |
| { | |
| Status s = mDelegate.HandleTrebleChanged(*treble); | |
| VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError(), | |
| ChipLogError(Zcl, "AudioControl scene recall: Treble rejected; recall partially applied")); | |
| SetAttributeValue(mTreble, *treble, Treble::Id); | |
| StoreTreble(); | |
| } | |
| return CHIP_NO_ERROR; | |
| } | |
| } // namespace chip::app::Clusters |