btclock_v3/src/lib/config.cpp

643 lines
16 KiB
C++
Raw Normal View History

2023-11-07 20:26:15 +00:00
#include "config.hpp"
#define MAX_ATTEMPTS_WIFI_CONNECTION 20
Preferences preferences;
Adafruit_MCP23X17 mcp1;
#ifdef IS_BTCLOCK_S3
Adafruit_MCP23X17 mcp2;
#endif
std::vector<std::string> screenNameMap(SCREEN_COUNT);
2023-11-13 19:02:58 +00:00
std::mutex mcpMutex;
2023-11-07 20:26:15 +00:00
void setup()
{
2023-11-30 21:38:01 +00:00
setupPreferences();
setupHardware();
setupDisplays();
if (preferences.getBool("ledTestOnPower", true))
{
2023-11-30 21:38:01 +00:00
queueLedEffect(LED_POWER_TEST);
}
{
std::lock_guard<std::mutex> lockMcp(mcpMutex);
if (mcp1.digitalRead(3) == LOW)
{
2023-11-30 21:38:01 +00:00
preferences.putBool("wifiConfigured", false);
preferences.remove("txPower");
WiFi.eraseAP();
queueLedEffect(LED_EFFECT_WIFI_ERASE_SETTINGS);
2023-11-07 20:26:15 +00:00
}
2023-11-30 21:38:01 +00:00
}
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
tryImprovSetup();
2023-11-07 20:34:26 +00:00
2023-11-30 21:38:01 +00:00
setupWebserver();
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
// setupWifi();
setupTime();
finishSetup();
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
setupTasks();
setupTimers();
2023-11-30 21:38:01 +00:00
xTaskCreate(setupWebsocketClients, "setupWebsocketClients", 4096, NULL,
tskIDLE_PRIORITY, NULL);
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
setupButtonTask();
setupOTA();
2023-11-30 21:38:01 +00:00
waitUntilNoneBusy();
forceFullRefresh();
2023-11-07 20:26:15 +00:00
}
void tryImprovSetup()
{
2023-11-30 21:38:01 +00:00
WiFi.onEvent(WiFiEvent);
WiFi.setAutoConnect(true);
WiFi.setAutoReconnect(true);
WiFi.begin();
if (preferences.getInt("txPower", 0))
{
if (WiFi.setTxPower(
static_cast<wifi_power_t>(preferences.getInt("txPower", 0))))
{
Serial.printf("WiFi max tx power set to %d\n",
preferences.getInt("txPower", 0));
}
}
// if (!preferences.getBool("wifiConfigured", false))
{
2023-11-30 21:38:01 +00:00
queueLedEffect(LED_EFFECT_WIFI_WAIT_FOR_CONFIG);
uint8_t x_buffer[16];
uint8_t x_position = 0;
2023-11-30 21:38:01 +00:00
bool buttonPress = false;
2023-11-08 11:18:59 +00:00
{
2023-11-30 21:38:01 +00:00
std::lock_guard<std::mutex> lockMcp(mcpMutex);
buttonPress = (mcp1.digitalRead(2) == LOW);
2023-11-07 20:26:15 +00:00
}
2023-11-30 21:38:01 +00:00
{
2023-11-30 21:38:01 +00:00
WiFiManager wm;
byte mac[6];
WiFi.macAddress(mac);
String softAP_SSID =
String("BTClock" + String(mac[5], 16) + String(mac[1], 16));
WiFi.setHostname(softAP_SSID.c_str());
String softAP_password =
base64::encode(String(mac[2], 16) + String(mac[4], 16) +
String(mac[5], 16) + String(mac[1], 16))
.substring(2, 10);
// wm.setConfigPortalTimeout(preferences.getUInt("wpTimeout", 600));
wm.setWiFiAutoReconnect(false);
wm.setDebugOutput(false);
wm.setConfigPortalBlocking(true);
wm.setAPCallback([&](WiFiManager *wifiManager)
{
2023-11-30 21:38:01 +00:00
// Serial.printf("Entered config mode:ip=%s, ssid='%s', pass='%s'\n",
// WiFi.softAPIP().toString().c_str(),
// wifiManager->getConfigPortalSSID().c_str(),
// softAP_password.c_str());
// delay(6000);
setFgColor(GxEPD_BLACK);
setBgColor(GxEPD_WHITE);
2023-11-30 21:38:01 +00:00
const String qrText = "qrWIFI:S:" + wifiManager->getConfigPortalSSID() +
";T:WPA;P:" + softAP_password.c_str() + ";;";
const String explainText = "*SSID: *\r\n" +
wifiManager->getConfigPortalSSID() +
"\r\n\r\n*Password:*\r\n" + softAP_password;
std::array<String, NUM_SCREENS> epdContent = {
"Welcome!",
"Bienvenidos!",
"To setup\r\nscan QR or\r\nconnect\r\nmanually",
"Para\r\nconfigurar\r\nescanear QR\r\no conectar\r\nmanualmente",
explainText,
"*Hostname*:\r\n" + getMyHostname(),
2023-11-30 21:38:01 +00:00
qrText};
setEpdContent(epdContent); });
2023-11-30 21:38:01 +00:00
wm.setSaveConfigCallback([]()
{
2023-11-30 21:38:01 +00:00
preferences.putBool("wifiConfigured", true);
delay(1000);
// just restart after succes
ESP.restart(); });
2023-11-30 21:38:01 +00:00
bool ac = wm.autoConnect(softAP_SSID.c_str(), softAP_password.c_str());
// waitUntilNoneBusy();
// std::array<String, NUM_SCREENS> epdContent = {"Welcome!",
// "Bienvenidos!", "Use\r\nweb-interface\r\nto configure", "Use\r\nla
// interfaz web\r\npara configurar", "Or
// restart\r\nwhile\r\nholding\r\n2nd button\r\r\nto start\r\n QR-config",
// "O reinicie\r\nmientras\r\n mantiene presionado\r\nel segundo
// botón\r\r\npara iniciar\r\nQR-config", ""}; setEpdContent(epdContent);
// esp_task_wdt_init(30, false);
// uint count = 0;
// while (WiFi.status() != WL_CONNECTED)
// {
// if (Serial.available() > 0)
// {
// uint8_t b = Serial.read();
// if (parse_improv_serial_byte(x_position, b, x_buffer,
// onImprovCommandCallback, onImprovErrorCallback))
// {
// x_buffer[x_position++] = b;
// }
// else
// {
// x_position = 0;
// }
// }
// count++;
// if (count > 2000000) {
// queueLedEffect(LED_EFFECT_HEARTBEAT);
// count = 0;
// }
// }
// esp_task_wdt_deinit();
// esp_task_wdt_reset();
}
setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
}
// else
// {
// while (WiFi.status() != WL_CONNECTED)
// {
// vTaskDelay(pdMS_TO_TICKS(400));
// }
// }
2023-11-30 21:38:01 +00:00
// queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
2023-11-07 20:26:15 +00:00
}
void setupTime()
{
2023-11-30 21:38:01 +00:00
configTime(preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS), 0,
NTP_SERVER);
struct tm timeinfo;
while (!getLocalTime(&timeinfo))
{
2023-11-30 21:38:01 +00:00
configTime(preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS), 0,
NTP_SERVER);
delay(500);
Serial.println(F("Retry set time"));
}
2023-11-07 20:26:15 +00:00
}
void setupPreferences()
{
2023-11-30 21:38:01 +00:00
preferences.begin("btclock", false);
2023-11-30 21:38:01 +00:00
setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
setBlockHeight(preferences.getUInt("blockHeight", 816000));
setPrice(preferences.getUInt("lastPrice", 30000));
2023-11-30 21:38:01 +00:00
screenNameMap[SCREEN_BLOCK_HEIGHT] = "Block Height";
screenNameMap[SCREEN_MSCW_TIME] = "Sats per dollar";
screenNameMap[SCREEN_BTC_TICKER] = "Ticker";
screenNameMap[SCREEN_TIME] = "Time";
screenNameMap[SCREEN_HALVING_COUNTDOWN] = "Halving countdown";
screenNameMap[SCREEN_MARKET_CAP] = "Market Cap";
2023-11-07 20:26:15 +00:00
}
void setupWebsocketClients(void *pvParameters)
{
2023-11-30 21:38:01 +00:00
setupBlockNotify();
2023-11-07 20:26:15 +00:00
if (preferences.getBool("fetchEurPrice", false))
{
2023-11-30 21:38:01 +00:00
setupPriceFetchTask();
}
else
{
2023-11-30 21:38:01 +00:00
setupPriceNotify();
}
2023-11-30 21:38:01 +00:00
vTaskDelete(NULL);
2023-11-08 11:18:59 +00:00
}
void setupTimers()
{
2023-11-30 21:38:01 +00:00
xTaskCreate(setupTimeUpdateTimer, "setupTimeUpdateTimer", 2048, NULL,
tskIDLE_PRIORITY, NULL);
xTaskCreate(setupScreenRotateTimer, "setupScreenRotateTimer", 2048, NULL,
tskIDLE_PRIORITY, NULL);
2023-11-07 20:26:15 +00:00
}
void finishSetup()
{
if (preferences.getBool("ledStatus", false))
{
2023-11-30 21:38:01 +00:00
restoreLedState();
}
else
{
2023-11-30 21:38:01 +00:00
clearLeds();
}
}
2023-11-30 21:38:01 +00:00
std::vector<std::string> getScreenNameMap() { return screenNameMap; }
void setupMcp()
{
2023-11-30 21:38:01 +00:00
#ifdef IS_BTCLOCK_S3
const int mcp1AddrPins[] = {MCP1_A0_PIN, MCP1_A1_PIN, MCP1_A2_PIN};
const int mcp1AddrValues[] = {LOW, LOW, LOW};
2023-11-30 21:38:01 +00:00
const int mcp2AddrPins[] = {MCP2_A0_PIN, MCP2_A1_PIN, MCP2_A2_PIN};
const int mcp2AddrValues[] = {LOW, LOW, HIGH};
2023-11-30 21:38:01 +00:00
pinMode(MCP_RESET_PIN, OUTPUT);
digitalWrite(MCP_RESET_PIN, HIGH);
for (int i = 0; i < 3; ++i)
{
2023-11-30 21:38:01 +00:00
pinMode(mcp1AddrPins[i], OUTPUT);
digitalWrite(mcp1AddrPins[i], mcp1AddrValues[i]);
pinMode(mcp2AddrPins[i], OUTPUT);
digitalWrite(mcp2AddrPins[i], mcp2AddrValues[i]);
}
digitalWrite(MCP_RESET_PIN, LOW);
delay(30);
digitalWrite(MCP_RESET_PIN, HIGH);
#endif
}
void setupHardware()
{
if (!LittleFS.begin(true))
{
2023-11-30 21:38:01 +00:00
Serial.println(F("An Error has occurred while mounting LittleFS"));
}
2023-11-20 17:59:33 +00:00
if (!LittleFS.open("/index.html.gz", "r"))
{
2023-11-30 21:38:01 +00:00
Serial.println("Error loading WebUI");
}
2023-11-30 21:38:01 +00:00
setupLeds();
2023-11-30 21:38:01 +00:00
WiFi.setHostname(getMyHostname().c_str());
if (!psramInit())
{
2023-11-30 21:38:01 +00:00
Serial.println(F("PSRAM not available"));
}
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
setupMcp();
2023-11-30 21:38:01 +00:00
Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000);
2023-11-07 20:26:15 +00:00
if (!mcp1.begin_I2C(0x20))
{
2023-11-30 21:38:01 +00:00
Serial.println(F("Error MCP23017"));
// while (1)
// ;
}
else
{
2023-11-30 21:38:01 +00:00
pinMode(MCP_INT_PIN, INPUT_PULLUP);
mcp1.setupInterrupts(false, false, LOW);
2023-11-07 20:26:15 +00:00
for (int i = 0; i < 4; i++)
{
2023-11-30 21:38:01 +00:00
mcp1.pinMode(i, INPUT_PULLUP);
mcp1.setupInterruptPin(i, LOW);
2023-11-07 20:26:15 +00:00
}
2023-11-30 21:38:01 +00:00
#ifndef IS_BTCLOCK_S3
for (int i = 8; i <= 14; i++)
{
2023-11-30 21:38:01 +00:00
mcp1.pinMode(i, OUTPUT);
}
2023-11-30 21:38:01 +00:00
#endif
}
2023-11-30 21:38:01 +00:00
#ifdef IS_BTCLOCK_S3
if (!mcp2.begin_I2C(0x21))
{
2023-11-30 21:38:01 +00:00
Serial.println(F("Error MCP23017"));
2023-11-30 21:38:01 +00:00
// while (1)
// ;
}
#endif
2023-11-07 20:26:15 +00:00
}
void improvGetAvailableWifiNetworks()
{
2023-11-30 21:38:01 +00:00
int networkNum = WiFi.scanNetworks();
2023-11-07 20:26:15 +00:00
for (int id = 0; id < networkNum; ++id)
{
2023-11-30 21:38:01 +00:00
std::vector<uint8_t> data = improv::build_rpc_response(
improv::GET_WIFI_NETWORKS,
{WiFi.SSID(id), String(WiFi.RSSI(id)),
(WiFi.encryptionType(id) == WIFI_AUTH_OPEN ? "NO" : "YES")},
false);
2023-11-07 20:26:15 +00:00
improv_send_response(data);
2023-11-30 21:38:01 +00:00
}
// final response
std::vector<uint8_t> data = improv::build_rpc_response(
improv::GET_WIFI_NETWORKS, std::vector<std::string>{}, false);
improv_send_response(data);
2023-11-07 20:26:15 +00:00
}
bool improv_connectWifi(std::string ssid, std::string password)
{
2023-11-30 21:38:01 +00:00
uint8_t count = 0;
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
WiFi.begin(ssid.c_str(), password.c_str());
2023-11-07 20:26:15 +00:00
while (WiFi.status() != WL_CONNECTED)
{
2023-11-30 21:38:01 +00:00
blinkDelay(500, 2);
if (count > MAX_ATTEMPTS_WIFI_CONNECTION)
{
2023-11-30 21:38:01 +00:00
WiFi.disconnect();
return false;
2023-11-07 20:26:15 +00:00
}
2023-11-30 21:38:01 +00:00
count++;
}
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
return true;
2023-11-07 20:26:15 +00:00
}
void onImprovErrorCallback(improv::Error err)
{
2023-11-30 21:38:01 +00:00
blinkDelayColor(100, 1, 255, 0, 0);
// pixels.setPixelColor(0, pixels.Color(255, 0, 0));
// pixels.setPixelColor(1, pixels.Color(255, 0, 0));
// pixels.setPixelColor(2, pixels.Color(255, 0, 0));
// pixels.setPixelColor(3, pixels.Color(255, 0, 0));
// pixels.show();
// vTaskDelay(pdMS_TO_TICKS(100));
// pixels.clear();
// pixels.show();
// vTaskDelay(pdMS_TO_TICKS(100));
2023-11-07 20:26:15 +00:00
}
std::vector<std::string> getLocalUrl()
{
2023-11-30 21:38:01 +00:00
return {// URL where user can finish onboarding or use device
// Recommended to use website hosted by device
String("http://" + WiFi.localIP().toString()).c_str()};
2023-11-07 20:26:15 +00:00
}
bool onImprovCommandCallback(improv::ImprovCommand cmd)
{
switch (cmd.command)
{
case improv::Command::GET_CURRENT_STATE:
{
if ((WiFi.status() == WL_CONNECTED))
{
improv_set_state(improv::State::STATE_PROVISIONED);
std::vector<uint8_t> data = improv::build_rpc_response(
improv::GET_CURRENT_STATE, getLocalUrl(), false);
improv_send_response(data);
}
else
{
improv_set_state(improv::State::STATE_AUTHORIZED);
2023-11-30 21:38:01 +00:00
}
2023-11-08 11:18:59 +00:00
break;
}
2023-11-07 20:26:15 +00:00
case improv::Command::WIFI_SETTINGS:
{
if (cmd.ssid.length() == 0)
{
improv_set_error(improv::Error::ERROR_INVALID_RPC);
break;
}
improv_set_state(improv::STATE_PROVISIONING);
queueLedEffect(LED_EFFECT_WIFI_CONNECTING);
if (improv_connectWifi(cmd.ssid, cmd.password))
{
queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
// std::array<String, NUM_SCREENS> epdContent = {"S", "U", "C", "C",
// "E", "S", "S"}; setEpdContent(epdContent);
2023-11-07 20:26:15 +00:00
preferences.putBool("wifiConfigured", true);
2023-11-07 20:26:15 +00:00
improv_set_state(improv::STATE_PROVISIONED);
std::vector<uint8_t> data = improv::build_rpc_response(
improv::WIFI_SETTINGS, getLocalUrl(), false);
improv_send_response(data);
2023-11-30 21:38:01 +00:00
delay(2500);
ESP.restart();
setupWebserver();
2023-11-30 21:56:50 +00:00
}
else
{
queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
2023-11-30 21:38:01 +00:00
improv_set_state(improv::STATE_STOPPED);
improv_set_error(improv::Error::ERROR_UNABLE_TO_CONNECT);
2023-11-30 21:56:50 +00:00
}
2023-11-30 21:38:01 +00:00
break;
}
2023-11-30 21:56:50 +00:00
case improv::Command::GET_DEVICE_INFO:
{
std::vector<std::string> infos = {// Firmware name
"BTClock",
// Firmware version
"1.0.0",
// Hardware chip/variant
"ESP32S3",
// Device name
"BTClock"};
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false);
improv_send_response(data);
break;
}
case improv::Command::GET_WIFI_NETWORKS:
{
improvGetAvailableWifiNetworks();
// std::array<String, NUM_SCREENS> epdContent = {"W", "E", "B", "W", "I",
// "F", "I"}; setEpdContent(epdContent);
break;
}
default:
{
improv_set_error(improv::ERROR_UNKNOWN_RPC);
return false;
}
2023-11-30 21:38:01 +00:00
}
return true;
}
2023-11-07 20:26:15 +00:00
void improv_set_state(improv::State state)
{
2023-11-30 21:38:01 +00:00
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
data.resize(11);
data[6] = improv::IMPROV_SERIAL_VERSION;
data[7] = improv::TYPE_CURRENT_STATE;
data[8] = 1;
data[9] = state;
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
uint8_t checksum = 0x00;
for (uint8_t d : data)
checksum += d;
2023-11-30 21:38:01 +00:00
data[10] = checksum;
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
Serial.write(data.data(), data.size());
2023-11-07 20:26:15 +00:00
}
void improv_send_response(std::vector<uint8_t> &response)
{
2023-11-30 21:38:01 +00:00
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
data.resize(9);
data[6] = improv::IMPROV_SERIAL_VERSION;
data[7] = improv::TYPE_RPC_RESPONSE;
data[8] = response.size();
data.insert(data.end(), response.begin(), response.end());
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
uint8_t checksum = 0x00;
for (uint8_t d : data)
checksum += d;
2023-11-30 21:38:01 +00:00
data.push_back(checksum);
2023-11-07 20:26:15 +00:00
2023-11-30 21:38:01 +00:00
Serial.write(data.data(), data.size());
2023-11-07 20:26:15 +00:00
}
void improv_set_error(improv::Error error)
{
2023-11-30 21:38:01 +00:00
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
data.resize(11);
data[6] = improv::IMPROV_SERIAL_VERSION;
data[7] = improv::TYPE_ERROR_STATE;
data[8] = 1;
data[9] = error;
2023-11-30 21:38:01 +00:00
uint8_t checksum = 0x00;
for (uint8_t d : data)
checksum += d;
2023-11-30 21:38:01 +00:00
data[10] = checksum;
2023-11-20 17:59:33 +00:00
2023-11-30 21:38:01 +00:00
Serial.write(data.data(), data.size());
}
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
2023-11-30 21:38:01 +00:00
static bool first_connect = true;
Serial.printf("[WiFi-event] event: %d\n", event);
switch (event)
{
case ARDUINO_EVENT_WIFI_READY:
Serial.println("WiFi interface ready");
break;
case ARDUINO_EVENT_WIFI_SCAN_DONE:
Serial.println("Completed scan for access points");
break;
case ARDUINO_EVENT_WIFI_STA_START:
Serial.println("WiFi client started");
break;
case ARDUINO_EVENT_WIFI_STA_STOP:
Serial.println("WiFi clients stopped");
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
Serial.println("Connected to access point");
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
{
if (!first_connect)
{
Serial.println("Disconnected from WiFi access point");
2023-11-30 21:56:50 +00:00
queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
uint8_t reason = info.wifi_sta_disconnected.reason;
if (reason)
Serial.printf("Disconnect reason: %s, ",
WiFi.disconnectReasonName((wifi_err_reason_t)reason));
}
break;
}
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
Serial.println("Authentication mode of access point has changed");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
{
Serial.print("Obtained IP address: ");
Serial.println(WiFi.localIP());
if (!first_connect)
queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
first_connect = false;
break;
}
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
Serial.println("Lost IP address and IP address is reset to 0");
queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
WiFi.reconnect();
break;
case ARDUINO_EVENT_WIFI_AP_START:
Serial.println("WiFi access point started");
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
Serial.println("WiFi access point stopped");
break;
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
Serial.println("Client connected");
break;
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
Serial.println("Client disconnected");
break;
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
Serial.println("Assigned IP address to client");
break;
case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
Serial.println("Received probe request");
break;
case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
Serial.println("AP IPv6 is preferred");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
Serial.println("STA IPv6 is preferred");
break;
default:
break;
2023-11-30 21:38:01 +00:00
}
2023-11-28 00:30:36 +00:00
}
String getMyHostname()
{
2023-11-30 21:38:01 +00:00
uint8_t mac[6];
// WiFi.macAddress(mac);
esp_efuse_mac_get_default(mac);
char hostname[15];
String hostnamePrefix = preferences.getString("hostnamePrefix", "btclock");
snprintf(hostname, sizeof(hostname), "%s-%02x%02x%02x", hostnamePrefix,
mac[3], mac[4], mac[5]);
return hostname;
2023-11-13 19:02:58 +00:00
}