Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement receiving of CAN messages via MQTT #1279

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading