Skip to content

Commit bcfeef8

Browse files
authored
Interface data storage in PROGMEM (#71)
Adds a webpack plugin to package interface as PROGMEM into a header file in the framework. Adds a build flag to optionally enable serving from PROGMEM or SPIFFS as required Adds documentation changes to describe changes
1 parent 14f50c1 commit bcfeef8

20 files changed

+320
-81
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
.pio
2-
.pioenvs
3-
.piolibdeps
42
.clang_complete
53
.gcc-flags.json
64
*Thumbs.db
75
/data/www
6+
/lib/framework/WWWData.h
87
/interface/build
98
/interface/node_modules
109
.vscode

.travis.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
language: python
22
python:
3-
- "2.7"
3+
- "3.8"
4+
5+
before_install:
6+
- nvm install 10.15.3
7+
- nvm use 10.15.3
48

59
sudo: false
610
cache:

README.md

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ You will need the following before you can get started.
3030

3131
* [PlatformIO](https://platformio.org/) - IDE for development
3232
* [Node.js](https://nodejs.org) - For building the interface with npm
33-
* Bash shell, or [Git Bash](https://gitforwindows.org/) if you are under windows
3433

3534
### Building and uploading the firmware
3635

@@ -74,53 +73,61 @@ Alternatively run the 'upload' target:
7473
platformio run -t upload
7574
```
7675

77-
### Building the interface
76+
### Building & uploading the interface
7877

7978
The interface has been configured with create-react-app and react-app-rewired so the build can customized for the target device. The large artefacts are gzipped and source maps and service worker are excluded from the production build. This reduces the production build to around ~200k, which easily fits on the device.
8079

81-
Change to the ['interface'](interface) directory with your bash shell (or Git Bash) and use the standard commands you would with any react app built with create-react-app:
82-
83-
#### Change to interface directory
80+
The interface will be automatically built by PlatformIO before it builds the firmware. The project can be configured to serve the interface from either SPIFFS or PROGMEM as your project requires. The default configuration is to serve the content from SPIFFS which requires an additional upload step which is documented below.
8481

85-
```bash
86-
cd interface
87-
```
82+
#### Uploading the file system image
8883

89-
#### Download and install the node modules
84+
If service content from SPIFFS (default), build the project first. Then the compiled interface may be uploaded to the device by pressing the "Upload File System image" button:
9085

91-
```bash
92-
npm install
93-
```
86+
![uploadfs](/media/uploadfs.png?raw=true "uploadfs")
9487

95-
#### Build the interface
88+
Alternatively run the 'uploadfs' target:
9689

9790
```bash
98-
npm run build
91+
platformio run -t uploadfs
9992
```
10093

101-
> **Note**: The build command will also delete the previously built interface, in the ['data/www'](data/www) directory, replacing it with the freshly built one ready to upload to the device.
94+
#### Serving the interface from PROGMEM
10295

103-
#### Uploading the file system image
96+
You can configure the project to serve the interface from PROGMEM by uncommenting the -D PROGMEM_WWW build flag in ['platformio.ini'](platformio.ini) then re-building and uploading the firmware to the device.
10497

105-
The compiled user interface may be uploaded to the device by pressing the "Upload File System image" button:
98+
Be aware that this will consume ~150k of program space which can be especially problematic if you already have a large build artefact or if you have added large javascript dependencies to the interface. The ESP32 binaries are large already, so this will be a problem if you are using one of these devices and require this type of setup.
10699

107-
![uploadfs](/media/uploadfs.png?raw=true "uploadfs")
100+
A method for working around this issue can be to reduce the amount of space allocated to SPIFFS by configuring the device to use a differnt strategy partitioning. If you don't require SPIFFS other than for storing config one approach might be to configure a minimal SPIFFS partition.
108101

109-
Alternatively run the 'uploadfs' target:
102+
For a ESP32 (4mb variant) there is a handy "min_spiffs.csv" partition table which can be enabled easily:
110103

111-
```bash
112-
platformio run -t uploadfs
104+
```yaml
105+
[env:node32s]
106+
board_build.partitions = min_spiffs.csv
107+
platform = espressif32
108+
board = node32s
113109
```
114110

111+
This is largley left as an exersise for the reader as everyone's requirements will vary.
112+
115113
### Running the interface locally
116114

117-
You can run a local development server to allow you preview changes to the front end without the need to upload a file system image to the device after each change. Change to the interface directory and run the following command:
115+
You can run a local development server to allow you preview changes to the front end without the need to upload a file system image to the device after each change.
116+
117+
Change to the ['interface'](interface) directory with your bash shell (or Git Bash) and use the standard commands you would with any react app built with create-react-app:
118+
119+
```bash
120+
cd interface
121+
```
122+
123+
Install the npm dependencies, if required and start the development server:
118124

119125
```bash
126+
npm install
120127
npm start
121128
```
122129

123-
> **Note**: To run the interface locally you will need to modify the endpoint root path and enable CORS.
130+
> **Note**: To run the interface locally you may need to modify the endpoint root path and enable CORS.
124131
125132
#### Changing the endpoint root
126133

@@ -141,7 +148,9 @@ You can enable CORS on the back end by uncommenting the -D ENABLE_CORS build fla
141148

142149
## Device Configuration
143150

144-
As well as containing the interface, the SPIFFS image (in the ['data'](data) folder) contains a JSON settings file for each of the configurable features. The config files can be found in the ['data/config'](data/config) directory:
151+
The SPIFFS image (in the ['data'](data) folder) contains a JSON settings file for each of the configurable features.
152+
153+
The config files can be found in the ['data/config'](data/config) directory:
145154

146155
File | Description
147156
---- | -----------
@@ -173,30 +182,31 @@ It is recommended that you change the JWT secret and user credentials from their
173182

174183
This project supports ESP8266 and ESP32 platforms. To support OTA programming, enough free space to upload the new sketch and file system image will be required. It is recommended that a board with at least 2mb of flash is used.
175184

176-
By default, the target device is "esp12e". This is a common ESP8266 variant with 4mb of flash:
185+
The pre-configured environments are "esp12e" and "node32s". These are common ESP8266/ESP32 variants with 4mb of flash:
177186

178-
![ESP12E](/media/esp12e.jpg?raw=true "ESP12E")
187+
![ESP12E](/media/esp12e.jpg?raw=true "ESP12E") ![ESP32](/media/esp32.jpg?raw=true "ESP32")
179188

180-
The settings file ['platformio.ini'](platformio.ini) configures the platform and board:
189+
The settings file ['platformio.ini'](platformio.ini) configures the supported environments. Modify these, or add new environments for the devides you need to support. The default environments are as follows:
181190

182-
```
191+
```yaml
183192
[env:esp12e]
184193
platform = espressif8266
185194
board = esp12e
186-
```
187-
188-
If you want to build for an ESP32 device, all you need to do is re-configure ['platformio.ini'](platformio.ini) with your devices settings.
189-
190-
![ESP32](/media/esp32.jpg?raw=true "ESP32")
191-
192-
Building for the common esp32 "node32s" board for example requires the following configuration:
195+
board_build.f_cpu = 160000000L
193196

194-
```
195197
[env:node32s]
196198
platform = espressif32
197199
board = node32s
198200
```
199201

202+
If you want to build for a different device, all you need to do is re-configure ['platformio.ini'](platformio.ini) and select an alternative environment by modifying the default_envs variable. Building for the common esp32 "node32s" board for example:
203+
204+
```yaml
205+
[platformio]
206+
;default_envs = esp12e
207+
default_envs = node32s
208+
```
209+
200210
## Customizing and theming
201211

202212
The framework, and MaterialUI allows for a reasonable degree of customization with little effort.
@@ -274,7 +284,11 @@ void setup() {
274284
Serial.begin(SERIAL_BAUD_RATE);
275285
276286
// start the file system (must be done before starting the framework)
287+
#ifdef ESP32
288+
SPIFFS.begin(true);
289+
#elif defined(ESP8266)
277290
SPIFFS.begin();
291+
#endif
278292
279293
// start the framework and demo project
280294
esp8266React.begin();

interface/config-overrides.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const ManifestPlugin = require('webpack-manifest-plugin');
22
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
33
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4-
const CompressionPlugin = require("compression-webpack-plugin");
4+
const CompressionPlugin = require('compression-webpack-plugin');
5+
const ProgmemGenerator = require('./progmem-generator.js');
56

67
const path = require('path');
78
const fs = require('fs');
@@ -21,6 +22,9 @@ module.exports = function override(config, env) {
2122
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
2223
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
2324

25+
// build progmem data files
26+
config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 }));
27+
2428
// add compression plugin, compress javascript
2529
config.plugins.push(new CompressionPlugin({
2630
filename: "[path].gz[query]",

interface/package-lock.json

Lines changed: 16 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

interface/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@material-ui/icons": "^4.5.1",
88
"compression-webpack-plugin": "^2.0.0",
99
"jwt-decode": "^2.2.0",
10+
"mime-types": "^2.1.25",
1011
"moment": "^2.24.0",
1112
"notistack": "^0.9.6",
1213
"prop-types": "^15.7.2",
@@ -17,11 +18,12 @@
1718
"react-material-ui-form-validator": "^2.0.9",
1819
"react-router": "^5.1.1",
1920
"react-router-dom": "^5.1.1",
20-
"react-scripts": "3.0.1"
21+
"react-scripts": "3.0.1",
22+
"zlib": "^1.0.5"
2123
},
2224
"scripts": {
2325
"start": "react-app-rewired start",
24-
"build": "react-app-rewired build && rm -rf ../data/www && cp -r build ../data/www",
26+
"build": "react-app-rewired build",
2527
"test": "react-app-rewired test --env=jsdom",
2628
"eject": "react-scripts eject"
2729
},

interface/progmem-generator.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const { resolve, relative, sep } = require('path');
2+
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
3+
var zlib = require('zlib');
4+
var mime = require('mime-types');
5+
6+
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
7+
8+
function getFilesSync(dir, files = []) {
9+
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
10+
const entryPath = resolve(dir, entry.name);
11+
if (entry.isDirectory()) {
12+
getFilesSync(entryPath, files);
13+
} else {
14+
files.push(entryPath);
15+
}
16+
})
17+
return files;
18+
}
19+
20+
function coherseToBuffer(input) {
21+
return Buffer.isBuffer(input) ? input : Buffer.from(input);
22+
}
23+
24+
function cleanAndOpen(path) {
25+
if (existsSync(path)) {
26+
unlinkSync(path);
27+
}
28+
return createWriteStream(path, { flags: "w+" });
29+
}
30+
31+
class ProgmemGenerator {
32+
33+
constructor(options = {}) {
34+
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
35+
this.options = { outputPath, bytesPerLine, indent, includes };
36+
}
37+
38+
apply(compiler) {
39+
compiler.hooks.emit.tapAsync(
40+
{ name: 'ProgmemGenerator' },
41+
(compilation, callback) => {
42+
const { outputPath, bytesPerLine, indent, includes } = this.options;
43+
const fileInfo = [];
44+
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
45+
try {
46+
const writeIncludes = () => {
47+
writeStream.write(includes);
48+
}
49+
50+
const writeFile = (relativeFilePath, buffer) => {
51+
const variable = "ESP_REACT_DATA_" + fileInfo.length;
52+
const mimeType = mime.lookup(relativeFilePath);
53+
var size = 0;
54+
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
55+
const zipBuffer = zlib.gzipSync(buffer);
56+
zipBuffer.forEach((b) => {
57+
if (!(size % bytesPerLine)) {
58+
writeStream.write("\n");
59+
writeStream.write(indent);
60+
}
61+
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
62+
size++;
63+
});
64+
if (size % bytesPerLine) {
65+
writeStream.write("\n");
66+
}
67+
writeStream.write("};\n\n");
68+
fileInfo.push({
69+
uri: '/' + relativeFilePath.replace(sep, '/'),
70+
mimeType,
71+
variable,
72+
size
73+
});
74+
};
75+
76+
const writeFiles = () => {
77+
// process static files
78+
const buildPath = compilation.options.output.path;
79+
for (const filePath of getFilesSync(buildPath)) {
80+
const readStream = readFileSync(filePath);
81+
const relativeFilePath = relative(buildPath, filePath);
82+
writeFile(relativeFilePath, readStream);
83+
}
84+
// process assets
85+
const { assets } = compilation;
86+
Object.keys(assets).forEach((relativeFilePath) => {
87+
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
88+
});
89+
}
90+
91+
const generateWWWClass = () => {
92+
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
93+
94+
class WWWData {
95+
${indent}public:
96+
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
97+
${fileInfo.map(file => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`).join('\n')}
98+
${indent.repeat(2)}}
99+
};
100+
`;
101+
}
102+
103+
const writeWWWClass = () => {
104+
writeStream.write(generateWWWClass());
105+
}
106+
107+
writeIncludes();
108+
writeFiles();
109+
writeWWWClass();
110+
111+
writeStream.on('finish', () => {
112+
callback();
113+
});
114+
} finally {
115+
writeStream.end();
116+
}
117+
}
118+
);
119+
}
120+
}
121+
122+
module.exports = ProgmemGenerator;

0 commit comments

Comments
 (0)