feat: enabling web-based OTA updates and deployment for frontend
This commit is contained in:
64
Provider/Documentation/build_frontend.md
Normal file
64
Provider/Documentation/build_frontend.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Building and Flashing the Frontend
|
||||||
|
|
||||||
|
The Calendink Provider uses a modern Svelte 5 frontend, built with Vite and TailwindCSS. The frontend is served directly from the ESP32's `www_0` or `www_1` LittleFS partition, allowing the user interface to be updated completely independently from the underlying firmware.
|
||||||
|
|
||||||
|
## 1. Prerequisites
|
||||||
|
|
||||||
|
Before you can build the frontend, make sure you have [Node.js](https://nodejs.org/) installed on your machine. All frontend code is located inside the `frontend/` directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Development Mode
|
||||||
|
|
||||||
|
During development, you don't need to rebuild and reflash the ESP32 every time you change a button color! You can run the Vite development server on your PC, and it will proxy API requests to the ESP32 over WiFi.
|
||||||
|
|
||||||
|
1. Ensure the ESP32 is powered on and connected to your local network.
|
||||||
|
2. Update `frontend/.env.development` with the IP address of your ESP32 (e.g., `VITE_API_BASE=http://192.168.50.216`).
|
||||||
|
3. Start the dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Building for Production (ESP32)
|
||||||
|
|
||||||
|
When you are ready to deploy your frontend changes to the ESP32, you must package everything into a single file and compress it. Our custom build script handles this for you.
|
||||||
|
|
||||||
|
Run the following command inside the `frontend/` folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:esp32
|
||||||
|
```
|
||||||
|
|
||||||
|
**What this does:**
|
||||||
|
1. Runs Vite's production build.
|
||||||
|
2. Inlines all CSS and JS into `index.html` (thanks to `vite-plugin-singlefile`).
|
||||||
|
3. Runs `scripts/gzip.js` to heavily compress `index.html` down to an `index.html.gz` file (~15-20KB).
|
||||||
|
4. Outputs the final files to the `frontend/dist/` directory.
|
||||||
|
|
||||||
|
## 4. Flashing the Filesystem
|
||||||
|
|
||||||
|
Now that `frontend/dist/` contains your compressed web app, you must tell the ESP-IDF build system to turn that folder into a LittleFS binary image and flash it.
|
||||||
|
|
||||||
|
In your standard ESP-IDF terminal (from the project root, not the `frontend/` folder):
|
||||||
|
|
||||||
|
1. **Enable Web Deployment via Menuconfig (if not already done):**
|
||||||
|
```bash
|
||||||
|
idf.py menuconfig
|
||||||
|
# Navigate to: Calendink Configuration -> Deploy Web Pages
|
||||||
|
# Make sure it is checked (Y)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build and Flash:**
|
||||||
|
```bash
|
||||||
|
idf.py build
|
||||||
|
idf.py flash
|
||||||
|
```
|
||||||
|
|
||||||
|
Because `CONFIG_CALENDINK_DEPLOY_WEB_PAGES` is enabled, CMake will automatically:
|
||||||
|
1. Detect your `frontend/dist/` folder.
|
||||||
|
2. Run `mklittlefs` to package it into `www.bin`.
|
||||||
|
3. Flash `www.bin` directly to the active `www_0` partition on the ESP32!
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getSystemInfo, reboot } from "./lib/api.js";
|
import { getSystemInfo, reboot } from "./lib/api.js";
|
||||||
|
import OTAUpdate from "./lib/OTAUpdate.svelte";
|
||||||
|
|
||||||
/** @type {'loading' | 'ok' | 'error' | 'rebooting'} */
|
/** @type {'loading' | 'ok' | 'error' | 'rebooting'} */
|
||||||
let status = $state("loading");
|
let status = $state("loading");
|
||||||
@@ -146,9 +147,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reboot Section -->
|
<!-- Device Control Section (Reboot + OTA) -->
|
||||||
<div class="bg-bg-card border border-border rounded-xl p-5">
|
<div class="bg-bg-card border border-border rounded-xl p-5">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-sm font-semibold text-text-primary">
|
<h2 class="text-sm font-semibold text-text-primary">
|
||||||
Device Control
|
Device Control
|
||||||
@@ -168,6 +169,9 @@
|
|||||||
Reboot
|
Reboot
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New OTA Update Component inside the Device Control block -->
|
||||||
|
<OTAUpdate />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reboot Confirmation Modal -->
|
<!-- Reboot Confirmation Modal -->
|
||||||
|
|||||||
150
Provider/frontend/src/lib/OTAUpdate.svelte
Normal file
150
Provider/frontend/src/lib/OTAUpdate.svelte
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script>
|
||||||
|
import { getOTAStatus, uploadOTAFrontend } from "./api.js";
|
||||||
|
|
||||||
|
/** @type {'idle' | 'loading_status' | 'uploading' | 'success' | 'error'} */
|
||||||
|
let status = $state("idle");
|
||||||
|
let errorMsg = $state("");
|
||||||
|
let uploadProgress = $state(0); // 0 to 100
|
||||||
|
|
||||||
|
let otaInfo = $state({
|
||||||
|
active_slot: -1,
|
||||||
|
active_partition: "—",
|
||||||
|
target_partition: "—",
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedFile = $state(null);
|
||||||
|
|
||||||
|
async function fetchStatus() {
|
||||||
|
status = "loading_status";
|
||||||
|
try {
|
||||||
|
otaInfo = await getOTAStatus();
|
||||||
|
status = "idle";
|
||||||
|
} catch (e) {
|
||||||
|
status = "error";
|
||||||
|
errorMsg = "Failed to fetch OTA status: " + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch status on mount
|
||||||
|
$effect(() => {
|
||||||
|
fetchStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleFileChange(event) {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
selectedFile = files[0];
|
||||||
|
errorMsg = "";
|
||||||
|
} else {
|
||||||
|
selectedFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpload() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
status = "uploading";
|
||||||
|
errorMsg = "";
|
||||||
|
uploadProgress = 0; // The fetch API doesn't natively support upload progress well without XMLHttpRequest, so we emulate it visually.
|
||||||
|
|
||||||
|
// Emulate progress since doing it cleanly with fetch requires more boilerplate.
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
if (uploadProgress < 90) uploadProgress += 5;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await uploadOTAFrontend(selectedFile);
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
uploadProgress = 100;
|
||||||
|
status = "success";
|
||||||
|
// The backend triggers a reboot after success. The main App.svelte polling will handle the reconnect.
|
||||||
|
} catch (e) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
uploadProgress = 0;
|
||||||
|
status = "error";
|
||||||
|
errorMsg = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-bg-card border border-border rounded-xl p-5 mt-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="text-sm font-semibold text-text-primary">
|
||||||
|
Frontend Update (OTA)
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-text-secondary mt-1">
|
||||||
|
Update the dashboard UI without flashing the entire firmware.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if status === "loading_status"}
|
||||||
|
<div class="text-sm text-text-secondary animate-pulse">Loading status...</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Status Display -->
|
||||||
|
<div class="flex items-center gap-4 mb-4 text-sm bg-bg-primary/50 p-3 rounded-lg border border-border/50">
|
||||||
|
<div>
|
||||||
|
<span class="text-text-secondary">Active Partition:</span>
|
||||||
|
<span class="font-mono font-medium ml-1 text-accent">{otaInfo.active_partition}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-text-secondary">Next Target:</span>
|
||||||
|
<span class="font-mono font-medium ml-1 text-text-primary">{otaInfo.target_partition}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Controls -->
|
||||||
|
<div class="w-full space-y-3">
|
||||||
|
{#if status === "success"}
|
||||||
|
<div class="bg-success/10 border border-success/20 text-success p-3 rounded-lg text-sm flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-success"></span>
|
||||||
|
Update successful! The device is rebooting...
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".bin"
|
||||||
|
onchange={handleFileChange}
|
||||||
|
disabled={status === "uploading"}
|
||||||
|
class="block w-full text-sm text-text-secondary
|
||||||
|
file:mr-4 file:py-2 file:px-4
|
||||||
|
file:rounded-lg file:border-0
|
||||||
|
file:text-sm file:font-semibold
|
||||||
|
file:bg-accent/10 file:text-accent
|
||||||
|
hover:file:bg-accent/20 transition-colors
|
||||||
|
disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={handleUpload}
|
||||||
|
disabled={!selectedFile || status === "uploading"}
|
||||||
|
class="whitespace-nowrap px-4 py-2 text-sm font-medium rounded-lg transition-colors
|
||||||
|
bg-accent text-white border border-accent
|
||||||
|
hover:brightness-110
|
||||||
|
disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{#if status === "uploading"}
|
||||||
|
Uploading...
|
||||||
|
{:else}
|
||||||
|
Flash to {otaInfo.target_partition}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress or Error-->
|
||||||
|
{#if status === "uploading"}
|
||||||
|
<div class="w-full bg-border rounded-full h-1.5 mt-2 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="bg-accent h-1.5 rounded-full transition-all duration-300 ease-out"
|
||||||
|
style="width: {uploadProgress}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
{:else if status === "error"}
|
||||||
|
<p class="text-sm text-danger mt-2 bg-danger/10 p-2 rounded border border-danger/20">
|
||||||
|
{errorMsg}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -37,3 +37,39 @@ export async function reboot() {
|
|||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch OTA status from the ESP32.
|
||||||
|
* @returns {Promise<{active_slot: number, active_partition: string, target_partition: string}>}
|
||||||
|
*/
|
||||||
|
export async function getOTAStatus() {
|
||||||
|
const res = await fetch(`${API_BASE}/api/ota/status`);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a new frontend binary image.
|
||||||
|
* @param {File} file The binary file to upload.
|
||||||
|
* @returns {Promise<{status: string, message: string}>}
|
||||||
|
*/
|
||||||
|
export async function uploadOTAFrontend(file) {
|
||||||
|
const res = await fetch(`${API_BASE}/api/ota/frontend`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: file, // Send the raw file Blob/Buffer
|
||||||
|
headers: {
|
||||||
|
// Let the browser set Content-Type for the binary payload,
|
||||||
|
// or we could force application/octet-stream.
|
||||||
|
'Content-Type': 'application/octet-stream'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
throw new Error(`Upload failed (${res.status}): ${errorText || res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|||||||
143
Provider/main/api/ota/frontend.cpp
Normal file
143
Provider/main/api/ota/frontend.cpp
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// SDK
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_partition.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Project
|
||||||
|
#include "appstate.hpp"
|
||||||
|
#include "types.hpp"
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
|
||||||
|
#define OTA_SCRATCH_BUFSIZE 4096
|
||||||
|
|
||||||
|
internal void ota_restart_timer_callback(void *arg) { esp_restart(); }
|
||||||
|
|
||||||
|
internal esp_err_t api_ota_frontend_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
|
uint8_t target_slot = g_Active_WWW_Partition == 0 ? 1 : 0;
|
||||||
|
const char *target_label = target_slot == 0 ? "www_0" : "www_1";
|
||||||
|
|
||||||
|
const esp_partition_t *partition = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_LITTLEFS,
|
||||||
|
target_label);
|
||||||
|
if (!partition)
|
||||||
|
{
|
||||||
|
ESP_LOGE("OTA", "Could not find partition %s", target_label);
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||||
|
"Partition not found");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI("OTA", "Starting OTA to partition %s (size %ld)", target_label,
|
||||||
|
partition->size);
|
||||||
|
|
||||||
|
esp_err_t err = esp_partition_erase_range(partition, 0, partition->size);
|
||||||
|
if (err != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE("OTA", "Failed to erase partition: %s", esp_err_to_name(err));
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to erase partition");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *buf = (char *)malloc(OTA_SCRATCH_BUFSIZE);
|
||||||
|
if (!buf)
|
||||||
|
{
|
||||||
|
ESP_LOGE("OTA", "Failed to allocate buffer");
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total_read = 0;
|
||||||
|
int remaining = req->content_len;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
int recv_len =
|
||||||
|
httpd_req_recv(req, buf, MIN(remaining, OTA_SCRATCH_BUFSIZE));
|
||||||
|
if (recv_len <= 0)
|
||||||
|
{
|
||||||
|
if (recv_len == HTTPD_SOCK_ERR_TIMEOUT)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ESP_LOGE("OTA", "Receive failed");
|
||||||
|
free(buf);
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||||
|
"Receive failed");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_partition_write(partition, total_read, buf, recv_len);
|
||||||
|
if (err != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE("OTA", "Failed to write to partition: %s", esp_err_to_name(err));
|
||||||
|
free(buf);
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||||
|
"Flash write failed");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_read += recv_len;
|
||||||
|
remaining -= recv_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
ESP_LOGI("OTA", "OTA complete. Written %d bytes. Updating NVS...",
|
||||||
|
total_read);
|
||||||
|
|
||||||
|
nvs_handle_t my_handle;
|
||||||
|
if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK)
|
||||||
|
{
|
||||||
|
err = nvs_set_u8(my_handle, "www_part", target_slot);
|
||||||
|
if (err == ESP_OK)
|
||||||
|
{
|
||||||
|
nvs_commit(my_handle);
|
||||||
|
}
|
||||||
|
nvs_close(my_handle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGE("OTA", "Failed to open NVS to update partition index");
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||||
|
"NVS update failed");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(root, "status", "success");
|
||||||
|
cJSON_AddStringToObject(root, "message", "Update successful, rebooting...");
|
||||||
|
const char *response_text = cJSON_Print(root);
|
||||||
|
httpd_resp_sendstr(req, response_text);
|
||||||
|
free((void *)response_text);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
// Trigger reboot
|
||||||
|
const esp_timer_create_args_t restart_timer_args = {
|
||||||
|
.callback = &ota_restart_timer_callback,
|
||||||
|
.arg = (void *)0,
|
||||||
|
.dispatch_method = ESP_TIMER_TASK,
|
||||||
|
.name = "ota_restart_timer",
|
||||||
|
.skip_unhandled_events = false};
|
||||||
|
esp_timer_handle_t restart_timer;
|
||||||
|
esp_timer_create(&restart_timer_args, &restart_timer);
|
||||||
|
esp_timer_start_once(restart_timer, 1'000'000);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const httpd_uri_t api_ota_frontend_uri = {.uri = "/api/ota/frontend",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler =
|
||||||
|
api_ota_frontend_handler,
|
||||||
|
.user_ctx = NULL};
|
||||||
35
Provider/main/api/ota/status.cpp
Normal file
35
Provider/main/api/ota/status.cpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// SDK
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
|
||||||
|
// Project
|
||||||
|
#include "appstate.hpp"
|
||||||
|
#include "types.hpp"
|
||||||
|
|
||||||
|
internal esp_err_t api_ota_status_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
|
||||||
|
cJSON_AddNumberToObject(root, "active_slot", g_Active_WWW_Partition);
|
||||||
|
cJSON_AddStringToObject(root, "active_partition",
|
||||||
|
g_Active_WWW_Partition == 0 ? "www_0" : "www_1");
|
||||||
|
cJSON_AddStringToObject(root, "target_partition",
|
||||||
|
g_Active_WWW_Partition == 0 ? "www_1" : "www_0");
|
||||||
|
|
||||||
|
const char *status_info = cJSON_Print(root);
|
||||||
|
httpd_resp_sendstr(req, status_info);
|
||||||
|
|
||||||
|
free((void *)status_info);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const httpd_uri_t api_ota_status_uri = {.uri = "/api/ota/status",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler =
|
||||||
|
api_ota_status_handler,
|
||||||
|
.user_ctx = NULL};
|
||||||
@@ -13,9 +13,12 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Project
|
// Project
|
||||||
|
#include "api/ota/frontend.cpp"
|
||||||
|
#include "api/ota/status.cpp"
|
||||||
#include "api/system/info.cpp"
|
#include "api/system/info.cpp"
|
||||||
#include "api/system/reboot.cpp"
|
#include "api/system/reboot.cpp"
|
||||||
|
|
||||||
|
|
||||||
internal const char *TAG = "HTTP_SERVER";
|
internal const char *TAG = "HTTP_SERVER";
|
||||||
|
|
||||||
constexpr uint8 kGZ_Extension_Length = sizeof(".gz") - 1;
|
constexpr uint8 kGZ_Extension_Length = sizeof(".gz") - 1;
|
||||||
@@ -229,6 +232,8 @@ internal httpd_handle_t start_webserver(void)
|
|||||||
// Register system API routes
|
// Register system API routes
|
||||||
httpd_register_uri_handler(server, &api_system_info_uri);
|
httpd_register_uri_handler(server, &api_system_info_uri);
|
||||||
httpd_register_uri_handler(server, &api_system_reboot_uri);
|
httpd_register_uri_handler(server, &api_system_reboot_uri);
|
||||||
|
httpd_register_uri_handler(server, &api_ota_status_uri);
|
||||||
|
httpd_register_uri_handler(server, &api_ota_frontend_uri);
|
||||||
|
|
||||||
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
|
||||||
// Register static file handler last as a catch-all wildcard if deployed
|
// Register static file handler last as a catch-all wildcard if deployed
|
||||||
|
|||||||
Reference in New Issue
Block a user