diff --git a/.gitignore b/.gitignore index ea1472e..0b5c425 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ output/ +secrets.h diff --git a/arduino/cyd-dashboard/User_Setup.h b/arduino/cyd-dashboard/User_Setup.h new file mode 100644 index 0000000..8f5665c --- /dev/null +++ b/arduino/cyd-dashboard/User_Setup.h @@ -0,0 +1,45 @@ +// ══════════════════════════════════════════════════════════ +// TFT_eSPI User_Setup for ESP32-32E-4" (Hosyond 320x480) +// This file is copied over vendor/esp32-32e-4/TFT_eSPI/User_Setup.h +// by the install-libs-32e-4 task. +// ══════════════════════════════════════════════════════════ + +#define USER_SETUP_ID 201 + +// ── Driver ── +#define ST7796_DRIVER + +// ── Resolution ── +#define TFT_WIDTH 320 +#define TFT_HEIGHT 480 + +// ── SPI Pins (from lcdwiki.com/4.0inch_ESP32-32E_Display) ── +#define TFT_MISO 12 +#define TFT_MOSI 13 +#define TFT_SCLK 14 +#define TFT_CS 15 +#define TFT_DC 2 +#define TFT_RST -1 // tied to EN, handled by board + +// ── Backlight ── +#define TFT_BL 27 +#define TFT_BACKLIGHT_ON HIGH + +// ── Touch (XPT2046 resistive) ── +#define TOUCH_CS 33 +#define TOUCH_IRQ 36 + +// ── SPI speed ── +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 20000000 +#define SPI_TOUCH_FREQUENCY 2500000 + +// ── Misc ── +#define LOAD_GLCD +#define LOAD_FONT2 +#define LOAD_FONT4 +#define LOAD_FONT6 +#define LOAD_FONT7 +#define LOAD_FONT8 +#define LOAD_GFXFF +#define SMOOTH_FONT \ No newline at end of file diff --git a/arduino/cyd-dashboard/cyd-dashboard.ino b/arduino/cyd-dashboard/cyd-dashboard.ino new file mode 100644 index 0000000..361c848 --- /dev/null +++ b/arduino/cyd-dashboard/cyd-dashboard.ino @@ -0,0 +1,586 @@ +// CYD Plug Dashboard — 4-button UI for zigbee2mqtt smart plugs +// +// Hardware: Hosyond ESP32-32E 4" (320x480, ST7796, XPT2046 touch) +// TFT pins: User_Setup.h (vendored from klubhaus-doorbell) +// Backlight: GPIO 27 +// Touch CS: GPIO 33, IRQ: GPIO 36 +// +// MQTT broker: 192.168.81.147:1883 (set in secrets.h) +// Subscribe: zigbee2mqtt/ (state JSON {"state":"ON"}) +// Publish: zigbee2mqtt//set (TOGGLE) +// +// Layout: 2x2 grid of buttons, status bar at top +// ┌─────────────────────────────┐ +// │ WiFi✓ MQTT✓ Squiggle● ON │ +// ├──────────────┬──────────────┤ +// │ Squiggle │ Sideboard │ +// │ ON │ OFF │ +// ├──────────────┼──────────────┤ +// │ Bamboo Lights│ DJ Booth │ +// │ ON │ OFF │ +// └──────────────┴──────────────┘ + +#include +#include +#include +#include +#include +#include +#include "secrets.h" + +// ── Plugs ────────────────────────────────────────────────────── +struct Plug { + const char* name; + const char* topic; + bool state; +}; + +static Plug plugs[] = { + {"Squiggle", "zigbee2mqtt/Squiggle"}, + {"Sideboard", "zigbee2mqtt/Sideboard Lamp"}, + {"Bamboo", "zigbee2mqtt/Bamboo Lights"}, + {"DJ Booth", "zigbee2mqtt/DJ Booth"}, +}; +static const int PLUG_COUNT = sizeof(plugs) / sizeof(plugs[0]); + +// ── Party Lock ───────────────────────────────────────────────── +#define PARTY_LOCK_TOPIC "party-lock/set" +bool partyLocked = false; +bool partyLockPending = false; + +// ── Double-tap ──────────────────────────────────────────────── +#define DOUBLE_TAP_MS 2000 +static int doubleTapIdx = -1; +static uint32_t doubleTapTime = 0; + +// ── State query ─────────────────────────────────────────────── +static int queryIdx = -1; +static uint32_t queryAt = 0; + +// ── Display ──────────────────────────────────────────────────── +#define PIN_LCD_BL 27 +#define TFT_ROTATION 1 // landscape (480x320) +TFT_eSPI tft = TFT_eSPI(); + +// ── MQTT ─────────────────────────────────────────────────────── +WiFiClient wifi; +PubSubClient mqtt(wifi); + +String clientId; +bool mqttConnected = false; +bool wifiConnected = false; + +// ── Touch ────────────────────────────────────────────────────── +// TFT_eSPI handles touch via TOUCH_CS / TOUCH_IRQ from User_Setup.h. +// Calibration from cyd-calibrate sketch: +// raw_x → screen_x (linear, both axes increase rightward) +// raw_y → screen_y (inverted — high raw_y = top of screen) +// Range: raw_x 31..454, raw_y 32..281 +#define TOUCH_MIN_RAW_X 31 +#define TOUCH_MAX_RAW_X 454 +#define TOUCH_MIN_RAW_Y 32 +#define TOUCH_MAX_RAW_Y 281 + +// ── Layout ───────────────────────────────────────────────────── +#define STATUS_BAR_H 28 +#define BUTTON_GAP 6 +#define BUTTON_PAD 6 + +struct Button { + int x, y, w, h; + int plug_index; +}; + +Button buttons[PLUG_COUNT]; + +// Screen-off button in the upper-right corner of the status bar. +// "OFF" at textSize(2) needs ~48px; make button 64px wide with padding. +#define SCREEN_OFF_W 64 +#define SCREEN_OFF_H STATUS_BAR_H +#define SCREEN_OFF_X (tft.width() - SCREEN_OFF_W - 4) +#define SCREEN_OFF_Y 0 + +// Party lock toggle in the status bar (between MQTT and OFF). +#define LOCK_BTN_W 64 +#define LOCK_BTN_H STATUS_BAR_H +#define LOCK_BTN_X (SCREEN_OFF_X - LOCK_BTN_W - 4) +#define LOCK_BTN_Y 0 + +bool displayOn = true; + +// ── Colors ───────────────────────────────────────────────────── +#define COL_BG 0x0000 +#define COL_STATUS_BG 0x18E3 // dark blue +#define COL_BTN_OFF 0x2945 // dark gray +#define COL_BTN_ON 0x07E0 // green +#define COL_BORDER 0x4208 +#define COL_TEXT 0xFFFF +#define COL_TEXT_DIM 0xAD55 +#define COL_OK 0x07E0 // green dot +#define COL_BAD 0xF800 // red dot +#define COL_TRACE 0x5D5D // gray trace dot + +// ── Forward decls ────────────────────────────────────────────── +void connectWifi(); +void connectMqtt(); +void mqttCallback(char* topic, byte* payload, unsigned int length); +void publishToggle(int idx); +void drawStatusBar(); +void drawButton(int idx); +void drawAll(); +int buttonAt(int tx, int ty); +void layoutButtons(); +uint32_t buttonColor(const Plug& p); + +// ── Forward decls ────────────────────────────────────────────── +void drawButton(int idx); +void drawAll(); +uint32_t buttonColor(const Plug& p); +int buttonAt(int tx, int ty); +void pushTrace(int x, int y); +void drawTraces(); +void pruneTraces(); + +// ── Trace particles ──────────────────────────────────────────── +#define MAX_TRACES 100 +#define TRACE_FADE_MS 1000 + +struct Trace { + int x, y; + uint32_t time; +}; + +static Trace traces[MAX_TRACES]; +static int traceHead = 0; +static int traceCount = 0; + +static uint32_t lastTraceDraw = 0; + +// ── Setup ────────────────────────────────────────────────────── +void setup() { + Serial.begin(115200); + delay(200); + Serial.println(); + Serial.println("=== cyd-dashboard ==="); + + // Backlight on + pinMode(PIN_LCD_BL, OUTPUT); + digitalWrite(PIN_LCD_BL, HIGH); + + // TFT init + tft.init(); + tft.setRotation(TFT_ROTATION); + tft.fillScreen(COL_BG); + + // Show "booting" message + tft.setTextColor(COL_TEXT, COL_BG); + tft.setTextSize(2); + tft.setCursor(10, 10); + tft.println("booting..."); + + layoutButtons(); + + // MQTT client id with random suffix so reconnects don't collide + uint32_t r = esp_random(); + char buf[32]; + snprintf(buf, sizeof(buf), "cyd-%08X", r); + clientId = buf; + mqtt.setServer(MQTT_HOST, MQTT_PORT); + mqtt.setCallback(mqttCallback); + mqtt.setBufferSize(512); + + connectWifi(); + + // Touch self-test: print raw Z to confirm XPT2046 is responding + uint16_t tz = tft.getTouchRawZ(); + Serial.printf("touch self-test: raw Z=%u (non-zero = controller detected)\n", tz); + + drawAll(); +} + +// ── Loop ─────────────────────────────────────────────────────── +void loop() { + if (WiFi.status() != WL_CONNECTED) { + wifiConnected = false; + // (Power note: WiFi TX on marginal hubs (unpowered USB) can cause brownouts. + // If disconnects recur every ~5s, move board to direct USB or powered hub.) + connectWifi(); + } else { + wifiConnected = true; + } + + if (wifiConnected && !mqtt.connected()) { + connectMqtt(); + } + mqtt.loop(); + + // Touch polling — simple debounced tap. + // From cyd-calibrate: raw_x → screen_x (linear), raw_y → screen_y (inverted). + // Clamp because the OFF button sits at sy=0 and user taps above the + // calibrated max produce negative sy without clamping. + // touchWasDown: only act on finger-DOWN edge, ignore drag/repeat. + // lastTraceX/Y: last position where a trace dot was drawn for drag feedback. + static bool touchWasDown = false; + static int lastTraceX = -1, lastTraceY = -1; + static uint32_t last_touch = 0; + uint16_t tx, ty; + bool nowTouching = tft.getTouch(&tx, &ty, 100); + if (nowTouching && !touchWasDown && millis() - last_touch > 250) { + int sx = constrain(map(tx, TOUCH_MIN_RAW_X, TOUCH_MAX_RAW_X, 0, tft.width()), + 0, tft.width()); + int sy = constrain(map(ty, TOUCH_MIN_RAW_Y, TOUCH_MAX_RAW_Y, tft.height(), 0), + 0, tft.height()); + + // Zone classification (for debug + action) + const char* zone = "unknown"; + if (!displayOn) { + zone = "WAKE"; + } else if (sy < STATUS_BAR_H) { + if (sx >= SCREEN_OFF_X) zone = "OFF-btn"; + else if (sx >= LOCK_BTN_X) zone = "lock-btn"; + else zone = "status-bar"; + } else { + int idx = buttonAt(sx, sy); + if (idx >= 0) zone = plugs[idx].name; + else zone = "between-buttons"; + } + Serial.printf("touch raw=(%u,%u) screen=(%d,%d) zone=%s (off_btn=%d..%d)\n", + tx, ty, sx, sy, zone, SCREEN_OFF_X, SCREEN_OFF_X + SCREEN_OFF_W); + + if (!displayOn) { + digitalWrite(PIN_LCD_BL, HIGH); + displayOn = true; + Serial.println("display: on (wake)"); + last_touch = millis(); + return; + } + + // Party lock toggle: in the status bar between MQTT and OFF. + if (sy < STATUS_BAR_H && + sx >= LOCK_BTN_X && sx < LOCK_BTN_X + LOCK_BTN_W) { + doubleTapIdx = -1; + partyLocked = !partyLocked; + mqtt.publish(PARTY_LOCK_TOPIC, partyLocked ? "ON" : "OFF"); + drawStatusBar(); + Serial.printf("party-lock: %s\n", partyLocked ? "ON" : "OFF"); + last_touch = millis(); + return; + } + + // Screen-off button: any tap in the upper-right of the status bar counts, + // even if sy would extrapolate slightly negative without clamping. + if (sy < STATUS_BAR_H && + sx >= SCREEN_OFF_X && sx < SCREEN_OFF_X + SCREEN_OFF_W) { + digitalWrite(PIN_LCD_BL, LOW); + displayOn = false; + Serial.println("display: off"); + last_touch = millis(); + return; + } + + int idx = buttonAt(sx, sy); + if (idx >= 0) { + char dbg[64]; + snprintf(dbg, sizeof(dbg), "tap %s at (%d,%d) locked=%d", + plugs[idx].name, sx, sy, partyLocked); + mqtt.publish("cyd-debug/touch", dbg); + if (partyLocked) { + tft.fillRoundRect(LOCK_BTN_X, LOCK_BTN_Y, LOCK_BTN_W, LOCK_BTN_H, 4, COL_BAD); + tft.setTextColor(COL_TEXT, COL_BAD); + tft.setTextSize(2); + tft.setCursor(LOCK_BTN_X + 6, 4); + tft.print("LOCK"); + delay(150); + drawStatusBar(); + } else if (idx == 3) { + // DJ Booth requires double-tap + uint32_t now = millis(); + if (doubleTapIdx == 3 && now - doubleTapTime < DOUBLE_TAP_MS) { + doubleTapIdx = -1; + publishToggle(idx); + } else { + doubleTapIdx = 3; + doubleTapTime = now; + drawButton(idx); + tft.setTextColor(COL_TEXT_DIM, COL_BTN_OFF); + tft.setTextSize(1); + tft.setCursor(buttons[idx].x + 4, buttons[idx].y + buttons[idx].h - 14); + tft.print("tap again"); + } + } else { + doubleTapIdx = -1; + publishToggle(idx); + } + last_touch = millis(); + } + touchWasDown = true; + } else if (nowTouching && touchWasDown) { + int sx = constrain(map(tx, TOUCH_MIN_RAW_X, TOUCH_MAX_RAW_X, 0, tft.width()), 0, tft.width()); + int sy = constrain(map(ty, TOUCH_MIN_RAW_Y, TOUCH_MAX_RAW_Y, tft.height(), 0), 0, tft.height()); + pushTrace(sx, sy); + touchWasDown = true; + } else if (!nowTouching) { + touchWasDown = false; + } + // Trace particle upkeep: prune + draw every 30ms + if (millis() - lastTraceDraw > 30) { + pruneTraces(); + drawTraces(); + lastTraceDraw = millis(); + } + + // Double-tap timeout: reset if user didn't tap again in time + if (doubleTapIdx >= 0 && millis() - doubleTapTime >= DOUBLE_TAP_MS) { + doubleTapIdx = -1; + if (PLUG_COUNT > 3) drawButton(3); + } + + // Delayed state query via /get to bypass party-lock ACL + if (queryIdx >= 0 && millis() >= queryAt) { + char get_topic[64]; + snprintf(get_topic, sizeof(get_topic), "%s/get", plugs[queryIdx].topic); + mqtt.publish(get_topic, "{\"state\":\"\"}"); + Serial.printf("queried state for %s via /get\n", plugs[queryIdx].name); + queryIdx = -1; + } +} + +// ── Trace particles ──────────────────────────────────────────── +int buttonAt(int tx, int ty); + +void pushTrace(int x, int y) { + if (buttonAt(x, y) >= 0) return; + traces[traceHead] = {x, y, millis()}; + traceHead = (traceHead + 1) % MAX_TRACES; + if (traceCount < MAX_TRACES) traceCount++; +} + +void drawTraces() { + uint32_t now = millis(); + for (int i = 0; i < traceCount; i++) { + int idx = (traceHead - traceCount + i + MAX_TRACES) % MAX_TRACES; + uint32_t age = now - traces[idx].time; + if (age > TRACE_FADE_MS) continue; + int bright = 31 - (31 * age / TRACE_FADE_MS); + if (bright < 0) bright = 0; + uint16_t color = tft.color565(bright, bright, bright); + tft.fillCircle(traces[idx].x, traces[idx].y, 3, color); + } +} + +void pruneTraces() { + uint32_t now = millis(); + int prune = 0; + while (prune < traceCount) { + int idx = (traceHead - traceCount + prune + MAX_TRACES) % MAX_TRACES; + if (now - traces[idx].time <= TRACE_FADE_MS) break; + tft.fillCircle(traces[idx].x, traces[idx].y, 3, COL_BG); + prune++; + } + if (prune > 0) { + traceCount -= prune; + } +} + +// ── WiFi ─────────────────────────────────────────────────────── +void connectWifi() { + if (WiFi.status() == WL_CONNECTED) { + if (!wifiConnected) { + Serial.printf("WiFi: connected to %s, IP=%s\n", + WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); + wifiConnected = true; + drawStatusBar(); + } + return; + } + + static int ssid_idx = 0; + static uint32_t last_attempt = 0; + static uint32_t first_try_at = 0; + if (millis() - last_attempt < 5000) return; // back off between attempts + + // If we've been trying the same SSID for >15s without success, give up and move on + if (first_try_at == 0) first_try_at = millis(); + if (millis() - first_try_at > 15000) { + Serial.printf("WiFi: giving up on %s after 15s\n", WIFI_SSIDS[ssid_idx]); + WiFi.disconnect(); + ssid_idx = (ssid_idx + 1) % WIFI_NETWORK_COUNT; + first_try_at = millis(); + last_attempt = millis(); + return; + } + + Serial.printf("WiFi: trying %s\n", WIFI_SSIDS[ssid_idx]); + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSIDS[ssid_idx], WIFI_PASSPHRASES[ssid_idx]); + + last_attempt = millis(); +} + +// ── MQTT ─────────────────────────────────────────────────────── +void connectMqtt() { + if (mqtt.connected()) return; + Serial.printf("MQTT: connecting as %s\n", clientId.c_str()); + if (mqtt.connect(clientId.c_str())) { + Serial.println("MQTT: connected"); + mqttConnected = true; + // Subscribe to all plug state topics + for (int i = 0; i < PLUG_COUNT; i++) { + mqtt.subscribe(plugs[i].topic); + } + mqtt.subscribe(PARTY_LOCK_TOPIC); + mqtt.publish(PARTY_LOCK_TOPIC, partyLocked ? "ON" : "OFF"); + // Query current state from each plug via /get to bypass party-lock ACL. + for (int i = 0; i < PLUG_COUNT; i++) { + char get_topic[64]; + snprintf(get_topic, sizeof(get_topic), "%s/get", plugs[i].topic); + mqtt.publish(get_topic, "{\"state\":\"\"}"); + } + } else { + Serial.printf("MQTT: failed rc=%d\n", mqtt.state()); + mqttConnected = false; + } + drawStatusBar(); +} + +void mqttCallback(char* topic, byte* payload, unsigned int length) { + // Party lock state + if (strcmp(topic, PARTY_LOCK_TOPIC) == 0) { + char s[4]; + int len = min((unsigned int)3, length); + memcpy(s, payload, len); + s[len] = 0; + bool new_state = (strcmp(s, "ON") == 0); + if (partyLocked != new_state) { + partyLocked = new_state; + drawStatusBar(); + } + return; + } + + // Match topic to a plug + int idx = -1; + for (int i = 0; i < PLUG_COUNT; i++) { + if (strcmp(topic, plugs[i].topic) == 0) { idx = i; break; } + } + if (idx < 0) return; + + // Parse JSON, look for "state" + StaticJsonDocument<256> doc; + DeserializationError err = deserializeJson(doc, payload, length); + if (err) { + Serial.printf("JSON parse err on %s: %s\n", topic, err.c_str()); + return; + } + const char* state = doc["state"] | ""; + bool new_state = (strcmp(state, "ON") == 0); + if (plugs[idx].state != new_state) { + Serial.printf("state update: %s -> %s\n", plugs[idx].name, state); + plugs[idx].state = new_state; + } + drawButton(idx); +} + +void publishToggle(int idx) { + if (!mqtt.connected()) return; + char topic[64]; + snprintf(topic, sizeof(topic), "%s/set", plugs[idx].topic); + mqtt.publish(topic, "{\"state\":\"TOGGLE\"}"); + Serial.printf("published TOGGLE on %s\n", topic); + // Schedule a state query 600ms later so display converges to real state + queryIdx = idx; + queryAt = millis() + 600; +} + +// ── Drawing ──────────────────────────────────────────────────── +void layoutButtons() { + // 2x2 grid below status bar + int grid_top = STATUS_BAR_H + BUTTON_PAD; + int grid_h = tft.height() - grid_top - BUTTON_PAD; + int grid_w = tft.width() - 2 * BUTTON_PAD; + int btn_w = (grid_w - BUTTON_GAP) / 2; + int btn_h = (grid_h - BUTTON_GAP) / 2; + + for (int i = 0; i < PLUG_COUNT; i++) { + int col = i % 2; + int row = i / 2; + buttons[i] = { + BUTTON_PAD + col * (btn_w + BUTTON_GAP), + grid_top + row * (btn_h + BUTTON_GAP), + btn_w, btn_h, + i, + }; + } +} + +void drawStatusBar() { + tft.fillRect(0, 0, tft.width(), STATUS_BAR_H, COL_STATUS_BG); + tft.setTextSize(2); + tft.setTextColor(COL_TEXT, COL_STATUS_BG); + + // WiFi dot + tft.fillCircle(10, STATUS_BAR_H / 2, 4, wifiConnected ? COL_OK : COL_BAD); + tft.setCursor(20, 6); + tft.print(WiFi.localIP()); + + // MQTT dot (left of screen-off button) + int mqtt_x = tft.width() / 2 - 20; + tft.fillCircle(mqtt_x, STATUS_BAR_H / 2, 4, mqttConnected ? COL_OK : COL_BAD); + tft.setCursor(mqtt_x + 10, 6); + tft.print("MQTT"); + + // Party lock toggle (between MQTT and OFF) + uint32_t lock_bg = partyLocked ? COL_BAD : COL_STATUS_BG; + tft.fillRoundRect(LOCK_BTN_X, LOCK_BTN_Y, LOCK_BTN_W, LOCK_BTN_H, 4, lock_bg); + tft.drawRoundRect(LOCK_BTN_X, LOCK_BTN_Y, LOCK_BTN_W, LOCK_BTN_H, 4, COL_BORDER); + tft.setTextColor(COL_TEXT, lock_bg); + tft.setTextSize(2); + tft.setCursor(LOCK_BTN_X + 6, 4); + tft.print(partyLocked ? "LOCK" : "UNLK"); + + // Screen-off button (upper-right corner) + tft.fillRoundRect(SCREEN_OFF_X, SCREEN_OFF_Y, SCREEN_OFF_W, SCREEN_OFF_H, 4, COL_BORDER); + tft.setTextColor(COL_TEXT, COL_BORDER); + tft.setCursor(SCREEN_OFF_X + 10, 4); + tft.print("OFF"); +} + +void drawButton(int idx) { + const Plug& p = plugs[idx]; + const Button& b = buttons[idx]; + + uint32_t bg = buttonColor(p); + tft.fillRoundRect(b.x, b.y, b.w, b.h, 8, bg); + tft.drawRoundRect(b.x, b.y, b.w, b.h, 8, COL_BORDER); + + // Plug name (top) — textSize(2) keeps "Bamboo Lights" within ~237px button + tft.setTextColor(COL_TEXT, bg); + tft.setTextSize(2); + tft.setCursor(b.x + 12, b.y + 10); + tft.println(p.name); + + // State (bottom) + tft.setTextSize(4); + int state_y = b.y + b.h - 40; + tft.setCursor(b.x + 12, state_y); + tft.println(p.state ? "ON" : "OFF"); +} + +void drawAll() { + drawStatusBar(); + for (int i = 0; i < PLUG_COUNT; i++) drawButton(i); +} + +uint32_t buttonColor(const Plug& p) { + if (p.state) return COL_BTN_ON; + return COL_BTN_OFF; +} + +int buttonAt(int tx, int ty) { + if (ty < STATUS_BAR_H) return -1; + for (int i = 0; i < PLUG_COUNT; i++) { + const Button& b = buttons[i]; + if (tx >= b.x && tx < b.x + b.w && ty >= b.y && ty < b.y + b.h) return i; + } + return -1; +} diff --git a/arduino/cyd-dashboard/secrets.h.example b/arduino/cyd-dashboard/secrets.h.example new file mode 100644 index 0000000..f7d7279 --- /dev/null +++ b/arduino/cyd-dashboard/secrets.h.example @@ -0,0 +1,16 @@ +#pragma once +// Copy this to secrets.h and fill in real values. +// secrets.h is gitignored. + +// WiFi networks to try in order +static const char* WIFI_SSIDS[] = { + "Dobro Veče", +}; +static const char* WIFI_PASSPHRASES[] = { + "goodnight", +}; +static const int WIFI_NETWORK_COUNT = 1; + +// MQTT broker +#define MQTT_HOST "192.168.81.147" +#define MQTT_PORT 1883