Skip to content

Commit

Permalink
Feature: support for JBD BMS using serial connection
Browse files Browse the repository at this point in the history
  • Loading branch information
MoleBre authored and schlimmchen committed Oct 30, 2024
1 parent 33b7697 commit 7cd1984
Show file tree
Hide file tree
Showing 16 changed files with 1,241 additions and 120 deletions.
30 changes: 30 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "AsyncJson.h"
#include "Arduino.h"
#include "JkBmsDataPoints.h"
#include "JbdBmsDataPoints.h"
#include "VeDirectShuntController.h"
#include <cfloat>

Expand Down Expand Up @@ -283,6 +284,35 @@ class JkBmsBatteryStats : public BatteryStats {
uint32_t _cellVoltageTimestamp = 0;
};

class JbdBmsBatteryStats : public BatteryStats {
public:
void getLiveViewData(JsonVariant& root) const final {
getJsonData(root, false);
}

void getInfoViewData(JsonVariant& root) const {
getJsonData(root, true);
}

void mqttPublish() const final;

uint32_t getMqttFullPublishIntervalMs() const final { return 60 * 1000; }

void updateFrom(JbdBms::DataPointContainer const& dp);

private:
void getJsonData(JsonVariant& root, bool verbose) const;

JbdBms::DataPointContainer _dataPoints;
mutable uint32_t _lastMqttPublish = 0;
mutable uint32_t _lastFullMqttPublish = 0;

uint16_t _cellMinMilliVolt = 0;
uint16_t _cellAvgMilliVolt = 0;
uint16_t _cellMaxMilliVolt = 0;
uint32_t _cellVoltageTimestamp = 0;
};

class VictronSmartShuntStats : public BatteryStats {
public:
void getLiveViewData(JsonVariant& root) const final;
Expand Down
119 changes: 119 additions & 0 deletions include/DataPoints.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#pragma once

#include <Arduino.h>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>

using tCellVoltages = std::map<uint8_t, uint16_t>;

template<typename... V>
class DataPoint {
template<typename, typename L, template<L> class>
friend class DataPointContainer;

public:
using tValue = std::variant<V...>;

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; }

bool operator==(DataPoint const& other) const {
return _value == other._value;
}

private:
std::string _strLabel;
std::string _strValue;
std::string _strUnit;
tValue _value;
uint32_t _timestamp;
};

template<typename T> std::string dataPointValueToStr(T const& v);

template<typename DataPoint, typename Label, template<Label> class Traits>
class DataPointContainer {
public:
DataPointContainer() = default;

//template<Label L> using Traits = LabelTraits<L>;

template<Label L>
void add(typename Traits<L>::type val) {
_dataPoints.emplace(
L,
DataPoint(
Traits<L>::name,
dataPointValueToStr(val),
Traits<L>::unit,
typename DataPoint::tValue(std::move(val)),
millis()
)
);
}

// make sure add() is only called with the type expected for the
// respective label, no implicit conversions allowed.
template<Label L, typename T>
void add(T) = delete;

template<Label L>
std::optional<DataPoint const> getDataPointFor() const {
auto it = _dataPoints.find(L);
if (it == _dataPoints.end()) { return std::nullopt; }
return it->second;
}

template<Label L>
std::optional<typename Traits<L>::type> get() const {
auto optionalDataPoint = getDataPointFor<L>();
if (!optionalDataPoint.has_value()) { return std::nullopt; }
return std::get<typename Traits<L>::type>(optionalDataPoint->_value);
}

using tMap = std::unordered_map<Label, DataPoint const>;
typename tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
typename tMap::const_iterator cend() const { return _dataPoints.cend(); }

// copy all data points from source into this instance, overwriting
// existing data points in this instance.
void updateFrom(DataPointContainer const& source)
{
for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
auto pos = _dataPoints.find(iter->first);

if (pos != _dataPoints.end()) {
// do not update existing data points with the same value
if (pos->second == iter->second) { continue; }

_dataPoints.erase(pos);
}
_dataPoints.insert(*iter);
}
}

private:
tMap _dataPoints;
};
87 changes: 87 additions & 0 deletions include/JbdBmsController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#pragma once

#include <memory>
#include <vector>
#include <frozen/string.h>

#include "Battery.h"
#include "JbdBmsDataPoints.h"
#include "JbdBmsSerialMessage.h"
#include "JbdBmsController.h"

namespace JbdBms {

class Controller : public BatteryProvider {
public:
Controller() = default;

bool init(bool verboseLogging) final;
void deinit() final;
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }

private:
static char constexpr _serialPortOwner[] = "JBD BMS";

#ifdef JBDBMS_DUMMY_SERIAL
std::unique_ptr<DummySerial> _upSerial;
#else
std::unique_ptr<HardwareSerial> _upSerial;
#endif

enum class Status : unsigned {
Initializing,
Timeout,
WaitingForPollInterval,
HwSerialNotAvailableForWrite,
BusyReading,
RequestSent,
FrameCompleted
};

frozen::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, // 1 Byte: 0xDD
StateReceived,
CommandCodeReceived,
ReadingDataContent,
DataContentReceived,
ReadingCheckSum,
CheckSumReceived,
};

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;
uint8_t _dataLength = 0;
JbdBms::SerialResponse::tData _buffer = {};
std::shared_ptr<JbdBmsBatteryStats> _stats =
std::make_shared<JbdBmsBatteryStats>();
};

} /* namespace JbdBms */
118 changes: 118 additions & 0 deletions include/JbdBmsDataPoints.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#pragma once

#include <Arduino.h>
#include <map>
#include <frozen/map.h>
#include <frozen/string.h>

#include "DataPoints.h"

namespace JbdBms {

#define JBD_PROTECTION_STATUS(fnc) \
fnc(CellOverVoltage, (1<<0)) \
fnc(CellUnderVoltage, (1<<1)) \
fnc(PackOverVoltage, (1<<2)) \
fnc(PackUnderVoltage, (1<<3)) \
fnc(ChargingOverTemperature, (1<<4)) \
fnc(ChargingLowTemperature, (1<<5)) \
fnc(DischargingOverTemperature, (1<<6)) \
fnc(DischargingLowTemperature, (1<<7)) \
fnc(ChargingOverCurrent, (1<<8)) \
fnc(DischargeOverCurrent, (1<<9)) \
fnc(ShortCircuit, (1<<10)) \
fnc(IcFrontEndError, (1<<11)) \
fnc(MosSotwareLock, (1<<12)) \
fnc(Reserved1, (1<<13)) \
fnc(Reserved2, (1<<14)) \
fnc(Reserved3, (1<<15))

enum class AlarmBits : uint16_t {
#define ALARM_ENUM(name, value) name = value,
JBD_PROTECTION_STATUS(ALARM_ENUM)
#undef ALARM_ENUM
};

static const frozen::map<AlarmBits, frozen::string, 16> AlarmBitTexts = {
#define ALARM_TEXT(name, value) { AlarmBits::name, #name },
JBD_PROTECTION_STATUS(ALARM_TEXT)
#undef ALARM_TEXT
};

enum class DataPointLabel : uint8_t {
CellsMilliVolt,
BatteryTempOneCelsius,
BatteryTempTwoCelsius,
BatteryVoltageMilliVolt,
BatteryCurrentMilliAmps,
BatterySoCPercent,
BatteryTemperatureSensorAmount,
BatteryCycles,
BatteryCellAmount,
AlarmsBitmask,
BalancingEnabled,
CellAmountSetting,
BatteryCapacitySettingAmpHours,
BatteryChargeEnabled,
BatteryDischargeEnabled,
DateOfManufacturing,
BmsSoftwareVersion,
BmsHardwareVersion,
ActualBatteryCapacityAmpHours
};

using tCells = tCellVoltages;

template<DataPointLabel> struct DataPointLabelTraits;

#define LABEL_TRAIT(n, t, u) template<> struct DataPointLabelTraits<DataPointLabel::n> { \
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 JbdBms::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(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(BatteryCellAmount, uint16_t, "");
LABEL_TRAIT(AlarmsBitmask, uint16_t, "");
LABEL_TRAIT(BalancingEnabled, bool, "");
LABEL_TRAIT(CellAmountSetting, uint8_t, "");
LABEL_TRAIT(BatteryCapacitySettingAmpHours, uint32_t, "Ah");
LABEL_TRAIT(BatteryChargeEnabled, bool, "");
LABEL_TRAIT(BatteryDischargeEnabled, bool, "");
LABEL_TRAIT(DateOfManufacturing, std::string, "");
LABEL_TRAIT(BmsSoftwareVersion, std::string, "");
LABEL_TRAIT(BmsHardwareVersion, std::string, "");
LABEL_TRAIT(ActualBatteryCapacityAmpHours, uint32_t, "Ah");
#undef LABEL_TRAIT

} /* namespace JbdBms */

using JbdBmsDataPoint = DataPoint<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, JbdBms::tCells>;

template class DataPointContainer<JbdBmsDataPoint, JbdBms::DataPointLabel, JbdBms::DataPointLabelTraits>;

namespace JbdBms {
using DataPointContainer = DataPointContainer<JbdBmsDataPoint, DataPointLabel, DataPointLabelTraits>;
} /* namespace JbdBms */
Loading

0 comments on commit 7cd1984

Please sign in to comment.