init
This commit is contained in:
commit
1d7af0615e
15 files changed
+942
No files matched your search
@@ -0,0 +1,97 @@
|
||||
#include <ESP32Servo.h>
|
||||
#include <Preferences.h>
|
||||
#include "app.h"
|
||||
#include "config.h"
|
||||
|
||||
static Servo servo;
|
||||
static Preferences prefs;
|
||||
|
||||
static int volumeToUs(uint8_t volume) {
|
||||
return map(volume, 0, 100, SERVO_MIN_US, SERVO_MAX_US);
|
||||
}
|
||||
|
||||
static void applyRelay(bool on) {
|
||||
digitalWrite(PIN_RELAY, on ? RELAY_ACTIVE_LEVEL : !RELAY_ACTIVE_LEVEL);
|
||||
}
|
||||
|
||||
// Owns the servo, the relay and NVS. Other tasks only send commands.
|
||||
void controlTask(void *arg) {
|
||||
pinMode(PIN_RELAY, OUTPUT);
|
||||
|
||||
prefs.begin("volume", false);
|
||||
AppState st;
|
||||
st.volume = prefs.getUChar("vol", 30);
|
||||
st.relay = prefs.getBool("relay", false);
|
||||
setState(st);
|
||||
applyRelay(st.relay);
|
||||
|
||||
servo.setPeriodHertz(50);
|
||||
int currentUs = volumeToUs(st.volume);
|
||||
servo.attach(PIN_SERVO, SERVO_MIN_US, SERVO_MAX_US);
|
||||
servo.writeMicroseconds(currentUs);
|
||||
TickType_t lastMove = xTaskGetTickCount();
|
||||
|
||||
bool dirty = false;
|
||||
TickType_t lastChange = 0;
|
||||
|
||||
Serial.printf("[control] restored volume=%u relay=%d\n", st.volume, st.relay);
|
||||
|
||||
for (;;) {
|
||||
int targetUs = volumeToUs(st.volume);
|
||||
bool moving = currentUs != targetUs;
|
||||
TickType_t wait = (moving || dirty || servo.attached())
|
||||
? pdMS_TO_TICKS(SERVO_UPDATE_MS)
|
||||
: portMAX_DELAY;
|
||||
|
||||
Command cmd;
|
||||
if (xQueueReceive(cmdQueue, &cmd, wait) == pdTRUE) {
|
||||
AppState prev = st;
|
||||
switch (cmd.type) {
|
||||
case CmdType::VolumeDelta:
|
||||
st.volume = constrain((int)st.volume + cmd.value, 0, 100);
|
||||
break;
|
||||
case CmdType::VolumeSet:
|
||||
st.volume = constrain((int)cmd.value, 0, 100);
|
||||
break;
|
||||
case CmdType::RelayToggle:
|
||||
st.relay = !st.relay;
|
||||
break;
|
||||
case CmdType::RelaySet:
|
||||
st.relay = cmd.value != 0;
|
||||
break;
|
||||
}
|
||||
if (st.relay != prev.relay) {
|
||||
applyRelay(st.relay);
|
||||
Serial.printf("[control] relay %s\n", st.relay ? "ON" : "OFF");
|
||||
}
|
||||
if (st.volume != prev.volume || st.relay != prev.relay) {
|
||||
setState(st);
|
||||
dirty = true;
|
||||
lastChange = xTaskGetTickCount();
|
||||
}
|
||||
targetUs = volumeToUs(st.volume);
|
||||
}
|
||||
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
|
||||
// Smooth servo movement toward the target
|
||||
if (currentUs != targetUs) {
|
||||
int diff = targetUs - currentUs;
|
||||
int step = constrain(diff, -SERVO_SPEED_US_PER_STEP, SERVO_SPEED_US_PER_STEP);
|
||||
currentUs += step;
|
||||
if (!servo.attached()) servo.attach(PIN_SERVO, SERVO_MIN_US, SERVO_MAX_US);
|
||||
servo.writeMicroseconds(currentUs);
|
||||
lastMove = now;
|
||||
} else if (servo.attached() && now - lastMove >= pdMS_TO_TICKS(SERVO_DETACH_IDLE_MS)) {
|
||||
servo.detach();
|
||||
}
|
||||
|
||||
// Delayed save to spare flash wear while the encoder is being turned
|
||||
if (dirty && now - lastChange >= pdMS_TO_TICKS(SAVE_DELAY_MS)) {
|
||||
prefs.putUChar("vol", st.volume);
|
||||
prefs.putBool("relay", st.relay);
|
||||
dirty = false;
|
||||
Serial.printf("[control] saved volume=%u relay=%d\n", st.volume, st.relay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
#include <TFT_eSPI.h>
|
||||
#include <WiFi.h>
|
||||
#include <Preferences.h>
|
||||
#include "app.h"
|
||||
#include "config.h"
|
||||
|
||||
static TFT_eSPI tft;
|
||||
|
||||
struct Rect {
|
||||
int x, y, w, h;
|
||||
bool contains(int px, int py) const { return px >= x && px < x + w && py >= y && py < y + h; }
|
||||
};
|
||||
|
||||
// Landscape 320x240 layout
|
||||
static const int kW = 320;
|
||||
static const Rect kBar = {20, 150, 280, 24};
|
||||
static const Rect kBarTouch = {0, 136, 320, 50}; // larger than the bar for easier dragging
|
||||
static const Rect kRelay = {60, 190, 200, 40};
|
||||
static const Rect kMinus = {10, 55, 60, 70};
|
||||
static const Rect kPlus = {250, 55, 60, 70};
|
||||
|
||||
static const uint16_t kBg = TFT_BLACK;
|
||||
static const uint16_t kAccent = 0x3D3F; // light blue
|
||||
static const uint16_t kBarBg = 0x2945; // dark grey
|
||||
static const uint16_t kBtn = 0x2945;
|
||||
static const uint16_t kBtnPressed = 0x3D3F;
|
||||
static const uint16_t kOn = TFT_GREEN;
|
||||
static const uint16_t kOff = 0x4208; // grey
|
||||
|
||||
enum class TouchTarget { None, Minus, Plus, Bar, Relay };
|
||||
|
||||
static void drawButton(const Rect &r, const char *label, bool pressed) {
|
||||
uint16_t bg = pressed ? kBtnPressed : kBtn;
|
||||
tft.fillRoundRect(r.x, r.y, r.w, r.h, 10, bg);
|
||||
tft.setTextColor(TFT_WHITE, bg);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2, 4);
|
||||
}
|
||||
|
||||
static void drawStatic() {
|
||||
tft.fillScreen(kBg);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
tft.setTextColor(TFT_WHITE, kBg);
|
||||
tft.drawString("VOLUME", 10, 8, 4);
|
||||
tft.drawFastHLine(0, 38, kW, kBarBg);
|
||||
tft.drawRect(kBar.x - 2, kBar.y - 2, kBar.w + 4, kBar.h + 4, kBarBg);
|
||||
drawButton(kMinus, "-", false);
|
||||
drawButton(kPlus, "+", false);
|
||||
}
|
||||
|
||||
static void drawVolume(uint8_t volume) {
|
||||
tft.setTextColor(kAccent, kBg);
|
||||
tft.setTextDatum(TR_DATUM);
|
||||
tft.setTextPadding(tft.textWidth("100", 7));
|
||||
tft.drawNumber(volume, 200, 70, 7);
|
||||
tft.setTextPadding(0);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
tft.drawString("%", 208, 94, 4);
|
||||
|
||||
int fill = kBar.w * volume / 100;
|
||||
tft.fillRect(kBar.x, kBar.y, fill, kBar.h, kAccent);
|
||||
tft.fillRect(kBar.x + fill, kBar.y, kBar.w - fill, kBar.h, kBarBg);
|
||||
}
|
||||
|
||||
static void drawRelay(bool on) {
|
||||
uint16_t bg = on ? kOn : kOff;
|
||||
tft.fillRoundRect(kRelay.x, kRelay.y, kRelay.w, kRelay.h, 10, bg);
|
||||
tft.setTextColor(on ? TFT_BLACK : TFT_WHITE, bg);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.drawString(on ? "POWER: ON" : "POWER: OFF", kRelay.x + kRelay.w / 2, kRelay.y + kRelay.h / 2 + 1, 4);
|
||||
}
|
||||
|
||||
static String networkText() {
|
||||
if (WiFi.getMode() & WIFI_MODE_AP) return "AP " + WiFi.softAPIP().toString();
|
||||
if (WiFi.status() == WL_CONNECTED) return WiFi.localIP().toString();
|
||||
return "WiFi...";
|
||||
}
|
||||
|
||||
static void drawNetwork(const String &text) {
|
||||
tft.setTextColor(TFT_LIGHTGREY, kBg);
|
||||
tft.setTextDatum(TR_DATUM);
|
||||
tft.setTextPadding(150);
|
||||
tft.drawString(text, kW - 8, 14, 2);
|
||||
tft.setTextPadding(0);
|
||||
}
|
||||
|
||||
// Loads touch calibration from NVS. Runs calibration if there is none
|
||||
// or if the screen is being touched at power-up.
|
||||
static void setupTouch() {
|
||||
Preferences prefs;
|
||||
prefs.begin("touch", false);
|
||||
uint16_t cal[5];
|
||||
bool haveCal = prefs.getBytes("cal", cal, sizeof(cal)) == sizeof(cal);
|
||||
|
||||
if (!haveCal || tft.getTouchRawZ() > TOUCH_RECAL_PRESSURE) {
|
||||
tft.fillScreen(kBg);
|
||||
tft.setTextColor(TFT_WHITE, kBg);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.drawString("Touch the corners", kW / 2, 100, 4);
|
||||
tft.drawString("as indicated", kW / 2, 130, 4);
|
||||
tft.calibrateTouch(cal, TFT_MAGENTA, kBg, 15);
|
||||
prefs.putBytes("cal", cal, sizeof(cal));
|
||||
Serial.println("[display] touch calibrated");
|
||||
}
|
||||
prefs.end();
|
||||
tft.setTouch(cal);
|
||||
}
|
||||
|
||||
static TouchTarget hitTest(int x, int y) {
|
||||
if (kMinus.contains(x, y)) return TouchTarget::Minus;
|
||||
if (kPlus.contains(x, y)) return TouchTarget::Plus;
|
||||
if (kBarTouch.contains(x, y)) return TouchTarget::Bar;
|
||||
if (kRelay.contains(x, y)) return TouchTarget::Relay;
|
||||
return TouchTarget::None;
|
||||
}
|
||||
|
||||
static int barToVolume(int x) {
|
||||
return constrain((x - kBar.x) * 100 / kBar.w, 0, 100);
|
||||
}
|
||||
|
||||
// The only task touching the TFT and its touch controller (shared SPI bus):
|
||||
// handles touch input, polls the shared state and redraws what changed
|
||||
void displayTask(void *arg) {
|
||||
tft.init();
|
||||
tft.setRotation(1);
|
||||
setupTouch();
|
||||
drawStatic();
|
||||
|
||||
int lastVolume = -1;
|
||||
int lastRelay = -1;
|
||||
String lastNet;
|
||||
TickType_t lastNetCheck = 0;
|
||||
|
||||
TouchTarget active = TouchTarget::None;
|
||||
TickType_t pressStart = 0, lastRepeat = 0;
|
||||
int releaseCount = 0;
|
||||
int lastBarVolume = -1;
|
||||
|
||||
for (;;) {
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
|
||||
// ---- Touch input ----
|
||||
uint16_t tx, ty;
|
||||
if (tft.getTouch(&tx, &ty)) {
|
||||
releaseCount = 0;
|
||||
if (active == TouchTarget::None) {
|
||||
// New press: the target is fixed until release
|
||||
active = hitTest(tx, ty);
|
||||
pressStart = lastRepeat = now;
|
||||
lastBarVolume = -1;
|
||||
switch (active) {
|
||||
case TouchTarget::Minus:
|
||||
drawButton(kMinus, "-", true);
|
||||
sendCommand(CmdType::VolumeDelta, -TOUCH_VOLUME_STEP);
|
||||
break;
|
||||
case TouchTarget::Plus:
|
||||
drawButton(kPlus, "+", true);
|
||||
sendCommand(CmdType::VolumeDelta, TOUCH_VOLUME_STEP);
|
||||
break;
|
||||
case TouchTarget::Relay:
|
||||
sendCommand(CmdType::RelayToggle);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (active == TouchTarget::Bar) {
|
||||
int v = barToVolume(tx);
|
||||
if (v != lastBarVolume) {
|
||||
sendCommand(CmdType::VolumeSet, v);
|
||||
lastBarVolume = v;
|
||||
}
|
||||
} else if ((active == TouchTarget::Minus || active == TouchTarget::Plus) &&
|
||||
now - pressStart >= pdMS_TO_TICKS(TOUCH_REPEAT_DELAY_MS) &&
|
||||
now - lastRepeat >= pdMS_TO_TICKS(TOUCH_REPEAT_MS)) {
|
||||
// Auto-repeat while holding -/+
|
||||
lastRepeat = now;
|
||||
int step = active == TouchTarget::Plus ? TOUCH_VOLUME_STEP : -TOUCH_VOLUME_STEP;
|
||||
sendCommand(CmdType::VolumeDelta, step);
|
||||
}
|
||||
} else if (active != TouchTarget::None && ++releaseCount >= 3) {
|
||||
// Several polls without touch: treat as release (filters short dropouts)
|
||||
if (active == TouchTarget::Minus) drawButton(kMinus, "-", false);
|
||||
if (active == TouchTarget::Plus) drawButton(kPlus, "+", false);
|
||||
active = TouchTarget::None;
|
||||
}
|
||||
|
||||
// ---- State rendering ----
|
||||
AppState s = getState();
|
||||
if (s.volume != lastVolume) {
|
||||
drawVolume(s.volume);
|
||||
lastVolume = s.volume;
|
||||
}
|
||||
if ((int)s.relay != lastRelay) {
|
||||
drawRelay(s.relay);
|
||||
lastRelay = s.relay;
|
||||
}
|
||||
|
||||
if (now - lastNetCheck >= pdMS_TO_TICKS(1000)) {
|
||||
lastNetCheck = now;
|
||||
String net = networkText();
|
||||
if (net != lastNet) {
|
||||
drawNetwork(net);
|
||||
lastNet = net;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(DISPLAY_UPDATE_MS));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "app.h"
|
||||
#include "config.h"
|
||||
|
||||
// Quadrature transition table: index = (prev << 2) | current
|
||||
static const int8_t kTransitions[16] = {
|
||||
0, -1, 1, 0,
|
||||
1, 0, 0, -1,
|
||||
-1, 0, 0, 1,
|
||||
0, 1, -1, 0,
|
||||
};
|
||||
|
||||
static uint8_t readAB() {
|
||||
return (digitalRead(PIN_ENC_CLK) << 1) | digitalRead(PIN_ENC_DT);
|
||||
}
|
||||
|
||||
// Polls the encoder every 1 ms: tolerant to contact bounce, no ISR needed
|
||||
void encoderTask(void *arg) {
|
||||
pinMode(PIN_ENC_CLK, INPUT_PULLUP);
|
||||
pinMode(PIN_ENC_DT, INPUT_PULLUP);
|
||||
pinMode(PIN_ENC_SW, INPUT_PULLUP);
|
||||
|
||||
uint8_t prevAB = readAB();
|
||||
int8_t acc = 0;
|
||||
|
||||
bool btnStable = digitalRead(PIN_ENC_SW);
|
||||
bool btnLast = btnStable;
|
||||
TickType_t btnChanged = xTaskGetTickCount();
|
||||
|
||||
TickType_t lastWake = xTaskGetTickCount();
|
||||
for (;;) {
|
||||
uint8_t ab = readAB();
|
||||
if (ab != prevAB) {
|
||||
acc += kTransitions[(prevAB << 2) | ab];
|
||||
prevAB = ab;
|
||||
if (acc >= ENCODER_STEPS_PER_DETENT || acc <= -ENCODER_STEPS_PER_DETENT) {
|
||||
int dir = acc > 0 ? 1 : -1;
|
||||
if (ENCODER_REVERSE) dir = -dir;
|
||||
acc = 0;
|
||||
sendCommand(CmdType::VolumeDelta, dir * VOLUME_STEP);
|
||||
}
|
||||
}
|
||||
|
||||
// Button: debounce, act on press (active LOW)
|
||||
bool btn = digitalRead(PIN_ENC_SW);
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if (btn != btnLast) {
|
||||
btnLast = btn;
|
||||
btnChanged = now;
|
||||
} else if (btn != btnStable && now - btnChanged >= pdMS_TO_TICKS(BUTTON_DEBOUNCE_MS)) {
|
||||
btnStable = btn;
|
||||
if (btnStable == LOW) sendCommand(CmdType::RelayToggle);
|
||||
}
|
||||
|
||||
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#include <Arduino.h>
|
||||
#include "app.h"
|
||||
|
||||
QueueHandle_t cmdQueue;
|
||||
|
||||
static SemaphoreHandle_t stateMutex;
|
||||
static AppState state = {0, false};
|
||||
|
||||
bool sendCommand(CmdType type, int16_t value) {
|
||||
Command c{type, value};
|
||||
return xQueueSend(cmdQueue, &c, pdMS_TO_TICKS(50)) == pdTRUE;
|
||||
}
|
||||
|
||||
AppState getState() {
|
||||
xSemaphoreTake(stateMutex, portMAX_DELAY);
|
||||
AppState s = state;
|
||||
xSemaphoreGive(stateMutex);
|
||||
return s;
|
||||
}
|
||||
|
||||
void setState(const AppState &s) {
|
||||
xSemaphoreTake(stateMutex, portMAX_DELAY);
|
||||
state = s;
|
||||
xSemaphoreGive(stateMutex);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
cmdQueue = xQueueCreate(32, sizeof(Command));
|
||||
stateMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Control first, so the restored state is ready before others read it
|
||||
xTaskCreatePinnedToCore(controlTask, "control", 4096, nullptr, 3, nullptr, 1);
|
||||
xTaskCreatePinnedToCore(encoderTask, "encoder", 3072, nullptr, 4, nullptr, 1);
|
||||
xTaskCreatePinnedToCore(webTask, "web", 8192, nullptr, 1, nullptr, 0);
|
||||
xTaskCreatePinnedToCore(displayTask, "display", 4096, nullptr, 1, nullptr, 1);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// All work is done in FreeRTOS tasks
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
static const char INDEX_HTML[] PROGMEM = R"HTML(<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Громкость</title>
|
||||
<style>
|
||||
:root { --bg:#111; --card:#1d1d1d; --fg:#eee; --accent:#3fa7ff; --on:#2ecc71; --off:#555; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:system-ui,sans-serif; background:var(--bg); color:var(--fg);
|
||||
display:flex; justify-content:center; padding:24px 16px; }
|
||||
.card { background:var(--card); border-radius:16px; padding:24px; width:100%; max-width:380px; }
|
||||
h1 { font-size:18px; margin:0 0 20px; font-weight:600; }
|
||||
.vol { font-size:64px; font-weight:700; text-align:center; font-variant-numeric:tabular-nums; }
|
||||
.vol span { font-size:24px; opacity:.6; }
|
||||
input[type=range] { width:100%; margin:16px 0; accent-color:var(--accent); height:32px; }
|
||||
.row { display:flex; gap:12px; }
|
||||
button { flex:1; font-size:22px; padding:14px; border:0; border-radius:12px;
|
||||
background:#2a2a2a; color:var(--fg); cursor:pointer; }
|
||||
button:active { transform:scale(.97); }
|
||||
#relay { margin-top:20px; width:100%; font-size:18px; background:var(--off); }
|
||||
#relay.on { background:var(--on); color:#000; }
|
||||
.status { margin-top:16px; font-size:12px; opacity:.5; text-align:center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Регулятор громкости</h1>
|
||||
<div class="vol"><b id="val">--</b><span>%</span></div>
|
||||
<input id="slider" type="range" min="0" max="100" step="1">
|
||||
<div class="row">
|
||||
<button onclick="delta(-5)">−</button>
|
||||
<button onclick="delta(5)">+</button>
|
||||
</div>
|
||||
<button id="relay" onclick="relay()">Реле</button>
|
||||
<div class="status" id="status">подключение…</div>
|
||||
</div>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
let dragging = false, sendTimer = null;
|
||||
|
||||
function render(s) {
|
||||
$('val').textContent = s.volume;
|
||||
if (!dragging) $('slider').value = s.volume;
|
||||
$('relay').classList.toggle('on', s.relay);
|
||||
$('relay').textContent = s.relay ? 'Реле: ВКЛ' : 'Реле: ВЫКЛ';
|
||||
$('status').textContent = 'онлайн';
|
||||
}
|
||||
|
||||
async function api(path, method = 'GET') {
|
||||
try {
|
||||
const r = await fetch(path, { method });
|
||||
if (r.ok) render(await r.json());
|
||||
} catch (e) { $('status').textContent = 'нет связи'; }
|
||||
}
|
||||
|
||||
const delta = d => api('/api/volume?delta=' + d, 'POST');
|
||||
const relay = () => api('/api/relay?state=toggle', 'POST');
|
||||
|
||||
const slider = $('slider');
|
||||
slider.addEventListener('pointerdown', () => dragging = true);
|
||||
slider.addEventListener('pointerup', () => dragging = false);
|
||||
slider.addEventListener('input', () => {
|
||||
$('val').textContent = slider.value;
|
||||
clearTimeout(sendTimer);
|
||||
sendTimer = setTimeout(() => api('/api/volume?value=' + slider.value, 'POST'), 80);
|
||||
});
|
||||
slider.addEventListener('change', () => {
|
||||
dragging = false;
|
||||
api('/api/volume?value=' + slider.value, 'POST');
|
||||
});
|
||||
|
||||
api('/api/state');
|
||||
setInterval(() => { if (!dragging) api('/api/state'); }, 700);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)HTML";
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include "app.h"
|
||||
#include "config.h"
|
||||
#include "web_page.h"
|
||||
|
||||
static WebServer server(80);
|
||||
|
||||
static void sendState() {
|
||||
AppState s = getState();
|
||||
char json[48];
|
||||
snprintf(json, sizeof(json), "{\"volume\":%u,\"relay\":%s}", s.volume, s.relay ? "true" : "false");
|
||||
server.send(200, "application/json", json);
|
||||
}
|
||||
|
||||
// Command is processed asynchronously by controlTask, so answer with the expected state
|
||||
static void sendStateAfter(CmdType type, int16_t value) {
|
||||
if (!sendCommand(type, value)) {
|
||||
server.send(503, "text/plain", "busy");
|
||||
return;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
sendState();
|
||||
}
|
||||
|
||||
static void handleVolume() {
|
||||
if (server.hasArg("value")) {
|
||||
sendStateAfter(CmdType::VolumeSet, server.arg("value").toInt());
|
||||
} else if (server.hasArg("delta")) {
|
||||
sendStateAfter(CmdType::VolumeDelta, server.arg("delta").toInt());
|
||||
} else {
|
||||
server.send(400, "text/plain", "need value or delta");
|
||||
}
|
||||
}
|
||||
|
||||
static void handleRelay() {
|
||||
String st = server.arg("state");
|
||||
if (st == "toggle" || st.isEmpty()) {
|
||||
sendStateAfter(CmdType::RelayToggle, 0);
|
||||
} else {
|
||||
sendStateAfter(CmdType::RelaySet, (st == "1" || st == "on") ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void startWifi() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
Serial.printf("[web] connecting to %s", WIFI_SSID);
|
||||
|
||||
TickType_t start = xTaskGetTickCount();
|
||||
while (WiFi.status() != WL_CONNECTED &&
|
||||
xTaskGetTickCount() - start < pdMS_TO_TICKS(WIFI_CONNECT_TIMEOUT_MS)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
Serial.print('.');
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("[web] connected, IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
} else {
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(AP_SSID, AP_PASSWORD);
|
||||
Serial.printf("[web] STA failed, AP \"%s\" started, IP: %s\n", AP_SSID,
|
||||
WiFi.softAPIP().toString().c_str());
|
||||
}
|
||||
|
||||
if (MDNS.begin(MDNS_HOSTNAME)) {
|
||||
MDNS.addService("http", "tcp", 80);
|
||||
Serial.printf("[web] http://%s.local\n", MDNS_HOSTNAME);
|
||||
}
|
||||
}
|
||||
|
||||
void webTask(void *arg) {
|
||||
startWifi();
|
||||
|
||||
server.on("/", HTTP_GET, [] { server.send_P(200, "text/html", INDEX_HTML); });
|
||||
server.on("/api/state", HTTP_GET, sendState);
|
||||
server.on("/api/volume", HTTP_POST, handleVolume);
|
||||
server.on("/api/relay", HTTP_POST, handleRelay);
|
||||
server.onNotFound([] { server.send(404, "text/plain", "not found"); });
|
||||
server.begin();
|
||||
|
||||
for (;;) {
|
||||
server.handleClient();
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user