diff --git a/resources/AGENTS.md b/resources/AGENTS.md
new file mode 100644
index 00000000..5dec3726
--- /dev/null
+++ b/resources/AGENTS.md
@@ -0,0 +1,86 @@
+# Device dashboard
+
+This is the AI specification file for the Mongoose based Web device dashboard functionality
+
+Mongoose-base device dashboard requires following files:
+- mongoose.h
+- mongoose.c
+- dashboard.c
+- file_data.c
+
+The `file_data.c` is a generated file from `dashboard.html` using Nodejs:
+```sh
+node html2c.js dashboard.html -o file_data.c
+```
+The `html2c.js` file can be taken from https://github.com/cesanta/mongoose/blob/master/resources/html2c.js
+
+
+## General rules
+
+Your goal is to produce a dashboard.c and dashboard.html files
+If your project does not yet have those, fetch from here:
+
+https://github.com/cesanta/mongoose/blob/master/tutorials/device-dashboard/minimal/dashboard.c
+https://github.com/cesanta/mongoose/blob/master/tutorials/device-dashboard/minimal/dashboard.html
+
+If your project already has dashboard.c and dashboard.html, do NOT fetch anything.
+
+The C API header file is at https://github.com/cesanta/mongoose/blob/master/src/dash.h
+The minimal reference project is at https://github.com/cesanta/mongoose/blob/master/tutorials/device-dashboard/minimal/
+The full reference project is at https://github.com/cesanta/mongoose/blob/master/tutorials/device-dashboard/full/
+
+## dashboard.html
+
+For editing dashboard.html, use only Mongoose `data-bind` and `${...}` template syntax
+No vanilla JS event listeners, no fetch calls, no custom reactive logic.
+Do not make any changes to dashboard.html unless asked by a user.
+
+## dashboard.c
+
+Do not make any changes to dashboard.c unless asked by a user.
+
+## CubeMX project
+
+If you have installed I-CUBE-Mongoose pack, then all required files are installed
+in the `Middlewares/Third_Party/Cesanta_Mongoose/` directory. Do not fetch
+dashboard.html, or any other file, from the Mongoose repository.
+
+Add `#include "mongoose.h"` to the "USER CODE BEGIN Includes" block
+
+Add automatic `file_data.c` generation for the build step - either Makefile,
+or CMakeLists.txt or whatever is appropriate for the selected IDE.
+```sh
+node html2c.js dashboard.html -o file_data.c
+```
+
+
+To the "USER CODE BEGIN 0" block, add debug print redirect. If syscalls.c already
+has a weak _write override, ignore it and still add this.
+```c
+int _write(int fd, unsigned char *buf, int len) {
+ HAL_UART_Transmit(&huart3, buf, len, HAL_MAX_DELAY);
+ return len;
+}
+```
+
+Make sure main() initialises and processes dashboard:
+
+```c
+/* USER CODE BEGIN WHILE */
+struct mg_mgr mgr;
+mg_mgr_init(&mgr);
+mg_dash_init(&mgr);
+
+while (1)
+{
+ mg_mgr_poll(&mgr, 1);
+ mg_dash_poll(&mgr);
+/* USER CODE END WHILE */
+```
+
+In order to verify the validity of your edits, compile `dashboard.c` using
+embedded or host compiler in `Middlewares/Third_Party/Cesanta_Mongoose/`:
+
+```sh
+$(CC) -c dashboard.c
+```
diff --git a/resources/I-CUBE-Mongoose/Cesanta.I-CUBE-Mongoose.pdsc b/resources/I-CUBE-Mongoose/Cesanta.I-CUBE-Mongoose.pdsc
index a0a3c131..b6a49aaf 100644
--- a/resources/I-CUBE-Mongoose/Cesanta.I-CUBE-Mongoose.pdsc
+++ b/resources/I-CUBE-Mongoose/Cesanta.I-CUBE-Mongoose.pdsc
@@ -8,6 +8,7 @@
License.txt
https://mongoose.ws/contact/
+ See https://github.com/cesanta/mongoose/releases/tag/7.21
See https://github.com/cesanta/mongoose/releases/tag/7.21
See https://github.com/cesanta/mongoose/releases/tag/7.21
See https://github.com/cesanta/mongoose/releases/tag/7.21
@@ -23,24 +24,26 @@
-
+
Networking Core
+
-
+
Web Device Dashboard
-
+
+
-
+
MQTT Client
diff --git a/resources/I-CUBE-Mongoose/Makefile b/resources/I-CUBE-Mongoose/Makefile
index 4cd7b3ba..e11d5b2f 100644
--- a/resources/I-CUBE-Mongoose/Makefile
+++ b/resources/I-CUBE-Mongoose/Makefile
@@ -1,12 +1,14 @@
-VERSION = 7.21.5
+VERSION = 7.21.6
REPO_ROOT = ../..
+MX ?= /Applications/STMicroelectronics/STM32CubeMX.app/Contents/MacOs/STM32CubeMX
FILES = Cesanta.I-CUBE-Mongoose.pdsc Release_Notes.html mongoose_config.h README.md
GENERATED = License.txt \
mongoose.h mongoose.c \
- dashboard.c file_data.c dashboard.html \
+ dashboard.c file_data.c dashboard.html html2c.js \
mongoose_mqtt.c
-all: Cesanta.I-CUBE-Mongoose.$(VERSION).pack
+PACK = Cesanta.I-CUBE-Mongoose.$(VERSION).pack
+all: $(PACK)
Cesanta.I-CUBE-Mongoose.$(VERSION).pack: $(FILES) $(GENERATED)
zip -q -r $@ $^
@@ -33,6 +35,9 @@ file_data.c:
make -C $(REPO_ROOT)/tutorials/device-dashboard/full $@
cp $(REPO_ROOT)/tutorials/device-dashboard/full/$@ $@
+html2c.js:
+ cp $(REPO_ROOT)/resources/$@ $@
+
mongoose_mqtt.c:
cp $(REPO_ROOT)/tutorials/mqtt/mqtt-client/mongoose_mqtt.c $@
@@ -42,5 +47,23 @@ mongoose.h: Mongoose
mongoose.c: Mongoose
cp $(REPO_ROOT)/mongoose.c $@
+pk: $(PACK)
+ rm -f gen.mx
+ echo swmgr remove Cesanta.I-CUBE-Mongoose.$(VERSION) >> gen.mx
+ echo swmgr install `pwd`/Cesanta.I-CUBE-Mongoose.$(VERSION).pack ask >> gen.mx
+ echo exit >> gen.mx
+ $(MX) -q `pwd`/gen.mx
+
+
+mx: $(PACK)
+ mkdir -p $@
+ cp ../downloads/nucleo_h563zi.ioc $@
+ V=`grep 'Mcu.ThirdParty0' $@/nucleo_h563zi.ioc | sed 's,^.*se.,,'`; sed -i '' "s,$$V,$(VERSION),g" $@/nucleo_h563zi.ioc
+ rm -f gen.mx
+ echo config load `pwd`/$@/nucleo_h563zi.ioc >> gen.mx
+ echo project generate >> gen.mx
+ echo exit >> gen.mx
+ $(MX) -q `pwd`/gen.mx
+
clean:
- rm -rf *.pack STM32CubeMX/*.$(VERSION)_*.xml $(GENERATED)
+ rm -rf *.pack STM32CubeMX/*.$(VERSION)_*.xml $(GENERATED) gen.mx mx
diff --git a/resources/I-CUBE-Mongoose/README.md b/resources/I-CUBE-Mongoose/README.md
index 28c4c27d..bca3bca7 100644
--- a/resources/I-CUBE-Mongoose/README.md
+++ b/resources/I-CUBE-Mongoose/README.md
@@ -102,6 +102,11 @@ Hi from Mongoose! Tick 38187
## Add full device dashboard
Enable "Mongoose Web Device Dashboard" component in CubeMX. Regenerate code.
+This component installs the following files:
+- `dashboard.c` - a C backend side
+- `dashboard.html` - an HTML frontend side: the Web UI itself
+- `file_data.c` - this file is generated from `dashboard.html`
+- `html2c.js` - a node.js script that generates `file_data.c` using this command: `node html2c.js dashboard.html -o file_data.c`. This script first inlines all external resources into an amalgamated HTML file - so it does not depend on anything external, and then zips it and packs into a Mongoose embedded filesystem. This way it takes the minimum space on flash.
Remove the hello world server if you added it. Modify your while loop
to look like this:
@@ -122,6 +127,19 @@ while (1)
Rebuild, reflash your board and enjoy the fully functional device dashboard
that you can tailor to your firmware.
+To customise your dashboard, edit `dashboard.c` and `dashboard.html`.
+After editing `dashboard.html`, regenerate `file_data.c` using this command:
+
+```sh
+node html2c.js dashboard.html -o file_data.c
+```
+
+For agentic development, use https://github.com/cesanta/mongoose/tree/master/resources/AGENTS.md
+Use a prompt like this:
+
+> Read and apply https://github.com/cesanta/mongoose/tree/master/resources/AGENTS.md
+
+
## Add MQTT Client
Enable "Mongoose MQTT Client" component in CubeMX. Regenerate code.
diff --git a/resources/downloads/Makefile b/resources/downloads/Makefile
index 18b8ca78..64aecb47 100644
--- a/resources/downloads/Makefile
+++ b/resources/downloads/Makefile
@@ -3,7 +3,7 @@ ST_TARGETS = nucleo_h723zg nucleo_f756zg nucleo_h563zi
# ST_TARGETS = $(subst .ioc,,$(wildcard *.ioc))
PROJECTS = full minimal
TARGETS = desktop $(ST_TARGETS)
-MX ?= /Applications/STM32CubeMX.app/Contents/MacOs/STM32CubeMX
+MX ?= /Applications/STMicroelectronics/STM32CubeMX.app/Contents/MacOs/STM32CubeMX
DIRS = $(foreach P,$(PROJECTS),$(foreach T,$(TARGETS),$(T)_$(P)))
ZIPS = $(foreach D,$(DIRS),$(D).zip)
diff --git a/resources/html2c.js b/resources/html2c.js
new file mode 100644
index 00000000..ec2253db
--- /dev/null
+++ b/resources/html2c.js
@@ -0,0 +1,179 @@
+// Copyright (c) 2026 Cesanta Software Limited
+// All rights reserved
+//
+// Generate a Mongoose packed filesystem C file from an HTML file after
+// inlining referenced CSS, JS, images, and icons.
+//
+// Usage:
+// node html2c.js [-o OUTPUT.c] HTML_FILE
+
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const https = require('https');
+
+const args = process.argv.slice(2);
+let outputFile = null;
+let inputFile = null;
+
+for (let i = 0; i < args.length; i++) {
+ if (args[i] === '-o' && i + 1 < args.length) {
+ outputFile = args[++i];
+ } else if (!args[i].startsWith('-') && inputFile === null) {
+ inputFile = args[i];
+ }
+}
+
+if (!inputFile) {
+ console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE');
+ process.exit(1);
+}
+
+const baseDir = path.dirname(path.resolve(inputFile));
+const html = fs.readFileSync(inputFile, 'utf8');
+
+function fetchUrl(url) {
+ return new Promise((resolve, reject) => {
+ const client = url.startsWith('https') ? https : http;
+ client
+ .get(url, res => {
+ const chunks = [];
+ res.on('data', chunk => chunks.push(chunk));
+ res.on('end', () => resolve(Buffer.concat(chunks)));
+ res.on('error', reject);
+ })
+ .on('error', reject);
+ });
+}
+
+async function readAsset(name, encoding) {
+ if (name.startsWith('http://') || name.startsWith('https://')) {
+ const data = await fetchUrl(name);
+ return encoding ? data.toString(encoding) : data;
+ }
+ return fs.readFileSync(path.join(baseDir, name), encoding || null);
+}
+
+function attr(match, name) {
+ const re = new RegExp(`${name}=["']([^"']+)["']`, 'i');
+ const m = match.match(re);
+ return m ? m[1] : null;
+}
+
+function replaceAttr(match, name, value) {
+ const re = new RegExp(`${name}=(["'])[^"']+\\1`, 'i');
+ return match.replace(re, `${name}="${value}"`);
+}
+
+async function inlineCss(match) {
+ const href = attr(match, 'href');
+ if (!href) return match;
+ const css = await readAsset(href, 'utf8');
+ return ``;
+}
+
+async function inlineJs(match) {
+ const src = attr(match, 'src');
+ if (!src) return match;
+ const js = await readAsset(src, 'utf8');
+ return ``;
+}
+
+async function inlineImg(match) {
+ const src = attr(match, 'src');
+ if (!src) return match;
+ const ext = path.extname(src).toLowerCase();
+ let mime = 'image/png';
+ if (ext === '.svg') mime = 'image/svg+xml';
+ else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg';
+ else if (ext === '.gif') mime = 'image/gif';
+ else if (ext === '.webp') mime = 'image/webp';
+ else if (ext === '.ico') mime = 'image/x-icon';
+
+ const data = await readAsset(src);
+ if (ext === '.svg') {
+ const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data;
+ return replaceAttr(
+ match,
+ 'src',
+ `data:${mime};utf8,${encodeURIComponent(svg)}`,
+ );
+ }
+ return replaceAttr(
+ match,
+ 'src',
+ `data:${mime};base64,${Buffer.from(data).toString('base64')}`,
+ );
+}
+
+async function inlineSvg(match) {
+ const href = attr(match, 'href');
+ if (!href || !href.toLowerCase().endsWith('.svg')) return match;
+ const data = await readAsset(href);
+ return replaceAttr(
+ match,
+ 'href',
+ `data:image/svg+xml;base64,${Buffer.from(data).toString('base64')}`,
+ );
+}
+
+async function processHtml(content) {
+ let result = content;
+ const cssRegex = /]+rel=["']stylesheet["'][^>]*>/g;
+ const jsRegex = /