From 9468c0338f64dbca4e24501c6bcaf5c620c6ce7f Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Thu, 9 May 2024 21:31:55 +0200 Subject: [PATCH] libraries/Matter: Add MatterPowerSource.h and a battery percent reporting example --- .../matter_switch_with_battery.ino | 146 ++++++++++++++++++ libraries/Matter/src/MatterPowerSource.cpp | 140 +++++++++++++++++ libraries/Matter/src/MatterPowerSource.h | 32 ++++ .../Matter/src/devices/DevicePowerSource.cpp | 88 +++++++++++ .../Matter/src/devices/DevicePowerSource.h | 33 ++++ 5 files changed, 439 insertions(+) create mode 100644 libraries/Matter/examples/matter_switch_with_battery/matter_switch_with_battery.ino create mode 100644 libraries/Matter/src/MatterPowerSource.cpp create mode 100644 libraries/Matter/src/MatterPowerSource.h create mode 100644 libraries/Matter/src/devices/DevicePowerSource.cpp create mode 100644 libraries/Matter/src/devices/DevicePowerSource.h diff --git a/libraries/Matter/examples/matter_switch_with_battery/matter_switch_with_battery.ino b/libraries/Matter/examples/matter_switch_with_battery/matter_switch_with_battery.ino new file mode 100644 index 0000000..2efac2c --- /dev/null +++ b/libraries/Matter/examples/matter_switch_with_battery/matter_switch_with_battery.ino @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +SFE_MAX1704X lipo(MAX1704X_MAX17048); +uint32_t battery_gauge_timeout = 900000; // 15min +void read_battery(); + +MatterSwitch matter_switch; +MatterPowerSource matter_power_source; +volatile bool button_pressed = false; +void handle_button_press(); +void handle_button_release(); + +void setup() { + Serial.begin(115200); + Serial.println("Matter Switch with Battery"); + Wire.begin(); + Matter.begin(); + matter_switch.begin(); + matter_power_source.begin(); + + // Set up the onboard button + #ifndef BTN_BUILTIN + #define BTN_BUILTIN PA0 + #endif + pinMode(BTN_BUILTIN, INPUT_PULLUP); + + attachInterrupt(BTN_BUILTIN, &handle_button_press, FALLING); + attachInterrupt(BTN_BUILTIN, &handle_button_release, RISING); + + if (!Matter.isDeviceCommissioned()) { + Serial.println("Matter device is not commissioned"); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\n", Matter.getOnboardingQRCodeUrl().c_str()); + } + while (!Matter.isDeviceCommissioned()) { + delay(200); + } + Serial.println("Matter device is commissioned"); + + if (!Matter.isDeviceThreadConnected()) { + Serial.println("Waiting for Thread network connection..."); + } + while (!Matter.isDeviceThreadConnected()) { + delay(200); + } + Serial.println("Connected to Thread network"); + + if (!matter_switch.is_online()) { + Serial.println("Waiting for Matter switch device discovery..."); + } + while (!matter_switch.is_online()) { + delay(200); + } + if (!matter_power_source.is_online()) { + Serial.println("Waiting for Matter power source device discovery..."); + } + while (!matter_power_source.is_online()) { + delay(200); + } + Serial.println("Matter device is now online"); + + if (!lipo.begin()) { + Serial.println("No MAX17048 LiPo fuel gauge detected"); + } else { + lipo.quickStart(); + lipo.setThreshold(20); + read_battery(); + } +} + +void loop() { + static bool button_pressed_last = false; + static bool switch_state_last = false; + static uint32_t battery_gauge_last = 0; + + // If the physical button state changes - update the switch's state + if (button_pressed != button_pressed_last) { + button_pressed_last = button_pressed; + matter_switch.set_state(button_pressed); + } + + // Get the current state of the Matter switch + bool switch_state_current = matter_switch.get_state(); + + // If the current state is 'pressed' and the previous was 'not pressed' - switch pressed + if (switch_state_current && !switch_state_last) { + switch_state_last = switch_state_current; + Serial.println("Button pressed"); + read_battery(); + } + + // If the current state is 'not pressed' and the previous was 'pressed' - switch released + if (!switch_state_current && switch_state_last) { + switch_state_last = switch_state_current; + Serial.println("Button released"); + } + + uint32_t time = millis(); + if (time > battery_gauge_last + battery_gauge_timeout) { + battery_gauge_last = time; + read_battery(); + } +} + +void read_battery() { + double voltage = lipo.getVoltage(); + double soc = lipo.getSOC(); + bool alert = lipo.getAlert(); + + Serial.printf("Battery Voltage: %4.2f V\n", voltage); + Serial.printf("Battery Percent: %4.0f %%\n", soc); + matter_power_source.set_bat_percent_remaining(soc); + + if (alert) { + Serial.println("Battery Alert!"); + } +} + +void handle_button_press() { + static uint32_t button_press_last = 0; + uint32_t time = millis(); + if (time < button_press_last + 200) { + return; + } + + button_press_last = time; + button_pressed = true; +} + +void handle_button_release() { + static uint32_t button_press_last = 0; + uint32_t time = millis(); + if (time < button_press_last + 200) { + return; + } + + button_press_last = time; + button_pressed = false; +} diff --git a/libraries/Matter/src/MatterPowerSource.cpp b/libraries/Matter/src/MatterPowerSource.cpp new file mode 100644 index 0000000..7907a42 --- /dev/null +++ b/libraries/Matter/src/MatterPowerSource.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +#include "MatterPowerSource.h" + +using namespace ::chip; +using namespace ::chip::Platform; +using namespace ::chip::Credentials; +using namespace ::chip::app::Clusters; + +const EmberAfDeviceType gPowerSourceDeviceTypes[] = { { DEVICE_TYPE_POWER_SOURCE, DEVICE_VERSION_DEFAULT } }; + +// Power source cluster attributes +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(powerSourceAttrs) +DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::BatPercentRemaining::Id, INT8U, 1, 0), /* BatPercentRemaining */ +DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::FeatureMap::Id, BITMAP32, 4, 0), /* FeatureMap */ +DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::ClusterRevision::Id, INT16U, 2, 0), /* ClusterRevision */ +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + +// Power source cluster list +DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(powerSourceEndpointClusters) +DECLARE_DYNAMIC_CLUSTER(PowerSource::Id, powerSourceAttrs, nullptr, nullptr), +DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, nullptr, nullptr), +DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, nullptr, nullptr) +DECLARE_DYNAMIC_CLUSTER_LIST_END; + +/***************************************************************************//** + * Constructor for MatterPowerSource + ******************************************************************************/ +MatterPowerSource::MatterPowerSource() : + power_source_device(nullptr), + device_endpoint(nullptr), + endpoint_dataversion_storage(nullptr), + initialized(false) +{ + ; +} + +/***************************************************************************//** + * Destructor for MatterPowerSource + ******************************************************************************/ +MatterPowerSource::~MatterPowerSource() +{ + this->end(); +} + +/***************************************************************************//** + * Initializes the MatterPowerSource instance + * + * @return true if the initialization succeeded, false otherwise + ******************************************************************************/ +bool MatterPowerSource::begin() +{ + if (this->initialized) { + return false; + } + + // Create new device + DevicePowerSource* power_source = new (std::nothrow)DevicePowerSource("Power Source", 0); + if (power_source == nullptr) { + return false; + } + power_source->SetReachable(true); + power_source->SetProductName("Power Source"); + + // Set the device instance pointer in the base class + this->base_matter_device = power_source; + + // Create new endpoint + EmberAfEndpointType* new_endpoint = (EmberAfEndpointType*)malloc(sizeof(EmberAfEndpointType)); + if (new_endpoint == nullptr) { + delete(power_source); + return false; + } + new_endpoint->cluster = powerSourceEndpointClusters; + new_endpoint->clusterCount = ArraySize(powerSourceEndpointClusters); + new_endpoint->endpointSize = 0; + + // Create data version storage for the endpoint + size_t dataversion_size = ArraySize(powerSourceEndpointClusters) * sizeof(DataVersion); + DataVersion* new_power_source_data_version = (DataVersion*)malloc(dataversion_size); + if (new_power_source_data_version == nullptr) { + delete(power_source); + free(new_endpoint); + return false; + } + + // Add new endpoint + int result = AddDeviceEndpoint(power_source, + new_endpoint, + Span(gPowerSourceDeviceTypes), + Span(new_power_source_data_version, dataversion_size), 1); + if (result < 0) { + delete(power_source); + free(new_endpoint); + free(new_power_source_data_version); + return false; + } + + this->power_source_device = power_source; + this->device_endpoint = new_endpoint; + this->endpoint_dataversion_storage = new_power_source_data_version; + this->initialized = true; + return true; +} + +/***************************************************************************//** + * Deinitializes the MatterPowerSource instance + ******************************************************************************/ +void MatterPowerSource::end() +{ + if (!this->initialized) { + return; + } + (void)RemoveDeviceEndpoint(this->power_source_device); + free(this->device_endpoint); + free(this->endpoint_dataversion_storage); + delete(this->power_source_device); + this->initialized = false; +} + +void MatterPowerSource::set_bat_percent_remaining_raw(uint8_t value) +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->power_source_device->SetBatPercentRemaining(value); + PlatformMgr().UnlockChipStack(); +} + +void MatterPowerSource::set_bat_percent_remaining(double percent) +{ + uint8_t out_value = static_cast(percent * 2.0l); + this->set_bat_percent_remaining_raw(out_value); +} + +uint8_t MatterPowerSource::get_bat_percent_remaining() +{ + return this->power_source_device->GetBatPercentRemaining(); +} diff --git a/libraries/Matter/src/MatterPowerSource.h b/libraries/Matter/src/MatterPowerSource.h new file mode 100644 index 0000000..cb09f42 --- /dev/null +++ b/libraries/Matter/src/MatterPowerSource.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +#ifndef MATTER_POWER_SOURCE_H +#define MATTER_POWER_SOURCE_H + +#include "Matter.h" +#include "devices/DevicePowerSource.h" +#include +#include + +using namespace chip; +using namespace ::chip::DeviceLayer; + +class MatterPowerSource : public ArduinoMatterAppliance { +public: + MatterPowerSource(); + ~MatterPowerSource(); + bool begin(); + void end(); + void set_bat_percent_remaining_raw(uint8_t value); + void set_bat_percent_remaining(double percent); + uint8_t get_bat_percent_remaining(); + void operator=(uint8_t value); + +private: + DevicePowerSource* power_source_device; + EmberAfEndpointType* device_endpoint; + DataVersion* endpoint_dataversion_storage; + bool initialized; +}; + +#endif // MATTER_POWER_SOURCE_H diff --git a/libraries/Matter/src/devices/DevicePowerSource.cpp b/libraries/Matter/src/devices/DevicePowerSource.cpp new file mode 100644 index 0000000..d0e9768 --- /dev/null +++ b/libraries/Matter/src/devices/DevicePowerSource.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT + +#include "DevicePowerSource.h" + +DevicePowerSource::DevicePowerSource(const char* device_name, + uint8_t bat_percent_remaining) : + Device(device_name), + bat_percent_remaining(bat_percent_remaining) +{ + ; +} + +uint8_t DevicePowerSource::GetBatPercentRemaining() +{ + return this->bat_percent_remaining; +} + +void DevicePowerSource::SetBatPercentRemaining(uint8_t value) +{ + if (value < 0) { + value = 0; + } else if (value > 200) { + value = 200; + } + + bool changed = this->bat_percent_remaining != (value); // range 0-200 + ChipLogProgress(DeviceLayer, "PowerSourceDevice[%s]: new battery percent remaining='%d'", this->device_name, value); + this->bat_percent_remaining = value; + + if (changed) { + this->HandlePowerSourceDeviceStatusChanged(kChanged_BatPercentRemaining); + } +} + +uint32_t DevicePowerSource::GetPowerSourceClusterFeatureMap() +{ + return this->power_source_cluster_feature_map; +} + +uint16_t DevicePowerSource::GetPowerSourceClusterRevision() +{ + return this->power_source_cluster_revision; +} + +EmberAfStatus DevicePowerSource::HandleReadEmberAfAttribute(ClusterId clusterId, + chip::AttributeId attributeId, + uint8_t* buffer, + uint16_t maxReadLength) +{ + if (!this->reachable) { + return EMBER_ZCL_STATUS_FAILURE; + } + + using namespace ::chip::app::Clusters::PowerSource::Attributes; + ChipLogProgress(DeviceLayer, "HandleReadPowerSourceAttribute: clusterId=%lu attrId=%ld", clusterId, attributeId); + + if (clusterId == chip::app::Clusters::BridgedDeviceBasicInformation::Id) { + return this->HandleReadBridgedDeviceBasicAttribute(clusterId, attributeId, buffer, maxReadLength); + } + + if (clusterId != chip::app::Clusters::PowerSource::Id) { + return EMBER_ZCL_STATUS_FAILURE; + } + + if ((attributeId == BatPercentRemaining::Id) && (maxReadLength == 1)) { + uint8_t batPercentRemaining = this->GetBatPercentRemaining(); + memcpy(buffer, &batPercentRemaining, sizeof(batPercentRemaining)); + } else if ((attributeId == FeatureMap::Id) && (maxReadLength == 4)) { + uint32_t featureMap = this->GetPowerSourceClusterFeatureMap(); + memcpy(buffer, &featureMap, sizeof(featureMap)); + } else if ((attributeId == ClusterRevision::Id) && (maxReadLength == 2)) { + uint16_t clusterRevision = this->GetPowerSourceClusterRevision(); + memcpy(buffer, &clusterRevision, sizeof(clusterRevision)); + } else { + return EMBER_ZCL_STATUS_FAILURE; + } + + return EMBER_ZCL_STATUS_SUCCESS; +} + +void DevicePowerSource::HandlePowerSourceDeviceStatusChanged(Changed_t itemChangedMask) +{ + using namespace ::chip::app::Clusters; + + if (itemChangedMask & DevicePowerSource::kChanged_BatPercentRemaining) { + ScheduleMatterReportingCallback(this->endpoint_id, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id); + } +} diff --git a/libraries/Matter/src/devices/DevicePowerSource.h b/libraries/Matter/src/devices/DevicePowerSource.h new file mode 100644 index 0000000..9871386 --- /dev/null +++ b/libraries/Matter/src/devices/DevicePowerSource.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +#pragma once + +#include "MatterDevice.h" + +class DevicePowerSource : public Device +{ +public: + enum Changed_t{ + kChanged_BatPercentRemaining = kChanged_Last << 1, + } Changed; + + DevicePowerSource(const char* device_name, uint8_t bat_percent_remaining); + + uint8_t GetBatPercentRemaining(); + void SetBatPercentRemaining(uint8_t percent); + uint32_t GetPowerSourceClusterFeatureMap(); + uint16_t GetPowerSourceClusterRevision(); + + EmberAfStatus HandleReadEmberAfAttribute(ClusterId clusterId, + chip::AttributeId attributeId, + uint8_t* buffer, + uint16_t maxReadLength) override; + +private: + void HandlePowerSourceDeviceStatusChanged(Changed_t itemChangedMask); + + uint8_t bat_percent_remaining; + + static const uint32_t power_source_cluster_feature_map = 0u; // No additional features enabled + static const uint16_t power_source_cluster_revision = 1u; +};