Skip to content

Instantly share code, notes, and snippets.

@imneonizer
Last active July 26, 2025 15:44
Show Gist options
  • Save imneonizer/f9a5f9d6b91496a71649ea292336bb2c to your computer and use it in GitHub Desktop.
Save imneonizer/f9a5f9d6b91496a71649ea292336bb2c to your computer and use it in GitHub Desktop.
PC Power Button
// Wiring Diagram
// Motherboard Power LED Header
// +-----------------------+
// | |
// | [ + ] ─────┬───[1kΩ]──┬──→ D1 (GPIO5 on ESP)
// | | |
// | | [10kΩ]
// | | |
// | GND |
// | | ↓
// | [ − ]────── GND (ESP)
// +-----------------------+
// Pull-down: 10kΩ from D1 to GND (so D1 reads LOW when PC is off)
// Motherboard 5V Supply (e.g. Molex/SATA)
// +----------------------------+
// | |
// | +5V ─────────────┐ |
// | │ |
// | → VIN (ESP)| ← powers ESP
// | │ |
// | GND ─────────────┘ |
// +----------------------------+
// ⚠️ Only one power source: Disconnect USB while using this!
// Relay Module (Active-Low)
// +-------------------------------+
// | Relay Module |
// | |
// | IN ◄─────── D2 (GPIO4 on ESP)
// | VCC ◄─────── VIN (5V on ESP)|
// | GND ◄─────── GND (ESP)n |
// +-------------------------------+
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <WebSocketsServer.h>
const char* ssid = "YOU-WIFI-SSID";
const char* password = "YOU-WIFI-PASSWORD";
const char* hostname = "luna-button"; // visit http://luna-button.local to see webpage (make sure to be on same wifi network)
#define MAX_WIFI_RECONNECT_ATTEMPTS 10
#define ENABLE_LOGGING true
const bool AUTO_SWITCH_OFF = true;
const unsigned long TOGGLE_PRESS_DURATION = 1000;
const unsigned long LONG_TOGGLE_PRESS_DURATION = 10000;
#define PC_STATUS_GPIO 5 // D1
#define RELAY_GPIO 4 // D2
unsigned long buttonOnTimestamp = 0;
unsigned long lastADCCheck = 0;
ESP8266WebServer server(80);
WebSocketsServer webSocket(81);
bool ledState = false;
bool pcState = false;
#if ENABLE_LOGGING
#define LOG(msg) Serial.println(msg)
#define LOGP(msg) Serial.print(msg)
#define LOGF(fmt, ...) Serial.printf((fmt), ##__VA_ARGS__)
#else
#define LOG(msg)
#define LOGP(msg)
#define LOGF(fmt, ...)
#endif
void notifyClients(const String& type, const String& value) {
String json = "{\"" + type + "\":\"" + value + "\"}";
webSocket.broadcastTXT(json);
}
void sendStateToClient(uint8_t clientID) {
String btnState = "{\"button\":\"" + String(ledState ? "ON" : "OFF") + "\"}";
String pcStateStr = "{\"pc\":\"" + String(pcState ? "ON" : "OFF") + "\"}";
webSocket.sendTXT(clientID, btnState);
webSocket.sendTXT(clientID, pcStateStr);
}
void updateButtonState(bool newState) {
if (ledState != newState) {
ledState = newState;
digitalWrite(LED_BUILTIN, newState ? LOW : HIGH);
digitalWrite(RELAY_GPIO, newState ? HIGH : LOW);
notifyClients("button", newState ? "ON" : "OFF");
LOG(newState ? "BUTTON ON" : "BUTTON OFF");
if (AUTO_SWITCH_OFF && newState) {
buttonOnTimestamp = millis();
} else {
buttonOnTimestamp = 0;
}
}
}
void handleRoot(); // declared for HTML, defined later
void handleOn() { updateButtonState(true); server.send(200, "text/plain", "OK"); }
void handleOff() { updateButtonState(false); server.send(200, "text/plain", "OK"); }
void handleToggle(unsigned long duration) { updateButtonState(true); delay(duration); updateButtonState(false); }
void handleShortToggle() { server.send(200, "text/plain", "Short Toggle OK"); handleToggle(TOGGLE_PRESS_DURATION); }
void handleLongToggle() { server.send(200, "text/plain", "Long Toggle OK"); handleToggle(LONG_TOGGLE_PRESS_DURATION); }
void checkPCStatus() {
static bool lastState = false;
bool newState = digitalRead(PC_STATUS_GPIO);
if (newState != lastState) {
pcState = newState;
notifyClients("pc", pcState ? "ON" : "OFF");
LOGF("PC STATUS CHANGED: %s\n", pcState ? "ON" : "OFF");
lastState = newState;
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
pinMode(PC_STATUS_GPIO, INPUT); // PC Power LED input
pinMode(RELAY_GPIO, OUTPUT); // Relay control
digitalWrite(RELAY_GPIO, LOW); // Ensure relay is OFF initially (for low-triggered relays)
WiFi.hostname(hostname);
WiFi.begin(ssid, password);
LOGF("Connecting to %s", ssid);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts++ < MAX_WIFI_RECONNECT_ATTEMPTS) {
delay(500);
LOGP(".");
}
if (WiFi.status() != WL_CONNECTED) {
LOG("\nWiFi failed. Restarting...");
ESP.restart();
}
LOG("\nWiFi connected.");
LOGP("IP: "); LOG(WiFi.localIP());
if (MDNS.begin(hostname)) LOG("mDNS started.");
server.on("/", handleRoot);
server.on("/on", handleOn);
server.on("/off", handleOff);
server.on("/toggle", handleShortToggle);
server.on("/long-toggle", handleLongToggle);
server.begin();
LOG("HTTP server started.");
webSocket.begin();
webSocket.onEvent([](uint8_t clientID, WStype_t type, uint8_t* payload, size_t) {
if (type == WStype_CONNECTED) {
sendStateToClient(clientID);
} else if (type == WStype_TEXT) {
if (strcmp((char*)payload, "on") == 0) updateButtonState(true);
else if (strcmp((char*)payload, "off") == 0) updateButtonState(false);
}
});
LOG("WebSocket server started.");
}
void loop() {
server.handleClient();
webSocket.loop();
MDNS.update();
static unsigned long lastWiFiCheck = 0;
if (millis() - lastWiFiCheck > 5000) {
lastWiFiCheck = millis();
if (WiFi.status() != WL_CONNECTED) {
LOG("WiFi lost. Reconnecting...");
WiFi.disconnect();
WiFi.begin(ssid, password);
int tries = 0;
while (WiFi.status() != WL_CONNECTED && tries++ < MAX_WIFI_RECONNECT_ATTEMPTS) {
delay(500);
LOGP(".");
}
if (WiFi.status() != WL_CONNECTED) {
LOG("Reconnect failed. Restarting...");
ESP.restart();
}
}
}
unsigned long now = millis();
if (now - lastADCCheck > 500) {
lastADCCheck = now;
checkPCStatus();
}
if (AUTO_SWITCH_OFF && ledState && buttonOnTimestamp > 0 && millis() - buttonOnTimestamp >= LONG_TOGGLE_PRESS_DURATION) {
updateButtonState(false);
LOG("AUTO OFF: Button ON timeout.");
}
}
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>PC Power Button</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Roboto, sans-serif;
background-color: #0f0f0f;
color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.main-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 960px;
padding: 20px;
}
.status-container {
display: flex;
gap: 40px;
margin-bottom: 30px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.status-indicator {
display: flex;
align-items: center;
font-size: 1.1rem;
color: #ccc;
}
.dot {
height: 14px;
width: 14px;
margin-right: 10px;
border-radius: 50%;
background-color: #dc3545;
}
.dot.on {
background-color: #28a745;
}
.button {
font-size: 1.4rem;
padding: 1rem 2.5rem;
border: none;
border-radius: 10px;
background-color: #1e88e5;
color: white;
cursor: pointer;
transition: 0.2s ease;
box-shadow: 0 6px 15px rgba(0,0,0,0.3);
margin-bottom: 20px;
}
.button:active {
background-color: #1565c0;
transform: scale(0.98);
}
.log-toggle {
font-size: 1rem;
background: none;
color: #aaa;
border: 1px solid #555;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
transition: 0.2s ease;
}
.log-toggle:hover {
color: #fff;
border-color: #888;
}
.log-wrapper {
display: none;
flex-direction: column;
align-items: flex-end;
width: 95%;
max-width: 900px;
margin-top: 20px;
}
.log-wrapper.visible {
display: flex;
}
.log-container {
width: 100%;
height: 300px;
background-color: #000;
color: #0f0;
padding: 14px;
border: 1px solid #444;
border-radius: 8px;
font-family: monospace;
font-size: 0.75rem;
overflow-y: scroll;
white-space: pre-wrap;
scrollbar-width: none;
box-shadow: inset 0 0 6px #111;
}
.log-container::-webkit-scrollbar {
display: none;
}
.clear-btn {
margin-top: 8px;
align-self: flex-end;
font-size: 0.8rem;
padding: 5px 10px;
border: none;
border-radius: 4px;
background-color: #333;
color: #ccc;
cursor: pointer;
transition: 0.2s;
display: none;
}
.clear-btn:hover {
background-color: #555;
color: #fff;
}
.clear-btn.visible {
display: block;
}
@media (min-width: 1024px) {
.log-wrapper {
width: 80%;
}
}
</style>
<script>
let socket;
let reconnectInterval;
let logVisible = false;
function setStatus(dotId, state) {
const dot = document.getElementById(dotId);
dot.classList.toggle("on", state === "ON");
}
function logMessage(msg) {
const logBox = document.getElementById("logBox");
const now = new Date().toLocaleTimeString();
logBox.textContent += `[${now}] ${msg}\n`;
logBox.scrollTop = logBox.scrollHeight;
}
function toggleLog() {
logVisible = !logVisible;
document.getElementById("logWrapper").classList.toggle("visible", logVisible);
document.getElementById("clearBtn").classList.toggle("visible", logVisible);
document.getElementById("logToggle").textContent = logVisible ? "Hide Logs" : "Show Logs";
}
function clearLogs() {
document.getElementById("logBox").textContent = "";
}
function sendState(state) {
fetch("/" + state);
}
function press() {
sendState("on");
}
function release() {
sendState("off");
}
function initWebSocket() {
socket = new WebSocket(`ws://${location.hostname}:81/`);
socket.onopen = () => {
logMessage("WebSocket connected.");
clearInterval(reconnectInterval);
};
socket.onmessage = event => {
logMessage("Received: " + event.data);
try {
const data = JSON.parse(event.data);
if (data.button !== undefined) setStatus("btnDot", data.button);
if (data.pc !== undefined) setStatus("pcDot", data.pc);
} catch {
logMessage("Invalid WS data");
}
};
socket.onclose = () => {
logMessage("WebSocket disconnected. Reconnecting...");
reconnectInterval = setInterval(initWebSocket, 3000);
};
socket.onerror = () => {
logMessage("WebSocket error.");
socket.close();
};
}
window.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById("control");
btn.addEventListener("pointerdown", press);
btn.addEventListener("pointerup", release);
btn.addEventListener("pointerleave", release);
btn.addEventListener("pointercancel", release);
document.getElementById("logToggle").addEventListener("click", toggleLog);
document.getElementById("clearBtn").addEventListener("click", clearLogs);
initWebSocket();
});
</script>
</head>
<body>
<div class="main-container">
<div class="status-container">
<div class="status-indicator">
<div class="dot" id="btnDot"></div>
<span>Button Status</span>
</div>
<div class="status-indicator">
<div class="dot" id="pcDot"></div>
<span>PC Status</span>
</div>
</div>
<button class="button" id="control">Press Power Button</button>
<button class="log-toggle" id="logToggle">Show Log</button>
<div class="log-wrapper" id="logWrapper">
<div class="log-container" id="logBox"></div>
<button class="clear-btn" id="clearBtn">Clear Logs</button>
</div>
</div>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment