Feature: support for JBD BMS using serial connection

This commit is contained in:
Manuel Bruehl 2024-08-31 07:41:13 +02:00 committed by Bernhard Kirchen
parent 33b7697b37
commit 7cd1984d60
16 changed files with 1244 additions and 123 deletions

View File

@ -6,6 +6,7 @@
#include "AsyncJson.h"
#include "Arduino.h"
#include "JkBmsDataPoints.h"
#include "JbdBmsDataPoints.h"
#include "VeDirectShuntController.h"
#include <cfloat>
@ -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;

119
include/DataPoints.h Normal file
View File

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

View File

@ -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
include/JbdBmsDataPoints.h Normal file
View File

@ -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 */

View File

@ -0,0 +1,96 @@
#pragma once
#include <utility>
#include <vector>
#include <Arduino.h>
#include "JbdBmsDataPoints.h"
namespace JbdBms {
// Only valid for receiving messages
class SerialMessage {
public:
using tData = std::vector<uint8_t>;
SerialMessage() = delete;
enum class Command : uint8_t {
Init = 0x00,
ReadBasicInformation = 0x03,
ReadCellVoltages = 0x04,
ReadHardwareVersionNumber = 0x05,
ControlMosInstruction = 0xE1,
};
uint8_t getStartMarker() const { return _raw[0]; }
virtual Command getCommand() const = 0;
uint8_t getDataLength() const { return _raw[3]; }
uint16_t getChecksum() const { return get<uint16_t>(_raw.cend()-3); }
uint8_t getEndMarker() const { return *(_raw.cend()-1); }
void printMessage();
bool isValid() const;
uint8_t const* data() { return _raw.data(); }
size_t size() { return _raw.size(); }
static constexpr uint8_t startMarker = 0xDD;
static constexpr uint8_t endMarker = 0x77;
protected:
template <typename... Args>
explicit SerialMessage(Args&&... args) : _raw(std::forward<Args>(args)...) { }
template<typename T, typename It> T get(It&& pos) const;
template<typename It> bool getBool(It&& pos) const;
template<typename It> int16_t getTemperature(It&& pos) const;
template<typename It> std::string getString(It&& pos, size_t len, bool replaceZeroes = false) const;
template<typename It> std::string getProductionDate(It&& pos) const;
template<typename T> void set(tData::iterator const& pos, T val);
uint16_t calcChecksum() const;
void updateChecksum();
tData _raw;
JbdBms::DataPointContainer _dp;
};
class SerialResponse : public SerialMessage {
public:
enum class Status : uint8_t {
Ok = 0x00,
Error = 0x80
};
using tData = SerialMessage::tData;
explicit SerialResponse(tData&& raw);
Command getCommand() const { return static_cast<Command>(_raw[1]); }
Status getStatus() const { return static_cast<Status>(_raw[2]); }
bool isValid() const;
DataPointContainer const& getDataPoints() const { return _dp; }
};
class SerialCommand : public SerialMessage {
public:
enum class Status : uint8_t {
Read = 0xA5,
Write = 0x5A,
};
explicit SerialCommand(Status status, Command cmd);
Status getStatus() const { return static_cast<Status>(_raw[1]); }
Command getCommand() const { return static_cast<Command>(_raw[2]); }
static Command getLastCommand() { return _lastCmd; }
bool isValid() const;
private:
static Command _lastCmd;
};
} /* namespace JbdBms */

View File

@ -5,13 +5,12 @@
#include <frozen/string.h>
#include "Battery.h"
#include "JkBmsDataPoints.h"
#include "JkBmsSerialMessage.h"
#include "JkBmsDummy.h"
//#define JKBMS_DUMMY_SERIAL
class DataPointContainer;
namespace JkBms {
class Controller : public BatteryProvider {

View File

@ -2,13 +2,11 @@
#include <Arduino.h>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <frozen/map.h>
#include <frozen/string.h>
#include "DataPoints.h"
namespace JkBms {
#define ALARM_BITS(fnc) \
@ -121,7 +119,7 @@ enum class DataPointLabel : uint8_t {
ProtocolVersion = 0xc0
};
using tCells = std::map<uint8_t, uint16_t>;
using tCells = tCellVoltages;
template<DataPointLabel> struct DataPointLabelTraits;
@ -206,99 +204,13 @@ LABEL_TRAIT(ProductId, std::string, "");
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
#undef LABEL_TRAIT
class DataPoint {
friend class DataPointContainer;
public:
using tValue = std::variant<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, tCells>;
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);
class DataPointContainer {
public:
DataPointContainer() = default;
using Label = DataPointLabel;
template<Label L> using Traits = JkBms::DataPointLabelTraits<L>;
template<Label L>
void add(typename Traits<L>::type val) {
_dataPoints.emplace(
L,
DataPoint(
Traits<L>::name,
dataPointValueToStr(val),
Traits<L>::unit,
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>;
tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
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);
private:
tMap _dataPoints;
};
} /* namespace JkBms */
using JkBmsDataPoint = DataPoint<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, JkBms::tCells>;
template class DataPointContainer<JkBmsDataPoint, JkBms::DataPointLabel, JkBms::DataPointLabelTraits>;
namespace JkBms {
using DataPointContainer = DataPointContainer<JkBmsDataPoint, DataPointLabel, DataPointLabelTraits>;
} /* namespace JkBms */

View File

@ -4,6 +4,7 @@
#include "PylontechCanReceiver.h"
#include "SBSCanReceiver.h"
#include "JkBmsController.h"
#include "JbdBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
#include "PytesCanReceiver.h"
@ -65,6 +66,9 @@ void BatteryClass::updateSettings()
case 5:
_upProvider = std::make_unique<SBSCanReceiver>();
break;
case 6:
_upProvider = std::make_unique<JbdBms::Controller>();
break;
default:
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
return;

View File

@ -5,6 +5,7 @@
#include "Configuration.h"
#include "MqttSettings.h"
#include "JkBmsDataPoints.h"
#include "JbdBmsDataPoints.h"
#include "MqttSettings.h"
template<typename T>
@ -336,6 +337,79 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
}
}
void JbdBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
{
BatteryStats::getLiveViewData(root);
using Label = JbdBms::DataPointLabel;
auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value() && oCurrent.has_value()) {
auto current = static_cast<float>(*oCurrent) / 1000;
auto voltage = static_cast<float>(*oVoltage) / 1000;
addLiveViewValue(root, "power", current * voltage , "W", 2);
}
auto oBatteryChargeEnabled = _dataPoints.get<Label::BatteryChargeEnabled>();
if (oBatteryChargeEnabled.has_value()) {
addLiveViewTextValue(root, "chargeEnabled", (*oBatteryChargeEnabled?"yes":"no"));
}
auto oBatteryDischargeEnabled = _dataPoints.get<Label::BatteryDischargeEnabled>();
if (oBatteryDischargeEnabled.has_value()) {
addLiveViewTextValue(root, "dischargeEnabled", (*oBatteryDischargeEnabled?"yes":"no"));
}
auto oTemperatureOne = _dataPoints.get<Label::BatteryTempOneCelsius>();
if (oTemperatureOne.has_value()) {
addLiveViewInSection(root, "cells", "batOneTemp", *oTemperatureOne, "°C", 0);
}
auto oTemperatureTwo = _dataPoints.get<Label::BatteryTempTwoCelsius>();
if (oTemperatureTwo.has_value()) {
addLiveViewInSection(root, "cells", "batTwoTemp", *oTemperatureTwo, "°C", 0);
}
if (_cellVoltageTimestamp > 0) {
addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellAvgVoltage", static_cast<float>(_cellAvgMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0);
}
auto oBalancingEnabled = _dataPoints.get<Label::BalancingEnabled>();
if (oBalancingEnabled.has_value()) {
addLiveViewTextInSection(root, "cells", "balancingActive", (*oBalancingEnabled?"yes":"no"));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
#define ISSUE(t, x) \
auto x = *oAlarms & static_cast<uint16_t>(JbdBms::AlarmBits::x); \
addLiveView##t(root, "JbdBmsIssue"#x, x > 0);
//ISSUE(Warning, LowCapacity);
ISSUE(Alarm, CellOverVoltage);
ISSUE(Alarm, CellUnderVoltage);
ISSUE(Alarm, PackOverVoltage);
ISSUE(Alarm, PackUnderVoltage);
ISSUE(Alarm, ChargingOverTemperature);
ISSUE(Alarm, ChargingLowTemperature);
ISSUE(Alarm, DischargingOverTemperature);
ISSUE(Alarm, DischargingLowTemperature);
ISSUE(Alarm, ChargingOverCurrent);
ISSUE(Alarm, DischargeOverCurrent);
ISSUE(Alarm, ShortCircuit);
ISSUE(Alarm, IcFrontEndError);
ISSUE(Alarm, MosSotwareLock);
ISSUE(Alarm, Reserved1);
ISSUE(Alarm, Reserved2);
ISSUE(Alarm, Reserved3);
#undef ISSUE
}
}
void BatteryStats::mqttLoop()
{
auto& config = Configuration.get();
@ -571,6 +645,66 @@ void JkBmsBatteryStats::mqttPublish() const
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
}
void JbdBmsBatteryStats::mqttPublish() const
{
BatteryStats::mqttPublish();
using Label = JbdBms::DataPointLabel;
static std::vector<Label> mqttSkip = {
Label::CellsMilliVolt, // complex data format
Label::BatteryVoltageMilliVolt, // already published by base class
Label::BatterySoCPercent // already published by base class
};
// regularly publish all topics regardless of whether or not their value changed
bool neverFullyPublished = _lastFullMqttPublish == 0;
bool intervalElapsed = _lastFullMqttPublish + getMqttFullPublishIntervalMs() < millis();
bool fullPublish = neverFullyPublished || intervalElapsed;
for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) {
// skip data points that did not change since last published
if (!fullPublish && iter->second.getTimestamp() < _lastMqttPublish) { continue; }
auto skipMatch = std::find(mqttSkip.begin(), mqttSkip.end(), iter->first);
if (skipMatch != mqttSkip.end()) { continue; }
String topic((std::string("battery/") + iter->second.getLabelText()).c_str());
MqttSettings.publish(topic, iter->second.getValueText().c_str());
}
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value() && (fullPublish || _cellVoltageTimestamp > _lastMqttPublish)) {
unsigned idx = 1;
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
String topic("battery/Cell");
topic += String(idx);
topic += "MilliVolt";
MqttSettings.publish(topic, String(iter->second));
++idx;
}
MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt));
MqttSettings.publish("battery/CellAvgMilliVolt", String(_cellAvgMilliVolt));
MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt));
MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
for (auto iter = JbdBms::AlarmBitTexts.begin(); iter != JbdBms::AlarmBitTexts.end(); ++iter) {
auto bit = iter->first;
String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/alarms/") + iter->second.data(), value);
}
}
_lastMqttPublish = millis();
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
}
void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
{
using Label = JkBms::DataPointLabel;
@ -648,6 +782,62 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
_lastUpdate = millis();
}
void JbdBmsBatteryStats::updateFrom(JbdBms::DataPointContainer const& dp)
{
using Label = JbdBms::DataPointLabel;
setManufacturer("JBDBMS");
auto oSoCValue = dp.get<Label::BatterySoCPercent>();
if (oSoCValue.has_value()) {
auto oSoCDataPoint = dp.getDataPointFor<Label::BatterySoCPercent>();
BatteryStats::setSoC(*oSoCValue, 0/*precision*/,
oSoCDataPoint->getTimestamp());
}
auto oVoltage = dp.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value()) {
auto oVoltageDataPoint = dp.getDataPointFor<Label::BatteryVoltageMilliVolt>();
BatteryStats::setVoltage(static_cast<float>(*oVoltage) / 1000,
oVoltageDataPoint->getTimestamp());
}
auto oCurrent = dp.get<Label::BatteryCurrentMilliAmps>();
if (oCurrent.has_value()) {
auto oCurrentDataPoint = dp.getDataPointFor<Label::BatteryCurrentMilliAmps>();
BatteryStats::setCurrent(static_cast<float>(*oCurrent) / 1000, 2/*precision*/,
oCurrentDataPoint->getTimestamp());
}
_dataPoints.updateFrom(dp);
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value()) {
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
if (iter == oCellVoltages->cbegin()) {
_cellMinMilliVolt = _cellAvgMilliVolt = _cellMaxMilliVolt = iter->second;
continue;
}
_cellMinMilliVolt = std::min(_cellMinMilliVolt, iter->second);
_cellAvgMilliVolt = (_cellAvgMilliVolt + iter->second) / 2;
_cellMaxMilliVolt = std::max(_cellMaxMilliVolt, iter->second);
}
_cellVoltageTimestamp = millis();
}
auto oSoftwareVersion = _dataPoints.get<Label::BmsSoftwareVersion>();
if (oSoftwareVersion.has_value()) {
_fwversion = oSoftwareVersion->c_str();
}
auto oHardwareVersion = _dataPoints.get<Label::BmsHardwareVersion>();
if (oHardwareVersion.has_value()) {
_hwversion = oHardwareVersion->c_str();
}
_lastUpdate = millis();
}
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());

View File

@ -1,8 +1,6 @@
#include <stdio.h>
#include "JkBmsDataPoints.h"
namespace JkBms {
#include "DataPoints.h"
static char conversionBuffer[16];
@ -30,7 +28,7 @@ std::string dataPointValueToStr(bool const& v) {
}
template<>
std::string dataPointValueToStr(tCells const& v) {
std::string dataPointValueToStr(tCellVoltages const& v) {
std::string res;
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
res += "(";
@ -44,20 +42,3 @@ std::string dataPointValueToStr(tCells const& v) {
res += ")";
return std::move(res);
}
void DataPointContainer::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);
}
}
} /* namespace JkBms */

274
src/JbdBmsController.cpp Normal file
View File

@ -0,0 +1,274 @@
#include <Arduino.h>
#include <numeric>
#include "Configuration.h"
#include "HardwareSerial.h"
#include "PinMapping.h"
#include "MessageOutput.h"
#include "JbdBmsDataPoints.h"
#include "JbdBmsController.h"
#include "JbdBmsSerialMessage.h"
#include "SerialPortManager.h"
#include <frozen/map.h>
namespace JbdBms {
bool Controller::init(bool verboseLogging)
{
_verboseLogging = verboseLogging;
std::string ifcType = "transceiver";
if (Interface::Transceiver != getInterface()) { ifcType = "TTL-UART"; }
MessageOutput.printf("[JBD BMS] Initialize %s interface...\r\n", ifcType.c_str());
const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[JBD BMS] rx = %d, rxen = %d, tx = %d, txen = %d\r\n",
pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen);
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
MessageOutput.println("[JBD BMS] Invalid RX/TX pin config");
return false;
}
#ifdef JBDBMS_DUMMY_SERIAL
_upSerial = std::make_unique<DummySerial>();
#else
auto oHwSerialPort = SerialPortManager.allocatePort(_serialPortOwner);
if (!oHwSerialPort) { return false; }
_upSerial = std::make_unique<HardwareSerial>(*oHwSerialPort);
#endif
_upSerial->end(); // make sure the UART will be re-initialized
_upSerial->begin(9600, SERIAL_8N1, pin.battery_rx, pin.battery_tx);
_upSerial->flush();
if (Interface::Transceiver != getInterface()) { return true; }
_rxEnablePin = pin.battery_rxen;
_txEnablePin = pin.battery_txen;
if (_rxEnablePin < 0 || _txEnablePin < 0) {
MessageOutput.println("[JBD BMS] Invalid transceiver pin config");
return false;
}
pinMode(_rxEnablePin, OUTPUT);
pinMode(_txEnablePin, OUTPUT);
return true;
}
void Controller::deinit()
{
_upSerial->end();
if (_rxEnablePin > 0) { pinMode(_rxEnablePin, INPUT); }
if (_txEnablePin > 0) { pinMode(_txEnablePin, INPUT); }
SerialPortManager.freePort(_serialPortOwner);
}
Controller::Interface Controller::getInterface() const
{
CONFIG_T& config = Configuration.get();
if (0x00 == config.Battery.JkBmsInterface) { return Interface::Uart; }
if (0x01 == config.Battery.JkBmsInterface) { return Interface::Transceiver; }
return Interface::Invalid;
}
frozen::string const& Controller::getStatusText(Controller::Status status)
{
static constexpr frozen::string missing = "programmer error: missing status text";
static constexpr frozen::map<Status, frozen::string, 6> texts = {
{ Status::Timeout, "timeout wating for response from BMS" },
{ Status::WaitingForPollInterval, "waiting for poll interval to elapse" },
{ Status::HwSerialNotAvailableForWrite, "UART is not available for writing" },
{ Status::BusyReading, "busy waiting for or reading a message from the BMS" },
{ Status::RequestSent, "request for data sent" },
{ Status::FrameCompleted, "a whole frame was received" }
};
auto iter = texts.find(status);
if (iter == texts.end()) { return missing; }
return iter->second;
}
void Controller::announceStatus(Controller::Status status)
{
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
MessageOutput.printf("[%11.3f] JBD BMS: %s\r\n",
static_cast<double>(millis())/1000, getStatusText(status).data());
_lastStatus = status;
_lastStatusPrinted = millis();
}
void Controller::sendRequest(uint8_t pollInterval)
{
if (ReadState::Idle != _readState) {
return announceStatus(Status::BusyReading);
}
if ((millis() - _lastRequest) < pollInterval * 1000) {
return announceStatus(Status::WaitingForPollInterval);
}
if (!_upSerial->availableForWrite()) {
return announceStatus(Status::HwSerialNotAvailableForWrite);
}
SerialCommand::Command cmd;
switch (SerialCommand::getLastCommand()) {
case SerialCommand::Command::Init: // read only once
cmd = SerialCommand::Command::ReadHardwareVersionNumber;
break;
case SerialCommand::Command::ReadBasicInformation:
cmd = SerialCommand::Command::ReadCellVoltages;
break;
case SerialCommand::Command::ReadCellVoltages:
cmd = SerialCommand::Command::ReadBasicInformation;
break;
default:
cmd = SerialCommand::Command::ReadBasicInformation;
break;
}
SerialCommand readCmd(SerialCommand::Status::Read, cmd);
if (Interface::Transceiver == getInterface()) {
digitalWrite(_rxEnablePin, HIGH); // disable reception (of our own data)
digitalWrite(_txEnablePin, HIGH); // enable transmission
}
_upSerial->write(readCmd.data(), readCmd.size());
if (Interface::Transceiver == getInterface()) {
_upSerial->flush();
digitalWrite(_rxEnablePin, LOW); // enable reception
digitalWrite(_txEnablePin, LOW); // disable transmission (free the bus)
}
_lastRequest = millis();
setReadState(ReadState::WaitingForFrameStart);
return announceStatus(Status::RequestSent);
}
void Controller::loop()
{
CONFIG_T& config = Configuration.get();
uint8_t pollInterval = config.Battery.JkBmsPollingInterval;
while (_upSerial->available()) {
rxData(_upSerial->read());
}
sendRequest(pollInterval);
if (millis() > _lastRequest + 2 * pollInterval * 1000 + 250) {
reset();
return announceStatus(Status::Timeout);
}
}
void Controller::rxData(uint8_t inbyte)
{
_buffer.push_back(inbyte);
switch(_readState) {
case ReadState::Idle: // unsolicited message from BMS
case ReadState::WaitingForFrameStart:
if (inbyte == SerialMessage::startMarker) {
return setReadState(ReadState::FrameStartReceived);
}
break;
case ReadState::FrameStartReceived:
return setReadState(ReadState::StateReceived);
break;
case ReadState::StateReceived:
return setReadState(ReadState::CommandCodeReceived);
break;
case ReadState::CommandCodeReceived:
_dataLength = inbyte;
if (_dataLength == 0) {
return setReadState(ReadState::DataContentReceived);
}
return setReadState(ReadState::ReadingDataContent);
break;
case ReadState::ReadingDataContent:
_dataLength--;
if (_dataLength == 0) {
return setReadState(ReadState::DataContentReceived);
}
return setReadState(ReadState::ReadingDataContent);
break;
case ReadState::DataContentReceived:
return setReadState(ReadState::ReadingCheckSum);
break;
case ReadState::ReadingCheckSum:
return setReadState(ReadState::CheckSumReceived);
break;
case ReadState::CheckSumReceived:
if (inbyte == SerialMessage::endMarker) {
return frameComplete();
}
MessageOutput.printf("[JBD BMS] Invalid Frame: end marker not found.");
MessageOutput.println();
return reset();
break;
}
reset();
}
void Controller::reset()
{
_buffer.clear();
return setReadState(ReadState::Idle);
}
void Controller::frameComplete()
{
announceStatus(Status::FrameCompleted);
if (_verboseLogging) {
double ts = static_cast<double>(millis())/1000;
MessageOutput.printf("[%11.3f] JBD BMS: raw data (%d Bytes):",
ts, _buffer.size());
for (size_t ctr = 0; ctr < _buffer.size(); ++ctr) {
if (ctr % 16 == 0) {
MessageOutput.printf("\r\n[%11.3f] JBD BMS:", ts);
}
MessageOutput.printf(" %02x", _buffer[ctr]);
}
MessageOutput.println();
}
auto pResponse = std::make_unique<SerialResponse>(std::move(_buffer));
if (pResponse->isValid()) {
processDataPoints(pResponse->getDataPoints());
} // if invalid, error message has been produced by SerialResponse c'tor
reset();
}
void Controller::processDataPoints(DataPointContainer const& dataPoints)
{
_stats->updateFrom(dataPoints);
if (!_verboseLogging) { return; }
auto iter = dataPoints.cbegin();
while ( iter != dataPoints.cend() ) {
MessageOutput.printf("[%11.3f] JBD BMS: %s: %s%s\r\n",
static_cast<double>(iter->second.getTimestamp())/1000,
iter->second.getLabelText().c_str(),
iter->second.getValueText().c_str(),
iter->second.getUnitText().c_str());
++iter;
}
}
} /* namespace JbdBms */

269
src/JbdBmsSerialMessage.cpp Normal file
View File

@ -0,0 +1,269 @@
#include <numeric>
#include <sstream>
#include <iomanip>
#include "JbdBmsSerialMessage.h"
#include "MessageOutput.h"
namespace JbdBms {
SerialCommand::SerialCommand(SerialCommand::Status status, SerialCommand::Command cmd)
: SerialMessage(7, 0x00) // frame length 7 bytes initialized with zeros
{
set(_raw.begin(), startMarker);
set(_raw.begin() + 1, static_cast<uint8_t>(status));
set(_raw.begin() + 2, static_cast<uint8_t>(cmd));
set(_raw.begin() + 3, static_cast<uint16_t>(0x00)); // frame length
updateChecksum();
set(_raw.end() - 1, endMarker);
_lastCmd = cmd;
}
SerialCommand::Command SerialCommand::_lastCmd = SerialCommand::Command::Init;
using Label = JbdBms::DataPointLabel;
template<Label L> using Traits = DataPointLabelTraits<L>;
SerialResponse::SerialResponse(tData&& raw)
: SerialMessage(std::move(raw))
{
if (!isValid()) { return; }
auto pos = _raw.cbegin() + 4; // start of data content
auto end = pos + getDataLength(); // end of data content
if (pos < end) {
if (getCommand() == Command::ReadBasicInformation) {
_dp.add<Label::BatteryVoltageMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10); // Total voltage
_dp.add<Label::BatteryCurrentMilliAmps>(static_cast<int32_t>(get<int16_t>(pos)) * 10); // Current
_dp.add<Label::ActualBatteryCapacityAmpHours>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10 / 1000); // remaining capacity
_dp.add<Label::BatteryCapacitySettingAmpHours>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10 / 1000); // nominal capacity
_dp.add<Label::BatteryCycles>(get<uint16_t>(pos));
_dp.add<Label::DateOfManufacturing >(getProductionDate(pos));
bool balancingEnabled = false;
balancingEnabled |= static_cast<bool>(get<uint16_t>(pos)); // Equilibrium
balancingEnabled |= static_cast<bool>(get<uint16_t>(pos)); // Equilibrium_High
_dp.add<Label::BalancingEnabled>(balancingEnabled);
_dp.add<Label::AlarmsBitmask>(get<uint16_t>(pos)); // Protection status
uint8_t softwareVersion = get<uint8_t>(pos);
uint8_t digitOne = softwareVersion & 0x0F;
uint8_t digitTwo = softwareVersion >> 4;
char buffer[6];
snprintf(buffer, sizeof(buffer), "%d.%d", digitOne, digitTwo);
_dp.add<Label::BmsSoftwareVersion>(std::string(buffer)); // Software version
_dp.add<Label::BatterySoCPercent>(get<uint8_t>(pos)); // RSOC
uint8_t fetControl = get<uint8_t>(pos); // FET control status
const uint8_t chargingMask = (1 << 0);
const uint8_t dischargingMask = (1 << 1);
bool fetChargeEnabled = static_cast<bool>(fetControl & chargingMask);
bool fetDischargeEnabled = static_cast<bool>(fetControl & dischargingMask);
_dp.add<Label::BatteryChargeEnabled>(fetChargeEnabled);
_dp.add<Label::BatteryDischargeEnabled>(fetDischargeEnabled);
_dp.add<Label::BatteryCellAmount>(static_cast<uint16_t>(get<uint8_t>(pos))); // number of battery strings
_dp.add<Label::BatteryTemperatureSensorAmount>(get<uint8_t>(pos)); // number of ntc
_dp.add<Label::BatteryTempOneCelsius>(getTemperature(pos)); // ntc temperature one
_dp.add<Label::BatteryTempTwoCelsius>(getTemperature(pos)); // ntc temperature two
}
else if (getCommand() == Command::ReadCellVoltages)
{
uint8_t cellAmount = getDataLength() / 2;
std::map<uint8_t, uint16_t> voltages;
for (size_t cellCounter = 0; cellCounter < cellAmount; ++cellCounter) {
uint8_t idx = cellCounter;
auto cellMilliVolt = get<uint16_t>(pos);
voltages[idx] = cellMilliVolt;
}
_dp.add<Label::CellsMilliVolt>(voltages);
}
else if (getCommand() == Command::ReadHardwareVersionNumber)
{
_dp.add<Label::BmsHardwareVersion>(getString(pos, getDataLength()));
}
else if (getCommand() == Command::ControlMosInstruction)
{
/* Response doesn't contain any data content */
}
}
}
/**
* NOTE that this function moves the iterator by the amount of bytes read.
*/
template<typename T, typename It>
T SerialMessage::get(It&& pos) const
{
// add easy-to-understand error message when called with non-const iter,
// as compiler generated error message is hard to understand.
using ItNoRef = typename std::remove_reference<It>::type;
using PtrType = typename std::iterator_traits<ItNoRef>::pointer;
using ValueType = typename std::remove_pointer<PtrType>::type;
static_assert(std::is_const<ValueType>::value, "get() must be called with a const_iterator");
// avoid out-of-bound read
if (std::distance(pos, _raw.cend()) < sizeof(T)) { return 0; }
T res = 0;
for (unsigned i = 0; i < sizeof(T); ++i) {
res |= static_cast<T>(*(pos++)) << (sizeof(T)-1-i)*8;
}
return res;
}
template<typename It>
bool SerialMessage::getBool(It&& pos) const
{
uint8_t raw = get<uint8_t>(pos);
return raw > 0;
}
template<typename It>
int16_t SerialMessage::getTemperature(It&& pos) const
{
// raw in 0.1K
uint16_t raw = get<uint16_t>(pos);
return static_cast<int16_t>(raw - 2731) / 10;
}
template<typename It>
std::string SerialMessage::getProductionDate(It&& pos) const
{
// E.g. 0x2068 = 08.03.2016
// the date is the lowest 5: 0x2028 & 0x1f = 8 means the date;
// month (0x2068>>5) & 0x0f = 0x03 means March;
// the year is 2000+ (0x2068>>9) = 2000 + 0x10 =2016;
uint16_t raw = get<uint16_t>(pos);
uint16_t day = raw & 0x1f;
uint16_t month = (raw>>5) & 0x0f;
uint16_t year = 2000 + (raw>>9);
std::ostringstream oss;
oss << std::setw(2) << std::setfill('0') << day << "." << std::setw(2) << std::setfill('0') << month << "." << std::setw(4) << std::setfill('0') << year;
return oss.str();
}
template<typename It>
std::string SerialMessage::getString(It&& pos, size_t len, bool replaceZeroes) const
{
// avoid out-of-bound read
len = std::min<size_t>(std::distance(pos, _raw.cend()), len);
auto start = pos;
pos += len;
if (replaceZeroes) {
std::vector<uint8_t> copy(start, pos);
for (auto& c : copy) {
if (c == 0) { c = 0x20; } // replace by ASCII space
}
return std::string(copy.cbegin(), copy.cend());
}
return std::string(start, pos);
}
template<typename T>
void SerialMessage::set(tData::iterator const& pos, T val)
{
// avoid out-of-bound write
if (std::distance(pos, _raw.end()) < sizeof(T)) { return; }
for (unsigned i = 0; i < sizeof(T); ++i) {
*(pos+i) = static_cast<uint8_t>(val >> (sizeof(T)-1-i)*8);
}
}
uint16_t SerialMessage::calcChecksum() const
{
return (~std::accumulate(_raw.cbegin()+2, _raw.cend()-3, 0) + 0x01);
}
void SerialMessage::updateChecksum()
{
set(_raw.end()-3, calcChecksum());
}
void SerialMessage::printMessage() {
double ts = static_cast<double>(millis())/1000;
MessageOutput.printf("[%11.3f] JBD BMS: raw message (%d Bytes):",
ts, _raw.size());
for (size_t ctr = 0; ctr < _raw.size(); ++ctr) {
MessageOutput.printf(" %02x", _raw[ctr]);
}
MessageOutput.println();
}
bool SerialMessage::isValid() const {
uint8_t const actualStartMarker = getStartMarker();
if (actualStartMarker != startMarker) {
MessageOutput.printf("JbdBms::SerialMessage: invalid start marker 0x%02x, expected 0x%02x\r\n",
actualStartMarker, startMarker);
return false;
}
uint16_t const dataLength = getDataLength();
uint16_t const dataLengthExpected = _raw.size() - 7;
if (dataLength != _raw.size() - 7) {
MessageOutput.printf("JbdBms::SerialMessage: unexpected data length 0x%04x, expected 0x%04x\r\n",
dataLength, dataLengthExpected);
return false;
}
uint8_t const actualEndMarker = getEndMarker();
if (actualEndMarker != endMarker) {
MessageOutput.printf("JbdBms::SerialMessage: invalid end marker 0x%02x, expected 0x%02x\r\n",
actualEndMarker, endMarker);
return false;
}
uint16_t const actualChecksum = getChecksum();
uint16_t const expectedChecksum = calcChecksum();
if (actualChecksum != expectedChecksum) {
MessageOutput.printf("JbdBms::SerialMessage: invalid checksum 0x%04x, expected 0x%04x\r\n",
actualChecksum, expectedChecksum);
return false;
}
return true;
}
bool SerialResponse::isValid() const {
if(!SerialMessage::isValid()) {return false;}
Status const actualStatus = getStatus();
if (actualStatus != Status::Ok) {
MessageOutput.printf("JbdBms::SerialMessage: invalid status 0x%02x, expected 0x%02x\r\n",
(uint32_t) actualStatus, (uint32_t) Status::Ok);
return false;
}
return true;
}
bool SerialCommand::isValid() const {
if(!SerialMessage::isValid()) {return false;}
Status const actualStatus = getStatus();
if (actualStatus != Status::Read || actualStatus != Status::Write) {
MessageOutput.printf("JbdBms::SerialMessage: invalid status 0x%02x, expected 0x%02x or 0x%02x\r\n",
(uint32_t) actualStatus, (uint32_t) Status::Read, (uint32_t) Status::Write);
return false;
}
return true;
}
} /* namespace JbdBms */

View File

@ -707,6 +707,7 @@
"ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderSBSCan": "SBS Unipower per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderJbdBmsSerial": "Jiabaida (JBD) BMS per serieller Verbindung",
"ProviderMqtt": "Batteriewerte aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"ProviderPytesCan": "Pytes per CAN-Bus",
@ -721,6 +722,7 @@
"JkBmsInterface": "Schnittstellentyp",
"JkBmsInterfaceUart": "TTL-UART an der MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
"JbdBmsConfiguration": "JBD BMS Einstellungen",
"PollingInterval": "Abfrageintervall",
"Seconds": "@:base.Seconds",
"DischargeCurrentLimitConfiguration": "Einstellungen Entladestromlimit",

View File

@ -709,6 +709,7 @@
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderSBSCan": "SBS Unipower using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderJbdBmsSerial": "Jiabaida (JBD) BMS using serial connection",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"ProviderPytesCan": "Pytes using CAN bus",
@ -724,6 +725,7 @@
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
"JbdBmsConfiguration": "JBD BMS Settings",
"PollingInterval": "Polling Interval",
"Seconds": "@:base.Seconds",
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",

View File

@ -612,6 +612,7 @@
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderSBSCan": "SBS Unipower using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderJbdBmsSerial": "Jiabaida (JBD) BMS using serial connection",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttSocConfiguration": "SoC Settings",
@ -625,6 +626,7 @@
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
"JbdBmsConfiguration": "JBD BMS Settings",
"PollingInterval": "Polling Interval",
"Seconds": "@:base.Seconds",
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",

View File

@ -71,6 +71,41 @@
/>
</CardElement>
<CardElement
v-if="batteryConfigList.enabled && batteryConfigList.provider == 6"
:text="$t('batteryadmin.JbdBmsConfiguration')"
textVariant="text-bg-primary"
addSpace
>
<div class="row mb-3">
<label class="col-sm-4 col-form-label">
{{ $t('batteryadmin.JkBmsInterface') }}
</label>
<div class="col-sm-8">
<select class="form-select" v-model="batteryConfigList.jkbms_interface">
<option
v-for="jkBmsInterface in jkBmsInterfaceTypeList"
:key="jkBmsInterface.key"
:value="jkBmsInterface.key"
>
{{ $t(`batteryadmin.JkBmsInterface` + jkBmsInterface.value) }}
</option>
</select>
</div>
</div>
<InputElement
:label="$t('batteryadmin.PollingInterval')"
v-model="batteryConfigList.jkbms_polling_interval"
type="number"
min="2"
max="90"
step="1"
:postfix="$t('batteryadmin.Seconds')"
wide
/>
</CardElement>
<template v-if="batteryConfigList.enabled && batteryConfigList.provider == 2">
<CardElement :text="$t('batteryadmin.MqttSocConfiguration')" textVariant="text-bg-primary" addSpace>
<InputElement
@ -279,6 +314,7 @@ export default defineComponent({
{ key: 3, value: 'Victron' },
{ key: 4, value: 'PytesCan' },
{ key: 5, value: 'SBSCan' },
{ key: 6, value: 'JbdBmsSerial' },
],
jkBmsInterfaceTypeList: [
{ key: 0, value: 'Uart' },