bitwrench 2.0.21 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +1 -1
- package/README.md +4 -5
- package/bin/bwmcp.js +3 -0
- package/dist/bitwrench-bccl.cjs.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +3 -3
- package/dist/bitwrench-lean.cjs.min.js +2 -2
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +3 -3
- package/dist/bitwrench-lean.es5.min.js +2 -2
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +3 -3
- package/dist/bitwrench-lean.esm.min.js +2 -2
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +3 -3
- package/dist/bitwrench-lean.umd.min.js +2 -2
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-css.cjs.js +1 -1
- package/dist/bitwrench-util-css.cjs.min.js +1 -1
- package/dist/bitwrench-util-css.es5.js +1 -1
- package/dist/bitwrench-util-css.es5.min.js +1 -1
- package/dist/bitwrench-util-css.esm.js +1 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -1
- package/dist/bitwrench-util-css.umd.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +3 -3
- package/dist/bitwrench.cjs.min.js +2 -2
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +1 -1
- package/dist/bitwrench.es5.js +3 -3
- package/dist/bitwrench.es5.min.js +2 -2
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +3 -3
- package/dist/bitwrench.esm.min.js +2 -2
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.umd.js +3 -3
- package/dist/bitwrench.umd.min.js +2 -2
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +61 -61
- package/dist/bwserve.cjs.js +2 -2
- package/dist/bwserve.esm.js +2 -2
- package/dist/sri.json +45 -45
- package/docs/README.md +76 -0
- package/docs/app-patterns.md +264 -0
- package/docs/bitwrench-mcp.md +426 -0
- package/docs/bitwrench_api.md +2232 -0
- package/docs/bw-attach.md +399 -0
- package/docs/bwserve.md +841 -0
- package/docs/cli.md +307 -0
- package/docs/component-cheatsheet.md +144 -0
- package/docs/component-library.md +1099 -0
- package/docs/framework-translation-table.md +33 -0
- package/docs/llm-bitwrench-guide.md +672 -0
- package/docs/routing.md +562 -0
- package/docs/state-management.md +767 -0
- package/docs/taco-format.md +373 -0
- package/docs/theming.md +309 -0
- package/docs/thinking-in-bitwrench.md +1457 -0
- package/docs/tutorial-bwserve.md +297 -0
- package/docs/tutorial-embedded.md +314 -0
- package/docs/tutorial-website.md +255 -0
- package/package.json +11 -3
- package/readme.html +6 -5
- package/src/mcp/knowledge.js +231 -0
- package/src/mcp/live.js +226 -0
- package/src/mcp/server.js +216 -0
- package/src/mcp/tools.js +369 -0
- package/src/mcp/transport.js +55 -0
- package/src/version.js +3 -3
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Tutorial: Building a Server App with bwserve
|
|
2
|
+
|
|
3
|
+
This tutorial builds a Streamlit-style dashboard where all state lives on the server. The browser is a thin display — the server pushes UI updates over SSE.
|
|
4
|
+
|
|
5
|
+
## What you'll build
|
|
6
|
+
|
|
7
|
+
A real-time analytics dashboard:
|
|
8
|
+
- Live counter with increment/decrement buttons
|
|
9
|
+
- Auto-updating metrics (requests/sec, memory, uptime)
|
|
10
|
+
- Event log with scrolling entries
|
|
11
|
+
- All state on the server — refresh the page and you pick up where you left off
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Node.js 18+
|
|
16
|
+
- `npm install bitwrench` (local or global)
|
|
17
|
+
|
|
18
|
+
## Step 1: Minimal server
|
|
19
|
+
|
|
20
|
+
Create `server.js`:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import bwserve from 'bitwrench/bwserve';
|
|
24
|
+
|
|
25
|
+
var app = bwserve.create({ port: 7902, title: 'My Dashboard' });
|
|
26
|
+
|
|
27
|
+
app.page('/', function(client) {
|
|
28
|
+
client.render('#app', { t: 'h1', c: 'Hello from the server!' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.listen();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run it:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node server.js
|
|
38
|
+
# Open http://localhost:7902
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The browser shows "Hello from the server!" — rendered by bitwrench on the client from a TACO object sent over SSE.
|
|
42
|
+
|
|
43
|
+
## Step 2: Add a counter with buttons
|
|
44
|
+
|
|
45
|
+
Replace the page handler:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
app.page('/', function(client) {
|
|
49
|
+
var count = 0;
|
|
50
|
+
|
|
51
|
+
// Render initial UI
|
|
52
|
+
client.render('#app', {
|
|
53
|
+
t: 'div', c: [
|
|
54
|
+
{ t: 'h1', c: 'Counter' },
|
|
55
|
+
{ t: 'div', a: { id: 'count', style: 'font-size: 3rem; text-align: center' }, c: '0' },
|
|
56
|
+
{ t: 'div', a: { style: 'text-align: center; margin-top: 1rem' }, c: [
|
|
57
|
+
{ t: 'button', a: { 'data-bw-action': 'decrement', class: 'bw-btn bw-btn-secondary' }, c: '-1' },
|
|
58
|
+
{ t: 'button', a: { 'data-bw-action': 'increment', class: 'bw-btn bw-btn-primary', style: 'margin-left: 0.5rem' }, c: '+1' },
|
|
59
|
+
{ t: 'button', a: { 'data-bw-action': 'reset', class: 'bw-btn', style: 'margin-left: 0.5rem' }, c: 'Reset' }
|
|
60
|
+
]}
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Handle button clicks
|
|
65
|
+
client.on('increment', function() {
|
|
66
|
+
count++;
|
|
67
|
+
client.patch('count', String(count));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
client.on('decrement', function() {
|
|
71
|
+
count--;
|
|
72
|
+
client.patch('count', String(count));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
client.on('reset', function() {
|
|
76
|
+
count = 0;
|
|
77
|
+
client.patch('count', '0');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Key concepts:
|
|
83
|
+
- `data-bw-action="increment"` on the button tells the client to POST `{action: "increment"}` when clicked
|
|
84
|
+
- `client.on('increment', fn)` registers a server-side handler for that action
|
|
85
|
+
- `client.patch('count', '0')` sends an SSE message that updates the element with `id="count"`
|
|
86
|
+
|
|
87
|
+
## Step 3: Add live metrics
|
|
88
|
+
|
|
89
|
+
Add a metrics section that updates every 2 seconds:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
app.page('/', function(client) {
|
|
93
|
+
var count = 0;
|
|
94
|
+
var startTime = Date.now();
|
|
95
|
+
var requestCount = 0;
|
|
96
|
+
|
|
97
|
+
// ... counter UI from Step 2 ...
|
|
98
|
+
|
|
99
|
+
// Add metrics section below the counter
|
|
100
|
+
client.append('#app', {
|
|
101
|
+
t: 'div', a: { style: 'margin-top: 2rem' }, c: [
|
|
102
|
+
{ t: 'h2', c: 'Live Metrics' },
|
|
103
|
+
{ t: 'div', a: { style: 'display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem' }, c: [
|
|
104
|
+
{ t: 'div', a: { class: 'bw-card' }, c: [
|
|
105
|
+
{ t: 'div', a: { style: 'color: #666; font-size: 0.85rem' }, c: 'Uptime' },
|
|
106
|
+
{ t: 'div', a: { id: 'uptime', style: 'font-size: 1.5rem; font-weight: 700' }, c: '0s' }
|
|
107
|
+
]},
|
|
108
|
+
{ t: 'div', a: { class: 'bw-card' }, c: [
|
|
109
|
+
{ t: 'div', a: { style: 'color: #666; font-size: 0.85rem' }, c: 'Memory' },
|
|
110
|
+
{ t: 'div', a: { id: 'memory', style: 'font-size: 1.5rem; font-weight: 700' }, c: '--' }
|
|
111
|
+
]},
|
|
112
|
+
{ t: 'div', a: { class: 'bw-card' }, c: [
|
|
113
|
+
{ t: 'div', a: { style: 'color: #666; font-size: 0.85rem' }, c: 'Requests' },
|
|
114
|
+
{ t: 'div', a: { id: 'requests', style: 'font-size: 1.5rem; font-weight: 700' }, c: '0' }
|
|
115
|
+
]}
|
|
116
|
+
]}
|
|
117
|
+
]
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Push updates every 2 seconds
|
|
121
|
+
var interval = setInterval(function() {
|
|
122
|
+
var uptime = Math.floor((Date.now() - startTime) / 1000);
|
|
123
|
+
var mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
124
|
+
|
|
125
|
+
client.batch(
|
|
126
|
+
{ type: 'patch', target: 'uptime', content: uptime + 's' },
|
|
127
|
+
{ type: 'patch', target: 'memory', content: mem + ' MB' },
|
|
128
|
+
{ type: 'patch', target: 'requests', content: String(requestCount) }
|
|
129
|
+
);
|
|
130
|
+
}, 2000);
|
|
131
|
+
|
|
132
|
+
// Track actions as requests
|
|
133
|
+
client.on('increment', function() { count++; requestCount++; client.patch('count', String(count)); });
|
|
134
|
+
client.on('decrement', function() { count--; requestCount++; client.patch('count', String(count)); });
|
|
135
|
+
client.on('reset', function() { count = 0; requestCount++; client.patch('count', '0'); });
|
|
136
|
+
|
|
137
|
+
// Clean up when client disconnects
|
|
138
|
+
client.on('disconnect', function() {
|
|
139
|
+
clearInterval(interval);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`client.batch()` sends all three patches in a single SSE frame — the browser applies them atomically.
|
|
145
|
+
|
|
146
|
+
## Step 4: Add an event log
|
|
147
|
+
|
|
148
|
+
Append a scrolling log that records every action:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// Add log section
|
|
152
|
+
client.append('#app', {
|
|
153
|
+
t: 'div', a: { style: 'margin-top: 2rem' }, c: [
|
|
154
|
+
{ t: 'h2', c: 'Event Log' },
|
|
155
|
+
{ t: 'div', a: { id: 'log', style: 'max-height: 200px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 6px; padding: 0.5rem; font-family: monospace; font-size: 0.85rem' }, c: '' }
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
function logEvent(msg) {
|
|
160
|
+
var time = new Date().toLocaleTimeString();
|
|
161
|
+
client.append('#log', {
|
|
162
|
+
t: 'div', a: { style: 'padding: 2px 0; border-bottom: 1px solid #f3f4f6' },
|
|
163
|
+
c: time + ' — ' + msg
|
|
164
|
+
});
|
|
165
|
+
// Auto-scroll to bottom
|
|
166
|
+
client.call('scrollTo', { target: '#log', behavior: 'smooth' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// In each handler:
|
|
170
|
+
client.on('increment', function() {
|
|
171
|
+
count++; requestCount++;
|
|
172
|
+
client.patch('count', String(count));
|
|
173
|
+
logEvent('increment → ' + count);
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`client.call('scrollTo', ...)` invokes a built-in client-side function. Other built-ins: `focus`, `download`, `clipboard`, `redirect`, `log`.
|
|
178
|
+
|
|
179
|
+
## Step 5: Add a theme
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
var app = bwserve.create({
|
|
183
|
+
port: 7902,
|
|
184
|
+
title: 'Analytics Dashboard',
|
|
185
|
+
theme: 'ocean' // Built-in preset: ocean, sunset, forest, slate, etc.
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The auto-generated client page applies the theme automatically — all bitwrench components (buttons, cards, alerts) use the theme colors.
|
|
190
|
+
|
|
191
|
+
## Step 6: Multi-page app
|
|
192
|
+
|
|
193
|
+
Add a second page:
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
app.page('/settings', function(client) {
|
|
197
|
+
client.render('#app', {
|
|
198
|
+
t: 'div', c: [
|
|
199
|
+
{ t: 'h1', c: 'Settings' },
|
|
200
|
+
{ t: 'a', a: { href: '/' }, c: 'Back to Dashboard' },
|
|
201
|
+
{ t: 'p', c: 'Configure your dashboard preferences here.' }
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Each page gets its own handler, its own state, its own client connection. Navigate between them with regular links.
|
|
208
|
+
|
|
209
|
+
## How it works
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
Browser Server (Node.js)
|
|
213
|
+
| |
|
|
214
|
+
| GET / |
|
|
215
|
+
| <── HTML shell (bitwrench + SSE) |
|
|
216
|
+
| |
|
|
217
|
+
| GET /events/:id (SSE) |
|
|
218
|
+
| <── {type:'replace', target:'#app', |
|
|
219
|
+
| node: {t:'div', c:[...]}} |
|
|
220
|
+
| |
|
|
221
|
+
| User clicks [+1] button |
|
|
222
|
+
| POST /action/:id |
|
|
223
|
+
| {action:'increment', data:{}} ──> |
|
|
224
|
+
| | count++
|
|
225
|
+
| <── {type:'patch', |
|
|
226
|
+
| target:'count', |
|
|
227
|
+
| content:'1'} |
|
|
228
|
+
| |
|
|
229
|
+
| (every 2s) |
|
|
230
|
+
| <── {type:'batch', ops:[ |
|
|
231
|
+
| {type:'patch',target:'uptime'} |
|
|
232
|
+
| {type:'patch',target:'memory'} |
|
|
233
|
+
| ]} |
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Protocol messages
|
|
237
|
+
|
|
238
|
+
| Type | Method | What it does |
|
|
239
|
+
|------|--------|-------------|
|
|
240
|
+
| `replace` | `client.render(target, taco)` | Replace element content with TACO |
|
|
241
|
+
| `patch` | `client.patch(id, text, attrs)` | Update text/attributes of element |
|
|
242
|
+
| `append` | `client.append(target, taco)` | Add child element |
|
|
243
|
+
| `remove` | `client.remove(target)` | Remove element from DOM |
|
|
244
|
+
| `batch` | `client.batch(op1, op2, ...)` | Multiple ops in one frame |
|
|
245
|
+
| `message` | `client.message(level, text)` | Show notification |
|
|
246
|
+
| `call` | `client.call(name, ...args)` | Invoke registered or built-in function |
|
|
247
|
+
| `exec` | `client.exec(code)` | Run arbitrary JS (requires `allowExec`) |
|
|
248
|
+
| `register` | `client.register(name, body)` | Send named function to client |
|
|
249
|
+
|
|
250
|
+
### Screenshots
|
|
251
|
+
|
|
252
|
+
The server can capture what the browser is displaying:
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
// Enable in server options
|
|
256
|
+
var app = create({ port: 7902, allowScreenshot: true });
|
|
257
|
+
|
|
258
|
+
app.page('/', function(client) {
|
|
259
|
+
client.render('#app', myDashboard);
|
|
260
|
+
|
|
261
|
+
client.on('capture', async function() {
|
|
262
|
+
// Capture the full page as PNG
|
|
263
|
+
var img = await client.screenshot();
|
|
264
|
+
require('fs').writeFileSync('screenshot.png', img.data);
|
|
265
|
+
|
|
266
|
+
// Capture a specific element, resized to max 800px wide
|
|
267
|
+
var thumb = await client.screenshot('#chart', {
|
|
268
|
+
maxWidth: 800,
|
|
269
|
+
format: 'jpeg',
|
|
270
|
+
quality: 0.8
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Uses html2canvas (vendored, lazy-loaded). See [screenshot example](../examples/client-server/screenshot-server.js).
|
|
277
|
+
|
|
278
|
+
## Deployment
|
|
279
|
+
|
|
280
|
+
bwserve apps are regular Node.js servers. Deploy like any other:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
# systemd, PM2, Docker, etc.
|
|
284
|
+
node server.js
|
|
285
|
+
|
|
286
|
+
# Or with environment variables
|
|
287
|
+
PORT=3000 node server.js
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
For production, put a reverse proxy (nginx, Caddy) in front for TLS and static asset caching.
|
|
291
|
+
|
|
292
|
+
## Next steps
|
|
293
|
+
|
|
294
|
+
- [bwserve Reference](bwserve.md) — full API documentation
|
|
295
|
+
- [Component Library](component-library.md) — all 50+ `make*()` components work in bwserve
|
|
296
|
+
- [Tutorial: Embedded](tutorial-embedded.md) — same protocol on ESP32
|
|
297
|
+
- [examples/client-server/](../examples/client-server/) — runnable example
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Tutorial: ESP32 IoT Dashboard with Bitwrench
|
|
2
|
+
|
|
3
|
+
This tutorial builds a real-time sensor dashboard served by an ESP32 microcontroller. The device sends data — bitwrench renders the UI in the browser.
|
|
4
|
+
|
|
5
|
+
## What you'll build
|
|
6
|
+
|
|
7
|
+
- ESP32 serves a bitwrench-powered web page over WiFi
|
|
8
|
+
- Temperature and humidity readings pushed via SSE every 2 seconds
|
|
9
|
+
- LED control button sends commands back to the device
|
|
10
|
+
- Total HTML payload: ~5KB (excluding bitwrench library)
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
ESP32 Browser
|
|
16
|
+
| |
|
|
17
|
+
| GET / |
|
|
18
|
+
| <── index.html (from SPIFFS) |
|
|
19
|
+
| GET /bitwrench.umd.min.js |
|
|
20
|
+
| <── JS (from SPIFFS) |
|
|
21
|
+
| |
|
|
22
|
+
| GET /events (SSE) |
|
|
23
|
+
| <── sensor JSON every 2s |
|
|
24
|
+
| |
|
|
25
|
+
| POST /api/command |
|
|
26
|
+
| {cmd: 'led', val: 'on'} ──> |
|
|
27
|
+
| <── {ok: true} |
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The ESP32 never generates HTML. It only sends JSON data. The browser renders everything.
|
|
31
|
+
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
- ESP32 DevKit (any variant)
|
|
35
|
+
- DHT22 temperature/humidity sensor (GPIO 4)
|
|
36
|
+
- Arduino IDE or PlatformIO
|
|
37
|
+
- Arduino libraries: `ESPAsyncWebServer`, `AsyncTCP`, `DHT sensor library`, `ArduinoJson`
|
|
38
|
+
|
|
39
|
+
## Step 1: Set up the project
|
|
40
|
+
|
|
41
|
+
### With bitwrench C headers (recommended)
|
|
42
|
+
|
|
43
|
+
Copy the embedded C headers into your project:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
my_project/
|
|
47
|
+
my_project.ino
|
|
48
|
+
bitwrench.h ← from embedded_c/
|
|
49
|
+
bwserve.h ← from embedded_c/
|
|
50
|
+
data/
|
|
51
|
+
index.html ← the dashboard page
|
|
52
|
+
bitwrench.umd.min.js.gz ← gzip -k dist/bitwrench.umd.min.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### With PlatformIO
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
lib/
|
|
59
|
+
bitwrench/
|
|
60
|
+
bitwrench.h
|
|
61
|
+
bwserve.h
|
|
62
|
+
data/
|
|
63
|
+
index.html
|
|
64
|
+
bitwrench.umd.min.js.gz
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Store the gzipped file on flash (~40KB instead of ~150KB). ESPAsyncWebServer
|
|
68
|
+
serves `.gz` files transparently -- the browser decompresses automatically.
|
|
69
|
+
|
|
70
|
+
## Step 2: Write the Arduino sketch
|
|
71
|
+
|
|
72
|
+
```cpp
|
|
73
|
+
#include <WiFi.h>
|
|
74
|
+
#include <ESPAsyncWebServer.h>
|
|
75
|
+
#include <SPIFFS.h>
|
|
76
|
+
#include <DHT.h>
|
|
77
|
+
#include "bitwrench.h"
|
|
78
|
+
#include "bwserve.h"
|
|
79
|
+
|
|
80
|
+
const char* SSID = "YOUR_WIFI";
|
|
81
|
+
const char* PASS = "YOUR_PASSWORD";
|
|
82
|
+
|
|
83
|
+
#define DHT_PIN 4
|
|
84
|
+
#define LED_PIN 2
|
|
85
|
+
|
|
86
|
+
AsyncWebServer server(80);
|
|
87
|
+
AsyncEventSource events("/events");
|
|
88
|
+
DHT dht(DHT_PIN, DHT22);
|
|
89
|
+
|
|
90
|
+
void setup() {
|
|
91
|
+
Serial.begin(115200);
|
|
92
|
+
pinMode(LED_PIN, OUTPUT);
|
|
93
|
+
dht.begin();
|
|
94
|
+
|
|
95
|
+
// Mount flash filesystem
|
|
96
|
+
SPIFFS.begin(true);
|
|
97
|
+
|
|
98
|
+
// Connect WiFi
|
|
99
|
+
WiFi.begin(SSID, PASS);
|
|
100
|
+
while (WiFi.status() != WL_CONNECTED) delay(500);
|
|
101
|
+
Serial.print("IP: ");
|
|
102
|
+
Serial.println(WiFi.localIP());
|
|
103
|
+
|
|
104
|
+
// Serve static files from SPIFFS
|
|
105
|
+
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
|
|
106
|
+
|
|
107
|
+
// SSE endpoint
|
|
108
|
+
events.onConnect([](AsyncEventSourceClient* client) {
|
|
109
|
+
Serial.println("Browser connected");
|
|
110
|
+
});
|
|
111
|
+
server.addHandler(&events);
|
|
112
|
+
|
|
113
|
+
// Command endpoint
|
|
114
|
+
server.on("/api/command", HTTP_POST,
|
|
115
|
+
[](AsyncWebServerRequest* req) {},
|
|
116
|
+
NULL,
|
|
117
|
+
[](AsyncWebServerRequest* req, uint8_t* data, size_t len, size_t, size_t) {
|
|
118
|
+
if (strstr((char*)data, "\"led_on\"")) {
|
|
119
|
+
digitalWrite(LED_PIN, HIGH);
|
|
120
|
+
} else if (strstr((char*)data, "\"led_off\"")) {
|
|
121
|
+
digitalWrite(LED_PIN, LOW);
|
|
122
|
+
}
|
|
123
|
+
req->send(200, "application/json", "{\"ok\":true}");
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
server.begin();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
void loop() {
|
|
131
|
+
static unsigned long lastSend = 0;
|
|
132
|
+
if (millis() - lastSend < 2000) return;
|
|
133
|
+
lastSend = millis();
|
|
134
|
+
|
|
135
|
+
// Read sensors
|
|
136
|
+
float temp = dht.readTemperature();
|
|
137
|
+
float hum = dht.readHumidity();
|
|
138
|
+
|
|
139
|
+
// Send batch update using bwserve macros
|
|
140
|
+
bw_batch_t batch;
|
|
141
|
+
bw_batch_begin(&batch);
|
|
142
|
+
|
|
143
|
+
char m1[256], m2[256], m3[256];
|
|
144
|
+
char ts[16], hs[16], us[16];
|
|
145
|
+
|
|
146
|
+
snprintf(ts, sizeof(ts), "%.1f C", isnan(temp) ? 0.0 : temp);
|
|
147
|
+
BW_PATCH(m1, "val-temp", ts);
|
|
148
|
+
bw_batch_add(&batch, m1);
|
|
149
|
+
|
|
150
|
+
snprintf(hs, sizeof(hs), "%.1f%%", isnan(hum) ? 0.0 : hum);
|
|
151
|
+
BW_PATCH(m2, "val-humidity", hs);
|
|
152
|
+
bw_batch_add(&batch, m2);
|
|
153
|
+
|
|
154
|
+
snprintf(us, sizeof(us), "%lus", millis() / 1000);
|
|
155
|
+
BW_PATCH(m3, "val-uptime", us);
|
|
156
|
+
bw_batch_add(&batch, m3);
|
|
157
|
+
|
|
158
|
+
char out[1024];
|
|
159
|
+
bw_batch_end(out, sizeof(out), &batch);
|
|
160
|
+
events.send(out, NULL, millis());
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The `BW_PATCH` and `bw_batch_*` macros from `bwserve.h` compose the protocol messages. They use the r-prefix relaxed JSON format, so no double-quote escaping needed.
|
|
165
|
+
|
|
166
|
+
## Step 3: Write the dashboard HTML
|
|
167
|
+
|
|
168
|
+
Create `data/index.html`:
|
|
169
|
+
|
|
170
|
+
```html
|
|
171
|
+
<!DOCTYPE html>
|
|
172
|
+
<html lang="en">
|
|
173
|
+
<head>
|
|
174
|
+
<meta charset="UTF-8">
|
|
175
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
176
|
+
<title>ESP32 Dashboard</title>
|
|
177
|
+
<script src="/bitwrench.umd.min.js"></script>
|
|
178
|
+
<style>
|
|
179
|
+
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 1rem; }
|
|
180
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.75rem; max-width: 600px; margin: 1rem auto; }
|
|
181
|
+
.card { background: #1e293b; border-radius: 8px; padding: 1rem; text-align: center; }
|
|
182
|
+
.card h3 { margin: 0 0 0.5rem; font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; }
|
|
183
|
+
.card .val { font-size: 1.5rem; font-weight: 700; color: #38bdf8; }
|
|
184
|
+
h1 { text-align: center; color: #10b981; }
|
|
185
|
+
button { background: #334155; color: #e2e8f0; border: 1px solid #475569; border-radius: 6px; padding: 0.5rem 1.5rem; cursor: pointer; margin: 0.25rem; }
|
|
186
|
+
</style>
|
|
187
|
+
</head>
|
|
188
|
+
<body>
|
|
189
|
+
<h1>ESP32 Dashboard</h1>
|
|
190
|
+
<div class="grid">
|
|
191
|
+
<div class="card"><h3>Temperature</h3><div class="val" id="val-temp">--</div></div>
|
|
192
|
+
<div class="card"><h3>Humidity</h3><div class="val" id="val-humidity">--</div></div>
|
|
193
|
+
<div class="card"><h3>Uptime</h3><div class="val" id="val-uptime">--</div></div>
|
|
194
|
+
</div>
|
|
195
|
+
<div style="text-align: center; margin-top: 1rem;">
|
|
196
|
+
<button onclick="sendCmd('led_on')">LED On</button>
|
|
197
|
+
<button onclick="sendCmd('led_off')">LED Off</button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<script>
|
|
201
|
+
// Connect to SSE stream
|
|
202
|
+
var es = new EventSource('/events');
|
|
203
|
+
es.onmessage = function(e) {
|
|
204
|
+
var raw = e.data;
|
|
205
|
+
// Handle r-prefix relaxed JSON from ESP32
|
|
206
|
+
if (raw.charAt(0) === 'r') raw = raw.slice(1).replace(/'/g, '"');
|
|
207
|
+
try { var msg = JSON.parse(raw); } catch(x) { return; }
|
|
208
|
+
|
|
209
|
+
if (msg.type === 'batch') {
|
|
210
|
+
msg.ops.forEach(applyOp);
|
|
211
|
+
} else {
|
|
212
|
+
applyOp(msg);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function applyOp(op) {
|
|
217
|
+
if (op.type === 'patch') {
|
|
218
|
+
var el = document.getElementById(op.target);
|
|
219
|
+
if (el) el.textContent = op.content;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function sendCmd(cmd) {
|
|
224
|
+
fetch('/api/command', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: { 'Content-Type': 'application/json' },
|
|
227
|
+
body: JSON.stringify({ cmd: cmd })
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
</script>
|
|
231
|
+
</body>
|
|
232
|
+
</html>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This page is ~2KB. With bitwrench.umd.min.js.gz (~40KB), the total SPIFFS usage is ~42KB out of 1.5MB available. ESPAsyncWebServer serves `.gz` files transparently -- the browser decompresses automatically.
|
|
236
|
+
|
|
237
|
+
## Step 4: Upload and test
|
|
238
|
+
|
|
239
|
+
1. Edit `SSID` and `PASS` in the sketch
|
|
240
|
+
2. Upload the `data/` folder to SPIFFS (Arduino IDE: Tools > ESP32 Sketch Data Upload)
|
|
241
|
+
3. Upload the sketch
|
|
242
|
+
4. Open Serial Monitor — note the IP address
|
|
243
|
+
5. Navigate to `http://<ip-address>/` in your browser
|
|
244
|
+
|
|
245
|
+
## Try without hardware first
|
|
246
|
+
|
|
247
|
+
The `examples/embedded/cmake-demo/` directory contains a POSIX version that compiles on Linux/macOS:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
cd examples/embedded/cmake-demo
|
|
251
|
+
mkdir build && cd build
|
|
252
|
+
cmake .. && make
|
|
253
|
+
./bwserve_demo
|
|
254
|
+
# Open http://localhost:8080
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Same protocol, same macros, simulated sensors. Copy the pattern to your real sketch.
|
|
258
|
+
|
|
259
|
+
## Memory budget
|
|
260
|
+
|
|
261
|
+
| Item | Size |
|
|
262
|
+
|------|------|
|
|
263
|
+
| Dashboard HTML | ~2 KB |
|
|
264
|
+
| bitwrench.umd.min.js.gz | ~40 KB |
|
|
265
|
+
| **Total SPIFFS** | **~42 KB** |
|
|
266
|
+
| ESP32 SPIFFS partition | 1.5 MB |
|
|
267
|
+
| Free heap (runtime) | ~240 KB |
|
|
268
|
+
| SSE frame per update | ~200 bytes |
|
|
269
|
+
|
|
270
|
+
Store gzipped: `gzip -k dist/bitwrench.umd.min.js`. ESPAsyncWebServer
|
|
271
|
+
serves `.gz` files transparently for the matching uncompressed filename.
|
|
272
|
+
|
|
273
|
+
## The r-prefix relaxed JSON
|
|
274
|
+
|
|
275
|
+
The C macros produce strings like:
|
|
276
|
+
```
|
|
277
|
+
r{'type':'patch','target':'val-temp','content':'23.5 C'}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The `r` prefix tells the browser parser to convert single quotes to double quotes before `JSON.parse()`. This avoids escaping double quotes in C string literals — a major ergonomic win.
|
|
281
|
+
|
|
282
|
+
**Escaping rule**: Since single quotes delimit strings, apostrophes in values need escaping with `\'`:
|
|
283
|
+
|
|
284
|
+
```c
|
|
285
|
+
// Static text with apostrophe — escape in the literal:
|
|
286
|
+
BW_PATCH(msg, "room", "Barry\\'s Room");
|
|
287
|
+
|
|
288
|
+
// Dynamic user text — use BW_PATCH_SAFE (auto-escapes):
|
|
289
|
+
char user_text[] = "it's 23.5 C";
|
|
290
|
+
BW_PATCH_SAFE(msg, sizeof(msg), "status", user_text);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
This is still far better than standard JSON in C, where every quote needs `\"`.
|
|
294
|
+
|
|
295
|
+
Direction: r-prefix is outbound only (ESP32 to browser). The browser always sends strict JSON back via `fetch()`.
|
|
296
|
+
|
|
297
|
+
## Other languages
|
|
298
|
+
|
|
299
|
+
The same protocol works from any language:
|
|
300
|
+
|
|
301
|
+
| Platform | Language | Library |
|
|
302
|
+
|----------|----------|---------|
|
|
303
|
+
| ESP32 (Arduino) | C/C++ | `embedded_c/bwserve.h` |
|
|
304
|
+
| ESP32 (esp-idf) | Rust | `embedded_rust/` |
|
|
305
|
+
| ESP32/RPi (MicroPython) | Python | `embedded_python/bwserve.py` |
|
|
306
|
+
| Adafruit boards | CircuitPython | `embedded_python/bwserve.py` |
|
|
307
|
+
| Node.js | JavaScript | `import bwserve from 'bitwrench/bwserve'` |
|
|
308
|
+
|
|
309
|
+
## Next steps
|
|
310
|
+
|
|
311
|
+
- [bwserve Reference](bwserve.md) — full protocol documentation
|
|
312
|
+
- [embedded_c/ README](../embedded_c/README.md) — C/C++ macro reference
|
|
313
|
+
- [examples/embedded/](../examples/embedded/) — complete example with mock data
|
|
314
|
+
- [Tutorial: bwserve](tutorial-bwserve.md) — same pattern in Node.js
|