Sterowanie zasilaniem PC przez ESP8266 – HTTP, Telnet i zdalna konfiguracja WiFi

Sterowanie zasilaniem PC przez ESP8266 – HTTP, Telnet i zdalna konfiguracja WiFi
hzmzp
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 741
5

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?

bb.png

Kopiuj
===== 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;
};
Kopiuj
. 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
bakunet
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
  • Postów: 1681
0

Fajnie, właśnie takiego urządzenia szukałem na własny użytek, zamówiłem z Amazona za kilkadziesiąt złotych :)

hzmzp
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 741
1

@cerrato Do domowego serwerka jak trzeba go włączyć/resnąć. U klienta jak trzeba by zrobić coś na pc ale dopiero jak skończą jego ludzie prace. Serio miałem taki scenariusz że o 20 nikogo nie ma żeby kliknąć ten głupi przycisk, a komputer stoi wyłączony bo na chwile brakło prądu i przez to robota się opóźniła do następnego weekendu.

jurek1980
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 3581
1

Rezystor 470 może być za mały. Żebyś nie miał glitcha spróbuj wziąć większy, nawet z 10k. Najlpeiej jednak jakaś izolacja optyczna, na transoptorze. Jak masz oscyloskop, zobacz jak się zachowuje sygnał.
Ja bym monitorował włączenie przez pobranie napięcia z power LED. Jeśli chodzi z kolei o zasilanie to możesz rozważyć albo zasilanie z +5VSB z ATX.

MS
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 33
0

Czemu nie esp32? Szybsze i wielowątkowe

hzmzp
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 741
0

@jurek1980 Taki był mój pierwotny zamysł, ale chce zrobić coś nieinwazyjnego takie PnP bez dodatkowego grzebania po całości. Zastanawiałem się podpiąć pod port USB tylko tu tryb ECO może mi odciąć zasilanie. Z rezystorami słuszna uwaga.

jurek1980
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 3581
1

I tak już dostajesz się do obudowy, bo raczej będziesz chciał to umieścić w środku.
https://allegro.pl/oferta/kabel-zasilajacy-gembird-cc-psu-atx-btx-13221080486
Lub jeśli to jest jakiś zasilacz 1000Watt i boisz się jakości przedłużki i potencjalnych strat to https://allegro.pl/oferta/adapter-atx-24-pin-zlacza-wtykowe-zasilania-90-stopni-przedluzenie-24pin-do-24pin-15792822708
I wspinasz się w 5VSB. Też myślałem o USB ale w skrajnych przypadkach to nie zadziała. BIOS może wyłączyć zasilanie portów.

RI
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 13
1

Ja mam w swoim serwerze ustawione w bios Power after AC Loss:always on. Zastanowilbym się czy nie zrobić tego na przekaźniku zwykłym albo SSR po stronie 230V i mieć zero ingerencji. W komputer.
A najprościej w sumie gniazdko sonoff r26 czy coś tego typu.

hzmzp
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 741
0

@ripazha Z AC Back ON jest problem, jak ktoś wyłączy komputer to już sam się nie włączy. Takie gniazdo też jest super rozwiązaniem, ale brak opensource i to że się łączy do clouda też mi nie pasuje.
Ostatecznie chyba zrobię tek że będę ciągnął z USB (to pozwoli również podpiąć się do zewnętrznego zasilania) z możliwością wpięcia się przez szpilke do ATX, doszedłem do wniosku że jeżeli chce się wpiąć bez wyłączania maszyny no to już nie ma innego sposobu.

obscurity
  • Rejestracja: dni
  • Ostatnio: dni
0

a wake on lan? Kiedyś dodawałem skrypcik na samym routerze który pozwalał z zewnątrz włączyć zdalnie mój komputer i działało to ok, wcześniej kombinowałem żeby pakiety wol dało się nadawać spoza sieci ale mi się nie udało, nie wiem czy to w ogóle wykonalne

hzmzp
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 741
0

WOL działa na warstwie 2 więc konfiguracja tego zwłaszcza na konsumenckich routerach jest trudniejsza/niemożliwa, mnóstwo konfiguracji w bios, systemie, urządzeniach a i tak nie zawsze działa, poza tym, jak sie pc zawiesi to wolem go nie zrestartujesz. W tym wszystkim zależało mi na PnP żadnych restartów, instalacji i konfiguracji. Moje rozwiązanie jest przezroczyste dla urządzenia do którego go podpinam - nic o nim nie wiem, nie widzi żeby się coś zmieniło.

obscurity
  • Rejestracja: dni
  • Ostatnio: dni
0

A ile razy ci sie zdarzyło że pc sie zawiesił tak że nie dało sie go software'owo zrestartować przez ostatnie 10 lat?

hzmzp
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 741
0

@obscurity zaskakująco dużo, co mnie skłoniło do rozmyślań nad rozwiązaniem problemu. A nikt nie będzie siedzieć przy maszynie żeby mi ją restartować w razie potrzeby

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.