init
This commit is contained in:
commit
1d7af0615e
15 files changed
+942
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack",
|
||||||
|
"pioarduino.pioarduino-ide"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Регулятор громкости на ESP32
|
||||||
|
|
||||||
|
Регулятор крутит потенциометр громкости сервоприводом. Управлять им можно двумя способами: поворотом энкодера и через веб-интерфейс по Wi‑Fi. Кнопка энкодера включает и выключает питание через реле. Громкость и состояние реле видны на TFT-дисплее.
|
||||||
|
|
||||||
|
Прошивка написана на Arduino + FreeRTOS (PlatformIO, плата `esp32dev`).
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
### Энкодер (например, KY-040)
|
||||||
|
|
||||||
|
| Вывод энкодера | GPIO ESP32 | Примечание |
|
||||||
|
|---|---|---|
|
||||||
|
| CLK (A) | **32** | внутренняя подтяжка к 3.3 В |
|
||||||
|
| DT (B) | **33** | внутренняя подтяжка к 3.3 В |
|
||||||
|
| SW (кнопка) | **25** | активный уровень LOW, подтяжка к 3.3 В |
|
||||||
|
| + | 3.3V | |
|
||||||
|
| GND | GND | |
|
||||||
|
|
||||||
|
### Сервопривод
|
||||||
|
|
||||||
|
| Вывод серво | Подключение | Примечание |
|
||||||
|
|---|---|---|
|
||||||
|
| Сигнал (оранжевый/жёлтый) | **GPIO 13** | ШИМ 50 Гц, 500–2400 мкс |
|
||||||
|
| + (красный) | 5 В от отдельного источника | не от 3.3V ESP32 |
|
||||||
|
| GND (коричневый) | GND | общий с ESP32 |
|
||||||
|
|
||||||
|
### Реле
|
||||||
|
|
||||||
|
| Вывод модуля реле | Подключение | Примечание |
|
||||||
|
|---|---|---|
|
||||||
|
| IN | **GPIO 26** | уровень включения задаётся `RELAY_ACTIVE_LEVEL` |
|
||||||
|
| VCC | 5V / 3.3V (по модулю) | |
|
||||||
|
| GND | GND | |
|
||||||
|
|
||||||
|
### TFT-дисплей ILI9341 (240×320, SPI)
|
||||||
|
|
||||||
|
| Вывод дисплея | GPIO ESP32 |
|
||||||
|
|---|---|
|
||||||
|
| MOSI (SDI) | **23** |
|
||||||
|
| MISO (SDO) | **19** |
|
||||||
|
| SCK | **18** |
|
||||||
|
| CS | **5** |
|
||||||
|
| DC (RS) | **21** |
|
||||||
|
| RESET | **15** |
|
||||||
|
| LED (подсветка) | **22** |
|
||||||
|
| T_CS (тачскрин) | **4** |
|
||||||
|
| VCC | 3.3V |
|
||||||
|
| GND | GND |
|
||||||
|
|
||||||
|
Сенсор XPT2046 подключается к той же шине SPI: T_DIN к MOSI, T_DO к MISO, T_CLK к SCK, T_CS к GPIO 4. Вывод T_IRQ не нужен.
|
||||||
|
|
||||||
|
### Управление с сенсорного экрана
|
||||||
|
|
||||||
|
- Кнопки **−** и **+** меняют громкость на 1 %. Если держать кнопку нажатой, громкость меняется непрерывно.
|
||||||
|
- Тап по **полосе громкости** сразу ставит нужное значение, по полосе можно вести пальцем.
|
||||||
|
- Плашка **POWER** включает и выключает реле.
|
||||||
|
|
||||||
|
**Калибровка.** При первом включении на экране появятся стрелки в углах, нажимайте их по очереди. Калибровка сохраняется в NVS. Чтобы откалибровать заново, держите палец на экране при включении питания.
|
||||||
|
|
||||||
|
### Сводная таблица GPIO
|
||||||
|
|
||||||
|
| GPIO | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| 4 | TFT T_CS |
|
||||||
|
| 5 | TFT CS |
|
||||||
|
| 13 | Серво, сигнал |
|
||||||
|
| 15 | TFT RESET |
|
||||||
|
| 18 | TFT SCK |
|
||||||
|
| 19 | TFT MISO |
|
||||||
|
| 21 | TFT DC |
|
||||||
|
| 22 | TFT подсветка |
|
||||||
|
| 23 | TFT MOSI |
|
||||||
|
| 25 | Энкодер, кнопка SW |
|
||||||
|
| 26 | Реле IN |
|
||||||
|
| 32 | Энкодер CLK |
|
||||||
|
| 33 | Энкодер DT |
|
||||||
|
|
||||||
|
Пины энкодера, серво и реле меняются в [include/config.h](include/config.h). Пины дисплея меняются в `build_flags` в [platformio.ini](platformio.ini).
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
Все параметры находятся в [include/config.h](include/config.h):
|
||||||
|
|
||||||
|
| Параметр | Что задаёт |
|
||||||
|
|---|---|
|
||||||
|
| `WIFI_SSID`, `WIFI_PASSWORD` | домашняя сеть Wi‑Fi |
|
||||||
|
| `AP_SSID`, `AP_PASSWORD` | собственная точка доступа. ESP32 включает её, если не смог подключиться к сети за 15 с |
|
||||||
|
| `MDNS_HOSTNAME` | имя в сети, по умолчанию `http://volume.local` |
|
||||||
|
| `ENCODER_REVERSE` | `1`, если громкость меняется в обратную сторону |
|
||||||
|
| `VOLUME_STEP` | шаг громкости за один щелчок энкодера, % |
|
||||||
|
| `SERVO_MIN_US`, `SERVO_MAX_US` | длительность импульса для 0 % и 100 %. Подберите под ход потенциометра |
|
||||||
|
| `SERVO_SPEED_US_PER_STEP` | скорость плавного хода серво |
|
||||||
|
| `RELAY_ACTIVE_LEVEL` | `HIGH` или `LOW`. Многие модули реле включаются уровнем `LOW` |
|
||||||
|
| `TOUCH_VOLUME_STEP` | шаг громкости для кнопок −/+ на экране, % |
|
||||||
|
| `TOUCH_REPEAT_DELAY_MS`, `TOUCH_REPEAT_MS` | задержка перед автоповтором при удержании −/+ и его период |
|
||||||
|
|
||||||
|
## Сборка и прошивка
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pio run -t upload
|
||||||
|
pio device monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
После запуска прошивка пишет в монитор порта IP-адрес. Тот же адрес виден в правом верхнем углу дисплея.
|
||||||
|
|
||||||
|
## Веб-интерфейс и API
|
||||||
|
|
||||||
|
Откройте `http://volume.local` или IP-адрес устройства. На странице есть ползунок громкости, кнопки ±5 и кнопка реле.
|
||||||
|
|
||||||
|
| Запрос | Действие |
|
||||||
|
|---|---|
|
||||||
|
| `GET /api/state` | вернуть текущее состояние: `{"volume":30,"relay":false}` |
|
||||||
|
| `POST /api/volume?value=N` | установить громкость 0–100 |
|
||||||
|
| `POST /api/volume?delta=N` | изменить громкость на N |
|
||||||
|
| `POST /api/relay?state=toggle\|1\|0` | переключить, включить или выключить реле |
|
||||||
|
|
||||||
|
Громкость и состояние реле сохраняются в памяти ESP32 (NVS) и восстанавливаются после перезагрузки.
|
||||||
|
|
||||||
|
## Устройство прошивки
|
||||||
|
|
||||||
|
| Задача FreeRTOS | Файл | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| `control` | [src/control_task.cpp](src/control_task.cpp) | управляет серво, реле и сохранением в NVS. Только эта задача работает с этим оборудованием |
|
||||||
|
| `encoder` | [src/encoder_task.cpp](src/encoder_task.cpp) | опрашивает энкодер и кнопку раз в 1 мс |
|
||||||
|
| `web` | [src/web_task.cpp](src/web_task.cpp) | подключение к Wi‑Fi, HTTP-сервер, mDNS |
|
||||||
|
| `display` | [src/display_task.cpp](src/display_task.cpp) | выводит состояние на TFT, обрабатывает касания |
|
||||||
|
|
||||||
|
Энкодер, сенсорный экран и веб-интерфейс отправляют команды в общую очередь, а задача `control` их выполняет. Текущее состояние хранится в общей переменной, защищённой мьютексом.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
This directory is intended for project header files.
|
||||||
|
|
||||||
|
A header file is a file containing C declarations and macro definitions
|
||||||
|
to be shared between several project source files. You request the use of a
|
||||||
|
header file in your project source file (C, C++, etc) located in `src` folder
|
||||||
|
by including it, with the C preprocessing directive `#include'.
|
||||||
|
|
||||||
|
```src/main.c
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including a header file produces the same results as copying the header file
|
||||||
|
into each source file that needs it. Such copying would be time-consuming
|
||||||
|
and error-prone. With a header file, the related declarations appear
|
||||||
|
in only one place. If they need to be changed, they can be changed in one
|
||||||
|
place, and programs that include the header file will automatically use the
|
||||||
|
new version when next recompiled. The header file eliminates the labor of
|
||||||
|
finding and changing all the copies as well as the risk that a failure to
|
||||||
|
find one copy will result in inconsistencies within a program.
|
||||||
|
|
||||||
|
In C, the convention is to give header files names that end with `.h'.
|
||||||
|
|
||||||
|
Read more about using header files in official GCC documentation:
|
||||||
|
|
||||||
|
* Include Syntax
|
||||||
|
* Include Operation
|
||||||
|
* Once-Only Headers
|
||||||
|
* Computed Includes
|
||||||
|
|
||||||
|
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
enum class CmdType : uint8_t {
|
||||||
|
VolumeDelta, // value: signed change in %
|
||||||
|
VolumeSet, // value: absolute 0..100
|
||||||
|
RelayToggle,
|
||||||
|
RelaySet, // value: 0 / 1
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Command {
|
||||||
|
CmdType type;
|
||||||
|
int16_t value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
uint8_t volume; // 0..100
|
||||||
|
bool relay;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Created in main.cpp
|
||||||
|
extern QueueHandle_t cmdQueue;
|
||||||
|
|
||||||
|
bool sendCommand(CmdType type, int16_t value = 0);
|
||||||
|
AppState getState();
|
||||||
|
void setState(const AppState &s);
|
||||||
|
|
||||||
|
void controlTask(void *arg);
|
||||||
|
void encoderTask(void *arg);
|
||||||
|
void webTask(void *arg);
|
||||||
|
void displayTask(void *arg);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// ---------- Wi-Fi ----------
|
||||||
|
#define WIFI_SSID "your_ssid"
|
||||||
|
#define WIFI_PASSWORD "your_password"
|
||||||
|
#define WIFI_CONNECT_TIMEOUT_MS 15000
|
||||||
|
|
||||||
|
// If the station connection fails, the ESP32 starts its own access point
|
||||||
|
#define AP_SSID "VolumeControl"
|
||||||
|
#define AP_PASSWORD "12345678" // min 8 chars
|
||||||
|
|
||||||
|
#define MDNS_HOSTNAME "volume" // http://volume.local
|
||||||
|
|
||||||
|
// ---------- Pins (chosen to not clash with the TFT pins in platformio.ini) ----------
|
||||||
|
#define PIN_ENC_CLK 32
|
||||||
|
#define PIN_ENC_DT 33
|
||||||
|
#define PIN_ENC_SW 25
|
||||||
|
#define PIN_SERVO 13
|
||||||
|
#define PIN_RELAY 26
|
||||||
|
|
||||||
|
// ---------- Encoder ----------
|
||||||
|
#define ENCODER_REVERSE 0 // set to 1 if rotation direction is inverted
|
||||||
|
#define ENCODER_STEPS_PER_DETENT 4
|
||||||
|
#define VOLUME_STEP 2 // % per encoder detent
|
||||||
|
#define BUTTON_DEBOUNCE_MS 30
|
||||||
|
|
||||||
|
// ---------- Servo ----------
|
||||||
|
// Pulse widths for volume 0% and 100%. Tune to the real travel of the pot.
|
||||||
|
#define SERVO_MIN_US 500
|
||||||
|
#define SERVO_MAX_US 2400
|
||||||
|
#define SERVO_SPEED_US_PER_STEP 20 // smoothing: max pulse change per update
|
||||||
|
#define SERVO_UPDATE_MS 10
|
||||||
|
#define SERVO_DETACH_IDLE_MS 1000 // release servo when idle to avoid hum/jitter
|
||||||
|
|
||||||
|
// ---------- Relay ----------
|
||||||
|
#define RELAY_ACTIVE_LEVEL HIGH // many relay modules are active LOW
|
||||||
|
|
||||||
|
// ---------- Display ----------
|
||||||
|
#define DISPLAY_UPDATE_MS 20 // also the touch polling period
|
||||||
|
|
||||||
|
// ---------- Touch ----------
|
||||||
|
#define TOUCH_VOLUME_STEP 1 // % per tap on -/+
|
||||||
|
#define TOUCH_REPEAT_DELAY_MS 400 // hold -/+ this long to start auto-repeat
|
||||||
|
#define TOUCH_REPEAT_MS 80
|
||||||
|
#define TOUCH_RECAL_PRESSURE 400 // touch at power-up stronger than this -> recalibrate
|
||||||
|
|
||||||
|
// ---------- Persistence ----------
|
||||||
|
#define SAVE_DELAY_MS 2000 // write to NVS after values stop changing
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
This directory is intended for project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link into the executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in a separate directory
|
||||||
|
("lib/your_library_name/[Code]").
|
||||||
|
|
||||||
|
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||||
|
| |
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |
|
||||||
|
| |- README --> THIS FILE
|
||||||
|
|
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
Example contents of `src/main.c` using Foo and Bar:
|
||||||
|
```
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||||
|
libraries by scanning project source files.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
bodmer/TFT_eSPI @ ^2.5.43
|
||||||
|
madhephaestus/ESP32Servo @ ^3.0.5
|
||||||
|
|
||||||
|
build_flags =
|
||||||
|
-D USER_SETUP_LOADED=1
|
||||||
|
-D ILI9341_DRIVER=1
|
||||||
|
-D TFT_WIDTH=240
|
||||||
|
-D TFT_HEIGHT=320
|
||||||
|
-D TFT_MISO=19
|
||||||
|
-D TFT_MOSI=23
|
||||||
|
-D TFT_SCLK=18
|
||||||
|
-D TFT_CS=5
|
||||||
|
-D TFT_DC=21
|
||||||
|
-D TFT_RST=15
|
||||||
|
-D TFT_BL=22
|
||||||
|
-D TFT_BACKLIGHT_ON=HIGH
|
||||||
|
-D TOUCH_CS=4
|
||||||
|
-D USE_HSPI_PORT=1
|
||||||
|
-D SPI_FREQUENCY=40000000
|
||||||
|
-D SPI_READ_FREQUENCY=16000000
|
||||||
|
-D SPI_TOUCH_FREQUENCY=2500000
|
||||||
|
-D LOAD_GLCD=1
|
||||||
|
-D LOAD_FONT2=1
|
||||||
|
-D LOAD_FONT4=1
|
||||||
|
-D LOAD_FONT7=1
|
||||||
|
-D LOAD_GFXFF=1
|
||||||
|
-D SMOOTH_FONT=1
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
This directory is intended for PlatformIO Test Runner and project tests.
|
||||||
|
|
||||||
|
Unit Testing is a software testing method by which individual units of
|
||||||
|
source code, sets of one or more MCU program modules together with associated
|
||||||
|
control data, usage procedures, and operating procedures, are tested to
|
||||||
|
determine whether they are fit for use. Unit testing finds problems early
|
||||||
|
in the development cycle.
|
||||||
|
|
||||||
|
More information about PlatformIO Unit Testing:
|
||||||
|
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||||
Reference in new issue
Block a user