Skip to content

Commit

Permalink
Merge pull request #30 from sidoh/megabin
Browse files Browse the repository at this point in the history
* Use correct cache directory

* Move support scripts to ./scripts directory

* Build full binary images which include:

  * The bootloader
  * Partition table
  * App loader
  * Firmware

This is done in a way generic enough to not break if new platforms are added.

* Distribute "megabins" with tagged releases.  These will be prefixed
with "INITIALIZER_".

* Ignore updates from filenames starting with INITIALIZER_

* Document uploading megabin via esptool

* Show firmware update errors in UI
  • Loading branch information
sidoh authored Apr 4, 2020
2 parents dd85661 + 1fb87e6 commit 9f9cf08
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 47 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ python:
sudo: false
cache:
directories:
- "~/.platformio"
- "~/.pio"
env:
- NODE_VERSION="10"
before_install:
Expand All @@ -17,7 +17,7 @@ before_script:
script:
- platformio run
before_deploy:
- ./.prepare_release
- ./scripts/travis/prepare_release
deploy:
provider: releases
prerelease: true
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ The [examples directory](./examples) has a few sample templates. Here are a few

1. Connect display to MCU. (see [waveshare site](https://www.waveshare.com/wiki/1.54inch_e-Paper_Module) and **SPI Bus and Pin Selection** below for more information)
1. Flash your MCU.
1. Use a pre-compiled binary from the [releases page](https://github.com/sidoh/epaper_templates/releases). You can use [esptool](https://github.com/espressif/esptool) or the [ESP32 flash tool](https://www.espressif.com/en/support/download/other-tools).
1. With PlatformIO: for example `pio run -e esp32 -t upload`. You will need to have [nodejs](https://nodejs.org/en/) installed in order to buidl the web assets.
1. Setup WiFi. A setup AP will appear named `epaper_XXXXXX`. The default password is **waveshare**.
1. Visit the Web UI to configure further.
1. Use a pre-compiled binary from the [releases page](https://github.com/sidoh/epaper_templates/releases). **Make sure to select the file starting with `INITIALIZER_`**. You can use [esptool](https://github.com/espressif/esptool) or the [ESP32 flash tool](https://www.espressif.com/en/support/download/other-tools). Example command:
```
esptool.py --chip esp32 --baud 460800 write_flash 0x1000 INITIALIZER_epaper_templates_esp32-v2.3.0.bin
```
2. With PlatformIO: for example `pio run -e esp32 -t upload`. You will need to have [nodejs](https://nodejs.org/en/) installed in order to buidl the web assets.
2. Setup WiFi. A setup AP will appear named `epaper_XXXXXX`. The default password is **waveshare**.
3. Visit the Web UI to configure further.
## Deep Sleep
Expand Down
40 changes: 33 additions & 7 deletions lib/HTTP/EpaperWebServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ EpaperWebServer::EpaperWebServer(
, changeFn(nullptr)
, cancelSleepFn(nullptr)
, wsServer("/socket")
, deepSleepActive(false) {
, deepSleepActive(false)
, updateSuccessful(false) {
driver->onVariableUpdate(
std::bind(&EpaperWebServer::handleVariableUpdate, this, _1, _2));
driver->onRegionUpdate(
Expand Down Expand Up @@ -193,6 +194,23 @@ void EpaperWebServer::begin() {

void EpaperWebServer::handleFirmwareUpdateUpload(RequestContext& request) {
if (request.upload.index == 0) {
// Give up if the filename starts with "INITIALIZER_". These binary images
// are built by custom platformio tooling that includes all parts of flash
// necessary get started (including bootloader and partition table).
//
// OTA updates _only_ have the firmware part of this image, and trying to
// include the other parts will corrupt flash.
if (request.upload.filename.startsWith("INITIALIZER_")) {
Serial.println(
F("Refusing to process OTA update with filename beginning with "
"INITIALIZER_"));
request.response.setCode(400);
request.response.json[F("error")] =
F("Invalid firmware image. This is an initializer binary. Please "
"choose the firmware image without the INITIALIZER_ prefix.");
return;
}

if (request.rawRequest->contentLength() > 0) {
if (this->cancelSleepFn) {
this->cancelSleepFn();
Expand All @@ -206,7 +224,8 @@ void EpaperWebServer::handleFirmwareUpdateUpload(RequestContext& request) {
}

if (Update.size() > 0) {
if (Update.write(request.upload.data, request.upload.length) != request.upload.length) {
if (Update.write(request.upload.data, request.upload.length) !=
request.upload.length) {
Update.printError(Serial);

#if defined(ESP32)
Expand All @@ -215,20 +234,26 @@ void EpaperWebServer::handleFirmwareUpdateUpload(RequestContext& request) {
}

if (request.upload.isFinal) {
if (! Update.end(true)) {
if (!Update.end(true)) {
Update.printError(Serial);
#if defined(ESP32)
Update.abort();
#endif
} else {
this->updateSuccessful = true;
}
}
}
}

void EpaperWebServer::handleFirmwareUpdateComplete(RequestContext& request) {
request.rawRequest->send(200, "text/plain", "success");
delay(1000);
ESP.restart();
// Upload handler can decide that there was something wrong with the upload.
// Don't reset if that's the case
if (this->updateSuccessful) {
request.rawRequest->send(200, "text/plain", "success");
delay(1000);
ESP.restart();
}
}

void EpaperWebServer::handleVariableUpdate(
Expand Down Expand Up @@ -650,7 +675,8 @@ void EpaperWebServer::onSettingsChange(std::function<void()> changeFn) {
this->changeFn = changeFn;
}

void EpaperWebServer::onCancelSleep(EpaperWebServer::OnCancelSleepFn cancelSleepFn) {
void EpaperWebServer::onCancelSleep(
EpaperWebServer::OnCancelSleepFn cancelSleepFn) {
this->cancelSleepFn = cancelSleepFn;
}

Expand Down
1 change: 1 addition & 0 deletions lib/HTTP/EpaperWebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class EpaperWebServer {
OnCancelSleepFn cancelSleepFn;
AsyncWebSocket wsServer;
bool deepSleepActive;
bool updateSuccessful;

// firmware update handlers
void handleFirmwareUpdateUpload(RequestContext& request);
Expand Down
5 changes: 3 additions & 2 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ lib_deps_external =
; sidoh/GxEPD2#partial_display
GxEPD2@~1.2.3
extra_scripts =
pre:.build_web.py
pre:scripts/platformio/build_web.py
post:scripts/platformio/build_full_image.py
lib_ldf_mode = deep+
build_flags = !python3 .get_version.py -DMQTT_DEBUG -Idist
build_flags = !python3 scripts/platformio/get_version.py -DMQTT_DEBUG -Idist
-D ENABLE_GxEPD2_GFX
-D RICH_HTTP_ASYNC_WEBSERVER
-D JSON_TEMPLATE_BUFFER_SIZE=20048
Expand Down
51 changes: 51 additions & 0 deletions scripts/platformio/build_full_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/python3

from pathlib import Path

Import("env")

SOURCE_START_LOCATION = 0x10000


def create_full_bin(source, target, env):
firmware_file = target[0].get_abspath()
full_image_filename = env.subst("$BUILD_DIR/${PROGNAME}-full.bin")
parts = []

# Will contain 3 parts outside of the main firmware image:
# * Bootloader
# * Compiled partition table
# * App loader
for part in env.get("FLASH_EXTRA_IMAGES", []):
start, filename = part

# Parse hext string
if isinstance(start, str):
start = int(start, 16)

filename = env.subst(filename)

parts.append((start, filename))

# End with the main firmware image
parts.append((SOURCE_START_LOCATION, firmware_file))

# Start at location of earliest image (don't start at 0x0)
ix = parts[0][0]

with open(full_image_filename, "wb") as f:
while len(parts) > 0:
part_ix, part_filename = parts.pop(0)
part_filesize = Path(part_filename).stat().st_size
padding = part_ix - ix

for _ in range(padding):
f.write(b"\x00")

with open(part_filename, "rb") as part_file:
f.write(part_file.read())

ix = part_ix + part_filesize


env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", create_full_bin)
File renamed without changes.
File renamed without changes.
7 changes: 7 additions & 0 deletions .prepare_release → scripts/travis/prepare_release
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ for file in $(ls ${firmware_prefix}/**/firmware.bin); do

cp "$file" "dist/${PROJECT_NAME}_${env}-${VERSION}.bin"
done

for file in $(ls ${firmware_prefix}/**/firmware-full.bin); do
env_dir=$(dirname "$file")
env=$(basename "$env_dir")

cp "$file" "dist/INITIALIZER_${PROJECT_NAME}_${env}-${VERSION}.bin"
done
1 change: 1 addition & 0 deletions web/.neutrinorc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
},
proxy: {
'/api': `http://${API_SERVER_ADDRESS}`,
'/firmware': `http://${API_SERVER_ADDRESS}`,
'/socket': {
target: `ws://${API_SERVER_ADDRESS}`,
ws: true
Expand Down
12 changes: 12 additions & 0 deletions web/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Dashboard from "./dashboard/Dashboard";
import BitmapsIndex from "./bitmaps/BitmapsIndex";
import useGlobalState from "./state/global_state";
import ErrorBoundary from "./util/ErrorBoundary";
import { Alert } from "react-bootstrap";

const App = () => {
const [globalState, globalActions] = useGlobalState();
Expand All @@ -27,6 +28,17 @@ const App = () => {

<Container className="main-content">
<ErrorBoundary>
{globalState.errors.map((msg, i) => {
return (
<Alert
variant="danger"
onClose={() => globalActions.dismissError(i)}
dismissible
>
{msg}
</Alert>
);
})}
<Switch>
<Route path="/templates">
<TemplatesIndex />
Expand Down
57 changes: 36 additions & 21 deletions web/src/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import {
faPowerOff,
faRedoAlt,
faCheck,
faTimes
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { useInterval, useBoolean } from "react-use";
import MemoizedFontAwesomeIcon from "../util/MemoizedFontAwesomeIcon";
import { useMemo } from "react";
import useGlobalState from "../state/global_state";

const DisplayStatusCard = ({ settings }) => {
return (
Expand All @@ -46,7 +47,7 @@ const SleepStatus = ({
awakeSecondsLeft,
isCancelling,
onCancel,
isRebooting
isRebooting,
}) => {
return (
<>
Expand Down Expand Up @@ -92,19 +93,20 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {
const [successMessage, setSuccessMessage] = useState(null);
const [awakeSecondsLeft, setAwakeSecondsLeft] = useState(null);
const [isCancellingDeepSleep, setCancellingDeepSleep] = useState(false);
const [globalState, globalActions] = useGlobalState();

const firmwareFile = useRef(null);
const deepSleepActive = systemStatus && systemStatus["deep_sleep_active"];

const onCancelSleep = useCallback(e => {
const onCancelSleep = useCallback((e) => {
e.preventDefault();
setCancellingDeepSleep(true);

api.post("/system", { command: "cancel_sleep" }).then(
e => {
(e) => {
onReload();
},
e => {
(e) => {
setCancellingDeepSleep(false);
}
);
Expand All @@ -116,9 +118,9 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {
.get("/system", { timeout: 1000 })
.then(() => {
onReload();
setRebooting(false)
setRebooting(false);
})
.catch(e => console.log("Caught exception", e));
.catch((e) => console.log("Caught exception", e));
},
!!isRebooting ? 5000 : null
);
Expand All @@ -144,7 +146,7 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {
deepSleepActive ? 100 : null
);

const reboot = useCallback(e => {
const reboot = useCallback((e) => {
e.preventDefault();

if (confirm("Are you sure you want to reboot?")) {
Expand All @@ -153,7 +155,7 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {
}
}, []);

const onFirmwareSubmit = useCallback(e => {
const onFirmwareSubmit = useCallback((e) => {
e.preventDefault();

if (firmwareFile.current.files.length > 0) {
Expand All @@ -165,16 +167,25 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {

const config = {
baseURL: "/",
onUploadProgress: e => {
onUploadProgress: (e) => {
setUploadProgress(Math.floor((100 * e.loaded) / e.total));
}
},
};

api.post("/firmware", formData, config).then(e => {
setUploadProgress(null);
setRebooting(false);
setSuccessMessage("Firmware updated successfully.");
});
api.post("/firmware", formData, config).then(
(e) => {
setUploadProgress(null);
setRebooting(false);
setSuccessMessage("Firmware updated successfully.");
},
(e) => {
setUploadProgress(null);
setRebooting(false);
globalActions.addError(
"Error updating firmware - " + e.response.data.error
);
}
);
}
});

Expand Down Expand Up @@ -261,7 +272,11 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {
Update Firmware
</Button>

<Button disabled={!!isRebooting} variant="warning" onClick={reboot}>
<Button
disabled={!!isRebooting}
variant="warning"
onClick={reboot}
>
<MemoizedFontAwesomeIcon
icon={faPowerOff}
className="fa-fw mr-1"
Expand All @@ -276,7 +291,7 @@ const SystemStatusCard = ({ settings, systemStatus, onReload }) => {
);
};

export default props => {
export default (props) => {
const [settings, setSettings] = useState(null);
const [systemStatus, setSystemStatus] = useState(null);
const [loadedAt, setLoadedAt] = useState(new Date());
Expand All @@ -286,14 +301,14 @@ export default props => {
}, []);

useEffect(() => {
api.get("/settings").then(e => setSettings(e.data));
api.get("/settings").then((e) => setSettings(e.data));
}, [loadedAt]);

useEffect(() => {
api.get("/system").then(e =>
api.get("/system").then((e) =>
setSystemStatus({
__loaded_at: loadedAt,
...e.data
...e.data,
})
);
}, [loadedAt]);
Expand Down
Loading

0 comments on commit 9f9cf08

Please sign in to comment.