Skip to content

Commit 764fd26

Browse files
Add app: OSUpdate
1 parent 3920ce1 commit 764fd26

File tree

8 files changed

+576
-0
lines changed

8 files changed

+576
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Manifest-Version: 1.0
2+
Name: OSUpdate
3+
Start-Script: assets/osupdate.py
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import lvgl as lv
2+
import ota.update
3+
from esp32 import Partition
4+
import urequests
5+
6+
7+
subwindow.clean()
8+
canary = lv.obj(subwindow)
9+
canary.add_flag(lv.obj.FLAG.HIDDEN)
10+
11+
12+
import ota.status
13+
ota.status.status()
14+
current = Partition(Partition.RUNNING)
15+
current
16+
current.get_next_update()
17+
18+
# Initialize LVGL display (assuming setup is done)
19+
label = lv.label(subwindow)
20+
label.set_text("OTA Update: 0.00%")
21+
label.align(lv.ALIGN.CENTER, 0, -30)
22+
progress_bar = lv.bar(subwindow)
23+
progress_bar.set_size(200, 20)
24+
progress_bar.align(lv.ALIGN.BOTTOM_MID, 0, -50)
25+
progress_bar.set_range(0, 100)
26+
progress_bar.set_value(0, lv.ANIM.OFF)
27+
28+
# Custom OTA update with LVGL progress
29+
def update_with_lvgl(url):
30+
def progress_callback(percent):
31+
print(f"OTA Update: {percent:.1f}%")
32+
label.set_text(f"OTA Update: {percent:.2f}%") # Cloud upload symbol
33+
progress_bar.set_value(int(percent), lv.ANIM.ON)
34+
current = Partition(Partition.RUNNING)
35+
next_partition = current.get_next_update()
36+
response = urequests.get(url, stream=True)
37+
total_size = int(response.headers.get('Content-Length', 0))
38+
bytes_written = 0
39+
chunk_size = 4096
40+
i = 0
41+
print(f"Starting OTA update of size: {total_size}")
42+
while canary.is_valid():
43+
chunk = response.raw.read(chunk_size)
44+
if not chunk:
45+
print("No chunk, breaking...")
46+
break
47+
if len(chunk) < chunk_size:
48+
print(f"Padding chunk {i} from {len(chunk)} to {chunk_size} bytes")
49+
chunk = chunk + b'\xFF' * (chunk_size - len(chunk))
50+
print(f"Writing chunk {i}")
51+
next_partition.writeblocks(i, chunk)
52+
bytes_written += len(chunk)
53+
i += 1
54+
if total_size:
55+
progress_callback(bytes_written / total_size * 100)
56+
response.close()
57+
next_partition.set_boot()
58+
import machine
59+
machine.reset()
60+
61+
# Start OTA update
62+
update_with_lvgl("http://demo.lnpiggy.com:2121/ESP32_GENERIC_S3-SPIRAM_OCT_micropython.bin")

internal_filesystem/lib/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
This /lib folder contains:
22
- https://github.com/echo-lalia/qmi8658-micropython/blob/main/qmi8685.py but given the correct name "qmi8658.py"
33
- traceback.mpy from https://github.com/micropython/micropython-lib
4+
- https://github.com/glenn20/micropython-esp32-ota/ installed with import mip; mip.install('github:glenn20/micropython-esp32-ota/mip/ota')
5+
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# partition_writer module for MicroPython on ESP32
2+
# MIT license; Copyright (c) 2023 Glenn Moloney @glenn20
3+
4+
# Based on OTA class by Thorsten von Eicken (@tve):
5+
# https://github.com/tve/mqboard/blob/master/mqrepl/mqrepl.py
6+
7+
import hashlib
8+
import io
9+
10+
from micropython import const
11+
12+
IOCTL_BLOCK_COUNT: int = const(4) # type: ignore
13+
IOCTL_BLOCK_SIZE: int = const(5) # type: ignore
14+
IOCTL_BLOCK_ERASE: int = const(6) # type: ignore
15+
16+
17+
# An IOBase compatible class to wrap access to an os.AbstractBlockdev() device
18+
# such as a partition on the device flash. Writes must be aligned to block
19+
# boundaries.
20+
# https://docs.micropython.org/en/latest/library/os.html#block-device-interface
21+
# Extend IOBase so we can wrap this with io.BufferedWriter in BlockdevWriter
22+
class Blockdev(io.IOBase):
23+
def __init__(self, device):
24+
self.device = device
25+
self.blocksize = int(device.ioctl(IOCTL_BLOCK_SIZE, None))
26+
self.blockcount = int(device.ioctl(IOCTL_BLOCK_COUNT, None))
27+
self.pos = 0 # Current position (bytes from beginning) of device
28+
self.end = 0 # Current end of the data written to the device
29+
30+
# Data must be a multiple of blocksize unless it is the last write to the
31+
# device. The next write after a partial block will raise ValueError.
32+
def write(self, data: bytes | bytearray | memoryview) -> int:
33+
block, remainder = divmod(self.pos, self.blocksize)
34+
if remainder:
35+
raise ValueError(f"Block {block} write not aligned at block boundary.")
36+
data_len = len(data)
37+
nblocks, remainder = divmod(data_len, self.blocksize)
38+
mv = memoryview(data)
39+
if nblocks: # Write whole blocks
40+
self.device.writeblocks(block, mv[: nblocks * self.blocksize])
41+
block += nblocks
42+
if remainder: # Write left over data as a partial block
43+
self.device.ioctl(IOCTL_BLOCK_ERASE, block) # Erase block first
44+
self.device.writeblocks(block, mv[-remainder:], 0)
45+
self.pos += data_len
46+
self.end = self.pos # The "end" of the data written to the device
47+
return data_len
48+
49+
# Read data from the block device.
50+
def readinto(self, data: bytearray | memoryview):
51+
size = min(len(data), self.end - self.pos)
52+
block, remainder = divmod(self.pos, self.blocksize)
53+
self.device.readblocks(block, memoryview(data)[:size], remainder)
54+
self.pos += size
55+
return size
56+
57+
# Set the current file position for reading or writing
58+
def seek(self, offset: int, whence: int = 0):
59+
start = [0, self.pos, self.end]
60+
self.pos = start[whence] + offset
61+
62+
63+
# Calculate the SHA256 sum of a file (has a readinto() method)
64+
def sha_file(f, buffersize=4096) -> str:
65+
mv = memoryview(bytearray(buffersize))
66+
read_sha = hashlib.sha256()
67+
while (n := f.readinto(mv)) > 0:
68+
read_sha.update(mv[:n])
69+
return read_sha.digest().hex()
70+
71+
72+
# BlockdevWriter provides a convenient interface to writing images to any block
73+
# device which implements the micropython os.AbstractBlockDev interface (eg.
74+
# Partition on flash storage on ESP32).
75+
# https://docs.micropython.org/en/latest/library/os.html#block-device-interface
76+
# https://docs.micropython.org/en/latest/library/esp32.html#flash-partitions
77+
class BlockDevWriter:
78+
def __init__(
79+
self,
80+
device, # Block device to recieve the data (eg. esp32.Partition)
81+
verify: bool = True, # Should we read back and verify data after writing
82+
verbose: bool = True,
83+
):
84+
self.device = Blockdev(device)
85+
self.writer = io.BufferedWriter(
86+
self.device, self.device.blocksize # type: ignore
87+
)
88+
self._sha = hashlib.sha256()
89+
self.verify = verify
90+
self.verbose = verbose
91+
self.sha: str = ""
92+
self.length: int = 0
93+
blocksize, blockcount = self.device.blocksize, self.device.blockcount
94+
if self.verbose:
95+
print(f"Device capacity: {blockcount} x {blocksize} byte blocks.")
96+
97+
def set_sha_length(self, sha: str, length: int):
98+
self.sha = sha
99+
self.length = length
100+
blocksize, blockcount = self.device.blocksize, self.device.blockcount
101+
if length > blocksize * blockcount:
102+
raise ValueError(f"length ({length} bytes) is > size of partition.")
103+
if self.verbose and length:
104+
blocks, remainder = divmod(length, blocksize)
105+
print(f"Writing {blocks} blocks + {remainder} bytes.")
106+
107+
def print_progress(self):
108+
if self.verbose:
109+
block, remainder = divmod(self.device.pos, self.device.blocksize)
110+
print(f"\rBLOCK {block}", end="")
111+
if remainder:
112+
print(f" + {remainder} bytes")
113+
114+
# Append data to the block device
115+
def write(self, data: bytearray | bytes | memoryview) -> int:
116+
self._sha.update(data)
117+
n = self.writer.write(data)
118+
self.print_progress()
119+
return n
120+
121+
# Append data from f (a stream object) to the block device
122+
def write_from_stream(self, f: io.BufferedReader) -> int:
123+
mv = memoryview(bytearray(self.device.blocksize))
124+
tot = 0
125+
while (n := f.readinto(mv)) != 0:
126+
tot += self.write(mv[:n])
127+
return tot
128+
129+
# Flush remaining data to the block device and confirm all checksums
130+
# Raises:
131+
# ValueError("SHA mismatch...") if SHA of received data != expected sha
132+
# ValueError("SHA verify fail...") if verified SHA != written sha
133+
def close(self) -> None:
134+
self.writer.flush()
135+
self.print_progress()
136+
# Check the checksums (SHA256)
137+
nbytes: int = self.device.end
138+
if self.length and self.length != nbytes:
139+
raise ValueError(f"Received {nbytes} bytes (expect {self.length}).")
140+
write_sha = self._sha.digest().hex()
141+
if not self.sha:
142+
self.sha = write_sha
143+
if self.sha != write_sha:
144+
raise ValueError(f"SHA mismatch recv={write_sha} expect={self.sha}.")
145+
if self.verify:
146+
if self.verbose:
147+
print("Verifying SHA of the written data...", end="")
148+
self.device.seek(0) # Reset to start of partition
149+
read_sha = sha_file(self.device, self.device.blocksize)
150+
if read_sha != write_sha:
151+
raise ValueError(f"SHA verify failed write={write_sha} read={read_sha}")
152+
if self.verbose:
153+
print("Passed.")
154+
if self.verbose or not self.sha:
155+
print(f"SHA256={self.sha}")
156+
self.device.seek(0) # Reset to start of partition
157+
158+
def __enter__(self):
159+
return self
160+
161+
def __exit__(self, e_t, e_v, e_tr):
162+
if e_t is None:
163+
self.close()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from esp32 import Partition
2+
3+
4+
# Mark this boot as successful: prevent rollback to last image on next reboot.
5+
# Raises OSError(-261) if bootloader is not OTA capable.
6+
def cancel() -> None:
7+
try:
8+
Partition.mark_app_valid_cancel_rollback()
9+
except OSError as e:
10+
if e.args[0] == -261:
11+
print(f"{__name__}.cancel(): The bootloader does not support OTA rollback.")
12+
else:
13+
raise e
14+
15+
16+
# Force a rollback on the next reboot to the previously booted ota partition
17+
def force() -> None:
18+
from .status import force_rollback
19+
20+
force_rollback()
21+
22+
23+
# Undo a previous force rollback: ie. boot off the current partition on next reboot
24+
def cancel_force() -> None:
25+
from .status import current_ota
26+
27+
current_ota.set_boot()

0 commit comments

Comments
 (0)