Skip to content

Commit

Permalink
Implement receiving of CAN messages via MQTT
Browse files Browse the repository at this point in the history
This is mainly intended for debugging.
  • Loading branch information
ranma committed Sep 28, 2024
1 parent aa159fd commit 4868df6
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 0 deletions.
11 changes: 11 additions & 0 deletions include/BatteryCanReceiver.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "Battery.h"
#include <driver/twai.h>
#include <Arduino.h>
#include <espMqttClient.h>

class BatteryCanReceiver : public BatteryProvider {
public:
Expand All @@ -26,4 +27,14 @@ class BatteryCanReceiver : public BatteryProvider {

private:
char const* _providerName = "Battery CAN";

enum CanInterface {
kTwai,
kMqtt,
} _canInterface;
String _canTopic;

void postMessage(twai_message_t&& rx_message);
void onMqttMessageCAN(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
};
2 changes: 2 additions & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,14 @@ struct BATTERY_CONFIG_T {
bool Enabled;
bool VerboseLogging;
uint8_t Provider;
uint8_t CanInterface;
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
char MqttCANTopic[MQTT_MAX_TOPIC_STRLEN + 1];
BatteryVoltageUnit MqttVoltageUnit;
bool EnableDischargeCurrentLimit;
float DischargeCurrentLimit;
Expand Down
2 changes: 2 additions & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@
#define BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT false
#define BATTERY_DISCHARGE_CURRENT_LIMIT 0
#define BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT false
#define BATTERY_CAN_INTERFACE 0
#define BATTERY_CAN_TOPIC "debug/battery/can/message"

#define HUAWEI_ENABLED false
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
Expand Down
103 changes: 103 additions & 0 deletions src/BatteryCanReceiver.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h"
#include "BatteryCanReceiver.h"
#include "MqttSettings.h"
#include "MessageOutput.h"
#include "PinMapping.h"
#include <driver/twai.h>
Expand All @@ -12,6 +14,24 @@ bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName)
MessageOutput.printf("[%s] Initialize interface...\r\n",
_providerName);

auto const& config = Configuration.get();
_canTopic = config.Battery.MqttCANTopic;
_canInterface = static_cast<enum CanInterface>(config.Battery.CanInterface);
if (_canInterface == kMqtt) {
MqttSettings.subscribe(_canTopic, 0/*QoS*/,
std::bind(&BatteryCanReceiver::onMqttMessageCAN,
this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6)
);

if (_verboseLogging) {
MessageOutput.printf("BatteryCanReceiver: Subscribed to '%s' for CAN messages\r\n",
_canTopic.c_str());
}
return true;
}

const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[%s] Interface rx = %d, tx = %d\r\n",
_providerName, pin.battery_rx, pin.battery_tx);
Expand Down Expand Up @@ -82,6 +102,11 @@ bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName)

void BatteryCanReceiver::deinit()
{
if (_canInterface == kMqtt) {
MqttSettings.unsubscribe(_canTopic);
return;
}

// Stop TWAI driver
esp_err_t twaiLastResult = twai_stop();
switch (twaiLastResult) {
Expand Down Expand Up @@ -111,6 +136,10 @@ void BatteryCanReceiver::deinit()

void BatteryCanReceiver::loop()
{
if (_canInterface == kMqtt) {
return; // Mqtt CAN messages are event-driven
}

// Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer
twai_status_info_t status_info;
esp_err_t twaiLastResult = twai_get_status_info(&status_info);
Expand Down Expand Up @@ -139,6 +168,80 @@ void BatteryCanReceiver::loop()
return;
}

postMessage(std::move(rx_message));
}


void BatteryCanReceiver::onMqttMessageCAN(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
std::string value(reinterpret_cast<const char*>(payload), len);
JsonDocument json;

auto log = [this, topic](char const* format, auto&&... args) -> void {
MessageOutput.printf("[%s] Topic '%s': ", _providerName, topic);
MessageOutput.printf(format, args...);
MessageOutput.println();
};

const DeserializationError error = deserializeJson(json, value);
if (error) {
log("cannot parse payload '%s' as JSON", value.c_str());
return;
}

if (json.overflowed()) {
log("payload too large to process as JSON");
return;
}

int canID = json["id"] | -1;
if (canID == -1) {
log("JSON is missing message id");
return;
}

twai_message_t rx_message = {};
rx_message.identifier = canID;
int maxLen = sizeof(rx_message.data);

JsonVariant canData = json["data"];
if (canData.isNull()) {
log("JSON is missing message data");
return;
}

if (canData.is<char const*>()) {
String strData = canData.as<String>();
int len = strData.length();
if (len > maxLen) {
log("JSON data has more than %d elements", maxLen);
return;
}

rx_message.data_length_code = len;
for (int i = 0; i < len; i++) {
rx_message.data[i] = strData[i];
}
} else {
JsonArray arrayData = canData.as<JsonArray>();
int len = arrayData.size();
if (len > maxLen) {
log("JSON data has more than %d elements", maxLen);
return;
}

rx_message.data_length_code = len;
for (int i = 0; i < len; i++) {
rx_message.data[i] = arrayData[i];
}
}

postMessage(std::move(rx_message));
}

void BatteryCanReceiver::postMessage(twai_message_t&& rx_message)
{
if (_verboseLogging) {
MessageOutput.printf("[%s] Received CAN message: 0x%04X -",
_providerName, rx_message.identifier);
Expand Down
4 changes: 4 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, Jso
target["mqtt_discharge_current_topic"] = config.Battery.MqttDischargeCurrentTopic;
target["mqtt_discharge_current_json_path"] = config.Battery.MqttDischargeCurrentJsonPath;
target["mqtt_amperage_unit"] = config.Battery.MqttAmperageUnit;
target["can_interface"] = config.Battery.CanInterface;
target["mqtt_can_topic"] = config.Battery.MqttCANTopic;
}

bool ConfigurationClass::write()
Expand Down Expand Up @@ -380,6 +382,8 @@ void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, Batt
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.
strlcpy(target.MqttVoltageTopic, source["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
strlcpy(target.MqttVoltageJsonPath, source["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath));
target.CanInterface = source["can_interface"] | BATTERY_CAN_INTERFACE;
strlcpy(target.MqttCANTopic, source["mqtt_can_topic"] | BATTERY_CAN_TOPIC, sizeof(config.Battery.MqttCANTopic));
target.MqttVoltageUnit = source["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts;
target.EnableDischargeCurrentLimit = source["enable_discharge_current_limit"] | BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT;
target.DischargeCurrentLimit = source["discharge_current_limit"] | BATTERY_DISCHARGE_CURRENT_LIMIT;
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,12 @@
"batteryadmin": {
"BatterySettings": "Batterie Einstellungen",
"BatteryConfiguration": "Generelle Schnittstelleneinstellungen",
"CanConfiguration": "CAN Einstellungen",
"CanInterface": "Schnittstellentyp",
"CanInterfaceTwai": "CAN-Transceiver an der MCU",
"CanInterfaceMqtt": "MQTT Broker",
"CanMqttTopic": "Topic für CAN-Nachrichten",
"CanMqttTopicHint": "Nachrichten sollte im JSON-Format mit 'id' und 'data' feldern sein.",
"EnableBattery": "Aktiviere Schnittstelle",
"VerboseLogging": "@:base.VerboseLogging",
"Provider": "Datenanbieter",
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,12 @@
"batteryadmin": {
"BatterySettings": "Battery Settings",
"BatteryConfiguration": "General Interface Settings",
"CanConfiguration": "CAN Interface Settings",
"CanInterface": "Interface Type",
"CanInterfaceTwai": "CAN Transceiver on MCU",
"CanInterfaceMqtt": "MQTT Topic",
"CanMqttTopic": "CAN Message Topic",
"CanMqttTopicHint": "Messages should be JSON with 'id' and 'data' fields.",
"EnableBattery": "Enable Interface",
"VerboseLogging": "@:base.VerboseLogging",
"Provider": "Data Provider",
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/types/BatteryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface BatteryConfig {
provider: number;
jkbms_interface: number;
jkbms_polling_interval: number;
can_interface: number;
mqtt_can_topic: string;
mqtt_soc_topic: string;
mqtt_soc_json_path: string;
mqtt_voltage_topic: string;
Expand Down
39 changes: 39 additions & 0 deletions webapp/src/views/BatteryAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@
</div>
</CardElement>

<CardElement
v-show="$route.query.debug && batteryConfigList.enabled && providerUsesCanList.includes(batteryConfigList.provider)"
:text="$t('batteryadmin.CanConfiguration')"
textVariant="text-bg-primary"
addSpace
>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">
{{ $t('batteryadmin.CanInterface') }}
</label>
<div class="col-sm-10">
<select class="form-select" v-model="batteryConfigList.can_interface">
<option
v-for="canInterface in canInterfaceTypeList"
:key="canInterface.key"
:value="canInterface.key"
>
{{ $t(`batteryadmin.CanInterface` + canInterface.value) }}
</option>
</select>
</div>
</div>

<InputElement
v-show="batteryConfigList.can_interface == 1"
:label="$t('batteryadmin.CanMqttTopic')"
v-model="batteryConfigList.mqtt_can_topic"
type="text"
maxlength="256"
>
<div class="alert alert-secondary" role="alert" v-html="$t('batteryadmin.CanMqttTopicHint')"></div>
</InputElement>
</CardElement>

<CardElement
v-show="batteryConfigList.enabled && batteryConfigList.provider == 1"
:text="$t('batteryadmin.JkBmsConfiguration')"
Expand Down Expand Up @@ -235,6 +269,11 @@ export default defineComponent({
{ key: 4, value: 'PytesCan' },
{ key: 5, value: 'SBSCan' },
],
providerUsesCanList: [0, 4, 5],
canInterfaceTypeList: [
{ key: 0, value: 'Twai' },
{ key: 1, value: 'Mqtt' },
],
jkBmsInterfaceTypeList: [
{ key: 0, value: 'Uart' },
{ key: 1, value: 'Transceiver' },
Expand Down

0 comments on commit 4868df6

Please sign in to comment.