Instantly share code, notes, and snippets.
Last active
July 26, 2025 15:44
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save imneonizer/f9a5f9d6b91496a71649ea292336bb2c to your computer and use it in GitHub Desktop.
PC Power Button
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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