feature cloud working again

This commit is contained in:
2021-08-10 05:03:50 +02:00
parent 8e3c0b4204
commit a4b76623ae
13 changed files with 352 additions and 89 deletions

View File

@ -73,7 +73,7 @@ add_definitions(
# -DDEFAULT_GAMETRAKYMAX=4095
# -DDEFAULT_GAMETRAKDISTMIN=0
# -DDEFAULT_GAMETRAKDISTMAX=4095
# -DFEATURE_CLOUD
-DFEATURE_CLOUD
-DFEATURE_LEDBACKLIGHT
-DPINS_LEDBACKLIGHT=23
-DLEDBACKLIGHT_INVERTED

View File

@ -42,6 +42,11 @@ struct AutoBluetoothModeAccessor : public RefAccessorSaveSettings<BluetoothMode>
struct BleEnabledAccessor : public RefAccessorSaveSettings<bool> { bool &getRef() const override { return settings.bleSettings.bleEnabled; } };
#endif
#ifdef FEATURE_CLOUD
struct CloudEnabledAccessor : public RefAccessorSaveSettings<bool> { bool &getRef() const override { return settings.cloudSettings.cloudEnabled; } };
struct CloudTransmitTimeoutAccessor : public RefAccessorSaveSettings<int16_t> { int16_t &getRef() const override { return settings.cloudSettings.cloudTransmitTimeout; } };
#endif
struct FrontLeftEnabledAccessor : public RefAccessorSaveSettings<bool> { bool &getRef() const override { return settings.controllerHardware.enableFrontLeft; } };
struct FrontRightEnabledAccessor : public RefAccessorSaveSettings<bool> { bool &getRef() const override { return settings.controllerHardware.enableFrontRight; } };
struct BackLeftEnabledAccessor : public RefAccessorSaveSettings<bool> { bool &getRef() const override { return settings.controllerHardware.enableBackLeft; } };
@ -97,6 +102,9 @@ struct DisplayRedrawRateAccessor : public RefAccessorSaveSettings<int16_t> { int
#ifdef FEATURE_CAN
struct CanReceiveRateAccessor : public RefAccessorSaveSettings<int16_t> { int16_t &getRef() const override { return settings.boardcomputerHardware.timersSettings.canReceiveRate; } };
#endif
#ifdef FEATURE_CLOUD
struct CloudSendRateAccessor : public RefAccessorSaveSettings<int16_t> { int16_t &getRef() const override { return settings.boardcomputerHardware.timersSettings.cloudSendRate; } };
#endif
struct DefaultModeModelModeAccessor : public RefAccessorSaveSettings<UnifiedModelMode> { UnifiedModelMode &getRef() const override { return settings.defaultMode.modelMode; } };
struct DefaultModeSquareGasAccessor : public RefAccessorSaveSettings<bool> { bool &getRef() const override { return settings.defaultMode.squareGas; } };

View File

@ -1,8 +1,5 @@
#pragma once
// 3rdparty lib includes
#include <fmt/core.h>
// local includes
#include "textinterface.h"
#include "ble.h"

View File

@ -1,106 +1,190 @@
#pragma once
// esp-idf includes
#ifdef FEATURE_CLOUD
#include <esp_websocket_client.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#endif
#include <esp_log.h>
// 3rdparty lib includes
#include <wrappers/websocket_client.h>
#include <espwifistack.h>
// local includes
#include "globals.h"
namespace {
#ifdef FEATURE_CLOUD
void cloudTask(void*)
espcpputils::websocket_client cloudClient;
bool cloudStarted{};
espchrono::millis_clock::time_point lastCreateTry;
espchrono::millis_clock::time_point lastStartTry;
void createCloud();
void destroyCloud();
void startCloud();
void initCloud()
{
if (settings.cloudSettings.cloudEnabled)
{
createCloud();
if (!cloudClient)
return;
if (wifi_stack::get_sta_status() != wifi_stack::WiFiStaStatus::WL_CONNECTED)
return;
startCloud();
}
}
void handleCloud()
{
if (settings.cloudSettings.cloudEnabled)
{
if (!cloudClient)
{
if (espchrono::ago(lastCreateTry) < 10s)
return;
createCloud();
}
if (!cloudClient)
return;
if (!cloudStarted)
{
if (espchrono::ago(lastStartTry) < 10s)
return;
if (wifi_stack::get_sta_status() != wifi_stack::WiFiStaStatus::WL_CONNECTED)
return;
startCloud();
}
if (!cloudStarted)
return;
if (!cloudClient.is_connected())
return;
std::string rssi = "null";
if (wifi_stack::get_sta_status() == wifi_stack::WiFiStaStatus::WL_CONNECTED)
if (const auto &result = wifi_stack::get_sta_ap_info(); result)
rssi = std::to_string(result->rssi);
std::string msg = "{"
"\"type\": \"fullStatus\","
"\"partial\": false, "
"\"status\": {"
"\"millis\":" + std::to_string(std::chrono::milliseconds{espchrono::millis_clock::now().time_since_epoch()}.count()) + ","
"\"rssi\":" + rssi + ","
"\"front.valid\":" + std::to_string(controllers.front.feedbackValid) + ","
"\"back.valid\":" + std::to_string(controllers.back.feedbackValid) + ","
"\"front.left.pwm\":" + std::to_string(controllers.front.command.left.pwm) + ","
"\"front.right.pwm\":" + std::to_string(controllers.front.command.right.pwm) + ","
"\"back.left.pwm\":" + std::to_string(controllers.back.command.left.pwm) + ","
"\"back.right.pwm\":" + std::to_string(controllers.back.command.right.pwm) + ","
"\"front.volt\":" + std::to_string(controllers.front.feedback.batVoltage) + ","
"\"back.volt\":" + std::to_string(controllers.back.feedback.batVoltage) + ","
"\"front.temp\":" + std::to_string(controllers.front.feedback.boardTemp) + ","
"\"back.temp\":" + std::to_string(controllers.back.feedback.boardTemp) + ","
"\"front.bad\":" + std::to_string(controllers.front.feedback.timeoutCntSerial) + ","
"\"back.bad\":" + std::to_string(controllers.back.feedback.timeoutCntSerial) + ","
"\"front.left.speed\":" + std::to_string(controllers.front.feedback.left.speed) + ","
"\"front.right.speed\":" + std::to_string(controllers.front.feedback.right.speed) + ","
"\"back.left.speed\":" + std::to_string(controllers.back.feedback.left.speed) + ","
"\"back.right.speed\":" + std::to_string(controllers.back.feedback.right.speed) + ","
"\"front.left.current\":" + std::to_string(controllers.front.feedback.left.dcLink) + ","
"\"front.right.current\":" + std::to_string(controllers.front.feedback.right.dcLink) + ","
"\"back.left.current\":" + std::to_string(controllers.back.feedback.left.dcLink) + ","
"\"back.right.current\":" + std::to_string(controllers.back.feedback.right.dcLink) + ","
"\"front.left.error\":" + std::to_string(controllers.front.feedback.left.error) + ","
"\"front.right.error\":" + std::to_string(controllers.front.feedback.right.error) + ","
"\"back.left.error\":" + std::to_string(controllers.back.feedback.left.error) + ","
"\"back.right.error\":" + std::to_string(controllers.back.feedback.right.error) +
"}"
"}";
const auto timeout = std::chrono::ceil<espcpputils::ticks>(espchrono::milliseconds32{settings.cloudSettings.cloudTransmitTimeout}).count();
const auto written = cloudClient.send_text(msg, timeout);
if (written < 0)
{
ESP_LOGE("BOBBY", "cloudClient.send_text() failed with %i", written);
}
else if (written != msg.size())
{
ESP_LOGE("BOBBY", "websocket sent size mismatch, sent=%i, expected=%i", written, msg.size());
}
}
else if (cloudClient)
{
destroyCloud();
}
}
void createCloud()
{
ESP_LOGI("BOBBY", "called");
if (cloudClient)
{
ESP_LOGE(TAG, "cloud client already created");
return;
}
lastCreateTry = espchrono::millis_clock::now();
const esp_websocket_client_config_t config = {
.uri = "ws://iot.wattpilot.io:8080/charger/bobbycar1",
.uri = stringSettings.cloudUrl.c_str(),
};
esp_websocket_client_handle_t handle = esp_websocket_client_init(&config);
if (handle)
cloudClient = espcpputils::websocket_client{&config};
if (!cloudClient)
{
//Serial.println("esp websocket init succeeded");
if (const auto result = esp_websocket_client_start(handle); result == ESP_OK)
{
//Serial.println("esp websocket start succeeded");
while (true)
{
if (esp_websocket_client_is_connected(handle))
{
std::string msg = "{"
"\"type\": \"fullStatus\","
"\"partial\": false, "
"\"status\": {"
"\"millis\":" + std::to_string(std::chrono::milliseconds{espchrono::millis_clock::now().time_since_epoch()}.count()) + ","
"\"front.valid\":" + std::to_string(controllers.front.feedbackValid) + ","
"\"back.valid\":" + std::to_string(controllers.back.feedbackValid) + ","
"\"front.left.pwm\":" + std::to_string(controllers.front.command.left.pwm) + ","
"\"front.right.pwm\":" + std::to_string(controllers.front.command.right.pwm) + ","
"\"back.left.pwm\":" + std::to_string(controllers.back.command.left.pwm) + ","
"\"back.right.pwm\":" + std::to_string(controllers.back.command.right.pwm) + ","
"\"front.volt\":" + std::to_string(controllers.front.feedback.batVoltage) + ","
"\"back.volt\":" + std::to_string(controllers.back.feedback.batVoltage) + ","
"\"front.temp\":" + std::to_string(controllers.front.feedback.boardTemp) + ","
"\"back.temp\":" + std::to_string(controllers.back.feedback.boardTemp) + ","
"\"front.bad\":" + std::to_string(controllers.front.feedback.timeoutCntSerial) + ","
"\"back.bad\":" + std::to_string(controllers.back.feedback.timeoutCntSerial) + ","
"\"front.left.speed\":" + std::to_string(controllers.front.feedback.left.speed) + ","
"\"front.right.speed\":" + std::to_string(controllers.front.feedback.right.speed) + ","
"\"back.left.speed\":" + std::to_string(controllers.back.feedback.left.speed) + ","
"\"back.right.speed\":" + std::to_string(controllers.back.feedback.right.speed) + ","
"\"front.left.current\":" + std::to_string(controllers.front.feedback.left.dcLink) + ","
"\"front.right.current\":" + std::to_string(controllers.front.feedback.right.dcLink) + ","
"\"back.left.current\":" + std::to_string(controllers.back.feedback.left.dcLink) + ","
"\"back.right.current\":" + std::to_string(controllers.back.feedback.right.dcLink) + ","
"\"front.left.error\":" + std::to_string(controllers.front.feedback.left.error) + ","
"\"front.right.error\":" + std::to_string(controllers.front.feedback.right.error) + ","
"\"back.left.error\":" + std::to_string(controllers.back.feedback.left.error) + ","
"\"back.right.error\":" + std::to_string(controllers.back.feedback.right.error) +
"}"
"}";
const auto sent = esp_websocket_client_send_text(handle, msg.c_str(), msg.length(), 1000 / portTICK_PERIOD_MS);
if (sent == msg.length())
{
//Serial.println("Sent cloud message");
}
else
{
//Serial.printf("sent=%i, msgsize=%i\r\n", sent, msg.length());
}
}
else
{
//Serial.println("Not sending cloud because not connected");
}
delay(100);
}
}
else
{
//Serial.printf("esp websocket start failed with %s\r\n", esp_err_to_name(result));
}
}
else
{
//Serial.println("esp websocket init failed");
ESP_LOGE(TAG, "websocket could not be constructed");
return;
}
vTaskDelete(NULL);
ESP_LOGI("BOBBY", "cloud client created");
}
void startCloud()
{
if (const auto result = xTaskCreatePinnedToCore(cloudTask, "cloudTask", 4096, nullptr, 10, nullptr, 1); result == pdTRUE)
ESP_LOGI("BOBBY", "called");
if (!cloudClient)
{
//Serial.println("cloud task create succeeded");
ESP_LOGE(TAG, "cloud client not created");
return;
}
else
if (cloudStarted)
{
//Serial.printf("cloud task create failed\r\n");
ESP_LOGE(TAG, "cloud client already started");
return;
}
lastStartTry = espchrono::millis_clock::now();
const auto result = cloudClient.start();
ESP_LOG_LEVEL_LOCAL((result == ESP_OK ? ESP_LOG_INFO : ESP_LOG_ERROR), "BOBBY", "cloudClient.start() returned: %s", esp_err_to_name(result));
if (result == ESP_OK)
cloudStarted = true;
}
void destroyCloud()
{
ESP_LOGI("BOBBY", "called");
if (!cloudClient)
{
ESP_LOGE(TAG, "cloud client not created");
return;
}
cloudClient = {};
cloudStarted = false;
}
#endif
} // namespace

37
main/cloudtexthelpers.h Normal file
View File

@ -0,0 +1,37 @@
#pragma once
// 3rdparty lib includes
#include <fmt/core.h>
// local includes
#include "textinterface.h"
#include "cloud.h"
namespace {
#ifdef FEATURE_CLOUD
struct CloudCreatedText : public virtual TextInterface { public: std::string text() const override {
return fmt::format("created: {}", cloudClient ? "true" : "false"); }};
struct CloudStartedText : public virtual TextInterface {
public:
std::string text() const override
{
std::string text = "started: ";
if (cloudClient)
text += cloudStarted ? "true" : "false";
return text;
}
};
struct CloudConnectedText : public virtual TextInterface {
public:
std::string text() const override
{
std::string text = "connected: ";
if (cloudClient)
text += cloudClient.is_connected() ? "true" : "false";
return text;
}
};
#endif
}

View File

@ -0,0 +1,50 @@
#pragma once
// local includes
#include "menudisplay.h"
#include "menuitem.h"
#include "changevaluedisplay.h"
#include "actions/switchscreenaction.h"
#include "actions/toggleboolaction.h"
#include "checkboxicon.h"
#include "cloudtexthelpers.h"
#include "accessors/settingsaccessors.h"
#include "icons/back.h"
#include "texts.h"
// forward declares
namespace {
class CloudSettingsMenu;
class SettingsMenu;
} // namespace
namespace {
using CloudTransmitTimeoutChangeScreen = makeComponent<
ChangeValueDisplay<int16_t>,
StaticText<TEXT_CLOUDTRANSMITTIMEOUT>,
CloudTransmitTimeoutAccessor,
BackActionInterface<SwitchScreenAction<CloudSettingsMenu>>,
SwitchScreenAction<CloudSettingsMenu>
>;
} // namespace
namespace {
#ifdef FEATURE_CLOUD
class CloudSettingsMenu :
public MenuDisplay,
public StaticText<TEXT_CLOUDSETTINGS>,
public BackActionInterface<SwitchScreenAction<SettingsMenu>>
{
public:
CloudSettingsMenu()
{
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_CLOUDENABLED>, ToggleBoolAction, CheckboxIcon, CloudEnabledAccessor>>();
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_CLOUDTRANSMITTIMEOUT>, SwitchScreenAction<CloudTransmitTimeoutChangeScreen>>>();
constructMenuItem<makeComponent<MenuItem, CloudCreatedText, DisabledColor, DummyAction>>();
constructMenuItem<makeComponent<MenuItem, CloudStartedText, DisabledColor, DummyAction>>();
constructMenuItem<makeComponent<MenuItem, CloudConnectedText, DisabledColor, DummyAction>>();
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_BACK>, SwitchScreenAction<SettingsMenu>, StaticMenuItemIcon<&icons::back>>>();
}
};
#endif
} // namespace

View File

@ -24,6 +24,7 @@ class LimitsSettingsMenu;
class WifiSettingsMenu;
class BluetoothSettingsMenu;
class BleSettingsMenu;
class CloudSettingsMenu;
class ModesSettingsMenu;
class ControllerHardwareSettingsMenu;
class BoardcomputerHardwareSettingsMenu;
@ -61,6 +62,9 @@ public:
#endif
#ifdef FEATURE_BLE
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_BLESETTINGS>, SwitchScreenAction<BleSettingsMenu>, StaticMenuItemIcon<&icons::bluetooth>>>();
#endif
#ifdef FEATURE_CLOUD
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_CLOUDSETTINGS>, SwitchScreenAction<CloudSettingsMenu>>>();
#endif
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_MODESSETTINGS>, SwitchScreenAction<ModesSettingsMenu>>>();
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_CONTROLLERHARDWARESETTINGS>, SwitchScreenAction<ControllerHardwareSettingsMenu>, StaticMenuItemIcon<&icons::hardware>>>();

View File

@ -67,6 +67,16 @@ using CanReceiveRateChangeDisplay = makeComponent<
>;
#endif
#ifdef FEATURE_CLOUD
using CloudSendRateChangeDisplay = makeComponent<
ChangeValueDisplay<int16_t>,
StaticText<TEXT_CLOUDSENDRATE>,
CloudSendRateAccessor,
BackActionInterface<SwitchScreenAction<TimersMenu>>,
SwitchScreenAction<TimersMenu>
>;
#endif
class TimersMenu :
public MenuDisplay,
public StaticText<TEXT_TIMERS>,
@ -82,6 +92,9 @@ public:
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_DISPLAYREDRAWRATE>, SwitchScreenAction<DisplayRedrawRateChangeDisplay>>>();
#ifdef FEATURE_CAN
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_CANRECEIVERATE>, SwitchScreenAction<CanReceiveRateChangeDisplay>>>();
#endif
#ifdef FEATURE_CLOUD
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_CLOUDSENDRATE>, SwitchScreenAction<CloudSendRateChangeDisplay>>>();
#endif
constructMenuItem<makeComponent<MenuItem, StaticText<TEXT_BACK>, SwitchScreenAction<BoardcomputerHardwareSettingsMenu>, StaticMenuItemIcon<&icons::back>>>();
}

View File

@ -30,6 +30,9 @@ using namespace std::chrono_literals;
#ifdef FEATURE_BLE
#include "displays/menus/blesettingsmenu.h"
#endif
#ifdef FEATURE_CLOUD
#include "displays/menus/cloudsettingsmenu.h"
#endif
#include "displays/menus/bmsmenu.h"
#include "displays/menus/buzzermenu.h"
#include "displays/menus/commanddebugmenu.h"
@ -116,6 +119,9 @@ std::optional<espchrono::millis_clock::time_point> lastCanParse;
#ifdef FEATURE_BLE
std::optional<espchrono::millis_clock::time_point> lastBleUpdate;
#endif
#ifdef FEATURE_CLOUD
std::optional<espchrono::millis_clock::time_point> lastCloudUpdate;
#endif
}
extern "C" void app_main()
@ -258,8 +264,8 @@ extern "C" void app_main()
readPotis();
#ifdef FEATURE_CLOUD
bootLabel.redraw("startCloud");
startCloud();
bootLabel.redraw("cloud");
initCloud();
#endif
bootLabel.redraw("switchScreen");
@ -381,6 +387,15 @@ extern "C" void app_main()
}
#endif
#ifdef FEATURE_CLOUD
if (!lastCloudUpdate || now - *lastCloudUpdate >= 1000ms/settings.boardcomputerHardware.timersSettings.cloudSendRate)
{
handleCloud();
lastCloudUpdate = now;
}
#endif
#ifdef FEATURE_WEBSERVER
handleWebserver();
#endif

View File

@ -112,6 +112,9 @@ constexpr Settings::BoardcomputerHardware::TimersSettings defaultTimersSettings
#ifdef FEATURE_CAN
.canReceiveRate = 100,
#endif
#ifdef FEATURE_CLOUD
.cloudSendRate = 1,
#endif
};
constexpr Settings::BoardcomputerHardware defaultBoardcomputerHardware {
@ -135,6 +138,13 @@ constexpr Settings::BoardcomputerHardware defaultBoardcomputerHardware {
.timersSettings = defaultTimersSettings
};
#ifdef FEATURE_CLOUD
constexpr Settings::CloudSettings defaultCloudSettings {
.cloudEnabled = true,
.cloudTransmitTimeout = 10
};
#endif
constexpr Settings::DefaultMode defaultDefaultMode {
.modelMode = UnifiedModelMode::FocTorque,
.squareGas = true,
@ -194,6 +204,9 @@ constexpr Settings defaultSettings {
#endif
.controllerHardware = defaultControllerHardware,
.boardcomputerHardware = defaultBoardcomputerHardware,
#ifdef FEATURE_CLOUD
.cloudSettings = defaultCloudSettings,
#endif
.defaultMode = defaultDefaultMode,
.tempomatMode = defaultTempomatMode,
.larsmMode = defaultLarsmMode
@ -215,7 +228,10 @@ StringSettings makeDefaultStringSettings()
ConfiguredWifi { .ssid = {}, .key = {} },
ConfiguredWifi { .ssid = {}, .key = {} },
ConfiguredWifi { .ssid = {}, .key = {} }
}
},
#ifdef FEATURE_CLOUD
.cloudUrl = {},
#endif
};
}
}

View File

@ -89,10 +89,20 @@ struct Settings
int16_t displayRedrawRate;
#ifdef FEATURE_CAN
int16_t canReceiveRate;
#endif
#ifdef FEATURE_CLOUD
int16_t cloudSendRate;
#endif
} timersSettings;
} boardcomputerHardware;
#ifdef FEATURE_CLOUD
struct CloudSettings {
bool cloudEnabled;
int16_t cloudTransmitTimeout; // in ms
} cloudSettings;
#endif
struct DefaultMode {
UnifiedModelMode modelMode;
bool squareGas;
@ -179,6 +189,14 @@ void Settings::executeForEveryCommonSetting(T &&callable)
#ifdef FEATURE_CAN
callable("canReceiveRate", boardcomputerHardware.timersSettings.canReceiveRate);
#endif
#ifdef FEATURE_CLOUD
callable("cloudSendRate", boardcomputerHardware.timersSettings.cloudSendRate);
#endif
#ifdef FEATURE_CLOUD
callable("cloudEnabled", cloudSettings.cloudEnabled);
callable("clodTransmTmout", cloudSettings.cloudTransmitTimeout);
#endif
}
template<typename T>

View File

@ -2,6 +2,7 @@
// system includes
#include <array>
#include <string>
namespace {
struct StringSettings
@ -13,6 +14,10 @@ struct StringSettings
std::array<ConfiguredWifi, 10> wifis;
#ifdef FEATURE_CLOUD
std::string cloudUrl;
#endif
template<typename T>
void executeForEveryCommonSetting(T &&callable);
@ -44,6 +49,10 @@ void StringSettings::executeForEveryCommonSetting(T &&callable)
callable("key8", wifis[8].key);
callable("ssid9", wifis[9].ssid);
callable("key9", wifis[9].key);
#ifdef FEATURE_CLOUD
callable("cloudUrl", cloudUrl);
#endif
}
template<typename T>

View File

@ -40,6 +40,14 @@ constexpr char TEXT_BLEENABLED[] = "BLE enabled";
//constexpr char TEXT_BACK[] = "Back";
#endif
#ifdef FEATURE_CLOUD
//CloudSettingsMenu
constexpr char TEXT_CLOUDSETTINGS[] = "Cloud settings";
constexpr char TEXT_CLOUDENABLED[] = "Cloud enabled";
constexpr char TEXT_CLOUDTRANSMITTIMEOUT[] = "Transmit timeout";
//constexpr char TEXT_BACK[] = "Back";
#endif
//DebugMenu
constexpr char TEXT_LOADSETTINGS[] = "Load settings";
constexpr char TEXT_SAVESETTINGS[] = "Save settings";
@ -82,6 +90,7 @@ constexpr char TEXT_LIMITSSETTINGS[] = "Limits settings";
constexpr char TEXT_WIFISETTINGS[] = "WiFi settings";
//constexpr char TEXT_BLUETOOTHSETTINGS[] = "Bluetooth settings";
//constexpr char TEXT_BLESETTINGS[] = "BLE settings";
//constexpr char TEXT_CLOUDSETTINGS[] = "Cloud settings";
constexpr char TEXT_MODESSETTINGS[] = "Modes settings";
constexpr char TEXT_CONTROLLERHARDWARESETTINGS[] = "Controller H/W settings";
constexpr char TEXT_BOARDCOMPUTERHARDWARESETTINGS[] = "Boardcomputer H/W settings";
@ -312,6 +321,9 @@ constexpr char TEXT_DISPLAYREDRAWRATE[] = "Display redraw rate";
#ifdef FEATURE_CAN
constexpr char TEXT_CANRECEIVERATE[] = "CAN receive rate";
#endif
#ifdef FEATURE_CLOUD
constexpr char TEXT_CLOUDSENDRATE[] = "Cloud send rate";
#endif
//constexpr char TEXT_BACK[] = "Back";
//ChangeValueDisplay<BluetoothMode>