diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 754dcd074..b7529427e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,11 @@ on: paths-ignore: - docs/** - '**/*.md' + branches: + - master + - development + tags-ignore: + - v* pull_request: paths-ignore: - docs/** diff --git a/include/Battery.h b/include/Battery.h index ab9399e8a..7e9290348 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -2,44 +2,32 @@ #pragma once #include +#include +#include + +#include "BatteryStats.h" + +class BatteryProvider { + public: + // returns true if the provider is ready for use, false otherwise + virtual bool init(bool verboseLogging) = 0; + + virtual void deinit() = 0; + virtual void loop() = 0; + virtual std::shared_ptr getStats() const = 0; +}; class BatteryClass { -public: - uint32_t lastUpdate; - - float chargeVoltage; - float chargeCurrentLimitation; - float dischargeCurrentLimitation; - uint16_t stateOfCharge; - uint32_t stateOfChargeLastUpdate; - uint16_t stateOfHealth; - float voltage; - float current; - float temperature; - bool alarmOverCurrentDischarge; - bool alarmUnderTemperature; - bool alarmOverTemperature; - bool alarmUnderVoltage; - bool alarmOverVoltage; - - bool alarmBmsInternal; - bool alarmOverCurrentCharge; - - - bool warningHighCurrentDischarge; - bool warningLowTemperature; - bool warningHighTemperature; - bool warningLowVoltage; - bool warningHighVoltage; - - bool warningBmsInternal; - bool warningHighCurrentCharge; - char manufacturer[9]; - bool chargeEnabled; - bool dischargeEnabled; - bool chargeImmediately; - -private: + public: + void init(); + void loop(); + + std::shared_ptr getStats() const; + + private: + uint32_t _lastMqttPublish = 0; + mutable std::mutex _mutex; + std::unique_ptr _upProvider = nullptr; }; extern BatteryClass Battery; diff --git a/include/BatteryStats.h b/include/BatteryStats.h new file mode 100644 index 000000000..ebdca11fd --- /dev/null +++ b/include/BatteryStats.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +#include "AsyncJson.h" +#include "Arduino.h" +#include "JkBmsDataPoints.h" + +// mandatory interface for all kinds of batteries +class BatteryStats { + public: + String const& getManufacturer() const { return _manufacturer; } + + // the last time *any* datum was updated + uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; } + bool updateAvailable(uint32_t since) const { return _lastUpdate > since; } + + uint8_t getSoC() const { return _SoC; } + uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; } + + // convert stats to JSON for web application live view + virtual void getLiveViewData(JsonVariant& root) const; + + virtual void mqttPublish() const; + + bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; } + + protected: + template + void addLiveViewValue(JsonVariant& root, std::string const& name, + T&& value, std::string const& unit, uint8_t precision) const; + void addLiveViewText(JsonVariant& root, std::string const& name, + std::string const& text) const; + void addLiveViewWarning(JsonVariant& root, std::string const& name, + bool warning) const; + void addLiveViewAlarm(JsonVariant& root, std::string const& name, + bool alarm) const; + + String _manufacturer = "unknown"; + uint8_t _SoC = 0; + uint32_t _lastUpdateSoC = 0; + uint32_t _lastUpdate = 0; +}; + +class PylontechBatteryStats : public BatteryStats { + friend class PylontechCanReceiver; + + public: + void getLiveViewData(JsonVariant& root) const final; + void mqttPublish() const final; + + private: + void setManufacturer(String&& m) { _manufacturer = std::move(m); } + void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = millis(); } + void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } + + float _chargeVoltage; + float _chargeCurrentLimitation; + float _dischargeCurrentLimitation; + uint16_t _stateOfHealth; + float _voltage; // total voltage of the battery pack + // total current into (positive) or from (negative) + // the battery, i.e., the charging current + float _current; + float _temperature; + + bool _alarmOverCurrentDischarge; + bool _alarmOverCurrentCharge; + bool _alarmUnderTemperature; + bool _alarmOverTemperature; + bool _alarmUnderVoltage; + bool _alarmOverVoltage; + bool _alarmBmsInternal; + + bool _warningHighCurrentDischarge; + bool _warningHighCurrentCharge; + bool _warningLowTemperature; + bool _warningHighTemperature; + bool _warningLowVoltage; + bool _warningHighVoltage; + bool _warningBmsInternal; + + bool _chargeEnabled; + bool _dischargeEnabled; + bool _chargeImmediately; +}; + +class JkBmsBatteryStats : public BatteryStats { + public: + void getLiveViewData(JsonVariant& root) const final; + void mqttPublish() const final; + + void updateFrom(JkBms::DataPointContainer const& dp); + + private: + JkBms::DataPointContainer _dataPoints; +}; diff --git a/include/Configuration.h b/include/Configuration.h index be1864d23..5c10e15bb 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -127,6 +127,7 @@ struct CONFIG_T { char Mqtt_ClientKey[MQTT_MAX_CERT_STRLEN +1]; bool Vedirect_Enabled; + bool Vedirect_VerboseLogging; bool Vedirect_UpdatesOnly; bool PowerMeter_Enabled; @@ -166,6 +167,11 @@ struct CONFIG_T { float PowerLimiter_FullSolarPassThroughStopVoltage; bool Battery_Enabled; + bool Battery_VerboseLogging; + uint8_t Battery_Provider; + uint8_t Battery_JkBmsInterface; + uint8_t Battery_JkBmsPollingInterval; + bool Huawei_Enabled; bool Huawei_Auto_Power_Enabled; float Huawei_Auto_Power_Voltage_Limit; diff --git a/include/JkBmsController.h b/include/JkBmsController.h new file mode 100644 index 000000000..0cb2e46e0 --- /dev/null +++ b/include/JkBmsController.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +#include "Battery.h" +#include "JkBmsSerialMessage.h" + +class DataPointContainer; + +namespace JkBms { + +class Controller : public BatteryProvider { + public: + Controller() = default; + + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final; + std::shared_ptr getStats() const final { return _stats; } + + private: + enum class Status : unsigned { + Initializing, + Timeout, + WaitingForPollInterval, + HwSerialNotAvailableForWrite, + BusyReading, + RequestSent, + FrameCompleted + }; + + std::string const& getStatusText(Status status); + void announceStatus(Status status); + void sendRequest(uint8_t pollInterval); + void rxData(uint8_t inbyte); + void reset(); + void frameComplete(); + void processDataPoints(DataPointContainer const& dataPoints); + + enum class Interface : unsigned { + Invalid, + Uart, + Transceiver + }; + + Interface getInterface() const; + + enum class ReadState : unsigned { + Idle, + WaitingForFrameStart, + FrameStartReceived, + StartMarkerReceived, + FrameLengthMsbReceived, + ReadingFrame + }; + ReadState _readState; + void setReadState(ReadState state) { + _readState = state; + } + + bool _verboseLogging = true; + int8_t _rxEnablePin = -1; + int8_t _txEnablePin = -1; + Status _lastStatus = Status::Initializing; + uint32_t _lastStatusPrinted = 0; + uint32_t _lastRequest = 0; + uint16_t _frameLength = 0; + uint8_t _protocolVersion = -1; + SerialResponse::tData _buffer = {}; + std::shared_ptr _stats = + std::make_shared(); +}; + +} /* namespace JkBms */ diff --git a/include/JkBmsDataPoints.h b/include/JkBmsDataPoints.h new file mode 100644 index 000000000..db7cd37ab --- /dev/null +++ b/include/JkBmsDataPoints.h @@ -0,0 +1,250 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace JkBms { + +enum class DataPointLabel : uint8_t { + CellsMilliVolt = 0x79, + BmsTempCelsius = 0x80, + BatteryTempOneCelsius = 0x81, + BatteryTempTwoCelsius = 0x82, + BatteryVoltageMilliVolt = 0x83, + BatteryCurrentMilliAmps = 0x84, + BatterySoCPercent = 0x85, + BatteryTemperatureSensorAmount = 0x86, + BatteryCycles = 0x87, + BatteryCycleCapacity = 0x89, + BatteryCellAmount = 0x8a, + AlarmsBitmask = 0x8b, + StatusBitmask = 0x8c, + TotalOvervoltageThresholdMilliVolt = 0x8e, + TotalUndervoltageThresholdMilliVolt = 0x8f, + CellOvervoltageThresholdMilliVolt = 0x90, + CellOvervoltageRecoveryMilliVolt = 0x91, + CellOvervoltageProtectionDelaySeconds = 0x92, + CellUndervoltageThresholdMilliVolt = 0x93, + CellUndervoltageRecoveryMilliVolt = 0x94, + CellUndervoltageProtectionDelaySeconds = 0x95, + CellVoltageDiffThresholdMilliVolt = 0x96, + DischargeOvercurrentThresholdAmperes = 0x97, + DischargeOvercurrentDelaySeconds = 0x98, + ChargeOvercurrentThresholdAmps = 0x99, + ChargeOvercurrentDelaySeconds = 0x9a, + BalanceCellVoltageThresholdMilliVolt = 0x9b, + BalanceVoltageDiffThresholdMilliVolt = 0x9c, + BalancingEnabled = 0x9d, + BmsTempProtectionThresholdCelsius = 0x9e, + BmsTempRecoveryThresholdCelsius = 0x9f, + BatteryTempProtectionThresholdCelsius = 0xa0, + BatteryTempRecoveryThresholdCelsius = 0xa1, + BatteryTempDiffThresholdCelsius = 0xa2, + ChargeHighTempThresholdCelsius = 0xa3, + DischargeHighTempThresholdCelsius = 0xa4, + ChargeLowTempThresholdCelsius = 0xa5, + ChargeLowTempRecoveryCelsius = 0xa6, + DischargeLowTempThresholdCelsius = 0xa7, + DischargeLowTempRecoveryCelsius = 0xa8, + CellAmountSetting = 0xa9, + BatteryCapacitySettingAmpHours = 0xaa, + BatteryChargeEnabled = 0xab, + BatteryDischargeEnabled = 0xac, + CurrentCalibrationMilliAmps = 0xad, + BmsAddress = 0xae, + BatteryType = 0xaf, + SleepWaitTime = 0xb0, // what's this? + LowCapacityAlarmThresholdPercent = 0xb1, + ModificationPassword = 0xb2, + DedicatedChargerSwitch = 0xb3, // what's this? + EquipmentId = 0xb4, + DateOfManufacturing = 0xb5, + BmsHourMeterMinutes = 0xb6, + BmsSoftwareVersion = 0xb7, + CurrentCalibration = 0xb8, + ActualBatteryCapacityAmpHours = 0xb9, + ProductId = 0xba, + ProtocolVersion = 0xc0 +}; + +using tCells = std::map; + +template struct DataPointLabelTraits; + +#define LABEL_TRAIT(n, t, u) template<> struct DataPointLabelTraits { \ + using type = t; \ + static constexpr char const name[] = #n; \ + static constexpr char const unit[] = u; \ +}; + +/** + * the types associated with the labels are the types for the respective data + * points in the JkBms::DataPoint class. they are *not* always equal to the + * type used in the serial message. + * + * it is unfortunate that we have to repeat all enum values here to define the + * traits. code generation could help here (labels are defined in a single + * source of truth and this code is generated -- no typing errors, etc.). + * however, the compiler will complain if an enum is misspelled or traits are + * defined for a removed enum, so we will notice. it will also complain when a + * trait is missing and if a data point for a label without traits is added to + * the DataPointContainer class, because the traits must be available then. + * even though this is tedious to maintain, human errors will be caught. + */ +LABEL_TRAIT(CellsMilliVolt, tCells, "mV"); +LABEL_TRAIT(BmsTempCelsius, int16_t, "°C"); +LABEL_TRAIT(BatteryTempOneCelsius, int16_t, "°C"); +LABEL_TRAIT(BatteryTempTwoCelsius, int16_t, "°C"); +LABEL_TRAIT(BatteryVoltageMilliVolt, uint32_t, "mV"); +LABEL_TRAIT(BatteryCurrentMilliAmps, int32_t, "mA"); +LABEL_TRAIT(BatterySoCPercent, uint8_t, "%"); +LABEL_TRAIT(BatteryTemperatureSensorAmount, uint8_t, ""); +LABEL_TRAIT(BatteryCycles, uint16_t, ""); +LABEL_TRAIT(BatteryCycleCapacity, uint32_t, "Ah"); +LABEL_TRAIT(BatteryCellAmount, uint16_t, ""); +LABEL_TRAIT(AlarmsBitmask, uint16_t, ""); +LABEL_TRAIT(StatusBitmask, uint16_t, ""); +LABEL_TRAIT(TotalOvervoltageThresholdMilliVolt, uint32_t, "mV"); +LABEL_TRAIT(TotalUndervoltageThresholdMilliVolt, uint32_t, "mV"); +LABEL_TRAIT(CellOvervoltageThresholdMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(CellOvervoltageRecoveryMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(CellOvervoltageProtectionDelaySeconds, uint16_t, "s"); +LABEL_TRAIT(CellUndervoltageThresholdMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(CellUndervoltageRecoveryMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(CellUndervoltageProtectionDelaySeconds, uint16_t, "s"); +LABEL_TRAIT(CellVoltageDiffThresholdMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(DischargeOvercurrentThresholdAmperes, uint16_t, "A"); +LABEL_TRAIT(DischargeOvercurrentDelaySeconds, uint16_t, "s"); +LABEL_TRAIT(ChargeOvercurrentThresholdAmps, uint16_t, "A"); +LABEL_TRAIT(ChargeOvercurrentDelaySeconds, uint16_t, "s"); +LABEL_TRAIT(BalanceCellVoltageThresholdMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(BalanceVoltageDiffThresholdMilliVolt, uint16_t, "mV"); +LABEL_TRAIT(BalancingEnabled, bool, ""); +LABEL_TRAIT(BmsTempProtectionThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(BmsTempRecoveryThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(BatteryTempProtectionThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(BatteryTempRecoveryThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(BatteryTempDiffThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(ChargeHighTempThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(DischargeHighTempThresholdCelsius, uint16_t, "°C"); +LABEL_TRAIT(ChargeLowTempThresholdCelsius, int16_t, "°C"); +LABEL_TRAIT(ChargeLowTempRecoveryCelsius, int16_t, "°C"); +LABEL_TRAIT(DischargeLowTempThresholdCelsius, int16_t, "°C"); +LABEL_TRAIT(DischargeLowTempRecoveryCelsius, int16_t, "°C"); +LABEL_TRAIT(CellAmountSetting, uint8_t, ""); +LABEL_TRAIT(BatteryCapacitySettingAmpHours, uint32_t, "Ah"); +LABEL_TRAIT(BatteryChargeEnabled, bool, ""); +LABEL_TRAIT(BatteryDischargeEnabled, bool, ""); +LABEL_TRAIT(CurrentCalibrationMilliAmps, uint16_t, "mA"); +LABEL_TRAIT(BmsAddress, uint8_t, ""); +LABEL_TRAIT(BatteryType, uint8_t, ""); +LABEL_TRAIT(SleepWaitTime, uint16_t, "s"); +LABEL_TRAIT(LowCapacityAlarmThresholdPercent, uint8_t, "%"); +LABEL_TRAIT(ModificationPassword, std::string, ""); +LABEL_TRAIT(DedicatedChargerSwitch, bool, ""); +LABEL_TRAIT(EquipmentId, std::string, ""); +LABEL_TRAIT(DateOfManufacturing, std::string, ""); +LABEL_TRAIT(BmsHourMeterMinutes, uint32_t, "min"); +LABEL_TRAIT(BmsSoftwareVersion, std::string, ""); +LABEL_TRAIT(CurrentCalibration, bool, ""); +LABEL_TRAIT(ActualBatteryCapacityAmpHours, uint32_t, "Ah"); +LABEL_TRAIT(ProductId, std::string, ""); +LABEL_TRAIT(ProtocolVersion, uint8_t, ""); +#undef LABEL_TRAIT + +class DataPoint { + friend class DataPointContainer; + + public: + using tValue = std::variant; + + DataPoint() = delete; + + DataPoint(DataPoint const& other) + : _strLabel(other._strLabel) + , _strValue(other._strValue) + , _strUnit(other._strUnit) + , _value(other._value) + , _timestamp(other._timestamp) { } + + DataPoint(std::string const& strLabel, std::string const& strValue, + std::string const& strUnit, tValue value, uint32_t timestamp) + : _strLabel(strLabel) + , _strValue(strValue) + , _strUnit(strUnit) + , _value(std::move(value)) + , _timestamp(timestamp) { } + + std::string const& getLabelText() const { return _strLabel; } + std::string const& getValueText() const { return _strValue; } + std::string const& getUnitText() const { return _strUnit; } + uint32_t getTimestamp() const { return _timestamp; } + + private: + std::string _strLabel; + std::string _strValue; + std::string _strUnit; + tValue _value; + uint32_t _timestamp; +}; + +template std::string dataPointValueToStr(T const& v); + +class DataPointContainer { + public: + DataPointContainer() = default; + + using Label = DataPointLabel; + template