Add temp sensor.

This commit is contained in:
2026-06-28 13:25:14 -05:00
parent 0f2a03044c
commit e188930755
6 changed files with 35 additions and 17 deletions

View File

@@ -116,7 +116,7 @@ The firmware in `src/main.cpp` implements the boilerplate as a compact Arduino E
- Configurable API access control where each route can be `PUBLIC` or require one role. - Configurable API access control where each route can be `PUBLIC` or require one role.
- Public boilerplate APIs: ping, add, and LED brightness. - Public boilerplate APIs: ping, add, and LED brightness.
- LED brightness is persisted in non-volatile memory, applied on boot, and loaded into the Admin UI slider. - LED brightness is persisted in non-volatile memory, applied on boot, and loaded into the Admin UI slider.
- DS18B20 temperature sensor readings are exposed through the live Admin UI card and `/api/uptime` compatibility endpoint. - DS18B20 temperature sensor readings are exposed in Celsius and Fahrenheit through the live Admin UI card and `/api/temperature` endpoint.
- User management with the standard roles `Sysadmin`, `UserAdmin`, `WebUIConnect`, and `Debugger`. - User management with the standard roles `Sysadmin`, `UserAdmin`, `WebUIConnect`, and `Debugger`.
- Custom role management. System roles are protected and cannot be deleted. - Custom role management. System roles are protected and cannot be deleted.
- Active/inactive user accounts with role checkboxes in the Admin UI. - Active/inactive user accounts with role checkboxes in the Admin UI.
@@ -204,7 +204,7 @@ The defaults can be changed at compile time:
### DS18B20 temperature sensor ### DS18B20 temperature sensor
The live Admin UI card reads a DS18B20 one-wire temperature sensor. The default data pin is GPIO3, which is exposed as `D1` on the Seeed Studio XIAO ESP32C3. The live Admin UI card reads a DS18B20 one-wire temperature sensor and displays both Celsius and Fahrenheit. The default data pin is GPIO3, which is exposed as `D1` on the Seeed Studio XIAO ESP32C3.
Connect the 3-wire sensor as follows: Connect the 3-wire sensor as follows:
@@ -222,6 +222,20 @@ Keep the sensor powered from `3V3`, not `5V`, so the data line stays safe for th
#define DS18B20_PIN 3 #define DS18B20_PIN 3
``` ```
Read the current temperature with:
```http
GET /api/temperature
Authorization: Bearer <token>
```
The response includes `temperatureC`, `temperatureF`, `sensorConnected`, and `pin`. The live dashboard subscribes to:
```http
GET /api/temperature/events
Authorization: Bearer <token>
```
### Authentication ### Authentication
Login: Login:
@@ -326,6 +340,8 @@ Protected APIs and their default roles:
| --- | --- | | --- | --- |
| `POST /api/logout` | `WebUIConnect` | | `POST /api/logout` | `WebUIConnect` |
| `GET /api/me` | `WebUIConnect` | | `GET /api/me` | `WebUIConnect` |
| `GET /api/temperature` | `WebUIConnect` |
| `GET /api/temperature/events` | `WebUIConnect` |
| `GET /api/apis` | `Sysadmin` | | `GET /api/apis` | `Sysadmin` |
| `POST /api/apis` | `Sysadmin` | | `POST /api/apis` | `Sysadmin` |
| `GET /api/users` | `UserAdmin` | | `GET /api/users` | `UserAdmin` |

View File

@@ -6,7 +6,7 @@
<link rel="stylesheet" href="/css/app.css"> <link rel="stylesheet" href="/css/app.css">
<script defer src="/js/app.js"></script> <script defer src="/js/app.js"></script>
<script type="module" src="/components/status-card.js"></script> <script type="module" src="/components/status-card.js"></script>
<script type="module" src="/components/uptime-card.js"></script> <script type="module" src="/components/temperature-card.js"></script>
<script type="module" src="/components/led-card.js"></script> <script type="module" src="/components/led-card.js"></script>
<script type="module" src="/components/add-card.js"></script> <script type="module" src="/components/add-card.js"></script>
</head> </head>
@@ -30,7 +30,7 @@
</div> </div>
<div id="dashboard" class="panel active"><div class="grid"> <div id="dashboard" class="panel active"><div class="grid">
<status-card></status-card> <status-card></status-card>
<uptime-card></uptime-card> <temperature-card></temperature-card>
<led-card></led-card> <led-card></led-card>
<add-card></add-card> <add-card></add-card>
</div></div> </div></div>

View File

@@ -1,4 +1,4 @@
class UptimeCard extends HTMLElement { class TemperatureCard extends HTMLElement {
constructor() { constructor() {
super(); super();
this.source = null; this.source = null;
@@ -7,12 +7,13 @@ class UptimeCard extends HTMLElement {
connectedCallback() { connectedCallback() {
this.innerHTML = ` this.innerHTML = `
<style> <style>
:host{display:block}section{height:100%;box-sizing:border-box}.liveValue{font-size:34px;font-weight:700;line-height:1.15;margin-top:10px}.state{display:inline-flex;align-items:center;gap:6px;margin-top:6px}.dot{width:9px;height:9px;border-radius:50%;background:#b3261e}.dot.on{background:#188038} :host{display:block}section{height:100%;box-sizing:border-box}.liveValue{font-size:34px;font-weight:700;line-height:1.15;margin-top:10px}.secondaryValue{font-size:18px;font-weight:600;margin-top:2px}.state{display:inline-flex;align-items:center;gap:6px;margin-top:6px}.dot{width:9px;height:9px;border-radius:50%;background:#b3261e}.dot.on{background:#188038}
</style> </style>
<section> <section>
<h3>Live temperature</h3> <h3>Live temperature</h3>
<div class="muted">Server-Sent Events</div> <div class="muted">Server-Sent Events</div>
<div class="liveValue"><span id="temperature">--</span>&deg;C</div> <div class="liveValue"><span id="temperatureC">--</span>&deg;C</div>
<div class="secondaryValue"><span id="temperatureF">--</span>&deg;F</div>
<div class="muted state"><span id="dot" class="dot"></span><span id="state">Disconnected</span></div> <div class="muted state"><span id="dot" class="dot"></span><span id="state">Disconnected</span></div>
</section>`; </section>`;
window.addEventListener('app:login', event => this.start(event.detail.token)); window.addEventListener('app:login', event => this.start(event.detail.token));
@@ -36,13 +37,14 @@ class UptimeCard extends HTMLElement {
return; return;
} }
this.setState('Connecting', false); this.setState('Connecting', false);
this.source = new EventSource('/api/uptime/events?token=' + encodeURIComponent(token)); this.source = new EventSource('/api/temperature/events?token=' + encodeURIComponent(token));
this.source.addEventListener('open', () => this.setState('Connected', true)); this.source.addEventListener('open', () => this.setState('Connected', true));
this.source.addEventListener('temperature', event => { this.source.addEventListener('temperature', event => {
try { try {
const json = JSON.parse(event.data); const json = JSON.parse(event.data);
const connected = !!json.sensorConnected && json.temperatureC !== null; const connected = !!json.sensorConnected && json.temperatureC !== null && json.temperatureF !== null;
this.querySelector('#temperature').textContent = connected ? Number(json.temperatureC).toFixed(2) : '--'; this.querySelector('#temperatureC').textContent = connected ? Number(json.temperatureC).toFixed(2) : '--';
this.querySelector('#temperatureF').textContent = connected ? Number(json.temperatureF).toFixed(2) : '--';
this.setState(connected ? 'Connected' : 'Sensor unavailable', connected); this.setState(connected ? 'Connected' : 'Sensor unavailable', connected);
} catch (error) { } catch (error) {
this.setState('Invalid event', false); this.setState('Invalid event', false);
@@ -57,4 +59,4 @@ class UptimeCard extends HTMLElement {
} }
} }
customElements.define('uptime-card', UptimeCard); customElements.define('temperature-card', TemperatureCard);

View File

@@ -3,5 +3,5 @@
void handlePing(); void handlePing();
void handleAdd(); void handleAdd();
void handleLed(); void handleLed();
void handleUptime(); void handleTemperature();
void handleUptimeEvents(); void handleTemperatureEvents();

View File

@@ -5,8 +5,8 @@ ApiDef customApiDefs[] = {
{"/api/ping", "GET", "", true, handlePing, nullptr}, {"/api/ping", "GET", "", true, handlePing, nullptr},
{"/api/add", "POST", "", true, handleAdd, nullptr}, {"/api/add", "POST", "", true, handleAdd, nullptr},
{"/api/led", "POST", "", true, handleLed, nullptr}, {"/api/led", "POST", "", true, handleLed, nullptr},
{"/api/uptime", "GET", "WebUIConnect", false, handleUptime, nullptr}, {"/api/temperature", "GET", "WebUIConnect", false, handleTemperature, nullptr},
{"/api/uptime/events", "GET", "WebUIConnect", false, handleUptimeEvents, nullptr}, {"/api/temperature/events", "GET", "WebUIConnect", false, handleTemperatureEvents, nullptr},
}; };
const size_t CUSTOM_API_DEF_COUNT = sizeof(customApiDefs) / sizeof(customApiDefs[0]); const size_t CUSTOM_API_DEF_COUNT = sizeof(customApiDefs) / sizeof(customApiDefs[0]);

View File

@@ -71,12 +71,12 @@ void handleLed() {
sendJson(200, jsonOk("\"brightness\":" + String(ledBrightness))); sendJson(200, jsonOk("\"brightness\":" + String(ledBrightness)));
} }
void handleUptime() { void handleTemperature() {
if (!authorize()) return; if (!authorize()) return;
sendJson(200, jsonOk(temperatureJsonFields())); sendJson(200, jsonOk(temperatureJsonFields()));
} }
void handleUptimeEvents() { void handleTemperatureEvents() {
if (!authorize()) return; if (!authorize()) return;
String payload = "retry: 1000\n"; String payload = "retry: 1000\n";
payload += "event: temperature\n"; payload += "event: temperature\n";