Chciałem podzielić się moim projektem opartym na ESP8266 – prostym menedżerem zasilania PC zdalnie sterowanym przez HTTP i Telnet po WIFI.
Założenia v1.0:
- Sterowanie zasilaniem i resetem PC
- Odczyt stanu PWR_ON i sygnału z „buzzer’a”
- Zdalne sterowanie przez HTTP, Telnet
- Autoryzacja prostym tokenem Bearer
- WiFi: tryb AP do konfiguracji i połączenie z zapisanym SSID/hasłem.
- Możliwość OTA przez HTTP
Zwarcie PWR i RST robię przez BC457 po R470Ω do IO ESP. Do PWR_LED i BUZZER myślę na transoptorami bo chyba łatwiej i prościej. Jeszcze mi zostało jak ssać zasilanie bez większej ingerencji.
Wszystkie "zmienne" jeszcze myślę żeby przenieśc do Config.h
Zastanawiam się nad dodaniem SSL, ale nie wiem, czy ESP8266 udźwignie szyfrowanie. Myślę nad dodaniem jakichś parametrów do monitorowania ale to w kolejnej wersji.
Teraz testuje stabilność. Zanim skończę wypiekać mój chlebek mam pytanie - czy coś można tu poprawić, zrobić lepiej?

===== main\Config.h =====
#pragma once
#include <Arduino.h>
// ---------------- GPIO PINS ----------------
static const uint8_t PIN_LED_BUILTIN = LED_BUILTIN;
static const uint8_t PIN_POWER = 4;
static const uint8_t PIN_RESET = 5;
static const uint8_t PIN_LED_INPUT = 12;
static const uint8_t PIN_BUZZER = 13;
static const uint8_t PIN_JUMPER = 14;
// --------------- TIMINGS -------------------
static const unsigned POWER_TIME_MS = 200;
static const unsigned POWER_FORCE_MS = 12000;
static const unsigned RESET_TIME_MS = 300;
static const unsigned SHORT_MS = 200;
static const unsigned LONG_MS = 500;
static const unsigned DEBOUNCE_MS = 50;
static const unsigned WIFI_RECONNECT = 5000;
// ---------------- TOKEN --------------------
static const size_t TOKEN_LENGTH = 64;
// ---------------- FILES --------------------
static const char TOKEN_FILE[] = "/token.txt";
static const char SSID_FILE[] = "/ssid.txt";
static const char PASS_FILE[] = "/password.txt";
static const char FILE_W[] = "w";
static const char FILE_R[] = "r";
// ---------------- STRINGS --------------------
static const char APP_NAME[] = "PowerManagerPC";
static const char APP_VERSION[] = "1.0";
static const char STRING_X[] = "X";
static const char STRING_APP_JSON[] = "application/json";
static const char STRING_TOKEN_CHARS[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
===== main\HttpApi.cpp =====
#include "HttpApi.h"
#include "StatusEncoder.h"
void HttpApi::begin(IOController* p, TokenManager* t) {
power = p;
tokens = t;
server.on("/status", HTTP_GET, [&]() {
if (!checkAuth()) {
sendNotAuth();
return;
}
server.send(200, STRING_APP_JSON, StatusEncoder::json(*power));
});
server.on("/power", HTTP_GET, [&]() {
if (!checkAuth()) {
sendNotAuth();
return;
}
power->togglePower();
sendOk();
});
server.on("/power-force", HTTP_GET, [&]() {
if (!checkAuth()) {
sendNotAuth();
return;
}
power->forcePower();
sendOk();
});
server.on("/reset", HTTP_GET, [&]() {
if (!checkAuth()) {
sendNotAuth();
return;
}
power->toggleReset();
sendOk();
});
server.on("/update", HTTP_POST, [&]() {
if (!checkAuth()) {
sendNotAuth();
return;
}
}, [&]() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update Start: %s\n", upload.filename.c_str());
if (!Update.begin(upload.totalSize,U_FLASH,PIN_LED_BUILTIN,1)) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
Serial.printf("Update Success: %u bytes\n", upload.totalSize);
server.send(200, "text/plain", "Update Success");
} else {
Update.printError(Serial);
server.send(500, "text/plain", "Update Failed");
}
}
});
server.begin();
}
void HttpApi::sendNotAuth(){
server.send(401, STRING_APP_JSON, "{\"error\":\"Unauthorized\"}");
}
void HttpApi::sendOk(){
server.send(200, STRING_APP_JSON, "{\"result\":1}");
}
bool HttpApi::checkAuth() {
if (server.hasHeader("Authorization")) {
String h = server.header("Authorization");
if (h.startsWith("Bearer ")) {
return tokens->check(h.substring(7));
}
}
if (server.hasArg("token")) {
return tokens->check(server.arg("token"));
}
return false;
}
void HttpApi::tick() {
server.handleClient();
}
===== main\HttpApi.h =====
#pragma once
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h>
#include "IOController.h"
#include "TokenManager.h"
class HttpApi {
public:
void begin(IOController* p, TokenManager* t);
void tick();
private:
ESP8266WebServer server{80};
IOController* power;
TokenManager* tokens;
bool checkAuth();
void sendNotAuth();
void sendOk();
};
===== main\IOController.cpp =====
#include "IOController.h"
volatile unsigned long IOController::times[BUFSZ];
volatile uint8_t IOController::index = 0;
void IOController::begin() {
pinMode(PIN_POWER, OUTPUT);
pinMode(PIN_RESET, OUTPUT);
pinMode(PIN_LED_INPUT, INPUT);
pinMode(PIN_BUZZER, INPUT);
pinMode(PIN_LED_BUILTIN, OUTPUT);
digitalWrite(PIN_POWER, LOW);
digitalWrite(PIN_RESET, LOW);
digitalWrite(PIN_LED_BUILTIN, LOW);
attachInterrupt(digitalPinToInterrupt(PIN_BUZZER), isrBuzzer, CHANGE);
}
void IOController::start(ToggleAction& a, unsigned duration) {
digitalWrite(a.pin, HIGH);
a.until = millis() + duration;
a.active = true;
}
void IOController::tickLed() {
static unsigned long lastToggle = 0;
static bool state = false;
unsigned long now = millis();
if (now - lastToggle >= 1000) {
state = !state;
digitalWrite(PIN_LED_BUILTIN, state ? HIGH : LOW);
lastToggle = now;
}
}
void IOController::checkActions() {
unsigned long now = millis();
if (pwrAct.active && now >= pwrAct.until) {
digitalWrite(pwrAct.pin, LOW);
pwrAct.active = false;
}
if (rstAct.active && now >= rstAct.until) {
digitalWrite(rstAct.pin, LOW);
rstAct.active = false;
}
}
void IOController::tick() {
checkActions();
}
void IOController::togglePower() { start(pwrAct, POWER_TIME_MS); }
void IOController::forcePower() { start(pwrAct, POWER_FORCE_MS); }
void IOController::toggleReset() { start(rstAct, RESET_TIME_MS); }
void IOController::isrBuzzer() {
times[index] = millis();
index = (index + 1) % BUFSZ;
}
String IOController::readBuzzerBits() {
noInterrupts();
uint8_t last = index;
unsigned long prev = 0;
String bits;
for (int i = 0; i < BUFSZ; i++) {
uint8_t idx = (last + i) % BUFSZ;
unsigned long t = times[idx];
if (t == 0) continue;
if (prev) {
unsigned long dt = t - prev;
bits += (dt <= SHORT_MS) ? '0' : '1';
}
prev = t;
}
interrupts();
return bits;
}
===== main\IOController.h =====
#pragma once
#include <Arduino.h>
#include "Config.h"
class IOController {
public:
void begin();
void tick();
void togglePower();
void forcePower();
void toggleReset();
void tickLed();
String readBuzzerBits();
private:
struct ToggleAction {
uint8_t pin;
unsigned long until;
bool active;
};
ToggleAction pwrAct { PIN_POWER, 0, false };
ToggleAction rstAct { PIN_RESET, 0, false };
void start(ToggleAction& a, unsigned duration);
void checkActions();
static const uint8_t BUFSZ = 64;
static volatile unsigned long times[BUFSZ];
static volatile uint8_t index;
static void IRAM_ATTR isrBuzzer();
};
===== main\main.ino =====
#include <Arduino.h>
#include "IOController.h"
#include "TokenManager.h"
#include "WiFiController.h"
#include "HttpApi.h"
#include "TelnetConsole.h"
IOController powerCtrl;
TokenManager tokenMgr;
WiFiController wifiCtrl;
HttpApi httpApi;
TelnetConsole telnet;
void setup() {
Serial.begin(115200);
Serial.println("Booting...");
powerCtrl.begin();
Serial.println("Device...");
tokenMgr.begin();
Serial.println("Token...");
wifiCtrl.begin(tokenMgr);
Serial.println("WIFI...");
httpApi.begin(&powerCtrl, &tokenMgr);
Serial.println("HTTP...");
telnet.begin(&powerCtrl, &tokenMgr);
Serial.println("TELNET...");
Serial.println("System Ready.");
}
void loop() {
powerCtrl.tickLed();
wifiCtrl.tick();
powerCtrl.tick();
httpApi.tick();
telnet.tick();
}
===== main\StatusEncoder.cpp =====
#include "StatusEncoder.h"
#include "IOController.h"
#include "Config.h"
String StatusEncoder::json(IOController& p) {
String buz = p.readBuzzerBits();
char buf[256];
snprintf(buf, sizeof(buf),
"{\"ver\":\"%s\",\"pwr\":%d,\"buzzer\":\"%s\"}",
APP_VERSION,
digitalRead(PIN_LED_INPUT),
buz.c_str());
return String(buf);
}
===== main\StatusEncoder.h =====
#pragma once
#include <Arduino.h>
class IOController;
class StatusEncoder {
public:
static String json(IOController& p);
};
===== main\TelnetConsole.cpp =====
#include "TelnetConsole.h"
void TelnetConsole::begin(IOController* p, TokenManager* t) {
power = p;
tokens = t;
TelnetStream.begin();
}
void TelnetConsole::handleCmd(const String& cmd) {
if (cmd == "/power") {
power->togglePower();
TelnetStream.println("OK");
}
else if (cmd == "/power-force") {
power->forcePower();
TelnetStream.println("OK");
}
else if (cmd == "/reset") {
power->toggleReset();
TelnetStream.println("OK");
}
else if (cmd == "/status") {
String buz = power->readBuzzerBits();
char buf[256];
snprintf(buf, sizeof(buf), "Version: %s\r\nPower: %d\r\nBuzzer:%s\r\n", APP_VERSION, digitalRead(PIN_LED_INPUT), buz.c_str());
TelnetStream.println(buf);
}
else if (cmd == "/token") {
TelnetStream.println(tokens->getToken());
}
else if (cmd == "/token-new") {
TelnetStream.println(tokens->newToken());
} else {
TelnetStream.println("Commands:\r\n\t/power\r\n\t/power-force\r\n\t/reset\r\n\t/status\r\n\t/token\r\n\t/token-new\r\n");
}
}
void TelnetConsole::tick() {
static String buf;
while (TelnetStream.available()) {
char c = TelnetStream.read();
if (c == '\n') {
buf.trim();
handleCmd(buf);
buf = "";
} else {
buf += c;
}
}
}
===== main\TelnetConsole.h =====
#pragma once
#include <TelnetStream.h>
#include "IOController.h"
#include "TokenManager.h"
class TelnetConsole {
public:
void begin(IOController* p, TokenManager* t);
void tick();
private:
IOController* power;
TokenManager* tokens;
void handleCmd(const String& cmd);
};
===== main\TokenManager.cpp =====
#include "TokenManager.h"
#include "Config.h"
#include <LittleFS.h>
void TokenManager::begin() {
LittleFS.begin();
ssid_ = load(SSID_FILE);
pass_ = load(PASS_FILE);
token = load(TOKEN_FILE);
if (token.length() != TOKEN_LENGTH) {
newToken();
}
}
String TokenManager::load(const char* path) {
if (!LittleFS.exists(path)) return "";
File f = LittleFS.open(path, FILE_R);
String val = f.readString();
f.close();
return val;
}
void TokenManager::save(const char* path, const String& s) {
File f = LittleFS.open(path, FILE_W);
f.print(s);
f.close();
}
String TokenManager::getToken() {
return token;
}
String TokenManager::newToken() {
char buf[TOKEN_LENGTH + 1];
for (int i = 0; i < TOKEN_LENGTH; i++) {
buf[i] = STRING_TOKEN_CHARS[random(0, 62)];
}
buf[TOKEN_LENGTH] = 0;
token = buf;
save(TOKEN_FILE, token);
return token;
}
bool TokenManager::check(const String& provided) {
return provided == token;
}
String TokenManager::ssid() { return ssid_; }
String TokenManager::pass() { return pass_; }
===== main\TokenManager.h =====
#pragma once
#include <Arduino.h>
class TokenManager {
public:
void begin();
String getToken();
String newToken();
bool check(const String& provided);
String ssid();
String pass();
private:
void save(const char* path, const String& s);
String load(const char* path);
String token;
String ssid_;
String pass_;
};
===== main\WiFiController.cpp =====
#include <ctime>
#include "WiFiController.h"
#include "Config.h"
void WiFiController::begin(TokenManager& tm) {
if (digitalRead(PIN_JUMPER) == LOW || tm.ssid().isEmpty()) {
WiFiManager wm;
wm.autoConnect(APP_NAME);
return;
}
WiFi.begin(tm.ssid().c_str(), tm.pass().c_str());
}
void WiFiController::tick() {
unsigned long now = millis();
if (WiFi.status() != WL_CONNECTED && now - lastTry > WIFI_RECONNECT) {
WiFi.disconnect();
Serial.println(STRING_X);
WiFi.begin();
lastTry = now;
}
}
===== main\WiFiController.h =====
#pragma once
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiManager.h>
#include "TokenManager.h"
class WiFiController {
public:
void begin(TokenManager& tm);
void tick();
private:
unsigned long lastTry = 0;
};
. Variables and constants in RAM (global, static), used 32120 / 80192 bytes (40%)
║ SEGMENT BYTES DESCRIPTION
╠══ DATA 1524 initialized variables
╠══ RODATA 3572 constants
╚══ BSS 27024 zeroed variables
. Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 60867 / 65536 bytes (92%)
║ SEGMENT BYTES DESCRIPTION
╠══ ICACHE 32768 reserved space for flash instruction cache
╚══ IRAM 28099 code in IRAM
. Code in flash (default, ICACHE_FLASH_ATTR), used 355340 / 1048576 bytes (33%)
║ SEGMENT BYTES DESCRIPTION
╚══ IROM 355340 code in flash