Feature: support for JBD BMS using serial connection
This commit is contained in:
parent
33b7697b37
commit
7cd1984d60
@ -6,6 +6,7 @@
|
|||||||
#include "AsyncJson.h"
|
#include "AsyncJson.h"
|
||||||
#include "Arduino.h"
|
#include "Arduino.h"
|
||||||
#include "JkBmsDataPoints.h"
|
#include "JkBmsDataPoints.h"
|
||||||
|
#include "JbdBmsDataPoints.h"
|
||||||
#include "VeDirectShuntController.h"
|
#include "VeDirectShuntController.h"
|
||||||
#include <cfloat>
|
#include <cfloat>
|
||||||
|
|
||||||
@ -283,6 +284,35 @@ class JkBmsBatteryStats : public BatteryStats {
|
|||||||
uint32_t _cellVoltageTimestamp = 0;
|
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 {
|
class VictronSmartShuntStats : public BatteryStats {
|
||||||
public:
|
public:
|
||||||
void getLiveViewData(JsonVariant& root) const final;
|
void getLiveViewData(JsonVariant& root) const final;
|
||||||
|
|||||||
119
include/DataPoints.h
Normal file
119
include/DataPoints.h
Normal 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;
|
||||||
|
};
|
||||||
87
include/JbdBmsController.h
Normal file
87
include/JbdBmsController.h
Normal 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
118
include/JbdBmsDataPoints.h
Normal 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 */
|
||||||
96
include/JbdBmsSerialMessage.h
Normal file
96
include/JbdBmsSerialMessage.h
Normal 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 */
|
||||||
@ -5,13 +5,12 @@
|
|||||||
#include <frozen/string.h>
|
#include <frozen/string.h>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
|
#include "JkBmsDataPoints.h"
|
||||||
#include "JkBmsSerialMessage.h"
|
#include "JkBmsSerialMessage.h"
|
||||||
#include "JkBmsDummy.h"
|
#include "JkBmsDummy.h"
|
||||||
|
|
||||||
//#define JKBMS_DUMMY_SERIAL
|
//#define JKBMS_DUMMY_SERIAL
|
||||||
|
|
||||||
class DataPointContainer;
|
|
||||||
|
|
||||||
namespace JkBms {
|
namespace JkBms {
|
||||||
|
|
||||||
class Controller : public BatteryProvider {
|
class Controller : public BatteryProvider {
|
||||||
|
|||||||
@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <variant>
|
|
||||||
#include <frozen/map.h>
|
#include <frozen/map.h>
|
||||||
#include <frozen/string.h>
|
#include <frozen/string.h>
|
||||||
|
|
||||||
|
#include "DataPoints.h"
|
||||||
|
|
||||||
namespace JkBms {
|
namespace JkBms {
|
||||||
|
|
||||||
#define ALARM_BITS(fnc) \
|
#define ALARM_BITS(fnc) \
|
||||||
@ -121,7 +119,7 @@ enum class DataPointLabel : uint8_t {
|
|||||||
ProtocolVersion = 0xc0
|
ProtocolVersion = 0xc0
|
||||||
};
|
};
|
||||||
|
|
||||||
using tCells = std::map<uint8_t, uint16_t>;
|
using tCells = tCellVoltages;
|
||||||
|
|
||||||
template<DataPointLabel> struct DataPointLabelTraits;
|
template<DataPointLabel> struct DataPointLabelTraits;
|
||||||
|
|
||||||
@ -206,99 +204,13 @@ LABEL_TRAIT(ProductId, std::string, "");
|
|||||||
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
|
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
|
||||||
#undef LABEL_TRAIT
|
#undef LABEL_TRAIT
|
||||||
|
|
||||||
class DataPoint {
|
} /* namespace JkBms */
|
||||||
friend class DataPointContainer;
|
|
||||||
|
using JkBmsDataPoint = DataPoint<bool, uint8_t, uint16_t, uint32_t,
|
||||||
public:
|
int16_t, int32_t, std::string, JkBms::tCells>;
|
||||||
using tValue = std::variant<bool, uint8_t, uint16_t, uint32_t,
|
|
||||||
int16_t, int32_t, std::string, tCells>;
|
template class DataPointContainer<JkBmsDataPoint, JkBms::DataPointLabel, JkBms::DataPointLabelTraits>;
|
||||||
|
|
||||||
DataPoint() = delete;
|
namespace JkBms {
|
||||||
|
using DataPointContainer = DataPointContainer<JkBmsDataPoint, DataPointLabel, DataPointLabelTraits>;
|
||||||
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 */
|
} /* namespace JkBms */
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "PylontechCanReceiver.h"
|
#include "PylontechCanReceiver.h"
|
||||||
#include "SBSCanReceiver.h"
|
#include "SBSCanReceiver.h"
|
||||||
#include "JkBmsController.h"
|
#include "JkBmsController.h"
|
||||||
|
#include "JbdBmsController.h"
|
||||||
#include "VictronSmartShunt.h"
|
#include "VictronSmartShunt.h"
|
||||||
#include "MqttBattery.h"
|
#include "MqttBattery.h"
|
||||||
#include "PytesCanReceiver.h"
|
#include "PytesCanReceiver.h"
|
||||||
@ -65,6 +66,9 @@ void BatteryClass::updateSettings()
|
|||||||
case 5:
|
case 5:
|
||||||
_upProvider = std::make_unique<SBSCanReceiver>();
|
_upProvider = std::make_unique<SBSCanReceiver>();
|
||||||
break;
|
break;
|
||||||
|
case 6:
|
||||||
|
_upProvider = std::make_unique<JbdBms::Controller>();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
|
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
#include "JkBmsDataPoints.h"
|
#include "JkBmsDataPoints.h"
|
||||||
|
#include "JbdBmsDataPoints.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
|
|
||||||
template<typename T>
|
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()
|
void BatteryStats::mqttLoop()
|
||||||
{
|
{
|
||||||
auto& config = Configuration.get();
|
auto& config = Configuration.get();
|
||||||
@ -571,6 +645,66 @@ void JkBmsBatteryStats::mqttPublish() const
|
|||||||
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
|
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)
|
void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
||||||
{
|
{
|
||||||
using Label = JkBms::DataPointLabel;
|
using Label = JkBms::DataPointLabel;
|
||||||
@ -648,6 +782,62 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
|||||||
_lastUpdate = millis();
|
_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) {
|
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
|
||||||
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
|
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
|
||||||
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());
|
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "JkBmsDataPoints.h"
|
#include "DataPoints.h"
|
||||||
|
|
||||||
namespace JkBms {
|
|
||||||
|
|
||||||
static char conversionBuffer[16];
|
static char conversionBuffer[16];
|
||||||
|
|
||||||
@ -30,7 +28,7 @@ std::string dataPointValueToStr(bool const& v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::string dataPointValueToStr(tCells const& v) {
|
std::string dataPointValueToStr(tCellVoltages const& v) {
|
||||||
std::string res;
|
std::string res;
|
||||||
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
|
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
|
||||||
res += "(";
|
res += "(";
|
||||||
@ -44,20 +42,3 @@ std::string dataPointValueToStr(tCells const& v) {
|
|||||||
res += ")";
|
res += ")";
|
||||||
return std::move(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
274
src/JbdBmsController.cpp
Normal 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
269
src/JbdBmsSerialMessage.cpp
Normal 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 */
|
||||||
@ -707,6 +707,7 @@
|
|||||||
"ProviderPylontechCan": "Pylontech per CAN-Bus",
|
"ProviderPylontechCan": "Pylontech per CAN-Bus",
|
||||||
"ProviderSBSCan": "SBS Unipower per CAN-Bus",
|
"ProviderSBSCan": "SBS Unipower per CAN-Bus",
|
||||||
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
|
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
|
||||||
|
"ProviderJbdBmsSerial": "Jiabaida (JBD) BMS per serieller Verbindung",
|
||||||
"ProviderMqtt": "Batteriewerte aus MQTT Broker",
|
"ProviderMqtt": "Batteriewerte aus MQTT Broker",
|
||||||
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
|
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
|
||||||
"ProviderPytesCan": "Pytes per CAN-Bus",
|
"ProviderPytesCan": "Pytes per CAN-Bus",
|
||||||
@ -721,6 +722,7 @@
|
|||||||
"JkBmsInterface": "Schnittstellentyp",
|
"JkBmsInterface": "Schnittstellentyp",
|
||||||
"JkBmsInterfaceUart": "TTL-UART an der MCU",
|
"JkBmsInterfaceUart": "TTL-UART an der MCU",
|
||||||
"JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
|
"JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
|
||||||
|
"JbdBmsConfiguration": "JBD BMS Einstellungen",
|
||||||
"PollingInterval": "Abfrageintervall",
|
"PollingInterval": "Abfrageintervall",
|
||||||
"Seconds": "@:base.Seconds",
|
"Seconds": "@:base.Seconds",
|
||||||
"DischargeCurrentLimitConfiguration": "Einstellungen Entladestromlimit",
|
"DischargeCurrentLimitConfiguration": "Einstellungen Entladestromlimit",
|
||||||
|
|||||||
@ -709,6 +709,7 @@
|
|||||||
"ProviderPylontechCan": "Pylontech using CAN bus",
|
"ProviderPylontechCan": "Pylontech using CAN bus",
|
||||||
"ProviderSBSCan": "SBS Unipower using CAN bus",
|
"ProviderSBSCan": "SBS Unipower using CAN bus",
|
||||||
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
||||||
|
"ProviderJbdBmsSerial": "Jiabaida (JBD) BMS using serial connection",
|
||||||
"ProviderMqtt": "Battery data from MQTT broker",
|
"ProviderMqtt": "Battery data from MQTT broker",
|
||||||
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
|
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
|
||||||
"ProviderPytesCan": "Pytes using CAN bus",
|
"ProviderPytesCan": "Pytes using CAN bus",
|
||||||
@ -724,6 +725,7 @@
|
|||||||
"JkBmsInterface": "Interface Type",
|
"JkBmsInterface": "Interface Type",
|
||||||
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
||||||
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
|
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
|
||||||
|
"JbdBmsConfiguration": "JBD BMS Settings",
|
||||||
"PollingInterval": "Polling Interval",
|
"PollingInterval": "Polling Interval",
|
||||||
"Seconds": "@:base.Seconds",
|
"Seconds": "@:base.Seconds",
|
||||||
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",
|
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",
|
||||||
|
|||||||
@ -612,6 +612,7 @@
|
|||||||
"ProviderPylontechCan": "Pylontech using CAN bus",
|
"ProviderPylontechCan": "Pylontech using CAN bus",
|
||||||
"ProviderSBSCan": "SBS Unipower using CAN bus",
|
"ProviderSBSCan": "SBS Unipower using CAN bus",
|
||||||
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
||||||
|
"ProviderJbdBmsSerial": "Jiabaida (JBD) BMS using serial connection",
|
||||||
"ProviderMqtt": "Battery data from MQTT broker",
|
"ProviderMqtt": "Battery data from MQTT broker",
|
||||||
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
|
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
|
||||||
"MqttSocConfiguration": "SoC Settings",
|
"MqttSocConfiguration": "SoC Settings",
|
||||||
@ -625,6 +626,7 @@
|
|||||||
"JkBmsInterface": "Interface Type",
|
"JkBmsInterface": "Interface Type",
|
||||||
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
||||||
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
|
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
|
||||||
|
"JbdBmsConfiguration": "JBD BMS Settings",
|
||||||
"PollingInterval": "Polling Interval",
|
"PollingInterval": "Polling Interval",
|
||||||
"Seconds": "@:base.Seconds",
|
"Seconds": "@:base.Seconds",
|
||||||
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",
|
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",
|
||||||
|
|||||||
@ -71,6 +71,41 @@
|
|||||||
/>
|
/>
|
||||||
</CardElement>
|
</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">
|
<template v-if="batteryConfigList.enabled && batteryConfigList.provider == 2">
|
||||||
<CardElement :text="$t('batteryadmin.MqttSocConfiguration')" textVariant="text-bg-primary" addSpace>
|
<CardElement :text="$t('batteryadmin.MqttSocConfiguration')" textVariant="text-bg-primary" addSpace>
|
||||||
<InputElement
|
<InputElement
|
||||||
@ -279,6 +314,7 @@ export default defineComponent({
|
|||||||
{ key: 3, value: 'Victron' },
|
{ key: 3, value: 'Victron' },
|
||||||
{ key: 4, value: 'PytesCan' },
|
{ key: 4, value: 'PytesCan' },
|
||||||
{ key: 5, value: 'SBSCan' },
|
{ key: 5, value: 'SBSCan' },
|
||||||
|
{ key: 6, value: 'JbdBmsSerial' },
|
||||||
],
|
],
|
||||||
jkBmsInterfaceTypeList: [
|
jkBmsInterfaceTypeList: [
|
||||||
{ key: 0, value: 'Uart' },
|
{ key: 0, value: 'Uart' },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user