Update Firmware via Browser
Update Firmware via Browser
Persiapan Untuk Client (esp 32)
- Buat folder OTAUpdateModule
- buat file OTAUpdateModule.h
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Update.h>
#include <WebServer.h>
#include "StorageModule.h"
class OTAUpdateModule {
public:
OTAUpdateModule(StorageModule* storage);
bool begin(); // Load config + start web routes
void loop(); // Handle WebServer loop
bool checkUpdate(); // Check JSON only
String getVersion(); // Return current version
String getEndpoint(); // Return update JSON endpoint
private:
StorageModule* storage;
WebServer server{80};
String latestVersion;
String firmwareVersion;
String updateJsonUrl;
bool updateAvailable = false;
bool updating = false;
bool loadConfig();
void handleHomePage();
void handleProgressUpdate();
void startOTAUpdate();
void performOTA();
};
- buat file OTAUpdateModule.cpp
#include "OTAUpdateModule.h"
OTAUpdateModule::OTAUpdateModule(StorageModule* storage) {
this->storage = storage;
}
bool OTAUpdateModule::begin() {
Serial.println("[OTA] Starting OTA module...");
if (!loadConfig()) {
Serial.println("[OTA] Config load failed.");
return false;
}
server.serveStatic("/home.css", LittleFS, "/web/home.css");
server.serveStatic("/home.js", LittleFS, "/web/home.js");
server.serveStatic("/update.css", LittleFS, "/web/update.css");
server.serveStatic("/update.js", LittleFS, "/web/update.js");
server.on("/", HTTP_GET, std::bind(&OTAUpdateModule::handleHomePage, this));
server.on("/update", HTTP_GET, std::bind(&OTAUpdateModule::handleProgressUpdate, this));
server.begin();
IPAddress ip = WiFi.localIP();
Serial.println("[OTA] WebServer started on " + ip.toString() + " port 80");
return true;
}
void OTAUpdateModule::loop() {
server.handleClient();
}
bool OTAUpdateModule::loadConfig() {
ArduinoJson::JsonDocument doc;
if (!storage->readJSON("/config.json", doc)) {
Serial.println("[OTA] ERROR: cannot read /config.json");
return false;
}
firmwareVersion = doc["version"] | "";
updateJsonUrl = doc["update_url"] | "";
if (firmwareVersion == "" || updateJsonUrl == "") {
Serial.println("[OTA] ERROR: config incomplete");
return false;
}
Serial.println("[OTA] Config loaded:");
Serial.println(" version = " + firmwareVersion);
return true;
}
bool OTAUpdateModule::checkUpdate() {
if (updateJsonUrl == "") {
Serial.println("[OTA] ERROR: updateJsonUrl empty");
return false;
}
HTTPClient http;
http.begin(updateJsonUrl);
int code = http.GET();
if (code != 200) {
Serial.println("[OTA] ERROR: Cannot GET update JSON");
return false;
}
String payload = http.getString();
http.end();
ArduinoJson::JsonDocument doc;
if (deserializeJson(doc, payload)) {
Serial.println("[OTA] JSON parse error");
return false;
}
latestVersion = doc["latest_version"] | "";
String url = doc["firmware_url"] | "";
if (latestVersion == "" || url == "") {
Serial.println("[OTA] JSON missing fields");
return false;
}
Serial.println("[OTA] Server version: " + latestVersion);
Serial.println("[OTA] Local version : " + firmwareVersion);
return (latestVersion != firmwareVersion);
}
void OTAUpdateModule::handleHomePage() {
Serial.println("[OTA] Serving Home Page");
HTTPClient http;
if (!http.begin(updateJsonUrl)) {
server.send(500, "text/html",
"<h3>Gagal mengakses update server!</h3>"
"<a href='/'><button>Kembali</button></a>"
);
return;
}
int code = http.GET();
if (code != 200) {
server.send(500, "text/html",
"<h3>Update server error: " + String(code) + "</h3>"
"<a href='/'><button>Kembali</button></a>"
);
http.end();
return;
}
String payload = http.getString();
http.end();
ArduinoJson::JsonDocument doc;
DeserializationError err = deserializeJson(doc, payload);
if (err) {
server.send(500, "text/html",
"<h3>Gagal parse JSON update!</h3>"
"<p>Error: " + String(err.c_str()) + "</p>"
"<a href='/'><button>Kembali</button></a>"
);
return;
}
String latest = doc["latest_version"] | "";
String fwUrl = doc["firmware_url"] | "";
bool updateAvailable = (latest != firmwareVersion);
File htmlFile = LittleFS.open("/web/home.html", "r");
String htmlContent = htmlFile.readString();
htmlFile.close();
String serverVars = "window.currentVersion='" + firmwareVersion + "';"
"window.latestVersion='" + latest + "';"
"window.updateAvailable=" + String(updateAvailable ? "true" : "false");
htmlContent.replace("<!--SERVER_VARS-->", serverVars);
server.send(200, "text/html", htmlContent);
}
void OTAUpdateModule::handleProgressUpdate() {
File file = LittleFS.open("/web/update.html", "r");
if (!file) {
server.send(500, "text/plain", "Gagal membuka halaman update!");
return;
}
server.streamFile(file, "text/html");
file.close();
startOTAUpdate();
}
void OTAUpdateModule::startOTAUpdate() {
if (updating) return;
updating = true;
// Jalankan OTA di task / thread terpisah jika menggunakan FreeRTOS
xTaskCreate([](void* param) {
OTAUpdateModule* self = (OTAUpdateModule*) param;
self->performOTA();
vTaskDelete(NULL);
}, "OTAUpdateTask", 8192, this, 1, NULL);
}
void OTAUpdateModule::performOTA() {
HTTPClient http;
http.begin(updateJsonUrl);
int code = http.GET();
if (code != 200) {
return;
}
String payload = http.getString();
http.end();
ArduinoJson::JsonDocument doc;
if (deserializeJson(doc, payload)) {
return;
}
latestVersion = doc["latest_version"] | "";
String url = doc["firmware_url"] | "";
WiFiClient client;
http.begin(client, url);
Serial.println("[OTA] Mengunduh firmware dari: " + url);
int httpCode = http.GET();
if (httpCode != 200) {
Serial.printf("[OTA] Gagal download firmware, code: %d\n", httpCode);
http.end();
updating = false;
return;
}
int contentLength = http.getSize();
WiFiClient* stream = http.getStreamPtr();
Serial.println("[OTA] HTTP Code = " + String(httpCode));
Serial.println("[OTA] Content-Type = " + http.header("Content-Type"));
Serial.println("[OTA] Content-Length = " + String(contentLength));
Serial.println("[OTA] Memulai update...");
if (!Update.begin(contentLength)) {
Serial.println("[OTA] Gagal memulai Update");
http.end();
updating = false;
return;
}
// baca firmware dari stream dan tulis ke flash
size_t written = Update.writeStream(*stream);
if (written == contentLength) {
Serial.println("[OTA] Firmware ditulis penuh");
} else {
Serial.printf("[OTA] Firmware tertulis %u dari %d\n", written, contentLength);
}
if (Update.end()) {
if (Update.isFinished()) {
Serial.println("[OTA] Update selesai, rebooting...");
ArduinoJson::JsonDocument config;
storage->readJSON("/config.json", config);
config["version"] = latestVersion;
Serial.println("[OTA] New version: " + latestVersion);
Serial.println("[OTA] Before version: " + firmwareVersion);
storage->writeJSON("/config.json", config);
delay(1000);
ESP.restart();
} else {
Serial.println("[OTA] Update gagal!");
}
} else {
Serial.printf("[OTA] Update error: %s\n", Update.errorString());
}
http.end();
updating = false;
}
- Edit config.json di folder data
{
"ssid": "[nama ssid]",
"password": "[password ssid]",
"version": "1.0.0",
"update_url": "http://ip:port/firmware/check.json"
}
- update file main.cpp
#include <Arduino.h>
#include <BlinkModule.h>
#include <WiFiModule.h>
#include <ArduinoOTA.h>
#include <StorageModule.h>
#include <BluetoothModule.h>
#include <OTAUpdateModule.h>
BlinkModule blink(2, 500);
StorageModule storage(true);
WiFiModule wifi(&storage);
BluetoothModule bt("ESP32_Device", &storage);
OTAUpdateModule ota(&storage);
void setup() {
Serial.begin(115200);
delay(200);
Serial.println("Project 01 - Blink Module");
blink.start();
storage.begin();
wifi.begin();
bt.begin();
ota.begin();
ArduinoOTA.setPort(3232);
ArduinoOTA.setPassword("otapassword");
ArduinoOTA.begin();
}
void loop() {
ArduinoOTA.handle();
bt.loop();
ota.loop();
}
- Buat file home.html di folder data/web
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Firmware Status</title>
<link rel="stylesheet" href="/home.css">
</head>
<body>
<div class="card">
<h2>ESP32 Firmware Status</h2>
<p>Current version: <span id="current-version"></span></p>
<p>Latest version: <span id="latest-version"></span></p>
<p id="update-msg"></p>
<div id="update-button"></div>
</div>
<!-- variabel JS dari server akan diletakkan sebelum script utama -->
<script id="server-vars">
<!--SERVER_VARS-->
</script>
<script src="/home.js"></script>
</body>
</html>
- buat file home.css
body {
font-family: Arial, sans-serif;
background: #f3f3f3;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.card {
background: #fff;
padding: 25px 30px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
max-width: 400px;
width: 90%;
text-align: center;
}
h2 {
margin-bottom: 20px;
color: #333;
}
p {
font-size: 16px;
margin: 10px 0;
}
.status {
font-weight: bold;
}
.update-available {
color: red;
}
.up-to-date {
color: green;
}
button {
margin-top: 15px;
padding: 10px 20px;
border: none;
border-radius: 6px;
background-color: #007bff;
color: white;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background-color: #0056b3;
}
a {
text-decoration: none;
}
-
buat file home.js
-
file update.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Updating Firmware...</title>
<link rel="stylesheet" href="/update.css">
</head>
<body>
<div class="box">
<h2>Sedang Update Firmware...</h2>
<div class="warn">
<b>Peringatan!</b><br>
Jangan tutup browser atau matikan perangkat.
</div>
<p>ESP32 sedang mengunduh dan memasang firmware terbaru.</p>
<p>Perangkat akan reboot otomatis.</p>
<p class="note">
Halaman ini akan mencoba masuk ke Home setelah reboot.
</p>
</div>
<script src="/update.js"></script>
</body>
</html>
- file update.css
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 30px;
background: #f3f3f3;
}
.box {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 400px;
margin: auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.warn {
background: #ffe5e5;
padding: 12px;
border-left: 4px solid #d00;
margin-bottom: 15px;
}
.note {
margin-top: 20px;
font-size: 12px;
color: #555;
}
- file update.js
// redirect ke home setelah 25 detik
setTimeout(() => {
window.location.href = "/";
}, 25000);
- upload firmware dengan perintah
pio run -t upload -d .\Project01\
- upload file sysytem dengan perintah
pio run -t uploadfs -d .\Project01\
- Pantau di serial monitor dengan perintah
pio device monitor -b 115200
Persiapan Server
- Install nodejs di laptop
- Buat sebuah folder beri nama web di folder project
- Salin package.json di folder web
{
"name": "esp_updater",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^5.1.0",
"nodemon": "^3.1.11"
}
}
- Jalankan npm update
- buat folder firmware
- salin hasil build ke folder firmaware.bin
- rename menjadi esp32.dev
- siapkan file server.js
const express = require("express");
const fs = require("fs");
const os = require("os");
const crypto = require("crypto");
const app = express();
function getAllIPs() {
const nets = os.networkInterfaces();
const results = [];
const ignorePatterns = [
/vmnet/i, // VMware Linux/Mac (vmnet1, vmnet8)
/vmware/i, // Windows: "VMware Network Adapter"
/vbox/i, // Optional: VirtualBox (hapus kalau tidak ingin)
];
for (const name of Object.keys(nets)) {
if (ignorePatterns.some(re => re.test(name))) {
continue;
}
for (const net of nets[name]) {
if (net.family === "IPv4" && !net.internal) {
results.push({
iface: name,
address: net.address
});
}
}
}
return results;
}
const PORT = 3000;
const FIRMWARE_FILE = "./firmware/esp32.bin";
// =======================
// HALAMAN UTAMA
// =======================
app.get("/", (req, res) => {
res.send(`
<html>
<head>
<title>ESP32 Firmware Server</title>
<style>
body {
font-family: Arial;
padding: 20px;
background: #f5f5f5;
}
.box {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
max-width: 500px;
margin: auto;
}
.btn {
padding: 10px 15px;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 6px;
}
</style>
</head>
<body>
<div class="box">
<h2>ESP32 Firmware Update Server</h2>
<p>Endpoint cek firmware:</p>
<a class="btn" href="/firmware/check.json">Test Endpoint</a>
</div>
</body>
</html>
`);
});
// =======================
// ENDPOINT CHECK FIRMWARE
// =======================
app.get("/firmware/check.json", (req, res) => {
// 1️⃣ Cek file ada atau tidak
if (!fs.existsSync(FIRMWARE_FILE)) {
return res.status(404).send(`
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-family: Arial; background: #f5f5f5; padding: 20px; }
.box {
background: white; padding: 20px; border-radius: 8px;
max-width: 500px; margin: auto; text-align: center;
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
}
.btn {
display: inline-block; margin-top: 15px; padding: 10px 15px;
background: #007bff; color: white; text-decoration: none;
border-radius: 6px;
}
</style>
</head>
<body>
<div class="box">
<h2>404 – Firmware Tidak Ditemukan</h2>
<p>File <b>esp32.bin</b> tidak tersedia di server.</p>
<a href="javascript:history.back()" class="btn">Kembali</a>
</div>
</body>
</html>
`);
}
// 2️⃣ Ambil data firmware + hitung SHA256
const firmware = fs.readFileSync(FIRMWARE_FILE);
const sha = crypto.createHash("sha256").update(firmware).digest("hex");
const proto = req.headers["x-forwarded-proto"] || req.protocol;
const host = `${proto}://${req.headers.host}`;
const url = `${host}/esp32.bin`;
// 3️⃣ Jika user membuka lewat browser → HTML + tombol Download
if (req.headers["user-agent"] && !req.headers["user-agent"].includes("ESP")) {
return res.send(`
<html>
<head>
<title>Firmware Info</title>
<style>
body { font-family: Arial; padding: 20px; background: #f0f0f0; }
.box { background:white; padding:20px; border-radius:8px; max-width:600px; margin:auto; }
.btn { padding:10px 15px; background:#28a745; color:white; text-decoration:none; border-radius:6px; }
pre { background:#eee; padding:10px; border-radius:6px; }
</style>
</head>
<body>
<div class="box">
<h2>Firmware Ditemukan</h2>
<div class="warn">
⚠️ <b>Peringatan!</b><br>
Jangan menutup browser atau mematikan perangkat selama proses update berlangsung.
</div>
<p>Versi terbaru: <b>1.0.6</b></p>
<p>SHA256:</p>
<pre>${sha}</pre>
<a class="btn" href="${url}" download>Download Firmware</a>
<br><br>
<a href="/" style="text-decoration:none;">⬅ Kembali</a>
</div>
</body>
</html>
`);
}
// 4️⃣ Jika diakses dari ESP32 → kirim JSON
res.json({
latest_version: "1.1.1",
firmware_url: url,
sha256: sha
});
});
// =======================
// FILE BIN
// =======================
app.get("/esp32.bin", (req, res) => {
res.sendFile(__dirname + "/firmware/esp32.bin");
});
// =======================
// JALANKAN SERVER
// =======================
app.listen(PORT, () =>
getAllIPs().forEach(ip => {
console.log(`${ip.iface} http://${ip.address}:${PORT}`);
})
);
- jalankan server dengan perintah
npm run dev
- salin ip dan port ke config.json dan upload ulang
Check it out!
Tags:
Ikuti terus tutorial saya di e-Project dan channel
saya di