Brightness sync, recovery listener, offline sweep, note edit fix
- mqtt_listener.php: brightness DB sync, LWT offline, remove 1883 - set_brightness.php e mqtt_publish.php adicionados - comandos.php: tabela de brilho com valor real do ESP - console.php: pausar refresh ao editar notas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a028fc6497
commit
e95740ec8c
@ -65,9 +65,6 @@ $mqttPass = 'xupa';
|
||||
|
||||
$deviceSecret = 'K82A91F0';
|
||||
|
||||
$bootstrapSecret = "XUPA_TEMP_SECRET_2026";
|
||||
$certPath = "/etc/mosquitto/certs/server.crt";
|
||||
|
||||
/* =========================
|
||||
* TLS CLIENT (8883)
|
||||
* ========================= */
|
||||
@ -75,50 +72,43 @@ $tlsSettings = (new ConnectionSettings())
|
||||
->setUsername($mqttUser)
|
||||
->setPassword($mqttPass)
|
||||
->setUseTls(true)
|
||||
->setTlsSelfSignedAllowed(true)
|
||||
->setTlsSelfSignedAllowed(true)
|
||||
->setKeepAliveInterval(30)
|
||||
->setConnectTimeout(5)
|
||||
->setSocketTimeout(1)
|
||||
->setReconnectAutomatically(true);
|
||||
|
||||
|
||||
$mqttTls = new MqttClient($mqttHost, 8883, 'listener-php');
|
||||
|
||||
/* =========================
|
||||
* BOOTSTRAP CLIENT (1883)
|
||||
* ========================= */
|
||||
$bootstrapSettings = (new ConnectionSettings())
|
||||
->setUsername($mqttUser)
|
||||
->setPassword($mqttPass)
|
||||
->setUseTls(false)
|
||||
->setKeepAliveInterval(30)
|
||||
->setReconnectAutomatically(true);
|
||||
|
||||
|
||||
$mqttBootstrap = new MqttClient($mqttHost, 1883, 'listener-bootstrap');
|
||||
$mqttTls = new MqttClient($mqttHost, 8883, 'listener-php');
|
||||
|
||||
/* =========================
|
||||
* SUBSCRIPTIONS TLS
|
||||
* ========================= */
|
||||
$subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
|
||||
// =========================
|
||||
// STATUS ESP
|
||||
// =========================
|
||||
$mqttTls->subscribe('esp/+/status', function (string $topic, string $msg)
|
||||
use ($db, $mqttTls, $deviceSecret)
|
||||
{
|
||||
$uid = explode('/', $topic)[1] ?? null;
|
||||
if (!$uid) return;
|
||||
|
||||
$json = json_decode($msg, true);
|
||||
$uptime = isset($json['uptime']) ? (int)$json['uptime'] : null;
|
||||
$heap = isset($json['heap']) ? (int)$json['heap'] : null;
|
||||
$rssi = isset($json['rssi']) ? (int)$json['rssi'] : null;
|
||||
$broker = isset($json['broker']) ? (string)$json['broker'] : null;
|
||||
$hmac = isset($json['hmac']) ? strtoupper((string)$json['hmac']) : null;
|
||||
$json = json_decode($msg, true);
|
||||
|
||||
// LWT do broker — marcar offline sem precisar de HMAC
|
||||
if (($json['status'] ?? '') === 'offline') {
|
||||
$db->prepare("UPDATE contadores SET online=0 WHERE uid=:uid")
|
||||
->execute(['uid' => $uid]);
|
||||
logmsg("LWT OFFLINE $uid");
|
||||
return;
|
||||
}
|
||||
|
||||
$uptime = isset($json['uptime']) ? (int)$json['uptime'] : null;
|
||||
$heap = isset($json['heap']) ? (int)$json['heap'] : null;
|
||||
$rssi = isset($json['rssi']) ? (int)$json['rssi'] : null;
|
||||
$broker = isset($json['broker']) ? (string)$json['broker'] : null;
|
||||
$brightness = isset($json['brightness']) ? (int)$json['brightness'] : null;
|
||||
$hmac = isset($json['hmac']) ? strtoupper((string)$json['hmac']) : null;
|
||||
|
||||
// Validar HMAC — apenas aceitar heartbeats assinados
|
||||
if ($hmac === null || $uptime === null) {
|
||||
logmsg("STATUS sem HMAC/uptime $uid — ignorado", LOG_ERROR);
|
||||
return;
|
||||
@ -131,35 +121,36 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
// HMAC OK → actualizar DB
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO contadores
|
||||
(uid, entradas, saidas, entradas_total, saidas_total, last_seen, online,
|
||||
uptime, heap, rssi, current_broker)
|
||||
uptime, heap, rssi, current_broker, brightness)
|
||||
VALUES
|
||||
(:uid, 0, 0, 0, 0, NOW(), 1,
|
||||
:uptime, :heap, :rssi, :broker)
|
||||
:uptime, :heap, :rssi, :broker, :brightness)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_seen=NOW(),
|
||||
online=1,
|
||||
uptime=COALESCE(:uptime2, uptime),
|
||||
heap=COALESCE(:heap2, heap),
|
||||
rssi=COALESCE(:rssi2, rssi),
|
||||
current_broker=COALESCE(:broker2, current_broker)
|
||||
current_broker=COALESCE(:broker2, current_broker),
|
||||
brightness=COALESCE(:brightness2, brightness)
|
||||
");
|
||||
$stmt->execute([
|
||||
'uid' => $uid,
|
||||
'uptime' => $uptime,
|
||||
'heap' => $heap,
|
||||
'rssi' => $rssi,
|
||||
'broker' => $broker,
|
||||
'uptime2' => $uptime,
|
||||
'heap2' => $heap,
|
||||
'rssi2' => $rssi,
|
||||
'broker2' => $broker,
|
||||
'uid' => $uid,
|
||||
'uptime' => $uptime,
|
||||
'heap' => $heap,
|
||||
'rssi' => $rssi,
|
||||
'broker' => $broker,
|
||||
'brightness' => $brightness,
|
||||
'uptime2' => $uptime,
|
||||
'heap2' => $heap,
|
||||
'rssi2' => $rssi,
|
||||
'broker2' => $broker,
|
||||
'brightness2' => $brightness,
|
||||
]);
|
||||
|
||||
// Responder com HEARTBEAT_OK → ESP arranca serviços (sem retain)
|
||||
$mqttTls->publish("esp/$uid/cmd",
|
||||
json_encode(['cmd' => 'HEARTBEAT_OK']), 1, false);
|
||||
|
||||
@ -167,84 +158,20 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
|
||||
}, 0);
|
||||
|
||||
// =========================
|
||||
// PEDIDO DE HORA GLOBAL
|
||||
// =========================
|
||||
$mqttTls->subscribe('time/request', function (string $topic, string $msg) use ($mqttTls) {
|
||||
|
||||
logmsg("Pedido global de hora");
|
||||
|
||||
$payload = json_encode([
|
||||
'h' => (int)date('H'),
|
||||
'm' => (int)date('i')
|
||||
]);
|
||||
|
||||
$payload = json_encode(['h' => (int)date('H'), 'm' => (int)date('i')]);
|
||||
$mqttTls->publish('time/now', $payload, 0);
|
||||
|
||||
logmsg("Hora enviada: $payload");
|
||||
|
||||
}, 0);
|
||||
|
||||
// =========================
|
||||
// PEDIDO DE HORA POR DEVICE
|
||||
// time/request/esp_xxxxxx
|
||||
// =========================
|
||||
$mqttTls->subscribe('time/request/+', function (string $topic, string $msg) use ($mqttTls) {
|
||||
|
||||
$uid = explode('/', $topic)[2] ?? null;
|
||||
if (!$uid) return;
|
||||
|
||||
logmsg("Pedido hora de $uid");
|
||||
|
||||
$payload = json_encode([
|
||||
'h' => (int)date('H'),
|
||||
'm' => (int)date('i')
|
||||
]);
|
||||
|
||||
$payload = json_encode(['h' => (int)date('H'), 'm' => (int)date('i')]);
|
||||
$mqttTls->publish("time/now/$uid", $payload, 0);
|
||||
|
||||
logmsg("Hora enviada para $uid");
|
||||
|
||||
}, 0);
|
||||
|
||||
};
|
||||
|
||||
/* =========================
|
||||
* SUBSCRIPTIONS BOOTSTRAP
|
||||
* ========================= */
|
||||
$subscribeBootstrap = function () use ($mqttBootstrap, $bootstrapSecret, $certPath) {
|
||||
|
||||
$mqttBootstrap->subscribe('esp/+/bootstrap_request', function (string $topic, string $msg)
|
||||
use ($mqttBootstrap, $bootstrapSecret, $certPath) {
|
||||
|
||||
$uid = explode('/', $topic)[1] ?? null;
|
||||
if (!$uid) return;
|
||||
|
||||
logmsg("BOOTSTRAP pedido de $uid");
|
||||
|
||||
if (!file_exists($certPath)) {
|
||||
logmsg("ERRO: certificado não encontrado", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
$cert = file_get_contents($certPath);
|
||||
$certB64 = base64_encode($cert);
|
||||
$ts = time();
|
||||
|
||||
$dataToSign = $certB64 . $ts;
|
||||
$hmac = hash_hmac('sha256', $dataToSign, $bootstrapSecret);
|
||||
|
||||
$payload = json_encode([
|
||||
'cmd' => 'update_cert',
|
||||
'cert' => $certB64,
|
||||
'ts' => $ts,
|
||||
'hmac' => $hmac
|
||||
]);
|
||||
|
||||
$mqttBootstrap->publish("esp/$uid/bootstrap_response", $payload, 0);
|
||||
|
||||
logmsg("BOOTSTRAP enviado para $uid");
|
||||
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@ -260,17 +187,11 @@ while (true) {
|
||||
$subscribeTls();
|
||||
logmsg("TLS ligado");
|
||||
|
||||
logmsg("Ligando Bootstrap 1883...");
|
||||
$mqttBootstrap->connect($bootstrapSettings, false);
|
||||
$subscribeBootstrap();
|
||||
logmsg("Bootstrap ligado");
|
||||
|
||||
$nextOfflineCheck = time() + 15;
|
||||
|
||||
while ($mqttTls->isConnected() && $mqttBootstrap->isConnected()) {
|
||||
while ($mqttTls->isConnected()) {
|
||||
|
||||
$mqttTls->loop(true, false);
|
||||
$mqttBootstrap->loop(true, false);
|
||||
|
||||
if (time() >= $nextOfflineCheck) {
|
||||
$nextOfflineCheck = time() + 15;
|
||||
@ -291,11 +212,10 @@ while (true) {
|
||||
}
|
||||
|
||||
$mqttTls->disconnect();
|
||||
$mqttBootstrap->disconnect();
|
||||
|
||||
} catch (Throwable $e) {
|
||||
|
||||
logmsg("Erro: " . $e->getMessage(), LOG_ERROR);
|
||||
sleep(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
api/mqtt_publish.php
Normal file
46
api/mqtt_publish.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/protecao.php';
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
use PhpMqtt\Client\ConnectionSettings;
|
||||
|
||||
$uid = trim($_POST['uid'] ?? '');
|
||||
$payload = trim($_POST['payload'] ?? '');
|
||||
|
||||
if ($uid === '' || $payload === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'parametros invalidos']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validar JSON
|
||||
if (json_decode($payload) === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'payload invalido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$settings = (new ConnectionSettings())
|
||||
->setUsername('xupa')
|
||||
->setPassword('xupa')
|
||||
->setUseTls(true)
|
||||
->setTlsSelfSignedAllowed(true)
|
||||
->setConnectTimeout(5)
|
||||
->setSocketTimeout(3);
|
||||
|
||||
$mqtt = new MqttClient('mqtt.xupas.mywire.org', 8883, 'web-pub-' . getmypid());
|
||||
$mqtt->connect($settings, false);
|
||||
$mqtt->publish("esp/$uid/cmd", $payload, 1, false);
|
||||
$mqtt->disconnect();
|
||||
|
||||
echo json_encode(['ok' => true, 'uid' => $uid]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
48
api/set_brightness.php
Normal file
48
api/set_brightness.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
require_once __DIR__ . '/protecao.php';
|
||||
|
||||
if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'vendor not found: ' . __DIR__ . '/../vendor/autoload.php']);
|
||||
exit;
|
||||
}
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
use PhpMqtt\Client\ConnectionSettings;
|
||||
|
||||
$uid = trim($_POST['uid'] ?? '');
|
||||
$value = (int)($_POST['value'] ?? -1);
|
||||
|
||||
if ($uid === '' || $value < 0 || $value > 255) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'parametros invalidos']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$settings = (new ConnectionSettings())
|
||||
->setUsername('xupa')
|
||||
->setPassword('xupa')
|
||||
->setUseTls(true)
|
||||
->setTlsSelfSignedAllowed(true)
|
||||
->setConnectTimeout(5)
|
||||
->setSocketTimeout(3);
|
||||
|
||||
$mqtt = new MqttClient('mqtt.xupas.mywire.org', 8883, 'web-brightness-' . getmypid());
|
||||
$mqtt->connect($settings, false);
|
||||
|
||||
$payload = json_encode(['cmd' => 'BRIGHTNESS', 'value' => $value]);
|
||||
$mqtt->publish("esp/$uid/cmd", $payload, 1, false);
|
||||
|
||||
$mqtt->disconnect();
|
||||
|
||||
echo json_encode(['ok' => true, 'uid' => $uid, 'value' => $value]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
239
comandos.php
239
comandos.php
@ -7,71 +7,210 @@ try {
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
// Se submeteu o formulário
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$uid = $_POST['uid'] ?? '';
|
||||
$cmd = $_POST['cmd'] ?? '';
|
||||
|
||||
// AJAX — enviar comando via DB queue
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax'])) {
|
||||
header('Content-Type: application/json');
|
||||
$uid = trim($_POST['uid'] ?? '');
|
||||
$cmd = trim($_POST['cmd'] ?? '');
|
||||
if ($uid && $cmd) {
|
||||
$stmt = $pdo->prepare("INSERT INTO comandos (uid, cmd, status, queued_at) VALUES (?, ?, 'pending', NOW())");
|
||||
$stmt->execute([$uid, $cmd]);
|
||||
$msg = "✅ Comando '$cmd' enviado para o dispositivo $uid!";
|
||||
$pdo->prepare("INSERT INTO comandos (uid, cmd, status, queued_at) VALUES (?, ?, 'pending', NOW())")
|
||||
->execute([$uid, $cmd]);
|
||||
echo json_encode(['ok' => true, 'uid' => $uid, 'cmd' => $cmd]);
|
||||
} else {
|
||||
$msg = "⚠️ Escolhe um dispositivo e um comando.";
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'parametros invalidos']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Buscar lista de devices
|
||||
$devices = $pdo->query("SELECT uid FROM contadores ORDER BY uid ASC")->fetchAll();
|
||||
$devices = $pdo->query("SELECT uid, COALESCE(brightness, 128) AS brightness FROM contadores ORDER BY uid ASC")->fetchAll();
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("Erro: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<!doctype html>
|
||||
<html lang="pt" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Enviar Comandos</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<meta charset="utf-8">
|
||||
<title>Controlo de Dispositivos</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
|
||||
.card { background:#151515; border:1px solid #2a2a2a; }
|
||||
.header-title { color:#00ffcc; text-shadow:0 0 8px rgba(0,255,180,0.4); }
|
||||
.form-range::-webkit-slider-thumb { background:#00ffcc; }
|
||||
.form-range::-moz-range-thumb { background:#00ffcc; }
|
||||
.form-range::-webkit-slider-runnable-track { background:#2a2a2a; }
|
||||
.feedback { font-size:.8rem; }
|
||||
.form-select, .form-control {
|
||||
background:#1e1e1e; border-color:#333; color:#e0e0e0;
|
||||
}
|
||||
.form-select:focus, .form-control:focus {
|
||||
background:#1e1e1e; border-color:#00ffcc; color:#e0e0e0;
|
||||
box-shadow:0 0 0 .2rem rgba(0,255,180,.15);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-5 bg-light">
|
||||
<div class="container">
|
||||
<h2 class="mb-4">🛠️ Enviar Comando</h2>
|
||||
<body>
|
||||
<div class="container-fluid py-4" style="max-width:1000px">
|
||||
|
||||
<?php if (!empty($msg)): ?>
|
||||
<div class="alert alert-info"><?= htmlspecialchars($msg) ?></div>
|
||||
<?php endif; ?>
|
||||
<h1 class="mb-4 header-title">🛠️ Controlo de Dispositivos</h1>
|
||||
|
||||
<form method="POST" class="card p-4 shadow-sm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dispositivo</label>
|
||||
<select name="uid" class="form-select" required>
|
||||
<option value="">-- Escolhe --</option>
|
||||
<?php foreach ($devices as $d): ?>
|
||||
<option value="<?= htmlspecialchars($d['uid']) ?>"><?= htmlspecialchars($d['uid']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<!-- ══════════════════════════════════════
|
||||
BRILHO — tabela
|
||||
══════════════════════════════════════ -->
|
||||
<h4 class="text-secondary mb-3">🔆 Brilho</h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Comando</label>
|
||||
<select name="cmd" class="form-select" required>
|
||||
<option value="">-- Seleciona --</option>
|
||||
<option value="RESTART">RESTART</option>
|
||||
<option value="UPDATE">UPDATE</option>
|
||||
<option value="LED_ON">LED_ON</option>
|
||||
<option value="LED_OFF">LED_OFF</option>
|
||||
<option value="RESET">RESET</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">🚀 Enviar</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
|
||||
</div>
|
||||
<div class="card mb-5" style="overflow:hidden">
|
||||
<table class="table table-dark table-hover mb-0" style="border-color:#2a2a2a">
|
||||
<thead style="background:#1a1a1a;color:#888;font-size:.8rem;text-transform:uppercase;letter-spacing:.05em">
|
||||
<tr>
|
||||
<th style="width:120px;padding:.6rem 1rem">Dispositivo</th>
|
||||
<th style="padding:.6rem 1rem">Brilho</th>
|
||||
<th style="width:52px;padding:.6rem .5rem;text-align:center">Val</th>
|
||||
<th style="width:100px;padding:.6rem 1rem"></th>
|
||||
<th style="padding:.6rem 1rem"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($devices as $d):
|
||||
$uid = htmlspecialchars($d['uid']); ?>
|
||||
<tr style="border-color:#222">
|
||||
<td style="padding:.5rem 1rem;font-family:monospace;color:#00ffcc;font-weight:700;vertical-align:middle"><?= $uid ?></td>
|
||||
<td style="padding:.5rem 1rem;vertical-align:middle">
|
||||
<input type="range" class="form-range bri-slider" min="0" max="255" value="<?= (int)$d['brightness'] ?>"
|
||||
data-uid="<?= $uid ?>" style="margin:0">
|
||||
</td>
|
||||
<td style="padding:.5rem .5rem;text-align:center;vertical-align:middle;font-weight:700;color:#00ffcc">
|
||||
<span class="bri-val"><?= (int)$d['brightness'] ?></span>
|
||||
</td>
|
||||
<td style="padding:.5rem 1rem;vertical-align:middle">
|
||||
<button class="btn btn-sm bri-save w-100"
|
||||
style="background:#00ffcc;color:#000;font-weight:600"
|
||||
data-uid="<?= $uid ?>">💾 Gravar</button>
|
||||
</td>
|
||||
<td style="padding:.5rem 1rem;vertical-align:middle;font-size:.8rem">
|
||||
<span class="feedback"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════
|
||||
COMANDOS
|
||||
══════════════════════════════════════ -->
|
||||
<h4 class="text-secondary mb-3">📡 Comandos</h4>
|
||||
|
||||
<div class="card p-4 mb-5" style="max-width:420px">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary">Dispositivo</label>
|
||||
<select id="cmd-uid" class="form-select">
|
||||
<option value="">-- Escolhe --</option>
|
||||
<?php foreach ($devices as $d): ?>
|
||||
<option value="<?= htmlspecialchars($d['uid']) ?>"><?= htmlspecialchars($d['uid']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary">Comando</label>
|
||||
<select id="cmd-sel" class="form-select">
|
||||
<option value="">-- Seleciona --</option>
|
||||
<optgroup label="Sistema">
|
||||
<option value="REBOOT">REBOOT</option>
|
||||
<option value="STATUS">STATUS</option>
|
||||
<option value="CLEAR_WIFI">CLEAR_WIFI</option>
|
||||
</optgroup>
|
||||
<optgroup label="Relógio">
|
||||
<option value="CLOCK_START">CLOCK_START</option>
|
||||
<option value="CLOCK_STOP">CLOCK_STOP</option>
|
||||
</optgroup>
|
||||
<optgroup label="Startup (grava NVS)">
|
||||
<option value="STARTUP_CLOCK">Startup → Relógio</option>
|
||||
<option value="STARTUP_BLANK">Startup → Branco</option>
|
||||
</optgroup>
|
||||
<optgroup label="Animações (só S3)">
|
||||
<option value="TETRIS_START">TETRIS_START</option>
|
||||
<option value="TETRIS_STOP">TETRIS_STOP</option>
|
||||
<option value="STOP_ANIM">STOP_ANIM</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<button id="cmd-btn" class="btn w-100"
|
||||
style="background:#00ffcc;color:#000;font-weight:600">
|
||||
🚀 Enviar Comando
|
||||
</button>
|
||||
<div class="feedback mt-2 text-center" id="cmd-msg"></div>
|
||||
</div>
|
||||
|
||||
<a href="api/menu.php" class="btn btn-outline-secondary">← Menu</a>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Brilho ──────────────────────────────────────────────
|
||||
document.querySelectorAll('.bri-slider').forEach(slider => {
|
||||
const row = slider.closest('tr');
|
||||
const val = row.querySelector('.bri-val');
|
||||
const btn = row.querySelector('.bri-save');
|
||||
const msg = row.querySelector('.feedback');
|
||||
|
||||
slider.addEventListener('input', () => val.textContent = slider.value);
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
msg.innerHTML = '<span class="text-secondary">A enviar…</span>';
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('uid', slider.dataset.uid);
|
||||
fd.append('value', slider.value);
|
||||
const r = await (await fetch('api/set_brightness.php', { method:'POST', body:fd })).json();
|
||||
msg.innerHTML = r.ok ? '' : `<span class="text-danger">❌</span>`;
|
||||
} catch(e) {
|
||||
msg.innerHTML = `<span class="text-danger">❌ ${e.message}</span>`;
|
||||
}
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Comandos ────────────────────────────────────────────
|
||||
const MQTT_DIRECT = {
|
||||
'STARTUP_CLOCK': { cmd: 'STARTUP_MODE', mode: 'clock' },
|
||||
'STARTUP_BLANK': { cmd: 'STARTUP_MODE', mode: 'blank' },
|
||||
};
|
||||
|
||||
document.getElementById('cmd-btn').addEventListener('click', async () => {
|
||||
const uid = document.getElementById('cmd-uid').value;
|
||||
const cmd = document.getElementById('cmd-sel').value;
|
||||
const msg = document.getElementById('cmd-msg');
|
||||
if (!uid || !cmd) {
|
||||
msg.innerHTML = '<span class="text-warning">⚠️ Escolhe dispositivo e comando.</span>';
|
||||
return;
|
||||
}
|
||||
msg.innerHTML = '<span class="text-secondary">A enviar…</span>';
|
||||
try {
|
||||
let r;
|
||||
if (MQTT_DIRECT[cmd]) {
|
||||
const fd = new FormData();
|
||||
fd.append('uid', uid);
|
||||
fd.append('payload', JSON.stringify(MQTT_DIRECT[cmd]));
|
||||
r = await (await fetch('api/mqtt_publish.php', { method:'POST', body:fd })).json();
|
||||
} else {
|
||||
const fd = new FormData();
|
||||
fd.append('ajax', '1');
|
||||
fd.append('uid', uid);
|
||||
fd.append('cmd', cmd);
|
||||
r = await (await fetch('comandos.php', { method:'POST', body:fd })).json();
|
||||
}
|
||||
msg.innerHTML = r.ok
|
||||
? `<span style="color:#00ffcc">✅ ${cmd} → ${uid}</span>`
|
||||
: `<span class="text-danger">❌ ${r.error}</span>`;
|
||||
} catch(e) {
|
||||
msg.innerHTML = `<span class="text-danger">❌ ${e.message}</span>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -470,6 +470,11 @@ function esc(s) {
|
||||
|
||||
async function refreshTable() {
|
||||
|
||||
// Não actualizar se o utilizador estiver a editar uma nota
|
||||
if (document.activeElement && document.activeElement.classList.contains('note-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const res =
|
||||
|
||||
Loading…
Reference in New Issue
Block a user