Skip to content

Commit b6d8e50

Browse files
committed
Implement receiving of CAN messages via MQTT
This is mainly intended for debugging.
1 parent 97f95f8 commit b6d8e50

9 files changed

+175
-0
lines changed

include/BatteryCanReceiver.h

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "Battery.h"
55
#include <driver/twai.h>
66
#include <Arduino.h>
7+
#include <espMqttClient.h>
78

89
class BatteryCanReceiver : public BatteryProvider {
910
public:
@@ -26,4 +27,14 @@ class BatteryCanReceiver : public BatteryProvider {
2627

2728
private:
2829
char const* _providerName = "Battery CAN";
30+
31+
enum CanInterface {
32+
kTwai,
33+
kMqtt,
34+
} _canInterface;
35+
String _canTopic;
36+
37+
void postMessage(twai_message_t&& rx_message);
38+
void onMqttMessageCAN(espMqttClientTypes::MessageProperties const& properties,
39+
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
2940
};

include/Configuration.h

+2
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,14 @@ struct BATTERY_CONFIG_T {
134134
bool Enabled;
135135
bool VerboseLogging;
136136
uint8_t Provider;
137+
uint8_t CanInterface;
137138
uint8_t JkBmsInterface;
138139
uint8_t JkBmsPollingInterval;
139140
char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
140141
char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
141142
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
142143
char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
144+
char MqttCANTopic[MQTT_MAX_TOPIC_STRLEN + 1];
143145
BatteryVoltageUnit MqttVoltageUnit;
144146
bool EnableDischargeCurrentLimit;
145147
float DischargeCurrentLimit;

include/defaults.h

+2
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@
154154
#define BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT false
155155
#define BATTERY_DISCHARGE_CURRENT_LIMIT 0
156156
#define BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT false
157+
#define BATTERY_CAN_INTERFACE 0
158+
#define BATTERY_CAN_TOPIC "debug/battery/can/message"
157159

158160
#define HUAWEI_ENABLED false
159161
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL

src/BatteryCanReceiver.cpp

+103
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
2+
#include "Configuration.h"
23
#include "BatteryCanReceiver.h"
4+
#include "MqttSettings.h"
35
#include "MessageOutput.h"
46
#include "PinMapping.h"
57
#include <driver/twai.h>
@@ -12,6 +14,24 @@ bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName)
1214
MessageOutput.printf("[%s] Initialize interface...\r\n",
1315
_providerName);
1416

17+
auto const& config = Configuration.get();
18+
_canTopic = config.Battery.MqttCANTopic;
19+
_canInterface = static_cast<enum CanInterface>(config.Battery.CanInterface);
20+
if (_canInterface == kMqtt) {
21+
MqttSettings.subscribe(_canTopic, 0/*QoS*/,
22+
std::bind(&BatteryCanReceiver::onMqttMessageCAN,
23+
this, std::placeholders::_1, std::placeholders::_2,
24+
std::placeholders::_3, std::placeholders::_4,
25+
std::placeholders::_5, std::placeholders::_6)
26+
);
27+
28+
if (_verboseLogging) {
29+
MessageOutput.printf("BatteryCanReceiver: Subscribed to '%s' for CAN messages\r\n",
30+
_canTopic.c_str());
31+
}
32+
return true;
33+
}
34+
1535
const PinMapping_t& pin = PinMapping.get();
1636
MessageOutput.printf("[%s] Interface rx = %d, tx = %d\r\n",
1737
_providerName, pin.battery_rx, pin.battery_tx);
@@ -82,6 +102,11 @@ bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName)
82102

83103
void BatteryCanReceiver::deinit()
84104
{
105+
if (_canInterface == kMqtt) {
106+
MqttSettings.unsubscribe(_canTopic);
107+
return;
108+
}
109+
85110
// Stop TWAI driver
86111
esp_err_t twaiLastResult = twai_stop();
87112
switch (twaiLastResult) {
@@ -111,6 +136,10 @@ void BatteryCanReceiver::deinit()
111136

112137
void BatteryCanReceiver::loop()
113138
{
139+
if (_canInterface == kMqtt) {
140+
return; // Mqtt CAN messages are event-driven
141+
}
142+
114143
// Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer
115144
twai_status_info_t status_info;
116145
esp_err_t twaiLastResult = twai_get_status_info(&status_info);
@@ -139,6 +168,80 @@ void BatteryCanReceiver::loop()
139168
return;
140169
}
141170

171+
postMessage(std::move(rx_message));
172+
}
173+
174+
175+
void BatteryCanReceiver::onMqttMessageCAN(espMqttClientTypes::MessageProperties const& properties,
176+
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
177+
{
178+
std::string value(reinterpret_cast<const char*>(payload), len);
179+
JsonDocument json;
180+
181+
auto log = [this, topic](char const* format, auto&&... args) -> void {
182+
MessageOutput.printf("[%s] Topic '%s': ", _providerName, topic);
183+
MessageOutput.printf(format, args...);
184+
MessageOutput.println();
185+
};
186+
187+
const DeserializationError error = deserializeJson(json, value);
188+
if (error) {
189+
log("cannot parse payload '%s' as JSON", value.c_str());
190+
return;
191+
}
192+
193+
if (json.overflowed()) {
194+
log("payload too large to process as JSON");
195+
return;
196+
}
197+
198+
int canID = json["id"] | -1;
199+
if (canID == -1) {
200+
log("JSON is missing message id");
201+
return;
202+
}
203+
204+
twai_message_t rx_message = {};
205+
rx_message.identifier = canID;
206+
int maxLen = sizeof(rx_message.data);
207+
208+
JsonVariant canData = json["data"];
209+
if (canData.isNull()) {
210+
log("JSON is missing message data");
211+
return;
212+
}
213+
214+
if (canData.is<char const*>()) {
215+
String strData = canData.as<String>();
216+
int len = strData.length();
217+
if (len > maxLen) {
218+
log("JSON data has more than %d elements", maxLen);
219+
return;
220+
}
221+
222+
rx_message.data_length_code = len;
223+
for (int i = 0; i < len; i++) {
224+
rx_message.data[i] = strData[i];
225+
}
226+
} else {
227+
JsonArray arrayData = canData.as<JsonArray>();
228+
int len = arrayData.size();
229+
if (len > maxLen) {
230+
log("JSON data has more than %d elements", maxLen);
231+
return;
232+
}
233+
234+
rx_message.data_length_code = len;
235+
for (int i = 0; i < len; i++) {
236+
rx_message.data[i] = arrayData[i];
237+
}
238+
}
239+
240+
postMessage(std::move(rx_message));
241+
}
242+
243+
void BatteryCanReceiver::postMessage(twai_message_t&& rx_message)
244+
{
142245
if (_verboseLogging) {
143246
MessageOutput.printf("[%s] Received CAN message: 0x%04X -",
144247
_providerName, rx_message.identifier);

src/Configuration.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, Jso
9292
target["mqtt_discharge_current_topic"] = config.Battery.MqttDischargeCurrentTopic;
9393
target["mqtt_discharge_current_json_path"] = config.Battery.MqttDischargeCurrentJsonPath;
9494
target["mqtt_amperage_unit"] = config.Battery.MqttAmperageUnit;
95+
target["can_interface"] = config.Battery.CanInterface;
96+
target["mqtt_can_topic"] = config.Battery.MqttCANTopic;
9597
}
9698

9799
bool ConfigurationClass::write()
@@ -375,6 +377,8 @@ void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, Batt
375377
strlcpy(target.MqttSocJsonPath, source["mqtt_soc_json_path"] | source["mqtt_json_path"] | "", sizeof(config.Battery.MqttSocJsonPath)); // mqtt_soc_json_path was previously saved as mqtt_json_path. Be nice and also try old key.
376378
strlcpy(target.MqttVoltageTopic, source["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
377379
strlcpy(target.MqttVoltageJsonPath, source["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath));
380+
target.CanInterface = source["can_interface"] | BATTERY_CAN_INTERFACE;
381+
strlcpy(target.MqttCANTopic, source["mqtt_can_topic"] | BATTERY_CAN_TOPIC, sizeof(config.Battery.MqttCANTopic));
378382
target.MqttVoltageUnit = source["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts;
379383
target.EnableDischargeCurrentLimit = source["enable_discharge_current_limit"] | BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT;
380384
target.DischargeCurrentLimit = source["discharge_current_limit"] | BATTERY_DISCHARGE_CURRENT_LIMIT;

webapp/src/locales/de.json

+6
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,12 @@
664664
"batteryadmin": {
665665
"BatterySettings": "Batterie Einstellungen",
666666
"BatteryConfiguration": "Generelle Schnittstelleneinstellungen",
667+
"CanConfiguration": "CAN Einstellungen",
668+
"CanInterface": "Schnittstellentyp",
669+
"CanInterfaceTwai": "CAN-Transceiver an der MCU",
670+
"CanInterfaceMqtt": "MQTT Broker",
671+
"CanMqttTopic": "Topic für CAN-Nachrichten",
672+
"CanMqttTopicHint": "Nachrichten sollte im JSON-Format mit 'id' und 'data' feldern sein.",
667673
"EnableBattery": "Aktiviere Schnittstelle",
668674
"VerboseLogging": "@:base.VerboseLogging",
669675
"Provider": "Datenanbieter",

webapp/src/locales/en.json

+6
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,12 @@
666666
"batteryadmin": {
667667
"BatterySettings": "Battery Settings",
668668
"BatteryConfiguration": "General Interface Settings",
669+
"CanConfiguration": "CAN Interface Settings",
670+
"CanInterface": "Interface Type",
671+
"CanInterfaceTwai": "CAN Transceiver on MCU",
672+
"CanInterfaceMqtt": "MQTT Topic",
673+
"CanMqttTopic": "CAN Message Topic",
674+
"CanMqttTopicHint": "Messages should be JSON with 'id' and 'data' fields.",
669675
"EnableBattery": "Enable Interface",
670676
"VerboseLogging": "@:base.VerboseLogging",
671677
"Provider": "Data Provider",

webapp/src/types/BatteryConfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export interface BatteryConfig {
44
provider: number;
55
jkbms_interface: number;
66
jkbms_polling_interval: number;
7+
can_interface: number;
8+
mqtt_can_topic: string;
79
mqtt_soc_topic: string;
810
mqtt_soc_json_path: string;
911
mqtt_voltage_topic: string;

webapp/src/views/BatteryAdminView.vue

+39
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,40 @@
3333
</div>
3434
</CardElement>
3535

36+
<CardElement
37+
v-show="batteryConfigList.enabled && providerUsesCanList.includes(batteryConfigList.provider)"
38+
:text="$t('batteryadmin.CanConfiguration')"
39+
textVariant="text-bg-primary"
40+
addSpace
41+
>
42+
<div class="row mb-3">
43+
<label class="col-sm-2 col-form-label">
44+
{{ $t('batteryadmin.CanInterface') }}
45+
</label>
46+
<div class="col-sm-10">
47+
<select class="form-select" v-model="batteryConfigList.can_interface">
48+
<option
49+
v-for="canInterface in canInterfaceTypeList"
50+
:key="canInterface.key"
51+
:value="canInterface.key"
52+
>
53+
{{ $t(`batteryadmin.CanInterface` + canInterface.value) }}
54+
</option>
55+
</select>
56+
</div>
57+
</div>
58+
59+
<InputElement
60+
v-show="batteryConfigList.can_interface == 1"
61+
:label="$t('batteryadmin.CanMqttTopic')"
62+
v-model="batteryConfigList.mqtt_can_topic"
63+
type="text"
64+
maxlength="256"
65+
>
66+
<div class="alert alert-secondary" role="alert" v-html="$t('batteryadmin.CanMqttTopicHint')"></div>
67+
</InputElement>
68+
</CardElement>
69+
3670
<CardElement
3771
v-show="batteryConfigList.enabled && batteryConfigList.provider == 1"
3872
:text="$t('batteryadmin.JkBmsConfiguration')"
@@ -235,6 +269,11 @@ export default defineComponent({
235269
{ key: 4, value: 'PytesCan' },
236270
{ key: 5, value: 'SBSCan' },
237271
],
272+
providerUsesCanList: [0, 4, 5],
273+
canInterfaceTypeList: [
274+
{ key: 0, value: 'Twai' },
275+
{ key: 1, value: 'Mqtt' },
276+
],
238277
jkBmsInterfaceTypeList: [
239278
{ key: 0, value: 'Uart' },
240279
{ key: 1, value: 'Transceiver' },

0 commit comments

Comments
 (0)