Compare commits

..
Author SHA1 Message Date
clyde 2503dc49e3 Merge branch 's-scale' into s-draw 2026-10-23 08:50:37 +11:00
clydeandClaude Opus 5.5 2cd6ea7496 [runtime_image] Test reduced scale JPEG decodes through draw() and decodeBegin()
Also move the boundary table comment back above its test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:50:36 +11:00
clyde 03a76a788f Merge branch 's-scale' into s-draw 2026-10-23 08:23:13 +11:00
clyde 807217ead6 Merge branch 's-progress' into s-scale 2026-10-23 08:23:13 +11:00
clyde 7eb279be69 Merge branch 's-jpeg-stream' into s-progress
# Conflicts:
#	esphome/components/online_image/online_image.cpp
2026-10-23 08:23:12 +11:00
clydeandClaude Opus 5.5 549aa0a6ba [online_image] Read other formats a little at a time too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:22:54 +11:00
clyde 6897c8a4a4 Merge branch 's-scale' into s-draw 2026-10-23 08:01:27 +11:00
clyde e70adbb060 Merge branch 's-progress' into s-scale 2026-10-23 08:01:26 +11:00
clyde 5bdcf2b39d Merge branch 's-jpeg-stream' into s-progress
# Conflicts:
#	esphome/components/online_image/online_image.cpp
2026-10-23 08:01:16 +11:00
clydeandClaude Opus 5.5 87855c5967 [online_image] Read a streamed JPEG a little at a time
esp_http_client_read() blocks until it has every byte asked for, so one read sized to a 64 KB buffer held
the loop for seconds on a slow link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:01:00 +11:00
clydeandClaude Opus 5.5 9dfa251d5b [runtime_image] Use draw_width_ and draw_height_ in the JPEG decoder
The scale fields were removed when drawing moved to integer maths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 07:24:51 +11:00
clyde eb66aafc30 Merge branch 's-progress' into s-scale 2026-10-23 07:24:03 +11:00
clyde e24418d201 Merge branch 's-jpeg-stream' into s-progress 2026-10-23 07:24:03 +11:00
clyde 62f1a9b08f Merge branch 's-scale' into s-draw 2026-10-23 07:24:03 +11:00
clydeandClaude Opus 5.5 4921173f6c [online_image] Fail a streamed download when the decoder stops reading a full buffer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 07:24:02 +11:00
clydeandClaude Opus 5.5 0bdd700c2e [runtime_image] Decode a JPEG at the smallest JPEGDEC scale that covers the buffer
JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work, but
every JPEG was decoded at full size and then shrunk to the buffer. A
3596 x 2840 image shown at 281 x 222 took 7.1 s on an ESP32-S3; at
1/8 size it takes 2.0 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:48 +10:00
clydeandClaude Opus 5.5 9545322be0 [runtime_image] Check once per draw() call whether the image is scaled
An image drawn at its own size needs no span maths at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:48 +10:00
clydeandClaude Opus 5.5 15afdf4dda [runtime_image] Say the generic draw() path keeps the same pixels as the RGB565 path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:48 +10:00
clydeandClaude Opus 5.5 74bdda341b [runtime_image] Skip the span division for an image drawn at its own size
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:48 +10:00
clydeandClaude Opus 5.5 6d90969f5f [runtime_image] Draw each target pixel once, with integer maths
ImageDecoder::draw() painted every source pixel into the buffer, so a
shrunk image wrote each target pixel many times, and worked out the
spans with double multiplications and ceil() for every pixel. The S3
has no double FPU, so drawing took about 6 us a pixel: 3.7 s of a 1000
x 617 PNG's 6 s. Give each source pixel the span from ceil(start *
scale) to ceil(end * scale), worked out with integers, so neighbouring
spans meet without overlapping. The PNG now loads in 3.2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:48 +10:00
clydeandClaude Opus 5.5 b1f83bc2dd [online_image] Use emplace_back in the download started test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 4b81c8712b [online_image] Brace the whole-file fallback for clang-tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 660b7b11f6 [online_image] Add on_download_started
Fires when the server has accepted the request and the image starts to
download, before the first on_download_progress; not for a 304 or an
HTTP error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 d039160876 [online_image] Download the whole JPEG when its headers do not fit the buffer
Before JPEG images were decoded as they download, the buffer grew to
hold the whole file, so an image with large EXIF or ICC data decoded
with any buffer_size. Keep that working: when the headers do not fit,
grow the buffer to the file size, keeping what has arrived, and decode
it at the end as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 67219ac41b [online_image] Add on_download_progress, to show how far a download has got
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 416762802e [online_image] Also give the buffer_size hint when unreadable headers end in a read error
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 f0b0801c3f [online_image] Open a streamed JPEG in a pass of its own, and only blame the buffer for unreadable headers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 331ee3a278 [online_image] Test decode and read errors part way through a streamed JPEG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
clydeandClaude Opus 5.5 7392ac662a [online_image] Decode a JPEG as it downloads
A JPEG had to be downloaded whole, into a buffer grown to the file size, and was
then decoded in one call. When the server gives the size, the JPEG decoder now
reads the download from the buffer itself and decodes a row of MCUs at a time,
within a time budget per loop pass. The buffer only has to hold the headers, so
buffer_size bounds the RAM used, and a slow link makes the decoder wait for the
rest rather than decode past what has arrived. Downloads without a size keep the
old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:47 +10:00
Clyde StubbsandClaude Opus 5.5 59c7178910 [runtime_image] Decode JPEG a step at a time (#21341)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:52:41 +11:00
J. Nick Koston e55741a26b [usb_uart] Read the channel limit when the config is validated, not at import (#21331) 2026-10-22 09:34:55 -10:00
dependabot[bot] 0bdc14154b Bump prek from 0.5.5 to 0.5.6 (#21419)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:52 +00:00
dependabot[bot] 1d6f839ee7 Bump platformdirs from 4.13.4 to 4.13.5 (#21418)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:20 +00:00
dependabot[bot] 2544518aca Bump filelock from 4.2.2 to 4.2.4 (#21417)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:17 +00:00
dependabot[bot] 70e56df630 Bump j178/prek-action from 3.0.1 to 3.0.2 (#21420)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:29:54 +00:00
Jonathan Swoboda 7d62aae996 [wifi] Allow the lease cache and a second WISP DNS server on the ESP32 Arduino framework (#21412) 2026-10-22 15:27:16 -04:00
c12baaa337 [mqtt] Support stop tilt on covers (#20251)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 19:14:24 +00:00
Jonathan Swoboda cb9eda3681 [spi] Gate psram_dma on the ESP-IDF version rather than the framework (#21411) 2026-10-22 14:48:04 -04:00
J. Nick Koston 4e27957c74 [ci] Add the UART loopback, WISP bridge, raw API action and LD2412 flows to the hardware-test skill (#21356) 2026-10-22 08:46:37 -10:00
Roman PriesolandJ. Nick Koston 0856211b9b [voice_assistant] Allow IPv4 to be disabled (#21389)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 08:46:26 -10:00
J. Nick Koston ff4ff5c5f5 [restart][shutdown][factory_reset][safe_mode] Use Apply buttons (#21158) 2026-10-22 08:37:53 -10:00
J. Nick Koston 980b336668 [core] Let Apply entities run a free statement without a parent hub (#21157) 2026-10-22 08:37:52 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] a3cb3732ee [sx127x][sx126x][cc1101][udp][uart] Send byte payloads with ApplyBytes (#21263)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 08:19:09 -10:00
Jonathan Swoboda ab50b3830a [wifi] Allow WISP mode on the ESP32 Arduino framework (#21410) 2026-10-22 18:10:47 +00:00
Roman Priesol 3131cf28a3 [esphome] Fix OTA build with IPv4 disabled (#21388) 2026-10-22 08:07:17 -10:00
100 changed files with 3232 additions and 539 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ Per-topic scripts, each described in the matching `reference/recipes/<component>
(host driver, Zephyr driver probe), `rtsp_server/` (protocol client, raw rate reader, measurement set,
API image grab), `cse7766/` (API observer with ping), `zephyr_coredump/` (reader flow, decode with the
Zephyr tools), `libretiny_prefs/` (rtc preference harness header), `libretiny_crash/` (BK72xx crash probe header and
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer).
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer), `api/` (raw plaintext native API client for user defined actions), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer).
## Templates in `$SKILL/templates`
@@ -7,6 +7,9 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
- Opening the port usually resets the board through DTR, but not always; `serial_capture.py --reset`
pulses RTS to capture from boot. 74880 baud shows the ROM boot messages (`rst cause`, `boot mode`).
- UART0 can be looped back onto itself (`USC0(0) |= (1 << UCLBE)` in an `on_boot` lambda) so a UART
component is testable without its device: frames written with `uart.write` arrive on RX. Log over the
API with `logger: baud_rate: 0`, since the serial port carries the component's bytes.
- `esphome logs` on some CH340s emits thousands of duplicated garbage lines right after the RTS reset;
a plain pyserial capture at 115200 is reliable.
- A failed esptool write can wedge the adapter (`termios` error 22, or an open that hangs) until replug.
@@ -4,12 +4,13 @@ Each flow names the pull requests it was used on, so their result tables show wh
One file per component, named after it:
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop, user defined action listing and execution with a raw plaintext client
- [`captive_portal.md`](captive_portal.md): fallback AP and /wifisave hammer
- [`cse7766.md`](cse7766.md): Sonoff S31 parser check over the API
- [`espnow.md`](espnow.md): ESP8266 broadcast receive
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar
- [`ld2412.md`](ld2412.md): output pin level through the basic configuration command, held back writes
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx crash report on the next boot
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT, WebSocket transports with a local and a public broker, TLS trust sources (own CA, client certificate, certificate bundle, no verification) with the CLI check
- [`nrf52.md`](nrf52.md): dongle board support
@@ -20,7 +21,7 @@ One file per component, named after it:
- [`scheduler.md`](scheduler.md): loop wake A/B
- [`udp.md`](udp.md): datagrams, multicast, sockets, `udp.write` payload shapes
- [`usb_cdc_acm.md`](usb_cdc_acm.md): nRF52840 dongle host driver
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP)
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream
- [`zephyr.md`](zephyr.md): preferences on the nRF52840 dongle
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
@@ -28,3 +28,15 @@ connected first, and the loop-time maxima. For the loop cost of polling, repeat
so after a drop every dial is closed and HA's incoming reconnect wins 11 to 33 s later; the measurable effect is the
first dial landing in the loop the network flag flips.
Results: https://github.com/esphome/esphome/pull/21085#issuecomment-6254742791
## User defined action listing and execution without a client library (#18882)
`scripts/api/rawactions.py HOST list` speaks the plaintext native API over a raw socket and decodes every
`ListEntitiesServicesResponse` by field number (argument fields 1 name, 2 type, 3 description, 4 example, 5 optional,
6 default_value), so new wire fields show up without waiting for the client library. `rawactions.py HOST exec ACTION
k=v ...` sends `ExecuteServiceRequest` with `sint32` ints (field 5, not the legacy int32 field 2), fixed32 floats,
length delimited strings and `int[]` as repeated sint32 (field 7); `ALLOW_MISSING=1` leaves arguments out on purpose
(the device then drops the call). `templates/api-actions-dut.yaml` (D1 mini, three actions, optional variables with
defaults) and `templates/api-actions-strings-dut.yaml` (required strings) log what arrived. String arguments are
`StringRef`s that are not null terminated: copy them with `snprintf("%.*s", size, c_str())` into a buffer before
logging, and do not pass `%.*s` to `logger.log`, whose validator counts it as one pattern.
@@ -0,0 +1,13 @@
# ld2412
## Output pin level and the basic configuration command (#21353)
`templates/ld2412-select-only-dut.yaml` (ESP32-S3, the module on GPIO43 and GPIO44 at 115200, only the output pin
level select, the module's OUT pin on GPIO38 as a binary sensor plus the presence sensor) with `uart: debug:
direction: BOTH`. Drive the select with `scripts/ld2450/lddrive.py HOST KEY LOGFILE sel:NAME=OPTION` and read the TX
dump for command `02` carrying the level byte, then the OUT pin binary sensor against presence: with level high the
pin follows presence, with level low it is inverted. A module that never answers the boot query (no `<<<` lines)
only shows the held back path: `Command 02 held back until the module reports its settings`, a query `12`, and after
2 s `Command 02 not sent, the module did not report its settings` with the select republished to the last known
level. With the full set of numbers configured a select change also needs every number to have a state, so wait
for the boot query answer before judging.
@@ -13,3 +13,30 @@ selects over the API and prints every LD2450 state update. After each set the hu
the radar and republishes all twelve coordinates from the answer, so set each coordinate of each zone to a distinct
value and check the readbacks; no target is needed. Readbacks can arrive late over Wi-Fi, so match them by content.
Results: https://github.com/esphome/esphome/pull/21146#issuecomment-6278026922
## ESP8266 loopback harness, no radar needed (#21349)
`templates/ld2450-loopback.yaml` (package; substitutions `name` and `baud`) runs the hub on UART0 (GPIO1 and GPIO3)
with `logger: baud_rate: 0` and sets the UART0 loopback bit in `on_boot` (`USC0(0) |= (1 << UCLBE)`), so everything
the hub or a `uart.write` action sends comes back on RX. Template buttons inject periodic frames (all four direction
strings, targets 2 and 3), query zone acks (zone type 0, 1 and 2 for the select readback) and call the name based
setters with a valid and a bogus name. `uart: debug: direction: TX` delimited on the command footer dumps every
command the hub sends. Drive it with `scripts/ld2450/lddrive.py HOST KEY LOGFILE press:NAME sel:NAME=OPTION wait:S`
(logs over the API at VERBOSE, 2 s between steps because the API log stream drops lines while `send_command_`
blocks), then `txsum.py LOGFILE` lists the TX frames per step; diff the summaries of the two builds. The hub reads its
own commands back and logs `Invalid status` for each, which is harmless. The boot readback of the baud rate select
comes from the UART setting, so a second build at `baud: "9600"` checks the flash table lookup.
Results: https://github.com/esphome/esphome/pull/21349#issuecomment-6332320026
## Frame splitter collisions (#21357)
The loopback template also carries the zone 1 numbers and seven collision buttons: a periodic frame with the
command footer inside target 1, zone acks with `55 CC` and with `04 03 02 01` inside the coordinates, a periodic
frame cut after 12 bytes followed by a zone ack, a frame missing its last byte followed by a complete one, a bogus
255 byte length then a frame, and garbage then a frame. Judge by the state updates plus the `Handling Periodic` and
`Handling Ack` lines. Through a real radar the set zone ack is sometimes corrupted by the radar itself (its tail
replaced by the periodic frame it was sending, for example `04 03 03 03 00 ... 55 CC`), so count readbacks over at
least 24 selects per build and read the raw bytes with `uart: debug: direction: RX` (`!extend` the package's uart
id) before blaming the parser; the API log stream drops lines under that dump volume, so some misses stay
unclassified.
Results: https://github.com/esphome/esphome/pull/21357#issuecomment-6333244886
@@ -23,3 +23,17 @@ Only an Android phone configures: Settings, network details, Add device, Share W
QR scanner makes the phone an enrollee). Render the URI with the `qrcode` module and open the PNG.
`esp32: framework: log_level: DEBUG` plus `sdkconfig_options: CONFIG_WPA_DEBUG_PRINT: y` prints the
supplicant state machine.
## WISP bridge with an ethernet upstream (#7147)
`templates/wisp-ethernet-dut.yaml` (ESP32-S3 with a W5500, `wifi: ap:` with `ap_mode: WISP` and no station) and
`templates/wisp-client.yaml` (D1 mini joining the bridge, `http_request` to a web server on the wired LAN and to
`http://example.com/` every 15 s, `time: sntp`). Secrets: `wisp_psk` and `lan_http` (serve a text file on the host
with `python -m http.server 8787 --bind 0.0.0.0` from a scratch directory). Judge by the client's `GET status 200`
lines, the web server log showing the device's ethernet address as the source (NAT), the client's DHCP info (DNS1 is
the LAN resolver) and `SNTP synced`. The device logs `Starting AP` and `DHCP DNS server: <resolver>` on its USB
console; flashing over the air reboots it for repeat boots. Before the AP address and DHCP server moved ahead of the
AP start (the fix in that PR), the first AP start failed on every boot with `esp_netif_dhcps_option failed: 20484`
(DHCP server already started by the AP start event) and `Failed to enable NAT`, and the bridge only came up on the
retry 90 s later; a client that cannot find the SSID for the first minute on an older build is that. With the fix
the bridge is up 6 s after boot. Any change to the ESP-IDF AP start should also rerun the captive portal hammer.
@@ -0,0 +1,211 @@
"""Plaintext native API client that lists user actions with their raw argument fields and executes one.
usage: rawactions.py HOST list
rawactions.py HOST exec ACTION key=value [...] (every argument must be given; types from the listing)
Fields of ListEntitiesServicesArgument: 1 name, 2 type, 3 description, 4 example, 5 optional, 6 default_value.
"""
import os
import socket
import struct
import sys
HOST = sys.argv[1]
CMD = sys.argv[2]
ARGS = sys.argv[3:]
TYPE_NAMES = {
0: "bool",
1: "int",
2: "float",
3: "string",
4: "bool[]",
5: "int[]",
6: "float[]",
7: "string[]",
}
def varint(v: int) -> bytes:
out = bytearray()
while True:
b = v & 0x7F
v >>= 7
if v:
out.append(b | 0x80)
else:
out.append(b)
return bytes(out)
def svarint(v: int) -> bytes:
return varint((v << 1) ^ (v >> 63))
def decode_varint(buf: bytes, pos: int) -> tuple[int, int]:
value = shift = 0
while True:
b = buf[pos]
pos += 1
value |= (b & 0x7F) << shift
if not b & 0x80:
return value, pos
shift += 7
def fields(payload: bytes) -> list[tuple[int, int, object]]:
out = []
pos = 0
while pos < len(payload):
key, pos = decode_varint(payload, pos)
fnum, wt = key >> 3, key & 7
if wt == 0:
v, pos = decode_varint(payload, pos)
elif wt == 2:
ln, pos = decode_varint(payload, pos)
v = payload[pos : pos + ln]
pos += ln
elif wt == 5:
v = payload[pos : pos + 4] # fixed32, caller unpacks
pos += 4
else:
raise ValueError(f"wire type {wt}")
out.append((fnum, wt, v))
return out
def frame(mtype: int, payload: bytes) -> bytes:
return b"\x00" + varint(len(payload)) + varint(mtype) + payload
def field_bytes(fnum: int, data: bytes) -> bytes:
return varint((fnum << 3) | 2) + varint(len(data)) + data
def field_varint(fnum: int, v: int) -> bytes:
return varint((fnum << 3) | 0) + varint(v)
def recv_frames(
sock: socket.socket, until: set[int], limit: float = 5.0
) -> list[tuple[int, bytes]]:
sock.settimeout(limit)
buf = bytearray()
frames = []
while True:
try:
chunk = sock.recv(4096)
except TimeoutError:
return frames
if not chunk:
return frames
buf += chunk
while True:
if len(buf) < 3:
break
if buf[0] != 0:
raise ValueError("not plaintext")
try:
ln, p = decode_varint(buf, 1)
mt, p = decode_varint(buf, p)
except IndexError:
break
if len(buf) < p + ln:
break
frames.append((mt, bytes(buf[p : p + ln])))
del buf[: p + ln]
if mt in until:
return frames
def main() -> None:
sock = socket.create_connection((HOST, 6053), timeout=5)
sock.sendall(
frame(
1, field_bytes(1, b"rawactions") + field_varint(2, 1) + field_varint(3, 10)
)
) # HelloRequest
recv_frames(sock, {2})
sock.sendall(frame(3, b"")) # ConnectRequest, no password
recv_frames(sock, {4})
sock.sendall(frame(11, b"")) # ListEntitiesRequest
frames = recv_frames(sock, {19})
services = {}
for mt, payload in frames:
if mt != 41:
continue
name = key = None
args = []
for fnum, _, v in fields(payload):
if fnum == 1:
name = v.decode()
elif fnum == 2:
key = struct.unpack("<I", v)[0]
elif fnum == 3:
a = {
"name": None,
"type": 0,
"description": None,
"example": None,
"optional": None,
"default": None,
}
for af, _, av in fields(v):
if af == 1:
a["name"] = av.decode()
elif af == 2:
a["type"] = av
elif af == 3:
a["description"] = av.decode()
elif af == 4:
a["example"] = av.decode()
elif af == 5:
a["optional"] = bool(av)
elif af == 6:
a["default"] = av.decode()
else:
a[f"field{af}"] = av
args.append(a)
services[name] = (key, args)
if CMD == "list":
for name, (key, args) in services.items():
print(f"action {name} key={key}")
for a in args:
extra = {
k: v
for k, v in a.items()
if k not in ("name", "type") and v is not None
}
print(f" {a['name']}: {TYPE_NAMES.get(a['type'], a['type'])} {extra}")
return
name = ARGS[0]
key, args = services[name]
payload = varint((1 << 3) | 5) + struct.pack("<I", key)
given = dict(kv.split("=", 1) for kv in ARGS[1:])
for a in args:
if a["name"] not in given:
if os.environ.get("ALLOW_MISSING"):
continue # leave the argument out of the request on purpose
sys.exit(f"missing {a['name']}")
raw = given[a["name"]]
t = a["type"]
if t == 0:
av = field_varint(1, 1 if raw.lower() == "true" else 0)
elif t == 1:
av = varint((5 << 3) | 0) + svarint(int(raw)) # sint32 int_
elif t == 2:
av = varint((3 << 3) | 5) + struct.pack("<f", float(raw))
elif t == 3:
av = field_bytes(4, raw.encode())
elif t == 5:
av = b"".join(
varint((7 << 3) | 0) + svarint(int(x)) for x in raw.split(",") if x
) # repeated sint32 int_array
else:
sys.exit(f"type {t} not supported here")
payload += field_bytes(2, av)
sock.sendall(frame(42, payload)) # ExecuteServiceRequest
print(f"sent {name} with {given}")
sock.close()
main()
@@ -0,0 +1,123 @@
"""Drive an LD24xx harness over the API: press buttons, set selects and numbers, print state updates, save the log.
usage: lddrive.py HOST KEY LOGFILE OP [OP ...] (KEY of - means a plaintext API)
OP: list | press:NAME | sel:NAME=OPTION | set:NAME=VALUE | wait:SECONDS
Steps are 2 s apart: the API log stream drops lines while the hub blocks in send_command_.
"""
import asyncio
from pathlib import Path
import sys
import time
from aioesphomeapi import (
APIClient,
BinarySensorState,
ButtonInfo,
LogLevel,
NumberInfo,
NumberState,
SelectInfo,
SelectState,
SensorInfo,
SensorState,
TextSensorInfo,
TextSensorState,
)
HOST, KEY, LOGFILE = sys.argv[1], sys.argv[2], sys.argv[3]
OPS = sys.argv[4:]
T0 = time.time()
STEP_GAP_S = 2.0
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def run(c: APIClient, log) -> None:
info = await c.device_info()
print(
f"device {info.name} esphome {info.esphome_version} compiled {info.compilation_time}"
)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
selects = {e.name: e for e in ents if isinstance(e, SelectInfo)}
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None:
return
missing = " (missing)" if getattr(s, "missing_state", False) else ""
if isinstance(s, TextSensorState):
print(f"{stamp()} TXT {e.name} = {s.state!r}{missing}", flush=True)
elif isinstance(s, BinarySensorState):
print(f"{stamp()} BIN {e.name} = {s.state}{missing}", flush=True)
elif isinstance(s, NumberState):
print(f"{stamp()} NUM {e.name} = {s.state:.0f}{missing}", flush=True)
elif isinstance(s, SelectState):
print(f"{stamp()} SEL {e.name} = {s.state!r}{missing}", flush=True)
elif (
isinstance(s, SensorState)
and isinstance(e, SensorInfo)
and "Count" in e.name
):
print(f"{stamp()} CNT {e.name} = {s.state:.0f}", flush=True)
def on_log(msg) -> None:
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERBOSE)
await asyncio.sleep(1.5)
for op in OPS:
if op == "list":
for n in sorted(buttons):
print("button:", n)
for n in sorted(selects):
print("select:", n, selects[n].options)
for e in ents:
if isinstance(e, TextSensorInfo):
print("text_sensor:", e.name)
continue
kind, _, rest = op.partition(":")
if kind == "wait":
await asyncio.sleep(float(rest))
continue
if kind == "press":
if rest not in buttons:
sys.exit(f"no button named {rest!r}; have {sorted(buttons)}")
print(f"{stamp()} >>> press {rest}", flush=True)
log.write(f"{stamp()} >>> press {rest}\n")
c.button_command(buttons[rest].key)
elif kind == "set":
name, _, value = rest.partition("=")
if name not in numbers:
sys.exit(f"no number named {name!r}; have {sorted(numbers)}")
print(f"{stamp()} >>> set {name} = {value}", flush=True)
log.write(f"{stamp()} >>> set {name} = {value}\n")
c.number_command(numbers[name].key, float(value))
elif kind == "sel":
name, _, value = rest.partition("=")
if name not in selects:
sys.exit(f"no select named {name!r}; have {sorted(selects)}")
print(f"{stamp()} >>> select {name} = {value}", flush=True)
log.write(f"{stamp()} >>> select {name} = {value}\n")
c.select_command(selects[name].key, value)
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(STEP_GAP_S)
await asyncio.sleep(1)
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
with Path(LOGFILE).open("w", encoding="utf-8") as log:
await run(c, log)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,33 @@
"""Summarize an lddrive.py log: for each step, the LD2450 TX frames, commands and publishes.
usage: txsum.py LOGFILE (diff the output of two builds)
"""
from pathlib import Path
import re
import sys
ANSI = re.compile(r"\x1b\[[0-9;]*m")
out: list[str] = []
with Path(sys.argv[1]).open(encoding="utf-8", errors="replace") as log:
for raw in log:
line = ANSI.sub("", raw.rstrip("\n"))
m = re.match(r"\s*[\d.]+ (.*)", line)
if not m:
continue
text = m.group(1)
if text.startswith(">>>"):
out.append(f"\n# {text[4:]}")
elif "[uart_debug" in text:
out.append(f" TX {text.split('>>> ')[1]}")
elif "[ld2450" in text and (
"Sending COMMAND" in text
or "zone type" in text.lower()
or "Query zone" in text
):
out.append(f" LOG {text.split(']: ', 1)[1]}")
elif "[select:" in text and ">>" in text:
out.append(f" SEL {text.split(']: ', 1)[1]}")
elif "[text_sensor:098]" in text:
out.append(f" TXT {text.split(']: ', 1)[1]}")
print("\n".join(out))
@@ -0,0 +1,65 @@
esphome:
name: ${name}
esp8266:
board: d1_mini
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
actions:
- action: play_buzzer
description: Play a song or build one from parts
variables:
song_str:
type: string
description: Full RTTTL string
example: "scale:d=4,o=5,b=100:c,d,e"
required: false
notes:
type: string
required: false
octave:
type: int
default: 5
bpm:
type: int
default: 100
tempo:
type: float
default: 1.5
loud:
type: bool
default: true
label:
type: string
default: ""
then:
- lambda: |-
char s[40], n[40], l[40];
snprintf(s, sizeof(s), "%.*s", (int) song_str.size(), song_str.c_str());
snprintf(n, sizeof(n), "%.*s", (int) notes.size(), notes.c_str());
snprintf(l, sizeof(l), "%.*s", (int) label.size(), label.c_str());
ESP_LOGD("main", "play_buzzer song=%s(%d) notes=%s(%d) octave=%d bpm=%d tempo=%.1f loud=%d label=%s(%d)", s,
(int) song_str.size(), n, (int) notes.size(), octave, bpm, tempo, loud, l, (int) label.size());
- action: plain_action
variables:
value: int
then:
- logger.log:
format: "plain_action value=%d"
args: ["value"]
- action: list_action
variables:
values: int[]
then:
- logger.log:
format: "list_action n=%d"
args: ["values.size()"]
@@ -0,0 +1,28 @@
esphome:
name: ${name}
esp8266:
board: d1_mini
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
actions:
- action: str_action
variables:
song_str: string
notes: string
octave: int
then:
- lambda: |-
char s[40], n[40];
snprintf(s, sizeof(s), "%.*s", (int) song_str.size(), song_str.c_str());
snprintf(n, sizeof(n), "%.*s", (int) notes.size(), notes.c_str());
ESP_LOGD("main", "str_action song=%s(%d) notes=%s(%d) octave=%d", s, (int) song_str.size(), n, (int) notes.size(), octave);
@@ -0,0 +1,57 @@
substitutions:
name: ld2412sel
esphome:
name: ${name}
esp32:
variant: esp32s3
board: esp32-s3-devkitc-1
flash_size: 8MB
framework:
type: esp-idf
logger:
level: VERBOSE
api:
reboot_timeout: 0s
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
uart:
- id: uart_ld2412
tx_pin: GPIO43
rx_pin: GPIO44
baud_rate: 115200
debug:
direction: BOTH
dummy_receiver: false
after:
timeout: 10ms
bytes: 120
ld2412:
id: ld2412_radar
uart_id: uart_ld2412
select:
- platform: ld2412
ld2412_id: ld2412_radar
out_pin_level:
name: "LD2412 Hardware output pin level"
binary_sensor:
- platform: gpio
name: "LD2412 Out Pin"
pin: GPIO38
- platform: ld2412
ld2412_id: ld2412_radar
has_target:
name: "LD2412 Presence"
@@ -0,0 +1,258 @@
esphome:
name: ${name}
on_boot:
priority: -100
then:
- lambda: |-
// UART0 TX looped back into its own RX so injected frames reach the hub
USC0(0) |= (1 << UCLBE);
ESP_LOGI("harness", "UART0 loopback on");
esp8266:
board: d1_mini
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
reboot_timeout: 0s
logger:
baud_rate: 0
level: VERBOSE
uart:
id: radar_uart
tx_pin: GPIO1
rx_pin: GPIO3
baud_rate: ${baud}
debug:
direction: TX
dummy_receiver: false
after:
delimiter: [0x04, 0x03, 0x02, 0x01]
ld2450:
id: radar
uart_id: radar_uart
select:
- platform: ld2450
baud_rate:
name: Baud Rate
zone_type:
name: Zone Type
text_sensor:
- platform: ld2450
version:
name: Radar Version
target_1:
direction:
name: T1 Direction
target_2:
direction:
name: T2 Direction
target_3:
direction:
name: T3 Direction
sensor:
- platform: ld2450
target_count:
name: Target Count
button:
# periodic frames: header, 3 x (x, y, speed, resolution), footer
- platform: template
name: Frame Approach
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame Away
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x80, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame Stationary
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame None
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame T2 T3
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF4, 0x81, 0x20, 0x83, 0x1E, 0x00, 0x64, 0x00,
0x2C, 0x01, 0xB0, 0x84, 0x0A, 0x80, 0x64, 0x00,
0x55, 0xCC]
# query zone acks: header, length, command C1, status 01, 00 00, zone type, 00, 24 coordinate bytes, footer
- platform: template
name: Zone Ack Disabled
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
- platform: template
name: Zone Ack Detection
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
- platform: template
name: Zone Ack Filter
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# name based setters, the path lambdas use
- platform: template
name: Set Zone Filter By Name
on_press:
- lambda: id(radar).set_zone_type("Filter");
- platform: template
name: Set Zone Bogus By Name
on_press:
- lambda: id(radar).set_zone_type("Bogus");
- platform: template
name: Set Baud 57600 By Name
on_press:
- lambda: id(radar).set_baud_rate("57600");
- platform: template
name: Set Baud Bogus By Name
on_press:
- lambda: id(radar).set_baud_rate("Bogus");
# target 1 at x 772, y 258: bytes 04 03 02 01 are the command footer
- platform: template
name: Frame CmdFooter In Payload
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
# zone ack, type Detection, zone 1 x1 = 0xCC55: bytes 55 CC are the periodic footer
- platform: template
name: Zone Ack DataFooter In Payload
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x01, 0x00,
0x55, 0xCC, 0xC8, 0x00, 0xDC, 0x05, 0xB8, 0x0B, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# zone ack, type Filter, zone 1 x1 = 0x0304 and y1 = 0x0102: bytes 04 03 02 01 are the command footer
- platform: template
name: Zone Ack CmdFooter In Payload
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x02, 0x00,
0x04, 0x03, 0x02, 0x01, 0xDC, 0x05, 0xB8, 0x0B, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# a periodic frame cut after 12 bytes, then a complete zone ack (type Disabled, zeros)
- platform: template
name: Cut Frame Then Zone Ack
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# a periodic frame missing its last byte, then a complete one (target 1 moving away)
- platform: template
name: Cut Frame Then Frame
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55,
0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x80, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
# a command header claiming a 255 byte payload, then a complete periodic frame (target 1 stationary)
- platform: template
name: Bogus Length Then Frame
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0xFF, 0x00,
0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
# five garbage bytes, then a complete periodic frame (target 1 approaching)
- platform: template
name: Garbage Then Frame
on_press:
- uart.write:
id: radar_uart
data: [0x01, 0x02, 0x03, 0x42, 0x99,
0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
number:
- platform: ld2450
presence_timeout:
name: Presence Timeout
zone_1:
x1:
name: Z1 X1
y1:
name: Z1 Y1
x2:
name: Z1 X2
y2:
name: Z1 Y2
@@ -0,0 +1,57 @@
substitutions:
name: wispcli
ap_ssid: wisp-bridge
esphome:
name: ${name}
esp8266:
board: d1_mini
logger:
level: DEBUG
wifi:
ssid: ${ap_ssid}
password: !secret wisp_psk
power_save_mode: none
reboot_timeout: 0s
http_request:
esp8266_disable_ssl_support: true
timeout: 8s
time:
- platform: sntp
id: sntp_time
on_time_sync:
- logger.log:
format: "SNTP synced"
level: INFO
interval:
- interval: 15s
then:
- http_request.get:
url: !secret lan_http
capture_response: true
on_response:
- logger.log:
format: "LAN GET status %d body %s"
args: ["response->status_code", "body.c_str()"]
level: INFO
on_error:
- logger.log:
format: "LAN GET failed"
level: WARN
- http_request.get:
url: http://example.com/
on_response:
- logger.log:
format: "WAN GET status %d"
args: ["response->status_code"]
level: INFO
on_error:
- logger.log:
format: "WAN GET failed"
level: WARN
@@ -0,0 +1,40 @@
substitutions:
name: wispeth
ap_ssid: wisp-bridge
esphome:
name: ${name}
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
logger:
hardware_uart: USB_SERIAL_JTAG
level: VERBOSE
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
ethernet:
type: W5500
clk_pin: GPIO13
mosi_pin: GPIO11
miso_pin: GPIO12
cs_pin: GPIO14
interrupt_pin: GPIO10
reset_pin: GPIO9
wifi:
ap:
ssid: ${ap_ssid}
password: !secret wisp_psk
ap_mode: WISP
+1 -1
View File
@@ -286,7 +286,7 @@ jobs:
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek
uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1
uses: j178/prek-action@af4d4fcb6922b8e6f65c0426947125fcc38794ce # v3.0.2
with:
prek-version: ${{ steps.prek.outputs.version }}
# This job only runs on pull requests, so nothing ever populates
+26 -11
View File
@@ -24,6 +24,7 @@ from esphome.const import (
)
from esphome.core import CORE, ID, EsphomeError, HexInt, Lambda
from esphome.cpp_generator import (
CallExpression,
Expression,
LambdaExpression,
MockObj,
@@ -446,9 +447,17 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# The generated function's parameters; a std::string arg is never copied.
# The generated function's parameters: a reference keeps its spelling, so a mutable
# one stays mutable; a value is taken by const reference, so a std::string is never copied.
return [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
(
(
t
if str(cg.safe_exp(t)).rstrip().endswith("&")
else cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &")
),
arg,
)
for t, arg in args
]
@@ -578,7 +587,7 @@ def apply_action_builder(
if isinstance(spec, ApplyBytes):
statements.extend(
await _render_bytes(
spec, config, receiver, parent, lambda_args, table_name
spec, config, receiver, parent, args, table_name
)
)
continue
@@ -608,10 +617,14 @@ async def _render_bytes(
config: ConfigType,
receiver: str,
parent: str,
lambda_args: TemplateArgsType,
args: TemplateArgsType,
table_name: str,
) -> list[str]:
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined."""
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined.
The payload lambda keeps the automation's own argument types, as ``TemplatableBytes``
gave it, and is always called, so a body that modifies an argument still compiles.
"""
value = _config_lookup(config, spec.conf_key)
if value is None:
return []
@@ -622,21 +635,23 @@ async def _render_bytes(
if isinstance(value, Lambda):
inner = await cg.process_lambda(
value, lambda_args, return_type=cg.std_vector.template(cg.uint8)
value, args, return_type=cg.std_vector.template(cg.uint8)
)
# Never reduced into the outer function, whose own parameters are const
call = CallExpression(inner, *[cg.MockObj(name) for _, name in args])
return [
"{",
f" const std::vector<uint8_t> esphome__bytes = ({call_lambda(inner)});",
f" const std::vector<uint8_t> esphome__bytes = {call};",
f" {send(spec.target, 'esphome__bytes.data(), esphome__bytes.size()')}",
"}",
]
table, size = _static_bytes(table_name, value)
# Qualified so a trigger argument cannot shadow a reused table; an empty payload is nullptr
args = f"::{table}, {size}" if size else f"{table}, {size}"
table_args = f"::{table}, {size}" if size else f"{table}, {size}"
if CORE.is_esp8266 and spec.progmem_target is not None:
return [send(spec.progmem_target, args)]
return [send(spec.progmem_target, table_args)]
if not CORE.is_esp8266:
return [send(spec.target, args)]
return [send(spec.target, table_args)]
# A PROGMEM table must be read with aligned loads on ESP8266, so copy it out first;
# the length is known here, so the copy is an exact array, on the heap past ``stack``.
# An empty payload still gets a one byte array: the raw lwIP socket rejects a null buffer.
@@ -649,7 +664,7 @@ async def _render_bytes(
)
statements = ["{", f" {buffer}"]
if size:
statements.append(f" esphome::progmem_memcpy({data}, {args});")
statements.append(f" esphome::progmem_memcpy({data}, {table_args});")
return [*statements, f" {send(spec.target, f'{data}, {size}')}", "}"]
+2 -19
View File
@@ -295,11 +295,6 @@ async def to_code(config):
)
# Actions
SendPacketAction = ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
)
CC1101_ACTION_SCHEMA = cv.Schema(
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
)
@@ -338,23 +333,11 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
)
@automation.register_action(
automation.register_apply_action(
"cc1101.send_packet",
SendPacketAction,
SEND_PACKET_ACTION_SCHEMA,
synchronous=True,
automation.ApplyBytes(CONF_DATA, "transmit_packet({})"),
)
async def send_packet_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await automation.templatable_bytes(
config[CONF_DATA],
args,
var.set_data_template,
var.set_data_static,
"cc1101_data",
)
return var
_CONFIG_VALIDATORS = {opt.schema: validator for opt, validator in CONFIG_MAP.items()}
-9
View File
@@ -132,13 +132,4 @@ class CC1101Component final : public Component,
bool enter_tx_();
};
// Action Wrappers
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<CC1101Component> {
TEMPLATABLE_BYTES(data)
void play(const Ts &...x) override {
this->data_.visit([this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
}
};
} // namespace esphome::cc1101
+5 -2
View File
@@ -1,5 +1,4 @@
#include "ch422g.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ch422g {
@@ -60,7 +59,11 @@ bool CH422GComponent::digital_read(uint8_t pin) {
}
void CH422GComponent::digital_write(uint8_t pin, bool value) {
assign_bits(this->output_bits_, 1 << pin, value);
if (value) {
this->output_bits_ |= (1 << pin);
} else {
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
+5 -2
View File
@@ -1,5 +1,4 @@
#include "ch423.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -63,7 +62,11 @@ bool CH423Component::digital_read(uint8_t pin) {
}
void CH423Component::digital_write(uint8_t pin, bool value) {
assign_bits(this->output_bits_, 1 << pin, value);
if (value) {
this->output_bits_ |= (1 << pin);
} else {
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
@@ -222,10 +222,13 @@ void ESPHomeOTAComponent::udp_open_(UdpLink &link) {
if (!random_bytes(token, sizeof(token)))
return;
#ifdef ESPHOME_OTA_UDP_LWIP
#if USE_NETWORK_IPV4
uint32_t ipv4;
if (socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&peer), &ipv4)) {
ip_addr_set_ip4_u32(&link.peer_ip, ipv4);
} else {
} else
#endif
{
#if USE_NETWORK_IPV6 && LWIP_IPV6
inet6_addr_to_ip6addr(ip_2_ip6(&link.peer_ip), &reinterpret_cast<const sockaddr_in6 *>(&peer)->sin6_addr);
IP_SET_TYPE_VAL(link.peer_ip, IPADDR_TYPE_V6);
@@ -1,29 +1,20 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART_ALERT,
)
from esphome.types import ConfigType
from .. import factory_reset_ns
FactoryResetButton = factory_reset_ns.class_(
"FactoryResetButton", button.Button, cg.Component
)
CONFIG_SCHEMA = button.button_schema(
FactoryResetButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await button.register_button(var, config)
await button.sub_buttons(config).create(
config, None, apply="factory_reset::factory_reset_device()"
)
@@ -13,24 +13,20 @@ namespace esphome::factory_reset {
ESPHOME_LOG_TAG(TAG, "factory_reset.button");
void FactoryResetButton::dump_config() { LOG_BUTTON("", "Factory Reset Button", this); }
void FactoryResetButton::press_action() {
static void reset_and_reboot() {
global_preferences->reset();
App.safe_reboot();
}
void factory_reset_device() {
ESP_LOGI(TAG, "Resetting");
// Let MQTT settle a bit
delay(100); // NOLINT
#ifdef USE_OPENTHREAD
openthread::global_openthread_component->on_factory_reset(FactoryResetButton::factory_reset_callback);
openthread::global_openthread_component->on_factory_reset(reset_and_reboot);
#else
global_preferences->reset();
App.safe_reboot();
reset_and_reboot();
#endif
}
#ifdef USE_OPENTHREAD
void FactoryResetButton::factory_reset_callback() {
global_preferences->reset();
App.safe_reboot();
}
#endif
} // namespace esphome::factory_reset
@@ -1,21 +1,8 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/components/button/button.h"
#include "esphome/core/component.h"
namespace esphome::factory_reset {
class FactoryResetButton final : public button::Button, public Component {
public:
void dump_config() override;
#ifdef USE_OPENTHREAD
static void factory_reset_callback();
#endif
protected:
void press_action() override;
};
/// Run by the factory reset button.
void factory_reset_device();
} // namespace esphome::factory_reset
+5 -2
View File
@@ -1,5 +1,4 @@
#include "mcp23016.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstdio>
@@ -80,7 +79,11 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg_(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << pin, pin_value);
if (pin_value) {
reg_value |= 1 << pin;
} else {
reg_value &= ~(1 << pin);
}
this->write_reg_(reg_addr, reg_value);
@@ -1,5 +1,4 @@
#include "mcp23x08_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mcp23x08_base {
@@ -79,7 +78,11 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_reg(reg_addr, reg_value);
@@ -92,7 +92,11 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_reg(reg_addr, reg_value);
+4
View File
@@ -50,6 +50,10 @@ void MQTTCoverComponent::setup() {
}
if (traits.get_supports_tilt()) {
this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) {
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("STOP")) == 0) {
this->cover_->make_call().set_command_stop_tilt().perform();
return;
}
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
@@ -1,5 +1,6 @@
#include "download_buffer.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cstring>
namespace esphome::online_image {
@@ -34,6 +35,46 @@ size_t DownloadBuffer::read(size_t len) {
return this->unread_;
}
void DownloadSource::compact() {
if (this->keep_read_ || this->pos_ == this->start_)
return;
this->buffer_.read(this->pos_ - this->start_);
this->start_ = this->pos_;
}
int32_t DownloadSource::read(uint8_t *buf, uint32_t len) {
const size_t offset = this->pos_ - this->start_;
const size_t n = std::min<size_t>(len, this->buffer_.unread() - offset);
memcpy(buf, this->buffer_.data(offset), n);
this->pos_ += n;
return static_cast<int32_t>(n);
}
bool DownloadSource::seek(uint32_t position) {
// Only what is still in the buffer can be read again; a decoder never skips ahead of the download
if (position < this->start_ || position > this->start_ + this->buffer_.unread())
return false;
this->pos_ = position;
return true;
}
bool DownloadBuffer::grow(size_t size) {
if (this->size_ >= size)
return true;
RAMAllocator<uint8_t> allocator;
uint8_t *buffer = allocator.allocate(size);
if (buffer == nullptr) {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
allocator.get_max_free_block_size());
return false;
}
memcpy(buffer, this->buffer_, this->unread_);
allocator.deallocate(this->buffer_, this->size_);
this->buffer_ = buffer;
this->size_ = size;
return true;
}
size_t DownloadBuffer::resize(size_t size) {
if (this->size_ >= size) {
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
@@ -1,5 +1,6 @@
#pragma once
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/core/helpers.h"
#include <cstddef>
#include <cstdint>
@@ -35,6 +36,8 @@ class DownloadBuffer {
void reset() { this->unread_ = 0; }
size_t resize(size_t size);
/// Make the buffer at least size bytes, keeping the unread data. Returns false if the allocation failed.
bool grow(size_t size);
protected:
uint8_t *buffer_;
@@ -43,4 +46,35 @@ class DownloadBuffer {
size_t unread_;
};
/**
* @brief Lets a decoder read a download as it arrives, from the download buffer.
*
* The decoder reads at its own position in the buffer. Bytes it has read stay there until compact() drops
* them, and compact() keeps everything while set_keep_read(true) is in effect, so the decoder can seek back
* while it reads the headers.
*/
class DownloadSource : public runtime_image::DecodeSource {
public:
explicit DownloadSource(DownloadBuffer &buffer) : buffer_(buffer) {}
/// Start again at the beginning of a new download, keeping what is read
void reset() {
this->start_ = 0;
this->pos_ = 0;
this->keep_read_ = true;
}
void set_keep_read(bool keep) { this->keep_read_ = keep; }
/// Drop the bytes the decoder has read, to make room for more of the download
void compact();
int32_t read(uint8_t *buf, uint32_t len) override;
bool seek(uint32_t position) override;
protected:
DownloadBuffer &buffer_;
size_t start_{0}; // Position in the download of the first byte in the buffer
size_t pos_{0}; // Position in the download the decoder reads next
bool keep_read_{true};
};
} // namespace esphome::online_image
+12
View File
@@ -22,6 +22,8 @@ DEPENDENCIES = ["http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_ON_DOWNLOAD_STARTED = "on_download_started"
CONF_ON_DOWNLOAD_PROGRESS = "on_download_progress"
CONF_UPDATE = "update"
online_image_ns = cg.esphome_ns.namespace("online_image")
@@ -43,7 +45,9 @@ ONLINE_IMAGE_SCHEMA = (
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_STARTED): automation.validate_automation({}),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_DOWNLOAD_PROGRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
@@ -96,9 +100,17 @@ automation.register_apply_action(
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_STARTED, "add_on_download_started_callback"
),
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_PROGRESS,
"add_on_progress_callback",
[(float, "progress"), (cg.size_t, "bytes_read")],
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
+144 -10
View File
@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
ESPHOME_LOG_TAG(TAG, "online_image");
@@ -15,6 +16,11 @@ static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
namespace esphome::online_image {
// Time one loop pass may spend reading, and decoding a JPEG as it downloads
static constexpr uint32_t PASS_BUDGET_MS = 20;
// Shortest time between two on_download_progress calls
static constexpr uint32_t PROGRESS_INTERVAL_MS = 100;
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
uint32_t buffer_size, bool is_big_endian)
@@ -140,8 +146,12 @@ void OnlineImage::update() {
return;
}
// JPEG requires the complete image in the download buffer before decoding
if (format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
// A JPEG of known size is decoded as it arrives, so the download buffer only has to hold its headers.
// Otherwise the decoder needs the complete image in the buffer.
this->streaming_ = total_size > 0 && total_size <= INT32_MAX && this->decoder_supports_source();
this->stream_started_ = false;
this->source_.reset();
if (!this->streaming_ && format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
this->download_buffer_.resize(total_size) == 0) {
// resize() logged the failed allocation; the next update() allocates the configured buffer again
this->end_connection_();
@@ -152,6 +162,8 @@ void OnlineImage::update() {
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
this->start_time_ = millis();
this->last_data_time_ = this->start_time_;
this->download_started_callback_.call();
this->report_progress_(true);
this->enable_loop();
}
@@ -169,6 +181,11 @@ void OnlineImage::loop() {
return;
}
if (this->streaming_) {
this->loop_stream_();
return;
}
// Check if download is complete — use decoder's format-specific completion check
// to handle both known content-length and chunked transfer encoding
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
@@ -179,15 +196,22 @@ void OnlineImage::loop() {
}
// Download and decode more data
size_t available = this->download_buffer_.free_capacity();
if (available > 0) {
// Download in chunks to avoid blocking
available = std::min(available, this->download_buffer_initial_size_);
auto len = this->downloader_->read(this->download_buffer_.append(), available);
if (len > 0) {
this->last_data_time_ = App.get_loop_component_start_time();
if (this->download_buffer_.free_capacity() > 0) {
const uint32_t now = App.get_loop_component_start_time();
size_t received = 0;
int len;
do {
len = this->downloader_->read(this->download_buffer_.append(),
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
if (len <= 0)
break;
this->download_buffer_.write(len);
received += len;
} while (this->download_buffer_.free_capacity() != 0 && millis() - now < PASS_BUDGET_MS);
if (received > 0) {
this->last_data_time_ = now;
this->report_progress_(false);
// Feed data to decoder
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
@@ -255,6 +279,114 @@ void OnlineImage::loop() {
}
}
void OnlineImage::loop_stream_() {
const uint32_t now = App.get_loop_component_start_time();
const size_t total = this->downloader_->content_length;
if (this->stream_started_)
this->source_.compact();
if (this->downloader_->get_bytes_read() < total) {
if (this->download_buffer_.free_capacity() == 0) {
// compact() already ran, so a full buffer means the decoder read nothing in the last pass
if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
ESP_LOGE(TAG, "Image does not fit in the %zu byte download buffer; increase buffer_size",
this->download_buffer_.size());
this->fail_download_();
return;
}
} else {
int len;
do {
len = this->downloader_->read(this->download_buffer_.append(),
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
if (len <= 0)
break;
this->last_data_time_ = now;
this->download_buffer_.write(len);
this->report_progress_(false);
} while (this->downloader_->get_bytes_read() < total && this->download_buffer_.free_capacity() != 0 &&
millis() - now < PASS_BUDGET_MS);
if (len < 0) {
ESP_LOGE(TAG, "Error downloading image: %d", len);
this->fail_download_();
return;
} else if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->fail_download_();
return;
}
}
}
const bool arrived = this->downloader_->get_bytes_read() >= total;
if (!this->stream_started_) {
// The decoder reads the headers when it opens the image, so wait until they are in the buffer
if (!arrived && this->download_buffer_.free_capacity() != 0)
return;
const int err = this->begin_source(&this->source_);
if (err != 0) {
// Headers cut short by a full buffer fail to parse, or make the decoder seek past the buffer. Such an image
// still decodes once it is all in memory, as it did before JPEG images were decoded as they download.
if (!arrived && (err == runtime_image::DECODE_ERROR_INVALID_TYPE || err == runtime_image::DECODE_ERROR_READ)) {
ESP_LOGW(TAG,
"The image headers do not fit the %zu byte download buffer, so the whole image is downloaded "
"before it is decoded; increase buffer_size to decode it as it downloads",
this->download_buffer_.size());
// grow() logs a failed allocation
if (this->download_buffer_.grow(total)) {
this->streaming_ = false;
} else {
this->fail_download_();
}
return;
}
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(err));
this->fail_download_();
return;
}
this->source_.set_keep_read(false);
this->stream_started_ = true;
// Opening the image and allocating its buffer is enough for one pass
return;
}
do {
const int result = this->decode_step();
if (result == runtime_image::DECODE_STEP_MORE)
continue;
if (result == runtime_image::DECODE_STEP_DONE) {
this->finish_download_();
} else if (result == runtime_image::DECODE_STEP_NEED_DATA) {
if (!arrived)
return;
// JPEGDEC only waits for bytes short of the size it was given, so this guards against a hang
ESP_LOGE(TAG, "Download ended before the image was complete");
this->fail_download_();
} else {
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(result));
this->fail_download_();
}
return;
} while (millis() - now < PASS_BUDGET_MS);
}
void OnlineImage::report_progress_(bool force) {
const uint32_t now = App.get_loop_component_start_time();
const size_t bytes = this->downloader_->get_bytes_read();
const size_t total = this->downloader_->content_length;
// The last bytes are always reported, so a progress bar reaches 100%
if (!force && now - this->last_progress_time_ < PROGRESS_INTERVAL_MS && (total == 0 || bytes < total))
return;
this->last_progress_time_ = now;
const float progress = total > 0 ? std::min(100.0f, bytes * 100.0f / total) : NAN;
this->download_progress_callback_.call(progress, bytes);
}
void OnlineImage::fail_download_() {
this->end_connection_();
this->download_error_callback_.call();
}
void OnlineImage::finish_download_() {
// Must be read before end_decode() ends the decoding session
bool decoder_finished = this->is_decode_finished();
@@ -288,6 +420,8 @@ void OnlineImage::end_connection_() {
this->downloader_ = nullptr;
}
this->download_buffer_.reset();
this->streaming_ = false;
this->stream_started_ = false;
this->disable_loop();
}
@@ -16,6 +16,10 @@ using t_http_codes = enum {
HTTP_CODE_NOT_FOUND = 404,
};
/// Largest download read(). A read() blocks until it has every byte it asked for, so this bounds how long one loop
/// pass waits on a slow link.
static constexpr size_t READ_CHUNK = 1460;
/**
* @brief Download an image from a given URL, and decode it using the specified decoder.
* The image will then be stored in a buffer, so that it can be re-displayed without the
@@ -70,9 +74,17 @@ class OnlineImage final : public PollingComponent,
*/
void release();
/// Called when the server has accepted the request and the image starts to download
template<typename F> void add_on_download_started_callback(F &&callback) {
this->download_started_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_finished_callback(F &&callback) {
this->download_finished_callback_.add(std::forward<F>(callback));
}
/// Called with the percentage downloaded (NAN when the server did not send the size) and the bytes read so far
template<typename F> void add_on_progress_callback(F &&callback) {
this->download_progress_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_error_callback(F &&callback) {
this->download_error_callback_.add(std::forward<F>(callback));
}
@@ -81,12 +93,20 @@ class OnlineImage final : public PollingComponent,
bool validate_url_(const std::string &url);
void end_connection_();
void finish_download_();
/// loop() for a JPEG that is decoded as it downloads
void loop_stream_();
void fail_download_();
/// Reports download progress, at most every PROGRESS_INTERVAL_MS unless force is set
void report_progress_(bool force);
CallbackManager<void(bool)> download_finished_callback_{};
CallbackManager<void()> download_error_callback_{};
LazyCallbackManager<void()> download_started_callback_{};
LazyCallbackManager<void(float, size_t)> download_progress_callback_{};
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
DownloadBuffer download_buffer_;
DownloadSource source_{download_buffer_};
/**
* This is the *initial* size of the download buffer, not the current size.
* The download buffer can be resized at runtime; the download_buffer_initial_size_
@@ -109,6 +129,9 @@ class OnlineImage final : public PollingComponent,
uint32_t start_time_{0};
uint32_t last_data_time_{0};
uint32_t last_progress_time_{0};
bool streaming_{false}; // The decoder reads the download itself, as it arrives
bool stream_started_{false}; // The decoder has read the headers
};
} // namespace esphome::online_image
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca6416a.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca6416a {
@@ -171,7 +170,11 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
this->read_register_(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_register_(reg_addr, reg_value);
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca9505.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca9505 {
@@ -75,7 +74,11 @@ bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
assign_bits(this->output_[bank], mask, value);
if (value) {
this->output_[bank] |= mask;
} else {
this->output_[bank] &= ~mask;
}
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
}
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca9554.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca9554 {
@@ -100,7 +99,11 @@ bool PCA9554Component::digital_read_cache(uint8_t pin) {
}
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
#ifdef USE_PCA9554_PCAL
if (this->write_register_(OUTPUT_REG, this->output_mask_))
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
+5 -1
View File
@@ -60,7 +60,11 @@ bool PCF8574Component::digital_read_hw(uint8_t pin) {
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->write_gpio_();
}
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
@@ -1,5 +1,4 @@
#include "pi4ioe5v6408.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pi4ioe5v6408 {
@@ -130,7 +129,11 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
if (!this->status_comm_result(this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_),
LOG_STR("Failed to write output register")))
return;
+6 -16
View File
@@ -1,26 +1,16 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART,
)
from esphome.const import DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
restart_ns = cg.esphome_ns.namespace("restart")
RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component)
CONFIG_SCHEMA = button.button_schema(
RestartButton,
button.ApplyButton,
icon=ICON_RESTART,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await button.register_button(var, config)
await button.sub_buttons(config).create(
config, None, apply="restart::restart_device()"
)
@@ -7,12 +7,11 @@ namespace esphome::restart {
ESPHOME_LOG_TAG(TAG, "restart.button");
void RestartButton::press_action() {
void restart_device() {
ESP_LOGI(TAG, "Restarting device");
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
void RestartButton::dump_config() { LOG_BUTTON("", "Restart Button", this); }
} // namespace esphome::restart
@@ -1,19 +1,8 @@
#pragma once
#include "esphome/components/button/button.h"
#include "esphome/core/component.h"
namespace esphome::restart {
class RestartButton final : public button::Button, public Component {
public:
// User provided, not "= default": `new(p) RestartButton()` would zero-fill .bss that is already zero.
RestartButton() {}
void dump_config() override;
protected:
void press_action() override;
};
/// Run by the restart button.
void restart_device();
} // namespace esphome::restart
+11 -1
View File
@@ -92,7 +92,17 @@ class JPEGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time and
# can wait for input that has not arrived yet, and fixes for: input that arrives in short
# reads, garbled RGB8888 output of grayscale images, and two ARM NEON faults seen on arm64 hosts
# (swapped red and blue in the 4:2:0 RGB8888 path, writes past the row on clipped MCUs). It also
# uses the ESP32-S3 SIMD colour conversion in ESP-IDF builds, not only Arduino ones, and is built
# with -O2.
cg.add_library(
"JPEGDEC",
"1.8.4",
"https://github.com/esphome-libs/JPEGDEC#1.8.4-esphome.3",
)
if CORE.is_host:
# JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only
# predefines the lowercase __linux__; without this a Linux host
@@ -2,7 +2,6 @@
#include "runtime_image.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
namespace esphome::runtime_image {
@@ -11,19 +10,48 @@ ESPHOME_LOG_TAG(TAG, "image_decoder");
bool ImageDecoder::set_size(int width, int height) {
bool success = this->image_->resize(width, height) > 0;
this->size_valid_ = success;
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
this->draw_width_ = width;
this->draw_height_ = height;
return success;
}
int ImageDecoder::scaled_x_(int x) const {
if (this->draw_width_ <= 0)
return 0;
const uint32_t buffer_width = this->image_->get_buffer_width();
return static_cast<int>((static_cast<uint32_t>(x) * buffer_width + this->draw_width_ - 1) / this->draw_width_);
}
int ImageDecoder::scaled_y_(int y) const {
if (this->draw_height_ <= 0)
return 0;
const uint32_t buffer_height = this->image_->get_buffer_height();
return static_cast<int>((static_cast<uint32_t>(y) * buffer_height + this->draw_height_ - 1) / this->draw_height_);
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
if (!this->size_valid_) {
return;
}
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
for (int i = x * this->x_scale_; i < width; i++) {
for (int j = y * this->y_scale_; j < height; j++) {
// A source pixel paints the target pixels from ceil(start * scale) up to ceil(end * scale). The spans of neighbouring
// pixels meet without overlapping, so when an image is shrunk each target pixel is painted once, by the first
// source pixel that reaches it, rather than by every pixel that lands on it. Integer maths: the S3 has no double FPU.
const int buffer_width = this->image_->get_buffer_width();
const int buffer_height = this->image_->get_buffer_height();
int x1 = x;
int y1 = y;
int x2 = x + w;
int y2 = y + h;
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
x1 = this->scaled_x_(x);
y1 = this->scaled_y_(y);
x2 = this->scaled_x_(x + w);
y2 = this->scaled_y_(y + h);
}
x2 = std::min(buffer_width, x2);
y2 = std::min(buffer_height, y2);
for (int i = x1; i < x2; i++) {
for (int j = y1; j < y2; j++) {
this->image_->draw_pixel(i, j, color);
}
}
@@ -1,4 +1,6 @@
#pragma once
#include <cstdint>
#include "esphome/core/color.h"
#include "image_format.h"
@@ -9,6 +11,14 @@ enum DecodeError : int {
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
DECODE_ERROR_OUT_OF_MEMORY = -3,
DECODE_ERROR_INTERNAL_DECODER_ERROR = -4,
DECODE_ERROR_READ = -5,
};
/// Results of ImageDecoder::decode_step() other than a DecodeError
enum DecodeStep : int {
DECODE_STEP_DONE = 0,
DECODE_STEP_MORE = 1,
DECODE_STEP_NEED_DATA = 2, // The source has nothing more yet; call again once more has arrived
};
constexpr const char *decode_error_to_string(int error) {
@@ -21,6 +31,8 @@ constexpr const char *decode_error_to_string(int error) {
return "Out of memory";
case DECODE_ERROR_INTERNAL_DECODER_ERROR:
return "Internal decoder error";
case DECODE_ERROR_READ:
return "Read error";
default:
return "Unknown error";
}
@@ -28,6 +40,19 @@ constexpr const char *decode_error_to_string(int error) {
class RuntimeImage;
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
class DecodeSource {
public:
virtual ~DecodeSource() = default;
/**
* Read up to len bytes at the current position. Returns the number read, which is less than len, even 0, when
* no more has arrived yet or at the end of the input, or negative on error.
*/
virtual int32_t read(uint8_t *buf, uint32_t len) = 0;
/// Move to a position counted from the start. Returns false on error.
virtual bool seek(uint32_t position) = 0;
};
/**
* @brief Class to abstract decoding different image formats.
*/
@@ -58,8 +83,8 @@ class ImageDecoder {
this->expected_size_ = 0;
this->decoded_bytes_ = 0;
this->size_valid_ = true;
this->x_scale_ = 1.0;
this->y_scale_ = 1.0;
this->draw_width_ = 0;
this->draw_height_ = 0;
}
/**
@@ -88,6 +113,28 @@ class ImageDecoder {
*/
virtual int decode(uint8_t *buffer, size_t size) = 0;
/// Whether this decoder can read a DecodeSource itself, a step at a time, instead of being fed data.
virtual bool supports_source() const { return false; }
/**
* @brief Start decoding from a source the decoder reads itself. Call after prepare(), in place of decode().
*
* The image headers (everything before the image data) must be readable from the source when this is called;
* only decode_step() can wait for more to arrive. The source must hold expected_size bytes in the end, and stay
* valid until the session ends.
* @return int 0 on success, or a {@see DecodeError} value.
*/
virtual int begin_source(DecodeSource *source) { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
/**
* @brief Decode a little more of an image started with begin_source(), keeping each call short.
*
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_NEED_DATA when the source has not
* delivered enough to go on, DECODE_STEP_DONE when the image is complete, or a {@see DecodeError}
* value.
*/
virtual int decode_step() { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
/**
* @brief Request the image to be resized once the actual dimensions are known.
* Called by the callback functions, to be able to access the parent Image class.
@@ -112,6 +159,9 @@ class ImageDecoder {
*/
void draw(int x, int y, int w, int h, const Color &color);
/// Bytes of the image the decoder has used so far
size_t get_decoded_bytes() const { return this->decoded_bytes_; }
/**
* @brief Check if the decoder has finished processing.
*
@@ -134,8 +184,13 @@ class ImageDecoder {
uint8_t *image_buffer_() const;
bool image_is_big_endian_() const;
double x_scale_ = 1.0;
double y_scale_ = 1.0;
/// The first buffer column, or row, that source column x, or row y, paints: ceil(x * buffer size / draw size)
int scaled_x_(int x) const;
int scaled_y_(int y) const;
// Size of the image as the decoder draws it, which draw() spreads over the buffer
int draw_width_ = 0;
int draw_height_ = 0;
RuntimeImage *image_;
size_t expected_size_ = 0; // Expected data size (0 if unknown)
size_t decoded_bytes_ = 0; // Bytes processed so far
+136 -24
View File
@@ -31,8 +31,8 @@ bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
const uint32_t target_width = this->image_->get_buffer_width();
const uint32_t target_height = this->image_->get_buffer_height();
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
// the generic draw() path rounds down and keeps the odd ones instead
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6,
// as the generic draw() path does
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
}
@@ -135,13 +135,24 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
return DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0)
return err;
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
return this->fail_();
this->decoded_bytes_ = size;
this->jpeg_.close();
return size;
}
int JpegDecoder::start_() {
auto jpeg_type = this->jpeg_.getJPEGType();
if (jpeg_type == JPEG_MODE_INVALID) {
ESP_LOGE(TAG, "Unsupported JPEG image");
return DECODE_ERROR_INVALID_TYPE;
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
ESP_LOGE(TAG, "Progressive JPEG images not supported");
return DECODE_ERROR_INVALID_TYPE;
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
@@ -154,32 +165,133 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
} else {
this->jpeg_.setPixelType(RGB8888);
}
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
const int width = this->jpeg_.getWidth();
const int height = this->jpeg_.getHeight();
if (!this->set_size(width, height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
// many pixels as the buffer, so a large image shown small is not decoded in full only to be thrown away.
const int buffer_width = this->image_->get_buffer_width();
const int buffer_height = this->image_->get_buffer_height();
this->scale_ = 1;
while (this->scale_ < JPEG_SCALE_EIGHTH && width / (this->scale_ * 2) >= buffer_width &&
height / (this->scale_ * 2) >= buffer_height) {
this->scale_ *= 2;
}
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
this->draw_width_ = decoded_width;
this->draw_height_ = decoded_height;
if (this->scale_ != 1) {
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
}
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (!this->jpeg_.decode(0, 0, 0)) {
auto error = this->jpeg_.getLastError();
ESP_LOGE(TAG, "Error while decoding: %d", error);
this->jpeg_.close();
switch (error) {
case JPEG_ERROR_MEMORY:
return DECODE_ERROR_OUT_OF_MEMORY;
case JPEG_UNSUPPORTED_FEATURE:
return DECODE_ERROR_UNSUPPORTED_FORMAT;
case JPEG_INVALID_FILE:
case JPEG_INVALID_PARAMETER:
return DECODE_ERROR_INVALID_TYPE;
case JPEG_DECODE_ERROR:
default:
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
}
this->decoded_bytes_ = size;
return 0;
}
int JpegDecoder::fail_() {
const bool read_failed = this->source_ != nullptr && this->source_failed_;
const int error = this->jpeg_.getLastError();
this->jpeg_.close();
return size;
this->source_ = nullptr;
if (read_failed) {
ESP_LOGE(TAG, "Reading the image failed");
return DECODE_ERROR_READ;
}
ESP_LOGE(TAG, "Error while decoding: %d", error);
switch (error) {
case JPEG_ERROR_MEMORY:
return DECODE_ERROR_OUT_OF_MEMORY;
case JPEG_UNSUPPORTED_FEATURE:
return DECODE_ERROR_UNSUPPORTED_FORMAT;
case JPEG_INVALID_FILE:
case JPEG_INVALID_PARAMETER:
return DECODE_ERROR_INVALID_TYPE;
case JPEG_DECODE_ERROR:
default:
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
}
void JpegDecoder::reset() {
// A decode from a source that was given up part way still has the image open
if (this->source_ != nullptr) {
this->jpeg_.close();
this->source_ = nullptr;
}
ImageDecoder::reset();
}
int32_t JpegDecoder::source_read(JPEGFILE *file, uint8_t *buf, int32_t len) {
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
const int32_t n = decoder->source_->read(buf, len);
if (n < 0) {
// JPEGDEC has no way to report a failed read, so remember it and give it no data
decoder->source_failed_ = true;
return 0;
}
file->iPos += n;
return n;
}
int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
if (position < 0 || !decoder->source_->seek(position)) {
decoder->source_failed_ = true;
return file->iPos;
}
file->iPos = position;
return position;
}
int JpegDecoder::begin_source(DecodeSource *source) {
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
return DECODE_ERROR_INVALID_TYPE;
}
if (!source->seek(0)) {
ESP_LOGE(TAG, "Could not read the image");
return DECODE_ERROR_READ;
}
this->source_ = source;
this->source_failed_ = false;
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
// The caller may recover, for example by reading the whole image first, so it reports the failure
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
this->source_ = nullptr;
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0) {
this->jpeg_.close();
this->source_ = nullptr;
return err;
}
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
return this->fail_();
return 0;
}
int HOT JpegDecoder::decode_step() {
if (this->source_ == nullptr)
return DECODE_ERROR_INVALID_TYPE;
const int result = this->jpeg_.decodeRows(1);
// JPEGDEC may decode on with the missing data, so check the read before its result
if (this->source_failed_)
return this->fail_();
if (result == JPEG_DECODE_MORE)
return DECODE_STEP_MORE;
if (result == JPEG_DECODE_NEED_DATA)
return DECODE_STEP_NEED_DATA;
if (result == 0)
return this->fail_();
this->decoded_bytes_ = this->expected_size_;
this->jpeg_.close();
this->source_ = nullptr;
return DECODE_STEP_DONE;
}
} // namespace esphome::runtime_image
@@ -22,19 +22,41 @@ class JpegDecoder : public ImageDecoder {
~JpegDecoder() override {}
int HOT decode(uint8_t *buffer, size_t size) override;
void reset() override;
/// The source is read as the decode needs it, through JPEGDEC's own 2 KB buffer
bool supports_source() const override { return true; }
int begin_source(DecodeSource *source) override;
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
int HOT decode_step() override;
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
int get_scale() const { return this->scale_; }
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
protected:
bool prepare_rgb565_scaling_(int width, int height);
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
int start_();
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
int scale_option_() const { return this->scale_ == 1 ? 0 : this->scale_; }
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
int fail_();
static int32_t source_read(JPEGFILE *file, uint8_t *buf, int32_t len);
static int32_t source_seek(JPEGFILE *file, int32_t position);
static void source_close(void *handle) {}
JPEGDEC jpeg_{};
FixedVector<uint16_t> x_boundaries_{};
FixedVector<uint16_t> y_boundaries_{};
int source_width_{0};
int source_height_{0};
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
bool direct_rgb565_{false};
bool source_failed_{false};
};
} // namespace esphome::runtime_image
@@ -239,6 +239,25 @@ int RuntimeImage::feed_data(uint8_t *data, size_t len) {
return consumed;
}
int RuntimeImage::begin_source(DecodeSource *source) {
if (!this->decoder_supports_source()) {
ESP_LOGE(TAG, "Decoder cannot read a source");
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
return this->decoder_->begin_source(source);
}
int RuntimeImage::decode_step() {
if (!this->is_decoding()) {
ESP_LOGE(TAG, "No decoder initialized");
return DECODE_ERROR_INVALID_TYPE;
}
const int result = this->decoder_->decode_step();
if (result == DECODE_STEP_DONE)
this->decoded_bytes_ = this->decoder_->get_decoded_bytes();
return result;
}
bool RuntimeImage::end_decode() {
if (!this->is_decoding()) {
return false;
@@ -76,6 +76,22 @@ class RuntimeImage : public image::Image {
*/
int feed_data(uint8_t *data, size_t len);
/// Whether the active decoder can read a DecodeSource itself (see begin_source()).
bool decoder_supports_source() const { return this->is_decoding() && this->decoder_->supports_source(); }
/**
* @brief Decode from a source the decoder reads itself, in place of feed_data().
*
* Call after begin_decode() when decoder_supports_source() is true, once the image headers can be read from the
* source, then call decode_step() until it
* returns DECODE_STEP_DONE, and finish with end_decode().
* @return 0 on success, or a DecodeError value.
*/
int begin_source(DecodeSource *source);
/// Decode a little more from the source; see ImageDecoder::decode_step().
int decode_step();
/**
* @brief Complete the decoding process.
*
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
@@ -9,26 +8,18 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import SafeModeComponent, safe_mode_ns
from .. import SafeModeComponent
DEPENDENCIES = ["safe_mode"]
SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component)
CONFIG_SCHEMA = (
button.button_schema(
SafeModeButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
)
.extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
).extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
async def to_code(config: ConfigType) -> None:
var = await button.new_button(config)
await cg.register_component(var, config)
await cg.add_variable(var.set_safe_mode, config[CONF_SAFE_MODE])
buttons = button.sub_buttons(config, parent=config[CONF_SAFE_MODE])
await buttons.create(config, None, apply="restart_in_safe_mode()")
@@ -1,21 +0,0 @@
#include "safe_mode_button.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::safe_mode {
ESPHOME_LOG_TAG(TAG, "safe_mode.button");
void SafeModeButton::press_action() {
ESP_LOGI(TAG, "Restarting in safe mode");
this->safe_mode_component_->set_safe_mode_pending(true);
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
void SafeModeButton::dump_config() { LOG_BUTTON("", "Safe Mode Button", this); }
} // namespace esphome::safe_mode
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "esphome/components/safe_mode/safe_mode.h"
#include "esphome/core/component.h"
namespace esphome::safe_mode {
class SafeModeButton final : public button::Button, public Component {
public:
void dump_config() override;
void set_safe_mode(SafeModeComponent *safe_mode_component) { this->safe_mode_component_ = safe_mode_component; }
protected:
SafeModeComponent *safe_mode_component_;
void press_action() override;
};
} // namespace esphome::safe_mode
@@ -174,6 +174,14 @@ bool SafeModeComponent::get_safe_mode_pending() {
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
}
void SafeModeComponent::restart_in_safe_mode() {
ESP_LOGI(TAG, "Restarting in safe mode");
this->set_safe_mode_pending(true);
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after,
bool in_flash) {
this->safe_mode_start_time_ = millis();
+2
View File
@@ -25,6 +25,8 @@ class SafeModeComponent final : public Component {
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
bool get_safe_mode_pending();
/// Set safe mode pending and reboot; run by the safe mode button.
void restart_in_safe_mode();
void dump_config() override;
float get_setup_priority() const override;
+6 -11
View File
@@ -1,18 +1,13 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_POWER
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_POWER
from esphome.types import ConfigType
shutdown_ns = cg.esphome_ns.namespace("shutdown")
ShutdownButton = shutdown_ns.class_("ShutdownButton", button.Button, cg.Component)
CONFIG_SCHEMA = button.button_schema(
ShutdownButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER
).extend(cv.COMPONENT_SCHEMA)
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await button.register_button(var, config)
await button.sub_buttons(config).create(
config, None, apply="shutdown::shutdown_device()"
)
@@ -14,8 +14,7 @@ namespace esphome::shutdown {
ESPHOME_LOG_TAG(TAG, "shutdown.button");
void ShutdownButton::dump_config() { LOG_BUTTON("", "Shutdown Button", this); }
void ShutdownButton::press_action() {
void shutdown_device() {
ESP_LOGI(TAG, "Shutting down");
// Let MQTT settle a bit
delay(100); // NOLINT
@@ -1,16 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/button/button.h"
namespace esphome::shutdown {
class ShutdownButton final : public button::Button, public Component {
public:
void dump_config() override;
protected:
void press_action() override;
};
/// Run by the shutdown button.
void shutdown_device();
} // namespace esphome::shutdown
+8 -2
View File
@@ -17,6 +17,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
idf_version,
only_on_variant,
)
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
@@ -142,13 +143,19 @@ TYPE_CLASS = {
}
def _require_psram_dma_idf(value: Any) -> Any:
if idf_version() < cv.Version(5, 5, 3):
raise cv.Invalid("psram_dma requires ESP-IDF 5.5.3 or newer")
return value
def _validate_psram_dma(value: Any) -> bool:
value = cv.boolean(value)
if not value:
return value
return cv.All(
cv.only_on_esp32,
cv.only_with_framework("esp-idf"),
_require_psram_dma_idf,
only_on_variant(
supported=[
VARIANT_ESP32C5,
@@ -159,7 +166,6 @@ def _validate_psram_dma(value: Any) -> bool:
],
msg_prefix="PSRAM DMA",
),
cv.require_framework_version(esp_idf=cv.Version(5, 5, 3)),
cv.requires_component(PSRAM_DOMAIN),
)(value)
+5 -27
View File
@@ -11,10 +11,9 @@ from esphome.components.const import (
CONF_RST_PIN,
)
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID, TimePeriod
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY
from esphome.core import TimePeriod
from esphome.types import ConfigType
DOMAIN = "sx126x"
MULTI_CONF = True
@@ -133,10 +132,6 @@ SHAPING = {
"NONE": SX126xPulseShape.NO_FILTER,
}
SendPacketAction = sx126x_ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(SX126x)
)
def validate_raw_data(value: Any) -> bytes | list[int]:
if isinstance(value, str):
@@ -324,25 +319,8 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
)
@automation.register_action(
automation.register_apply_action(
"sx126x.send_packet",
SendPacketAction,
SEND_PACKET_ACTION_SCHEMA,
synchronous=True,
automation.ApplyBytes(CONF_DATA, "transmit_packet({})", stack=MAX_PACKET_SIZE),
)
async def send_packet_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await automation.templatable_bytes(
config[CONF_DATA],
args,
var.set_data_template,
var.set_data_static,
"sx126x_data",
)
return var
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sx126x/sx126x.h"
namespace esphome::sx126x {
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX126x> {
TEMPLATABLE_BYTES(data)
void play(const Ts &...x) override {
this->data_.template visit<SX126X_MAX_PACKET_SIZE>(
[this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
}
};
} // namespace esphome::sx126x
+3 -26
View File
@@ -11,9 +11,7 @@ from esphome.components.const import (
CONF_RST_PIN,
)
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.const import CONF_DATA, CONF_FREQUENCY
from esphome.types import ConfigType
DOMAIN = "sx127x"
@@ -117,10 +115,6 @@ SHAPING = {
"NONE": SX127xPaRamp.SHAPING_NONE,
}
SendPacketAction = sx127x_ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(SX127x)
)
def validate_raw_data(value: Any) -> bytes | list[int]:
if isinstance(value, str):
@@ -291,25 +285,8 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
)
@automation.register_action(
automation.register_apply_action(
"sx127x.send_packet",
SendPacketAction,
SEND_PACKET_ACTION_SCHEMA,
synchronous=True,
automation.ApplyBytes(CONF_DATA, "transmit_packet({})", stack=MAX_PACKET_SIZE),
)
async def send_packet_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await automation.templatable_bytes(
config[CONF_DATA],
args,
var.set_data_template,
var.set_data_static,
"sx127x_data",
)
return var
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sx127x/sx127x.h"
namespace esphome::sx127x {
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX127x> {
TEMPLATABLE_BYTES(data)
public:
void play(const Ts &...x) override {
this->data_.template visit<SX127X_MAX_PACKET_SIZE>(
[this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
}
};
} // namespace esphome::sx127x
+24 -5
View File
@@ -92,9 +92,20 @@ void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) {
// If the pin is an output, write high/low
uint16_t temp_reg_data = 0;
this->read_byte_16(REG_DATA_B, &temp_reg_data);
assign_bits(this->output_state_, 1 << pin, bit_value);
// Outputs take the shadow state, inputs keep what was read
temp_reg_data = (temp_reg_data & this->ddr_mask_) | (this->output_state_ & ~this->ddr_mask_);
if (bit_value) {
output_state_ |= (1 << pin); // set bit in shadow register
} else {
output_state_ &= ~(1 << pin); // reset bit shadow register
}
for (uint16_t b = 0x8000; b; b >>= 1) {
if ((~ddr_mask_) & b) { // transfer bits of outputs, but don't mess with inputs
if (output_state_ & b) {
temp_reg_data |= b;
} else {
temp_reg_data &= ~b;
}
}
}
this->write_byte_16(REG_DATA_B, temp_reg_data);
}
}
@@ -137,12 +148,20 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
// Pullup
this->read_byte_16(REG_PULL_UP_B, &temp_word);
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLUP);
if (flags & gpio::FLAG_PULLUP) {
temp_word |= (1 << pin);
} else {
temp_word &= ~(1 << pin);
}
this->write_byte_16(REG_PULL_UP_B, temp_word);
// Pulldown
this->read_byte_16(REG_PULL_DOWN_B, &temp_word);
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLDOWN);
if (flags & gpio::FLAG_PULLDOWN) {
temp_word |= (1 << pin);
} else {
temp_word &= ~(1 << pin);
}
this->write_byte_16(REG_PULL_DOWN_B, temp_word);
// Set direction to input
+5 -2
View File
@@ -1,5 +1,4 @@
#include "tca9555.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
@@ -109,7 +108,11 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->status_comm_result(this->write_byte_16_le(TCA9555_OUTPUT_PORT_REGISTER_0, this->output_mask_),
LOG_STR("Failed to write output register"));
+5 -11
View File
@@ -121,7 +121,6 @@ HOST_BAUD_RATES = [
]
UARTDevice = uart_ns.class_("UARTDevice")
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action)
UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component)
MULTI_CONF = True
@@ -593,9 +592,8 @@ def new_uart_component(
return cg.new_bus_component(register_uart_device, config, *args)
@automation.register_action(
automation.register_apply_action(
"uart.write",
UARTWriteAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(UARTComponent),
@@ -603,15 +601,11 @@ def new_uart_component(
},
key=CONF_DATA,
),
synchronous=True,
# A constant stays in flash: write_array_progmem copies it out in chunks on ESP8266
automation.ApplyBytes(
CONF_DATA, "write_array({})", progmem_target="write_array_progmem({})"
),
)
async def uart_write_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await automation.templatable_bytes(
config[CONF_DATA], args, var.set_data_template, var.set_data_static, "uart_data"
)
return var
@coroutine_with_priority(CoroPriority.FINAL)
-22
View File
@@ -1,22 +0,0 @@
#pragma once
#include "uart.h"
#include "esphome/core/automation.h"
#include <vector>
namespace esphome::uart {
template<typename... Ts> class UARTWriteAction final : public Action<Ts...>, public Parented<UARTComponent> {
TEMPLATABLE_BYTES(data)
void play(const Ts &...x) override {
if (this->data_.is_static()) {
this->parent_->write_array_progmem(this->data_.data(), this->data_.size());
} else {
this->parent_->write_array(this->data_.value(x...));
}
}
};
} // namespace esphome::uart
+17 -17
View File
@@ -29,7 +29,6 @@ DOMAIN = "udp"
MULTI_CONF = True
udp_ns = cg.esphome_ns.namespace("udp")
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
trigger_argname = "data"
# Listener callback type (non-owning span from UDP component)
listener_args = cg.std_span.template(cg.uint8.operator("const"))
@@ -240,17 +239,23 @@ def validate_raw_data(value: Any) -> bytes | list[int]:
)
@automation.register_action(
_UDP_WRITE_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(UDPComponent),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
)
_udp_write_builder = automation.apply_action_builder(
"udp.write",
UDPWriteAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(UDPComponent),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
),
synchronous=True,
_UDP_WRITE_SCHEMA,
# Constant payloads up to this size are sent from the stack on ESP8266
automation.ApplyBytes(CONF_DATA, "send_packet({})", stack=128),
)
@automation.register_action(
"udp.write", automation.ApplyAction, _UDP_WRITE_SCHEMA, synchronous=True
)
async def udp_write_to_code(
config: ConfigType,
@@ -258,11 +263,6 @@ async def udp_write_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
udp_var = await cg.get_variable(config[CONF_ID])
await cg.register_parented(var, udp_var)
cg.add(udp_var.set_should_broadcast())
await automation.templatable_bytes(
config[CONF_DATA], args, var.set_data_template, var.set_data_static, "udp_data"
)
return var
return await _udp_write_builder(config, action_id, template_arg, args)
-24
View File
@@ -1,24 +0,0 @@
#pragma once
#include "udp_component.h"
#ifdef USE_NETWORK
#include "esphome/core/automation.h"
namespace esphome::udp {
/// Constant payloads up to this size are sent from the stack on ESP8266.
static constexpr size_t STACK_PAYLOAD_SIZE = 128;
template<typename... Ts> class UDPWriteAction final : public Action<Ts...>, public Parented<UDPComponent> {
TEMPLATABLE_BYTES(data)
void play(const Ts &...x) override {
// One sendto per packet, so a failed allocation never sends a partial packet
this->data_.template visit<STACK_PAYLOAD_SIZE>(
[this](const uint8_t *data, size_t len) { this->parent_->send_packet(data, len); }, x...);
}
};
} // namespace esphome::udp
#endif
+16 -4
View File
@@ -1,3 +1,5 @@
from collections.abc import Callable
import esphome.codegen as cg
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
@@ -154,6 +156,19 @@ FILTER_SOURCE_FILES = filter_source_files_from_defines(
)
def _channel_limit(type_: Type) -> Callable[[list], list]:
"""Check the channel count against the type's limit, which depends on the target."""
def validator(value: list) -> list:
if len(value) > (limit := type_.max_channels):
raise cv.Invalid(
f"Device type {type_.name} supports a maximum of {limit} channels"
)
return value
return validator
def channel_schema(type_: "Type") -> cv.Schema:
schema = cv.Schema(
{
@@ -192,10 +207,7 @@ def channel_schema(type_: "Type") -> cv.Schema:
}
)
),
cv.Length(
max=type_.max_channels,
msg=f"Device type {type_.name} supports a maximum of {type_.max_channels} channels",
),
_channel_limit(type_),
),
}
)
@@ -1,12 +1,6 @@
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import (
media_player,
micro_wake_word,
microphone,
network,
speaker,
)
from esphome.components import media_player, micro_wake_word, microphone, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
@@ -175,7 +169,6 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.key_requires((CONF_ON_TTS_STREAM_START, CONF_ON_TTS_STREAM_END), CONF_SPEAKER),
network.require_ipv4("voice_assistant"),
)
FINAL_VALIDATE_SCHEMA = cv.All(
@@ -82,7 +82,11 @@ void VoiceAssistant::setup() {
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
bool VoiceAssistant::start_udp_socket_() {
#if USE_NETWORK_IPV4
this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
#else
this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
#endif
if (this->socket_ == nullptr) {
ESP_LOGE(TAG, "Could not create socket");
this->mark_failed();
@@ -663,17 +667,20 @@ void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t por
}
memcpy(&this->dest_addr_, addr, sizeof(this->dest_addr_));
if (this->dest_addr_.ss_family == AF_INET) {
((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port);
}
#if LWIP_IPV6
else if (this->dest_addr_.ss_family == AF_INET6) {
((struct sockaddr_in6 *) &this->dest_addr_)->sin6_port = htons(port);
}
switch (this->dest_addr_.ss_family) {
#if USE_NETWORK_IPV4
case AF_INET:
((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port);
break;
#endif
else {
ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family);
return;
#if LWIP_IPV6
case AF_INET6:
((struct sockaddr_in6 *) &this->dest_addr_)->sin6_port = htons(port);
break;
#endif
default:
ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family);
return;
}
// Only primary microphone channel over UDP
@@ -1,5 +1,4 @@
#include "waveshare_io_ch32v003.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::waveshare_io_ch32v003 {
@@ -111,7 +110,11 @@ void WaveshareIOCH32V003Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
uint8_t data = this->output_mask_;
this->status_comm_result(this->write_byte(IO_EXTENSION_IO_OUTPUT_ADDR, data),
+5 -1
View File
@@ -210,7 +210,11 @@ bool WeikaiComponent::read_pin_val_(uint8_t pin) {
}
void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
assign_bits(this->output_state_, 1 << pin, value);
if (value) {
this->output_state_ |= (1 << pin);
} else {
this->output_state_ &= ~(1 << pin);
}
char bin_buf[9];
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
format_bin_to(bin_buf, this->output_state_));
+5 -9
View File
@@ -9,6 +9,7 @@ from esphome.components.esp32 import (
add_idf_sdkconfig_option,
const,
get_esp32_variant,
idf_version,
only_on_variant,
request_wifi,
require_mbedtls_tls,
@@ -65,7 +66,6 @@ from esphome.const import (
CONF_USE_ADDRESS,
CONF_USERNAME,
PLACEHOLDER_WIFI_SSID,
Framework,
Platform,
PlatformFramework,
)
@@ -224,19 +224,16 @@ WIFI_AP_MODE_WISP = "WISP"
def _validate_ap_mode(value: str) -> str:
value = cv.one_of(WIFI_AP_MODE_FALLBACK, WIFI_AP_MODE_WISP, upper=True)(value)
if value == WIFI_AP_MODE_WISP:
# WISP mode routes through NAT, which only ESP-IDF provides
cv.only_with_framework(Framework.ESP_IDF)(value)
# The bridge is part of the ESP32 backend, on both of its frameworks
cv.only_on_esp32(value)
return value
def _validate_dns_servers(value: list) -> list:
if any(ip.is_unspecified for ip in value):
raise cv.Invalid("0.0.0.0 is not a DNS server the access point can offer")
if len(value) > 1:
cv.require_framework_version(
esp_idf=cv.Version(6, 0, 0),
extra_message="The access point offers a second DNS server from ESP-IDF 6.0",
)(value)
if len(value) > 1 and CORE.is_esp32 and idf_version() < cv.Version(6, 0, 0):
raise cv.Invalid("The access point offers a second DNS server from ESP-IDF 6.0")
return value
@@ -645,7 +642,6 @@ def _lease_cache_schema(value: Any) -> ConfigType:
value = LEASE_CACHE_SCHEMA(value)
if value[CONF_ENABLED]:
cv.only_on_esp32(value)
cv.only_with_framework(Framework.ESP_IDF)(value)
if CONF_STORAGE not in value:
if not preferences.rtc_supported():
raise cv.Invalid(
+10 -4
View File
@@ -533,7 +533,8 @@ class SubEntities:
"""Create the entity from ``conf``; ``setter`` may be None when the hub keeps no pointer.
With ``apply`` the entity is the domain's ``Apply*`` class and its control runs
``parent->apply``, with ``publish`` ("before"/"after") placing the domain's publish.
``parent->apply``, or ``apply`` as a namespaced free statement when no parent is bound, with
``publish`` ("before"/"after") placing the domain's publish.
"""
entity_id: ID = conf[CONF_ID]
is_apply_class = self.apply_entity is not None and str(entity_id.type) == str(
@@ -545,8 +546,13 @@ class SubEntities:
raise ValueError(
f"{entity_id}: apply= needs the domain's Apply class, not {entity_id.type}"
)
if apply is not None and self.parent is None:
raise ValueError(f"{entity_id}: apply= needs a parent")
# A parentless statement is a namespaced free function, so a hub platform that forgot
# parent= fails here instead of in the generated C++
if apply is not None and self.parent is None and "::" not in apply:
raise ValueError(
f"{entity_id}: apply= without a parent must call a namespaced free function "
f"(ns::fn()); bind parent= for a hub method"
)
if publish is not None and (apply is None or self.apply_entity.publish is None):
raise ValueError(f"{entity_id}: publish= needs apply= on a stateful domain")
if publish not in (None, "before", "after"):
@@ -571,7 +577,7 @@ class SubEntities:
if isinstance(parent, ID):
parent = await get_variable(parent)
apply_entity = self.apply_entity
body = [f"{parent_ref(parent)}->{apply};"]
body = [f"{apply};" if parent is None else f"{parent_ref(parent)}->{apply};"]
if publish == "before":
body.insert(0, apply_entity.publish)
elif publish == "after":
+2 -2
View File
@@ -48,7 +48,7 @@ lib_deps =
improv/Improv@1.2.7 ; improv_serial / improv_ble
kikuchan98/pngle@1.1.0 ; runtime_image
; Using the repository directly, otherwise ESP-IDF can't use the library
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
; This dependency is used only in unit tests.
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
; See scripts/cpp_unit_test.py and tests/components/README.md
@@ -253,7 +253,7 @@ lib_deps =
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
improv/Improv@1.2.7 ; improv_serial
kikuchan98/pngle@1.1.0 ; runtime_image
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
build_flags =
${common:arduino.build_flags}
-DUSE_LIBRETINY
+2 -2
View File
@@ -31,9 +31,9 @@ smp==4.2.0; platform_system != "Darwin" or platform_machine != "x86_64"
smp==4.1.0; platform_system == "Darwin" and platform_machine == "x86_64"
requests==2.34.2
py7zr==1.1.4
platformdirs==4.13.4 # native esp-idf toolchain global cache dir
platformdirs==4.13.5 # native esp-idf toolchain global cache dir
ninja==1.13.2 # native host and esp8266 arduino toolchain build driver
filelock==4.2.2 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
filelock==4.2.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
# esp-idf >= 5.0 requires this
pyparsing >= 3.3.3
+1 -1
View File
@@ -1,7 +1,7 @@
pylint==4.1.3
ruff==0.17.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
pyupgrade==3.22.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
prek==0.5.5 # .github/workflows/ci.yml reads this pin
prek==0.5.6 # .github/workflows/ci.yml reads this pin
yamlrocks==0.7.1 # used by script/sync_dependency_versions.py
# Unit tests
@@ -14,7 +14,7 @@ def test_constant_payloads_share_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Equal send_packet payloads share one table; lambdas stay templates."""
"""Equal send_packet payloads share one table; a lambda is sent from its vector."""
main_cpp = generate_main(component_config_path("payload_tables.yaml"))
tables = re.findall(
@@ -23,8 +23,10 @@ def test_constant_payloads_share_progmem_tables(
assert [v for _, v in tables] == [
"{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}"
]
assert main_cpp.count(f"set_data_static({tables[0][0]}, 8);") == 2
assert "set_data_template(" in main_cpp
assert main_cpp.count(f"->transmit_packet(::{tables[0][0]}, 8);") == 2
assert (
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
)
@pytest.mark.parametrize("size", [64, 65])
+41 -5
View File
@@ -5,6 +5,7 @@ import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import (
KEY_BOARD,
KEY_IDF_VERSION,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
@@ -35,11 +36,17 @@ def _stage(
platform_framework: PlatformFramework,
variant: str,
version: cv.Version,
idf: cv.Version | None = None,
) -> None:
"""``idf`` is the ESP-IDF underneath an Arduino ``version``; an ESP-IDF build is its own."""
set_core_config(
platform_framework,
core_data={KEY_FRAMEWORK_VERSION: version},
platform_data={KEY_BOARD: "test-board", KEY_VARIANT: variant},
platform_data={
KEY_BOARD: "test-board",
KEY_VARIANT: variant,
KEY_IDF_VERSION: idf or version,
},
)
CORE.loaded_integrations.add("psram")
@@ -74,17 +81,46 @@ def test_psram_dma_accepts_esp32s31(
assert config[CONF_PSRAM_DMA] is True
def test_psram_dma_rejects_arduino(
def test_psram_dma_accepts_arduino_on_idf_5_5_3(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The ESP-IDF underneath an Arduino build is what counts; 3.3.7 is the first on 5.5.3."""
_stage(
set_core_config,
PlatformFramework.ESP32_ARDUINO,
VARIANT_ESP32S3,
cv.Version(3, 3, 7),
idf=cv.Version(5, 5, 3),
)
config = _schema()({CONF_PSRAM_DMA: True})
assert config[CONF_PSRAM_DMA] is True
def test_psram_dma_rejects_other_platforms(
set_core_config: SetCoreConfigCallable,
) -> None:
"""No ESP32 state to read a version from: the platform check comes first."""
set_core_config(PlatformFramework.ESP8266_ARDUINO)
CORE.loaded_integrations.add("psram")
with pytest.raises(cv.Invalid, match="only available on"):
_schema()({CONF_PSRAM_DMA: True})
def test_psram_dma_rejects_older_arduino(
set_core_config: SetCoreConfigCallable,
) -> None:
_stage(
set_core_config,
PlatformFramework.ESP32_ARDUINO,
VARIANT_ESP32S3,
cv.Version(5, 5, 3),
cv.Version(3, 3, 6),
idf=cv.Version(5, 5, 2),
)
with pytest.raises(cv.Invalid, match="only available with framework"):
with pytest.raises(cv.Invalid, match="requires ESP-IDF 5.5.3"):
_schema()({CONF_PSRAM_DMA: True})
@@ -127,7 +163,7 @@ def test_psram_dma_rejects_older_idf(
cv.Version(5, 5, 2),
)
with pytest.raises(cv.Invalid, match="requires at least framework version 5.5.3"):
with pytest.raises(cv.Invalid, match="requires ESP-IDF 5.5.3"):
_schema()({CONF_PSRAM_DMA: True})
@@ -14,7 +14,7 @@ def test_constant_packets_share_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Equal constant packets share one table; lambdas stay templates."""
"""Equal constant packets share one table; a lambda is sent from its vector."""
main_cpp = generate_main(component_config_path("packet_tables.yaml"))
tables = dict(
@@ -26,8 +26,10 @@ def test_constant_packets_share_progmem_tables(
["{0xC5, 0x51, 0x78, 0x82}", "{0x68, 0x69}"]
)
shared = next(k for k, v in tables.items() if v == "{0xC5, 0x51, 0x78, 0x82}")
assert main_cpp.count(f"set_data_static({shared}, 4);") == 2
assert "set_data_template(" in main_cpp
assert main_cpp.count(f"->transmit_packet(::{shared}, 4);") == 2
assert (
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
)
assert "set_sx1261" not in main_cpp
@@ -14,7 +14,7 @@ def test_constant_packets_share_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Equal constant packets share one table; lambdas stay templates."""
"""Equal constant packets share one table; a lambda is sent from its vector."""
main_cpp = generate_main(component_config_path("packet_tables.yaml"))
tables = dict(
@@ -26,8 +26,10 @@ def test_constant_packets_share_progmem_tables(
["{0xC5, 0x51, 0x78, 0x82}", "{0x68, 0x69}"]
)
shared = next(k for k, v in tables.items() if v == "{0xC5, 0x51, 0x78, 0x82}")
assert main_cpp.count(f"set_data_static({shared}, 4);") == 2
assert "set_data_template(" in main_cpp
assert main_cpp.count(f"->transmit_packet(::{shared}, 4);") == 2
assert (
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
)
def test_packet_data_length_limit() -> None:
@@ -0,0 +1,34 @@
---
esphome:
name: test
on_boot:
then:
- uart.write: [0x01, 0x02, 0x03]
- uart.write: !lambda return {0x09};
- uart.write: ""
esp8266:
board: d1_mini
uart:
tx_pin: GPIO1
rx_pin: GPIO3
baud_rate: 9600
switch:
- platform: uart
id: shared_switch
name: Shared Switch
data: [0x01, 0x02, 0x03]
- platform: uart
id: on_off_switch
name: On Off Switch
data:
turn_on: "ON"
turn_off: [0x04]
button:
- platform: uart
id: shared_button
name: Shared Button
data: [0x01, 0x02, 0x03]
@@ -23,6 +23,19 @@ def test_constant_payloads_share_progmem_tables(
)
assert f"shared_switch->set_data_on({shared}, 3);" in main_cpp
assert f"shared_button->set_data({shared}, 3);" in main_cpp
assert f"set_data_static({shared}, 3);" in main_cpp
assert "set_data_static(nullptr, 0);" in main_cpp
assert "set_data_template(" in main_cpp
assert f"->write_array(::{shared}, 3);" in main_cpp
assert "->write_array(nullptr, 0);" in main_cpp
assert "->write_array(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
def test_esp8266_write_action_reads_flash_directly(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""On ESP8266 the write action hands the table to write_array_progmem instead of copying it."""
main_cpp = generate_main(component_config_path("payload_tables_esp8266.yaml"))
assert "->write_array_progmem(::uart_data, 3);" in main_cpp
assert "->write_array_progmem(nullptr, 0);" in main_cpp
assert "->write_array(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
assert "progmem_memcpy" not in main_cpp
+12 -5
View File
@@ -8,10 +8,17 @@ def test_write_payloads_share_a_progmem_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Identical constant payloads share one flash table; an empty one needs none."""
"""Identical constant payloads share one flash table and are copied out of it on
ESP8266 before sending; an empty one needs none; a lambda is sent from its vector."""
main_cpp = generate_main(component_config_path("udp_write.yaml"))
assert main_cpp.count("udp_data[] PROGMEM = {0xAA, 0x55, 0x01, 0x02};") == 1
assert main_cpp.count("->set_data_static(udp_data, 4);") == 2
assert "->set_data_static(nullptr, 0);" in main_cpp
assert "->set_data_template(" in main_cpp
table = "esphome__udp_write__bytes"
assert main_cpp.count(f"{table}[] PROGMEM = {{0xAA, 0x55, 0x01, 0x02}};") == 1
assert (
main_cpp.count(f"esphome::progmem_memcpy(esphome__bytes, ::{table}, 4);") == 2
)
assert main_cpp.count("->send_packet(esphome__bytes, 4);") == 2
assert "uint8_t esphome__bytes[1];" in main_cpp
assert "->send_packet(esphome__bytes, 0);" in main_cpp
assert "->send_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
assert main_cpp.count("->set_should_broadcast();") == 4
@@ -0,0 +1,38 @@
"""Tests for the per-type channel limit, which depends on the target."""
from pathlib import Path
import subprocess
import sys
import pytest
from esphome.components.esp32.const import KEY_VARIANT, VARIANT_ESP32, VARIANT_ESP32P4
from esphome.components.usb_uart import channel_schema, uart_types
import esphome.config_validation as cv
from esphome.const import CONF_BAUD_RATE, CONF_CHANNELS, PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
CH34X = next(t for t in uart_types if t.name == "CH34X")
def test_import_needs_no_core_config() -> None:
"""The C++ unit test runner imports every component before any config is loaded."""
subprocess.run(
[sys.executable, "-c", "from esphome.components import usb_uart"],
cwd=Path(__file__).parents[3],
check=True,
)
@pytest.mark.parametrize(
("variant", "limit"), [(VARIANT_ESP32, 3), (VARIANT_ESP32P4, 4)]
)
def test_channel_limit_follows_the_target(
set_core_config: SetCoreConfigCallable, variant: str, limit: int
) -> None:
set_core_config(PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: variant})
schema = channel_schema(CH34X)
channel = {CONF_BAUD_RATE: 9600}
assert len(schema({CONF_CHANNELS: [channel] * limit})[CONF_CHANNELS]) == limit
with pytest.raises(cv.Invalid, match=f"maximum of {limit} channels"):
schema({CONF_CHANNELS: [channel] * (limit + 1)})
+14 -4
View File
@@ -21,14 +21,24 @@ def _validate(lease_cache: dict) -> dict:
)
def test_storage_defaults_to_rtc(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
@pytest.mark.parametrize(
"framework", [PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO]
)
def test_storage_defaults_to_rtc(
set_core_config: SetCoreConfigCallable, framework: PlatformFramework
) -> None:
"""Both ESP32 frameworks share the backend the cache lives in."""
set_core_config(framework, platform_data={KEY_VARIANT: VARIANT_ESP32})
config = _validate({"max_age": "12h"})
assert config["lease_cache"]["storage"] == "rtc"
def test_lease_cache_requires_esp32(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP8266_ARDUINO)
with pytest.raises(cv.Invalid, match="only available on"):
_validate({"max_age": "12h"})
def test_no_rtc_variant_requires_flash(set_core_config: SetCoreConfigCallable) -> None:
"""A variant without RTC memory must say storage: flash; the error points at that key."""
set_core_config(
+2
View File
@@ -257,6 +257,8 @@ cover:
- logger.log: position_action
tilt_action:
- logger.log: tilt_action
stop_tilt_action:
- logger.log: stop_tilt_action
open_action:
- logger.log: open_action
close_action:
@@ -19,6 +19,11 @@ image:
} else {
ESP_LOGD("online_image", "Cache miss: fresh download");
}
on_download_started:
- logger.log: Download started
on_download_progress:
lambda: |-
ESP_LOGD("online_image", "Downloaded %.0f%% (%zu bytes)", progress, bytes_read);
- platform: online_image
id: online_binary_transparent_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
@@ -2,6 +2,7 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <memory>
@@ -27,6 +28,9 @@ class FakeContainer : public http_request::HttpContainer {
}
int read(uint8_t *buf, size_t max_len) override {
this->largest_read = std::max(this->largest_read, max_len);
if (this->read_error_)
return -1;
size_t len = std::min(max_len, this->available_ - this->bytes_read_);
std::memcpy(buf, this->body_.data() + this->bytes_read_, len);
this->bytes_read_ += len;
@@ -42,13 +46,21 @@ class FakeContainer : public http_request::HttpContainer {
}
void set_content_length(size_t length) { this->content_length = length; }
/// Make every read from now on fail, like a dropped connection
void fail_reads() { this->read_error_ = true; }
/// Let len more bytes of the body arrive
void deliver(size_t len) { this->available_ = std::min(this->body_.size(), this->available_ + len); }
void add_response_header(const std::string &name, const std::string &value) {
this->response_headers_.push_back({name, value});
}
/// The most bytes any one read() asked for
size_t largest_read{0};
protected:
std::vector<uint8_t> body_;
size_t available_;
bool read_error_{false};
};
class FakeHttpRequest : public http_request::HttpRequestComponent {
@@ -123,13 +135,173 @@ static constexpr std::array<uint8_t, 69> PNG_1X1{
0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9,
0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
// 8x8 progressive JPEG, 521 bytes
static const uint8_t JPEG_PROGRESSIVE[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10,
0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33,
0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67,
0x68, 0x67, 0x3E, 0x4D, 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11,
0x12, 0x12, 0x18, 0x15, 0x18, 0x2F, 0x1A, 0x1A, 0x2F, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC2, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0xC4, 0x00, 0x15, 0x01, 0x01, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xFF, 0xDA, 0x00, 0x0C, 0x03,
0x01, 0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01, 0x94, 0x05, 0x3F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08,
0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F,
0x01, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x01, 0x7F, 0xFF, 0xC4, 0x00,
0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
0x01, 0x00, 0x01, 0x3F, 0x21, 0x7F, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00,
0x10, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00,
0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
0x01, 0x00, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xD9,
};
// 48x40 4:2:0 JPEG (quality 95), 2344 bytes: three rows of 16 pixel MCUs, with the scan starting at byte 609
static const uint8_t JPEG_48X40[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02,
0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x02, 0x02, 0x05, 0x04, 0x04, 0x03, 0x04, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05,
0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x06, 0x07, 0x09, 0x07, 0x06, 0x06, 0x08, 0x0B, 0x08, 0x09, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x06, 0x08, 0x0B, 0x0C, 0x0B, 0x0A, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x05, 0x03, 0x03, 0x05, 0x0A, 0x07, 0x06, 0x07, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x28, 0x00, 0x30, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62,
0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A,
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85,
0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6,
0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00,
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09,
0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A,
0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xFC, 0xAA, 0xB8, 0xB7,
0xD3, 0xB5, 0xD8, 0x1E, 0xD6, 0xD1, 0xA5, 0x5B, 0x4B, 0x46, 0x10, 0xC1, 0x06, 0xA1, 0x29, 0x69, 0x25, 0xB7, 0xC2,
0xC6, 0x0C, 0x71, 0x26, 0x5E, 0x50, 0x15, 0x5D, 0xB6, 0x9C, 0x9E, 0x00, 0xC1, 0x67, 0x67, 0x4D, 0xCF, 0x84, 0xF7,
0x57, 0xA6, 0xDA, 0x08, 0x52, 0xF5, 0xE5, 0xBA, 0xB8, 0x7B, 0x5B, 0x68, 0xEE, 0x2F, 0x66, 0x89, 0xB7, 0xB7, 0x9B,
0xE5, 0x9C, 0x23, 0xAA, 0xFE, 0xF7, 0x68, 0x11, 0x9E, 0x5D, 0x18, 0x12, 0x48, 0x56, 0x5C, 0x9F, 0x57, 0xB6, 0xF8,
0x33, 0xF0, 0xE2, 0x5B, 0xFB, 0xCF, 0xF4, 0x51, 0x0C, 0xC6, 0x64, 0x30, 0x49, 0x71, 0x7C, 0xE2, 0x16, 0x90, 0x2E,
0xF4, 0x72, 0xEA, 0xE0, 0x34, 0x81, 0x77, 0x93, 0x9C, 0x07, 0xC2, 0x93, 0xB9, 0x61, 0xC0, 0xD3, 0xF0, 0x17, 0xC1,
0xBF, 0x03, 0xCD, 0xE1, 0xAD, 0x42, 0x3F, 0x0A, 0xD8, 0xC7, 0x2D, 0xAC, 0x36, 0xCB, 0xF6, 0x79, 0x22, 0x69, 0x84,
0xC4, 0x93, 0x29, 0x2A, 0x89, 0xB9, 0x00, 0x50, 0x62, 0x7C, 0xFF, 0x00, 0x16, 0xD9, 0x65, 0x00, 0x16, 0x65, 0x07,
0xFA, 0x5B, 0x36, 0xE2, 0xAC, 0xBF, 0x2D, 0x55, 0x79, 0x29, 0xB5, 0x26, 0x9B, 0xA9, 0x18, 0xB5, 0x15, 0x36, 0x9B,
0x53, 0xD1, 0xCA, 0x10, 0x93, 0x69, 0xB6, 0x94, 0x27, 0x16, 0xAF, 0x19, 0x29, 0xB8, 0xBB, 0x3F, 0x9A, 0xE1, 0x1E,
0x2A, 0xCB, 0x9E, 0x79, 0x4A, 0x92, 0x84, 0xDA, 0xD6, 0xED, 0xA4, 0xED, 0x7A, 0x72, 0xB5, 0xAC, 0xF6, 0xEB, 0xAD,
0xD5, 0x97, 0x91, 0xC9, 0x4D, 0xA4, 0xE9, 0xD7, 0x56, 0x45, 0x66, 0xD3, 0x9D, 0x85, 0xB1, 0x86, 0x59, 0x7E, 0xD9,
0x7E, 0xA0, 0xE0, 0xA8, 0x02, 0x17, 0x02, 0x3C, 0x36, 0x59, 0x18, 0xE2, 0x42, 0x1D, 0x40, 0x56, 0x0A, 0x77, 0x30,
0xAE, 0xFA, 0x56, 0xD4, 0xA4, 0x8A, 0xD9, 0xAD, 0xD6, 0x38, 0x1D, 0x7F, 0x77, 0x77, 0x1C, 0xB2, 0x4C, 0x22, 0x8C,
0x2B, 0xF3, 0x14, 0x64, 0x45, 0x85, 0x52, 0x47, 0x92, 0x7E, 0xF2, 0x0F, 0x34, 0x1D, 0xBC, 0x56, 0xDE, 0x85, 0xF0,
0x4B, 0xE1, 0x0A, 0xE9, 0xB0, 0xDB, 0x2D, 0x8C, 0x4C, 0xCD, 0x74, 0xF1, 0x42, 0x96, 0x93, 0xCF, 0xF6, 0x54, 0x0C,
0xB3, 0x44, 0x03, 0xAA, 0x6C, 0x49, 0x10, 0x87, 0x00, 0x88, 0xC2, 0xEE, 0x66, 0x6F, 0x9B, 0xAB, 0x57, 0xA4, 0x68,
0xDF, 0x0A, 0x3E, 0x1B, 0xDA, 0xE9, 0xDF, 0xD9, 0x96, 0xFE, 0x03, 0xB2, 0x8E, 0xD9, 0x8A, 0xDD, 0xC1, 0x77, 0x14,
0x97, 0x50, 0x2E, 0xE3, 0x20, 0x62, 0xA0, 0x33, 0x0C, 0xC6, 0x58, 0x48, 0x36, 0x6D, 0xC1, 0x20, 0xA2, 0x2B, 0x00,
0xB5, 0xF1, 0x99, 0xCF, 0x19, 0x65, 0x79, 0xAD, 0x0C, 0x3C, 0x2B, 0x61, 0xF9, 0x28, 0xD6, 0x52, 0x97, 0x35, 0x95,
0xD4, 0x61, 0x28, 0x5B, 0x99, 0xB8, 0xA9, 0xA9, 0x47, 0xD9, 0x3A, 0x69, 0xA9, 0xC6, 0x51, 0x52, 0x6A, 0x0D, 0x4A,
0x74, 0xA9, 0xC3, 0xFA, 0x7F, 0xC3, 0xFE, 0x25, 0xC1, 0x4E, 0xAE, 0x26, 0x2E, 0x13, 0x71, 0x5C, 0x8D, 0xFB, 0xB1,
0xD6, 0xEA, 0x6F, 0xF9, 0xAF, 0xBE, 0xB7, 0x4B, 0x55, 0x1D, 0x75, 0x4C, 0xF3, 0xDF, 0x05, 0xD9, 0xF8, 0x7B, 0xC3,
0xBA, 0xB5, 0xF1, 0xBC, 0x8D, 0x63, 0xDF, 0x1C, 0x32, 0x88, 0x1A, 0xF3, 0x74, 0x73, 0x89, 0x15, 0xC4, 0x8A, 0x25,
0x66, 0xDC, 0xAA, 0x98, 0x60, 0x06, 0x7F, 0xBD, 0xF2, 0x81, 0xF3, 0x36, 0xFE, 0x9D, 0xAF, 0x78, 0x8F, 0x54, 0xD3,
0xAD, 0xA6, 0x9E, 0xC5, 0xE5, 0x9C, 0x96, 0xDC, 0xB7, 0xE8, 0x84, 0x47, 0xFB, 0xB4, 0x54, 0x42, 0x0F, 0x03, 0xF8,
0xD7, 0xE5, 0x39, 0x60, 0xF8, 0x04, 0x79, 0x7B, 0xE3, 0xF4, 0x9F, 0x85, 0x5F, 0x08, 0xBE, 0x0D, 0x2C, 0x57, 0xCE,
0x6C, 0xA5, 0x2B, 0xA8, 0x08, 0x20, 0x82, 0x04, 0xB9, 0xB8, 0x82, 0x43, 0x28, 0x72, 0x55, 0xD5, 0x83, 0xB9, 0x05,
0xC2, 0x44, 0x55, 0x77, 0x8C, 0xFC, 0xEC, 0xD8, 0xCB, 0xE7, 0xBE, 0x9B, 0xE0, 0x5F, 0xC3, 0x9D, 0x32, 0xD4, 0x45,
0x26, 0x8B, 0x2C, 0x93, 0xC0, 0xF3, 0x95, 0x49, 0xEE, 0x2E, 0x0C, 0x26, 0x26, 0x31, 0xC9, 0xF3, 0xED, 0x95, 0x96,
0x32, 0xEC, 0x10, 0xE1, 0xB8, 0x60, 0x3A, 0x90, 0x1B, 0x3F, 0x9E, 0x66, 0x58, 0xFC, 0x14, 0x31, 0x15, 0xFD, 0x9C,
0x79, 0x57, 0x3A, 0x92, 0x84, 0x1C, 0x3D, 0xC9, 0xCD, 0x27, 0x6D, 0x12, 0xE5, 0xE7, 0xA6, 0xD5, 0x4D, 0x64, 0x93,
0xD1, 0x45, 0xC6, 0xAD, 0xE7, 0x2F, 0xE9, 0xEE, 0x17, 0xE3, 0x0C, 0x0A, 0xE1, 0x9C, 0x37, 0x34, 0x67, 0x2B, 0x73,
0x2B, 0xB4, 0xBA, 0x49, 0xF4, 0x72, 0x57, 0x51, 0xDD, 0x24, 0x9B, 0x5A, 0x36, 0x9A, 0x49, 0x1E, 0x15, 0xA7, 0x7E,
0xCE, 0xF6, 0x89, 0x6A, 0x96, 0x43, 0x55, 0xB5, 0xB3, 0xB6, 0xB8, 0x96, 0x2B, 0x9B, 0x78, 0xA0, 0xB4, 0x9E, 0xE5,
0x61, 0x90, 0xE0, 0xC2, 0xEC, 0x61, 0x91, 0x47, 0xCC, 0xDB, 0xF1, 0x80, 0xC0, 0x98, 0xD4, 0xE5, 0xDC, 0x64, 0xEC,
0x7C, 0x29, 0xFD, 0x98, 0x6C, 0xAF, 0xEC, 0xF5, 0x76, 0xB7, 0xF1, 0x36, 0x9A, 0x5D, 0x6F, 0x96, 0xE1, 0x74, 0xB5,
0xD3, 0x44, 0x91, 0xA8, 0x45, 0x0C, 0xE2, 0x3D, 0xAC, 0x85, 0xF9, 0x3B, 0x5B, 0x19, 0x8A, 0x43, 0x1F, 0xCA, 0x50,
0x9D, 0x87, 0xD2, 0x0F, 0xC2, 0x1F, 0x89, 0x4F, 0x6D, 0x04, 0x0F, 0xE1, 0x61, 0xAA, 0xEE, 0xB4, 0x8E, 0x56, 0xFB,
0x06, 0xAF, 0x04, 0xF3, 0x40, 0x9B, 0x55, 0x4E, 0xCB, 0x66, 0x91, 0xCB, 0x18, 0xC2, 0x88, 0xB6, 0xB1, 0x65, 0x60,
0xA4, 0x6E, 0xDC, 0xA4, 0x9E, 0xAF, 0xE1, 0xBF, 0xC2, 0x2F, 0x89, 0xA9, 0xE1, 0xFB, 0x8D, 0x6E, 0xC2, 0x3B, 0x4B,
0x30, 0xD7, 0x60, 0xDA, 0x41, 0x2D, 0xCC, 0x6C, 0x2E, 0x49, 0xF3, 0x1E, 0x42, 0x19, 0x5D, 0x58, 0x3C, 0x8A, 0x81,
0x82, 0xB8, 0x0C, 0xC9, 0xF2, 0xE5, 0x8F, 0x03, 0x8F, 0x36, 0xE3, 0x8A, 0x98, 0xCC, 0x14, 0xAB, 0x62, 0x2B, 0xC2,
0x34, 0xA2, 0xA5, 0x78, 0x37, 0x42, 0x12, 0x70, 0x8C, 0x62, 0xD2, 0x6D, 0x72, 0x7B, 0xAF, 0x9B, 0xDD, 0xFD, 0xE5,
0x4E, 0x79, 0x49, 0x45, 0x73, 0xA9, 0x72, 0x2F, 0xF0, 0x73, 0x83, 0xF8, 0xCE, 0xB4, 0x33, 0xE8, 0x3F, 0xAE, 0x42,
0xF7, 0x76, 0xB3, 0x83, 0x57, 0xE5, 0x95, 0xBE, 0xCE, 0x8F, 0x7B, 0xDE, 0xD7, 0x4B, 0x6B, 0x9C, 0x1E, 0x8D, 0xF0,
0x56, 0x7B, 0xDD, 0x26, 0x34, 0x5F, 0x1F, 0xDB, 0x9D, 0x37, 0x53, 0xB8, 0x96, 0xED, 0xA0, 0x16, 0xB1, 0xC9, 0x2C,
0x8B, 0xF2, 0x26, 0x58, 0x89, 0x37, 0xEE, 0x04, 0xB2, 0x97, 0x54, 0x2D, 0x88, 0xC0, 0x60, 0x0B, 0x03, 0x5E, 0x9B,
0x67, 0xFB, 0x36, 0xE9, 0x31, 0x2D, 0x9C, 0x37, 0x9E, 0x32, 0x84, 0x5D, 0x2C, 0x4A, 0xD6, 0xAD, 0x73, 0x63, 0x28,
0x36, 0xAA, 0x53, 0xF7, 0x90, 0x29, 0x8E, 0x5C, 0x92, 0xBE, 0x41, 0xDC, 0xC0, 0xB3, 0x2B, 0x46, 0x32, 0x4B, 0x96,
0x35, 0xAD, 0x1F, 0xC2, 0xDF, 0x89, 0x17, 0xF6, 0xB6, 0xFA, 0xB5, 0xED, 0xB8, 0xB7, 0x8E, 0x78, 0x66, 0x96, 0x09,
0x6C, 0x6E, 0x23, 0x32, 0x28, 0x08, 0xE8, 0xF2, 0xBA, 0xF9, 0x86, 0x48, 0xC0, 0x68, 0xFE, 0x63, 0xC8, 0x23, 0x82,
0xCC, 0xA5, 0x4A, 0xFA, 0x66, 0x8F, 0xF0, 0x37, 0xE2, 0x6D, 0xE5, 0xC9, 0x3A, 0x87, 0x86, 0xB4, 0xFB, 0x28, 0xAE,
0x61, 0x0F, 0x0B, 0x59, 0xDF, 0xC0, 0xD1, 0xAC, 0xE0, 0x24, 0x41, 0x42, 0xC4, 0x49, 0x1B, 0x10, 0x21, 0x50, 0xC0,
0x09, 0x11, 0x8B, 0xEE, 0x6E, 0x71, 0xE2, 0xE2, 0x78, 0xBB, 0x33, 0xFA, 0xC5, 0x1A, 0xB4, 0xEA, 0xC6, 0x74, 0xD2,
0x8C, 0x24, 0xA5, 0x2A, 0x3C, 0xB5, 0x2E, 0xB9, 0x9B, 0x71, 0x97, 0x34, 0xE0, 0xAA, 0x54, 0x93, 0x71, 0x56, 0x49,
0xB9, 0xC6, 0x4E, 0x9A, 0x9B, 0xD3, 0xFA, 0x7F, 0x81, 0xB8, 0xD6, 0xAC, 0xDD, 0x6F, 0x65, 0x8D, 0x82, 0x77, 0x85,
0x95, 0xE1, 0x7D, 0x1C, 0xD2, 0x8B, 0x7A, 0x27, 0xA2, 0xF4, 0xED, 0xEE, 0x9E, 0x67, 0xE0, 0x8F, 0x81, 0x37, 0xBA,
0xEF, 0x87, 0x67, 0xBF, 0x8F, 0xC4, 0xD2, 0xDD, 0x4F, 0x05, 0xA8, 0x61, 0x7B, 0x1E, 0x9E, 0xE1, 0x8C, 0xEA, 0xF3,
0x18, 0x99, 0xA3, 0xDE, 0x76, 0x66, 0x36, 0x2A, 0x30, 0xFB, 0x9B, 0x0F, 0xC3, 0x1C, 0xA3, 0x76, 0x4B, 0xFB, 0x3B,
0xE8, 0xBA, 0x66, 0xBF, 0x0E, 0x93, 0xFD, 0xAB, 0x23, 0xD9, 0xA5, 0xCE, 0x62, 0xF3, 0x34, 0xEB, 0x7F, 0x29, 0x0A,
0xA7, 0x04, 0x48, 0xF2, 0x79, 0x82, 0x42, 0x1D, 0x46, 0x4B, 0xA9, 0x1B, 0xD1, 0xBE, 0x52, 0x54, 0xD7, 0xA8, 0xF8,
0x33, 0xE1, 0xAF, 0xC4, 0xA8, 0x24, 0xD6, 0x6D, 0xED, 0xBC, 0x0A, 0xD3, 0x5A, 0x5C, 0xC4, 0xDF, 0x64, 0x17, 0x97,
0x36, 0xF0, 0x65, 0x52, 0x39, 0xD4, 0x23, 0x22, 0x9D, 0xD8, 0x64, 0x83, 0x04, 0x85, 0x38, 0xEF, 0xB8, 0x49, 0x91,
0xBB, 0xFF, 0x00, 0x0A, 0xDF, 0xE3, 0x77, 0x84, 0xAC, 0xD3, 0x4A, 0xBC, 0xF0, 0x3A, 0x5A, 0x5B, 0x4F, 0x72, 0x5E,
0xD7, 0x53, 0x8E, 0xE2, 0x12, 0x6D, 0xC6, 0x40, 0x20, 0x47, 0xE6, 0x3E, 0x71, 0x26, 0x70, 0x24, 0xE4, 0xB4, 0x63,
0x2A, 0xC0, 0xE0, 0x7E, 0x71, 0x98, 0x71, 0x35, 0x5C, 0x4E, 0x69, 0x5A, 0xBD, 0x7C, 0x7A, 0x9C, 0xE3, 0xC9, 0xCF,
0x38, 0x46, 0x8D, 0xA4, 0xDC, 0x63, 0x38, 0x4A, 0xE9, 0xED, 0x5A, 0x4E, 0x4A, 0x6D, 0xF3, 0xCA, 0x2D, 0xB9, 0x4D,
0xD3, 0x77, 0x8C, 0x7F, 0xA8, 0xF8, 0x53, 0x8D, 0x31, 0x32, 0xC8, 0xA9, 0xDB, 0x15, 0x04, 0x9F, 0xB4, 0xD2, 0xF4,
0xD3, 0xF8, 0xA5, 0x77, 0xCD, 0xB3, 0x49, 0x6C, 0xB4, 0xEA, 0x97, 0x46, 0x78, 0x45, 0xC4, 0x16, 0x5A, 0xDD, 0xA5,
0xA6, 0x8D, 0xA7, 0xEA, 0x77, 0x4A, 0x54, 0x47, 0x3C, 0x31, 0x41, 0x20, 0x06, 0x68, 0x00, 0x9A, 0x4F, 0x2A, 0x40,
0x5D, 0x5F, 0x69, 0xF3, 0x15, 0xCB, 0x32, 0x96, 0xDC, 0xC0, 0x90, 0xA5, 0xF0, 0xBD, 0xAF, 0xC3, 0x96, 0x83, 0x51,
0xB8, 0xD6, 0xA7, 0x48, 0xD2, 0xE8, 0x2B, 0x2C, 0x2D, 0x6B, 0xF6, 0xA4, 0x55, 0x8E, 0x45, 0x5F, 0xBA, 0x54, 0x2A,
0x36, 0x77, 0x12, 0x72, 0xE5, 0x9C, 0x14, 0x3B, 0x63, 0x6C, 0x62, 0x8A, 0x2B, 0xDE, 0xCD, 0x70, 0x74, 0xA7, 0x92,
0xE2, 0x31, 0x35, 0x2D, 0x29, 0x54, 0xBB, 0x97, 0xBB, 0x18, 0xF3, 0x38, 0x52, 0xA5, 0x38, 0xDF, 0x92, 0x31, 0x7E,
0xEB, 0x9B, 0x6A, 0xCD, 0x6B, 0xEF, 0x7C, 0x6E, 0x52, 0x7F, 0xE1, 0xF7, 0x0E, 0x54, 0x96, 0x1B, 0x3D, 0x52, 0x86,
0xF0, 0x73, 0x4B, 0xEE, 0x92, 0xD6, 0xD6, 0xBF, 0xC2, 0x8D, 0x9F, 0x0B, 0xDE, 0xEB, 0xF3, 0x69, 0x6B, 0x06, 0x9B,
0xA9, 0x5B, 0xE9, 0xF6, 0x6F, 0x76, 0x1E, 0x45, 0x8E, 0xE1, 0x54, 0xC8, 0xCD, 0xF3, 0x28, 0x8B, 0x1C, 0x9E, 0x25,
0x55, 0xDA, 0xA5, 0x58, 0x80, 0xCA, 0x46, 0x42, 0xA8, 0xF4, 0x1B, 0xEB, 0xBB, 0xBB, 0x8B, 0x69, 0xAF, 0xA5, 0x8E,
0xD0, 0x49, 0x25, 0x9C, 0x37, 0x73, 0xE9, 0xD7, 0xF2, 0x14, 0x8E, 0x55, 0x91, 0x64, 0x90, 0x46, 0xA8, 0xF9, 0x50,
0x57, 0x73, 0x2E, 0xE0, 0x55, 0xF6, 0x87, 0x45, 0x20, 0x2E, 0xC7, 0x28, 0xAF, 0xCD, 0x78, 0xA6, 0xA5, 0x5C, 0x26,
0x33, 0x01, 0x35, 0x2E, 0x67, 0x52, 0x35, 0xA0, 0xEE, 0x93, 0x56, 0x70, 0x4F, 0x48, 0xDB, 0x95, 0x6B, 0x76, 0xF4,
0xB4, 0xA5, 0x27, 0x29, 0x29, 0x35, 0x16, 0xBF, 0xA8, 0x7C, 0x3E, 0x92, 0xF6, 0x98, 0xA8, 0x5B, 0x4B, 0xC5, 0xDB,
0x5E, 0x8E, 0x4F, 0x7D, 0xFA, 0xDB, 0x57, 0xB2, 0x56, 0xD6, 0xED, 0xF7, 0x3E, 0x10, 0xB2, 0x82, 0xF6, 0xC2, 0xF2,
0xF3, 0x4E, 0xD4, 0xAF, 0xEE, 0x5A, 0xCA, 0xCE, 0xD2, 0xF1, 0xA7, 0xB8, 0x9D, 0xD4, 0x42, 0x8C, 0xF2, 0x3C, 0x8E,
0x42, 0x02, 0x24, 0xFD, 0xDA, 0x96, 0x60, 0xAD, 0xB0, 0xC9, 0x0E, 0x00, 0xC8, 0xC8, 0xEA, 0xEE, 0x12, 0x7B, 0x09,
0x6C, 0x6E, 0xCE, 0xA2, 0x0B, 0xAA, 0x32, 0xD9, 0xB9, 0x49, 0x66, 0xDF, 0x74, 0xCC, 0x31, 0x1B, 0xE4, 0xE6, 0x63,
0xF2, 0x91, 0x95, 0x42, 0x15, 0x8B, 0x7D, 0xE0, 0xAC, 0xB4, 0x51, 0x5F, 0x33, 0x3A, 0xD8, 0x8C, 0x46, 0x7A, 0x9C,
0xA6, 0xD7, 0x2C, 0x13, 0x49, 0x25, 0x6E, 0x6A, 0x73, 0xE4, 0x84, 0x9E, 0x9A, 0xC9, 0x29, 0x24, 0x9E, 0xE9, 0x42,
0x11, 0x8D, 0xA2, 0x9C, 0x5F, 0xF5, 0x37, 0x0D, 0x36, 0xF8, 0x7E, 0x9A, 0xEC, 0xE5, 0xE5, 0x7E, 0x67, 0x77, 0x7B,
0x7A, 0xBF, 0x5E, 0xB7, 0x3F, 0xFF, 0xD9,
};
static void set_loop_time(uint32_t now) { LoopBlockingGuard clock(nullptr, LOG_STR("test"), now); }
class OnlineImageDownloadEnd : public ::testing::Test {
protected:
std::unique_ptr<OnlineImage> make_image_(runtime_image::ImageFormat format) {
std::unique_ptr<OnlineImage> make_image_(runtime_image::ImageFormat format, uint32_t buffer_size = 256) {
auto img = std::make_unique<OnlineImage>("http://example.com/image", 0, 0, format, image::IMAGE_TYPE_RGB,
image::TRANSPARENCY_OPAQUE, nullptr, 256);
image::TRANSPARENCY_OPAQUE, nullptr, buffer_size);
img->set_parent(&this->http_);
return img;
}
@@ -286,4 +458,294 @@ TEST_F(OnlineImageDownloadEnd, JpegBufferAllocationFailureReportsError) {
EXPECT_EQ(this->finished_, 0);
}
// The JPEG decoded with all of it in memory at once, to compare the downloaded images with
static std::vector<uint8_t> reference_jpeg_pixels() {
runtime_image::RuntimeImage ref(runtime_image::JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
std::vector<uint8_t> data(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
EXPECT_TRUE(ref.begin_decode(data.size()));
EXPECT_EQ(ref.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
EXPECT_TRUE(ref.end_decode());
return std::vector<uint8_t>(ref.get_data_start(), ref.get_data_start() + ref.get_width_stride() * ref.get_height());
}
static std::vector<uint8_t> image_pixels(OnlineImage &image) {
return std::vector<uint8_t>(image.get_data_start(),
image.get_data_start() + image.get_width_stride() * image.get_height());
}
// A JPEG of known size is decoded as it arrives, so a buffer smaller than the file only has to hold the headers
TEST_F(OnlineImageDownloadEnd, JpegDecodesThroughASmallBuffer) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
// Bytes after the image are never needed. Waiting for the whole file before decoding would read them all.
std::vector<uint8_t> body(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
body.resize(body.size() + 8192, 0);
auto container = std::make_shared<FakeContainer>(body, body.size(), false);
container->add_response_header("etag", "\"abc\"");
this->start_(*image, container);
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
image->loop();
ASSERT_EQ(this->finished_, 1);
EXPECT_LT(container->get_bytes_read(), body.size()) << "the image was not decoded as it arrived";
EXPECT_EQ(this->errors_, 0);
ASSERT_EQ(image->get_width(), 48);
ASSERT_EQ(image->get_height(), 40);
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
// A complete image is cached
this->http_.next = std::make_shared<FakeContainer>(std::vector<uint8_t>{}, 0, false);
image->update();
EXPECT_TRUE(this->http_.sent_header("if-none-match"));
}
// A read() waits for every byte it asks for, so a large buffer is filled a little at a time rather than in one
// read that blocks the loop until the whole buffer has arrived
TEST_F(OnlineImageDownloadEnd, JpegReadsInSmallPiecesWithALargeBuffer) {
auto image = this->make_image_(runtime_image::JPEG, 65536);
auto container =
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 0, false);
this->start_(*image, container);
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
container->deliver(sizeof(JPEG_48X40));
image->loop();
}
ASSERT_EQ(this->finished_, 1);
EXPECT_EQ(this->errors_, 0);
EXPECT_LE(container->largest_read, READ_CHUNK);
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
}
// A downloaded PNG is read a little at a time too
TEST_F(OnlineImageDownloadEnd, PngReadsInSmallPiecesWithALargeBuffer) {
auto image = this->make_image_(runtime_image::PNG, 65536);
std::vector<uint8_t> png(PNG_1X1.begin(), PNG_1X1.end());
auto container = std::make_shared<FakeContainer>(png, png.size(), false);
this->start_(*image, container);
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
image->loop();
ASSERT_EQ(this->finished_, 1);
EXPECT_EQ(this->errors_, 0);
EXPECT_LE(container->largest_read, READ_CHUNK);
}
// A slow link: the decoder waits part way through the image for the rest to arrive
TEST_F(OnlineImageDownloadEnd, JpegWaitsForASlowDownload) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
auto container =
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1024, false);
this->start_(*image, container);
int passes = 0;
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++) {
image->loop();
if (passes == 5) {
EXPECT_EQ(this->finished_, 0) << "the image cannot be finished before the rest of it arrives";
}
container->deliver(40);
}
ASSERT_EQ(this->finished_, 1);
EXPECT_EQ(this->errors_, 0);
EXPECT_GT(passes, 10);
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
}
// A download that stops part way through times out rather than waiting for ever
TEST_F(OnlineImageDownloadEnd, JpegDownloadThatStopsTimesOut) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
auto container =
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1500, false);
set_loop_time(1000);
this->start_(*image, container);
for (int i = 0; i < 10; i++)
image->loop();
EXPECT_EQ(this->errors_, 0) << "still within the timeout";
set_loop_time(1000 + this->http_.get_timeout());
image->loop();
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->finished_, 0);
}
// Headers that do not fit the download buffer cannot be read as the image arrives, so the whole image is
// downloaded first, keeping the bytes already read
TEST_F(OnlineImageDownloadEnd, JpegHeadersLargerThanTheBufferDownloadTheWholeImage) {
auto image = this->make_image_(runtime_image::JPEG, 256);
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
sizeof(JPEG_48X40), false);
this->start_(*image, container);
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
image->loop();
container->deliver(256);
}
ASSERT_EQ(this->finished_, 1);
EXPECT_EQ(this->errors_, 0);
EXPECT_EQ(container->get_bytes_read(), sizeof(JPEG_48X40));
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
}
// The pass that opens the image does not decode any of it
TEST_F(OnlineImageDownloadEnd, JpegOpensInAPassOfItsOwn) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
sizeof(JPEG_48X40), false);
this->start_(*image, container);
image->loop();
EXPECT_EQ(this->finished_ + this->errors_, 0);
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
image->loop();
EXPECT_EQ(this->finished_, 1);
}
// A progressive JPEG is refused while it downloads, without blaming the buffer size
TEST_F(OnlineImageDownloadEnd, JpegProgressiveReportsError) {
auto image = this->make_image_(runtime_image::JPEG, 256);
std::vector<uint8_t> body(JPEG_PROGRESSIVE, JPEG_PROGRESSIVE + sizeof(JPEG_PROGRESSIVE));
auto container = std::make_shared<FakeContainer>(body, 256, false);
this->start_(*image, container);
for (int i = 0; i < 10 && this->finished_ + this->errors_ == 0; i++)
image->loop();
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->finished_, 0);
}
// on_download_started fires once the server has accepted the request, before the first progress report, and not
// for a response that has no image to download
TEST_F(OnlineImageDownloadEnd, StartedFiresOnlyWhenTheDownloadBegins) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
std::vector<std::string> events;
image->add_on_download_started_callback([&events]() { events.emplace_back("started"); });
image->add_on_progress_callback([&events](float, size_t) { events.emplace_back("progress"); });
this->start_(*image,
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
sizeof(JPEG_48X40), false));
ASSERT_GE(events.size(), 2u);
EXPECT_EQ(events[0], "started");
EXPECT_EQ(events[1], "progress");
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
image->loop();
EXPECT_EQ(std::count(events.begin(), events.end(), std::string("started")), 1);
for (int status : {304, 404}) {
events.clear();
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>{}, 0, false);
container->status_code = status;
this->http_.next = container;
image->update();
EXPECT_TRUE(events.empty()) << "HTTP " << status;
}
}
// Progress starts at 0, is reported at most every 100 ms, and always reaches 100%
TEST_F(OnlineImageDownloadEnd, ProgressIsReportedAsTheImageDownloads) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
std::vector<float> reports;
image->add_on_progress_callback([&reports](float progress, size_t) { reports.push_back(progress); });
auto container =
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 100, false);
uint32_t now = 1000;
set_loop_time(now);
this->start_(*image, container);
ASSERT_EQ(reports.size(), 1u);
EXPECT_EQ(reports[0], 0.0f);
int passes = 0;
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++) {
now += 50;
set_loop_time(now);
container->deliver(100);
image->loop();
}
ASSERT_EQ(this->finished_, 1);
EXPECT_EQ(reports.back(), 100.0f);
EXPECT_TRUE(std::is_sorted(reports.begin(), reports.end()));
// A report every 100 ms of a 50 ms loop, plus the start and the end
EXPECT_LE(reports.size(), static_cast<size_t>(passes / 2 + 2));
EXPECT_GT(reports.size(), 3u);
}
// Without a Content-Length the percentage is unknown, but the bytes read are still reported
TEST_F(OnlineImageDownloadEnd, ProgressOfAChunkedDownloadHasNoPercentage) {
auto image = this->make_image_(runtime_image::PNG);
std::vector<std::pair<float, size_t>> reports;
image->add_on_progress_callback(
[&reports](float progress, size_t bytes_read) { reports.emplace_back(progress, bytes_read); });
auto container =
std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_1X1.begin(), PNG_1X1.end()), PNG_1X1.size(), true);
set_loop_time(1000);
this->start_(*image, container);
set_loop_time(2000);
for (int i = 0; i < 10 && this->finished_ + this->errors_ == 0; i++)
image->loop();
ASSERT_GE(reports.size(), 2u);
EXPECT_TRUE(std::isnan(reports.back().first));
EXPECT_EQ(reports.back().second, PNG_1X1.size());
}
// Scan data that cannot be decoded fails the download part way through
TEST_F(OnlineImageDownloadEnd, JpegDecodeErrorFailsTheDownload) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
std::vector<uint8_t> body(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
std::fill(body.begin() + 700, body.end() - 2, 0xFE);
auto container = std::make_shared<FakeContainer>(body, 1024, false);
this->start_(*image, container);
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
image->loop();
container->deliver(256);
}
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->finished_, 0);
}
// A read that fails part way through the download ends it with an error
TEST_F(OnlineImageDownloadEnd, JpegReadErrorFailsTheDownload) {
auto image = this->make_image_(runtime_image::JPEG, 1024);
auto container =
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1024, false);
this->start_(*image, container);
for (int i = 0; i < 5; i++)
image->loop();
ASSERT_EQ(this->errors_, 0);
container->fail_reads();
image->loop();
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->finished_, 0);
}
TEST(DownloadBufferGrow, KeepsTheUnreadData) {
DownloadBuffer buffer(16);
std::memcpy(buffer.append(), "abcdef", 6);
buffer.write(6);
buffer.read(2);
ASSERT_TRUE(buffer.grow(64));
EXPECT_EQ(buffer.size(), 64u);
ASSERT_EQ(buffer.unread(), 4u);
EXPECT_EQ(std::memcmp(buffer.data(), "cdef", 4), 0);
// A failed allocation leaves the buffer as it was
EXPECT_FALSE(buffer.grow(SIZE_MAX / 2));
EXPECT_EQ(buffer.size(), 64u);
EXPECT_EQ(std::memcmp(buffer.data(), "cdef", 4), 0);
// Growing to a smaller size changes nothing
EXPECT_TRUE(buffer.grow(8));
EXPECT_EQ(buffer.size(), 64u);
}
} // namespace esphome::online_image::testing
@@ -0,0 +1,36 @@
# voice_assistant over Thread without the IPv4 stack
packages:
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-c6-idf.yaml
openthread:
channel: 13
network_name: OpenThread-8f28
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
pan_id: 0x8f28
ext_pan_id: 0xd63e8e3e495ebbc3
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
mesh_local_prefix: fd53:145f:ed22:ad81::/64
network:
enable_ipv6: true
enable_ipv4: false
api:
microphone:
- platform: i2s_audio
id: va_mic
i2s_audio_id: i2s_audio_bus
i2s_din_pin: GPIO7
adc_type: external
speaker:
- platform: i2s_audio
id: va_speaker
i2s_audio_id: i2s_audio_bus
dac_type: external
i2s_dout_pin: GPIO10
voice_assistant:
microphone: va_mic
speaker: va_speaker
@@ -11,3 +11,6 @@ openthread:
output_power: 1dBm
core_options:
OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT: "5"
ota:
- platform: esphome
@@ -4,9 +4,11 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <utility>
#include <vector>
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/components/runtime_image/jpeg_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
namespace esphome::runtime_image::testing {
@@ -362,6 +364,23 @@ static std::vector<uint8_t> pixel_bytes(TestableRuntimeImage &img) {
return std::vector<uint8_t>(start, start + img.get_width_stride() * img.get_height());
}
// Shrinking a PNG keeps the first source pixel of each span, as the JPEG path does: the even rows and columns
TEST(RuntimeImageDecoder, ShrunkPngKeepsTheFirstPixelOfEachSpan) {
TestableRuntimeImage img(PNG, image::IMAGE_TYPE_RGB, false, 2, 2);
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB), PNG));
ASSERT_EQ(img.get_width(), 2);
ASSERT_EQ(img.get_height(), 2);
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 2; col++) {
const Color pixel = img.get_pixel(col, row);
const uint8_t *expected = PNG_RGB_EXPECTED[row * 2][col * 2];
EXPECT_EQ(pixel.r, expected[0]) << col << "," << row;
EXPECT_EQ(pixel.g, expected[1]) << col << "," << row;
EXPECT_EQ(pixel.b, expected[2]) << col << "," << row;
}
}
}
TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
TestableRuntimeImage img(JPEG);
@@ -377,6 +396,12 @@ TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
EXPECT_EQ(pixel_bytes(img), first_pixels) << "reused decoder must reproduce identical pixels";
}
// The RGB565 word at (x, y) of a decoded image, in its own byte order
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
return p[0] | (p[1] << 8);
}
// JPEGDEC's colour conversion differs by one step between its SIMD and scalar builds, so each
// channel is checked within one step; the byte order is still exact
static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *order, size_t pixel) {
@@ -386,16 +411,12 @@ static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *o
}
TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
// Halving the 8x8 gradient keeps its even rows and columns. The words are JPEGDEC's output for those
// pixels, red in the top five bits: a decode of the fixture with PIL gives (51, 7, 30) for source
// pixel (2, 0), which is word 0x3803 here.
static const uint16_t EXPECTED[4 * 4] = {
0x0000, 0x3803, 0x8008, 0xB80B, 0x0203, 0x3A07, 0x81EC, 0xBA0F,
0x03E7, 0x3C0B, 0x83F0, 0xBC13, 0x05EC, 0x3E0F, 0x85F4, 0xBE17,
};
// Halving the 8x8 gradient makes JPEGDEC decode it at half size, which averages each 2x2 block of pixels
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
TestableRuntimeImage rgb565_little_endian(JPEG, image::IMAGE_TYPE_RGB565, false, 4, 4);
TestableRuntimeImage rgb565_big_endian(JPEG, image::IMAGE_TYPE_RGB565, true, 4, 4);
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_TRUE(decode_all(rgb565_little_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_TRUE(decode_all(rgb565_big_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_EQ(rgb565_little_endian.get_width(), 4);
@@ -405,9 +426,53 @@ TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
const uint8_t *little = rgb565_little_endian.get_data_start();
const uint8_t *big = rgb565_big_endian.get_data_start();
for (size_t i = 0; i < 4 * 4; i++) {
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), EXPECTED[i], "little-endian", i);
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], EXPECTED[i], "big-endian", i);
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
int red = 0;
int green = 0;
int blue = 0;
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
const uint16_t word = rgb565_word(full, col * 2 + dx, row * 2 + dy);
red += word >> 11;
green += (word >> 5) & 0x3F;
blue += word & 0x1F;
}
}
const uint16_t expected = ((red + 2) / 4) << 11 | ((green + 2) / 4) << 5 | (blue + 2) / 4;
const size_t i = row * 4 + col;
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), expected, "little-endian", i);
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], expected, "big-endian", i);
}
}
}
// The generic draw() path gets JPEGDEC's half size RGB8888 output, so it too averages each 2x2 block of pixels
TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB, false, 8, 8);
TestableRuntimeImage half(JPEG, image::IMAGE_TYPE_RGB, false, 4, 4);
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_TRUE(decode_all(half, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_EQ(static_cast<JpegDecoder *>(half.decoder())->get_scale(), 2);
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
int red = 0;
int green = 0;
int blue = 0;
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
const Color pixel = full.get_pixel(col * 2 + dx, row * 2 + dy);
red += pixel.r;
green += pixel.g;
blue += pixel.b;
}
}
const Color pixel = half.get_pixel(col, row);
EXPECT_NEAR(pixel.r, (red + 2) / 4, 8) << "pixel " << col << "," << row << " red";
EXPECT_NEAR(pixel.g, (green + 2) / 4, 8) << "pixel " << col << "," << row << " green";
EXPECT_NEAR(pixel.b, (blue + 2) / 4, 8) << "pixel " << col << "," << row << " blue";
}
}
}
@@ -415,10 +480,15 @@ TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
// The RGB565 word at (x, y) of a decoded image, in its own byte order
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
return p[0] | (p[1] << 8);
// JPEGDEC decodes at the smallest of full, 1/2, 1/4 and 1/8 size that still covers the buffer
TEST(RuntimeImageDecoder, JpegDecodesAtTheSmallestSizeThatCoversTheBuffer) {
for (auto [size, scale] :
{std::pair{8, 1}, std::pair{5, 1}, std::pair{4, 2}, std::pair{3, 2}, std::pair{2, 4}, std::pair{1, 8}}) {
TestableRuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
EXPECT_EQ(img.get_width(), size);
EXPECT_EQ(static_cast<JpegDecoder *>(img.decoder())->get_scale(), scale) << size << "x" << size;
}
}
// Every scaled direct decode must equal the unscaled one sampled through the boundary table,
@@ -427,7 +497,8 @@ TEST(RuntimeImageDecoder, JpegDirectRgb565ScalesBySourcePixelSpans) {
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
for (int size : {4, 6, 16}) {
// Sizes JPEGDEC cannot reach by scaling, so the full size decode is spread over the buffer
for (int size : {5, 6, 16}) {
TestableRuntimeImage scaled(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
ASSERT_TRUE(decode_all(scaled, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_EQ(scaled.get_width(), size);
@@ -0,0 +1,83 @@
#include <gtest/gtest.h>
#include <cstdlib>
#include <vector>
#include "esphome/components/runtime_image/runtime_image.h"
#ifdef USE_RUNTIME_IMAGE_JPEG
namespace esphome::runtime_image::testing {
// 16x8 grayscale JPEG (quality 95) of a smooth gradient, from 60 at the top left to 185 at the bottom right
static const uint8_t JPEG_GRAY[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02,
0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x02, 0x02, 0x05, 0x04, 0x04, 0x03, 0x04, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05,
0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x06, 0x07, 0x09, 0x07, 0x06, 0x06, 0x08, 0x0B, 0x08, 0x09, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x06, 0x08, 0x0B, 0x0C, 0x0B, 0x0A, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00,
0x08, 0x00, 0x10, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04,
0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61,
0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33,
0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36,
0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84,
0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5,
0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6,
0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6,
0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01,
0x01, 0x00, 0x00, 0x3F, 0x00, 0xF0, 0x9F, 0x82, 0x5F, 0x09, 0x3F, 0xD4, 0xFF, 0x00, 0xA2, 0xFA, 0x76, 0xAF, 0xB0,
0x3E, 0x09, 0x7C, 0x24, 0xFF, 0x00, 0x53, 0xFE, 0x8B, 0xE9, 0xDA, 0xBF, 0xFF, 0xD9,
};
// The pixels Pillow decodes from it
static const uint8_t JPEG_GRAY_PIXELS[8 * 16] = {
60, 66, 72, 78, 84, 90, 96, 102, 108, 114, 120, 126, 132, 138, 144, 150, 65, 71, 77, 83, 89, 95,
101, 107, 113, 119, 125, 131, 137, 143, 149, 155, 70, 76, 82, 88, 94, 100, 106, 112, 118, 124, 130, 136,
142, 148, 154, 160, 75, 81, 87, 93, 99, 105, 111, 117, 123, 129, 135, 141, 147, 153, 159, 165, 80, 86,
92, 98, 104, 110, 116, 122, 128, 134, 140, 146, 152, 158, 164, 170, 85, 91, 97, 103, 109, 115, 121, 127,
133, 139, 145, 151, 157, 163, 169, 175, 90, 96, 102, 108, 114, 120, 126, 132, 138, 144, 150, 156, 162, 168,
174, 180, 95, 101, 107, 113, 119, 125, 131, 137, 143, 149, 155, 161, 167, 173, 179, 185,
};
// JPEGDEC's faster inverse DCT is up to 4 steps from Pillow's on this image; the garbled output was off by hundreds
static constexpr int TOLERANCE = 5;
static bool decode(RuntimeImage &img) {
std::vector<uint8_t> data(JPEG_GRAY, JPEG_GRAY + sizeof(JPEG_GRAY));
if (!img.begin_decode(data.size()))
return false;
if (img.feed_data(data.data(), data.size()) != static_cast<int>(data.size()))
return false;
return img.end_decode();
}
// A grayscale JPEG decoded for a colour image goes through JPEGDEC's RGB8888 output, which used to be garbled
TEST(JpegGrayscale, DecodesToRgb) {
RuntimeImage img(JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_ALPHA_CHANNEL);
ASSERT_TRUE(decode(img));
ASSERT_EQ(img.get_width(), 16);
ASSERT_EQ(img.get_height(), 8);
const uint8_t *data = img.get_data_start();
for (int i = 0; i < 16 * 8; i++) {
const uint8_t *pixel = data + i * 4; // blue, green, red, alpha
EXPECT_NEAR(pixel[0], JPEG_GRAY_PIXELS[i], TOLERANCE) << "blue of pixel " << i;
EXPECT_NEAR(pixel[1], JPEG_GRAY_PIXELS[i], TOLERANCE) << "green of pixel " << i;
EXPECT_NEAR(pixel[2], JPEG_GRAY_PIXELS[i], TOLERANCE) << "red of pixel " << i;
EXPECT_EQ(pixel[3], 255) << "alpha of pixel " << i;
}
}
TEST(JpegGrayscale, DecodesToGrayscale) {
RuntimeImage img(JPEG, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE);
ASSERT_TRUE(decode(img));
const uint8_t *data = img.get_data_start();
for (int i = 0; i < 16 * 8; i++)
EXPECT_NEAR(data[i], JPEG_GRAY_PIXELS[i], TOLERANCE) << "pixel " << i;
}
} // namespace esphome::runtime_image::testing
#endif // USE_RUNTIME_IMAGE_JPEG
@@ -0,0 +1,453 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstring>
#include <vector>
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/components/runtime_image/jpeg_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
#ifdef USE_RUNTIME_IMAGE_JPEG
namespace esphome::runtime_image::testing {
// 48x40 4:2:0 JPEG of a noisy gradient (quality 95): three rows of 16 pixel MCUs, so a stepped decode takes
// three steps. At 2344 bytes it is larger than JPEGDEC's 2048 byte read buffer, so the source is read again
// part way through the image.
static const uint8_t JPEG_ROWS[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02,
0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x02, 0x02, 0x05, 0x04, 0x04, 0x03, 0x04, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05,
0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x06, 0x07, 0x09, 0x07, 0x06, 0x06, 0x08, 0x0B, 0x08, 0x09, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x06, 0x08, 0x0B, 0x0C, 0x0B, 0x0A, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x05, 0x03, 0x03, 0x05, 0x0A, 0x07, 0x06, 0x07, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x28, 0x00, 0x30, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62,
0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A,
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85,
0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6,
0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00,
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09,
0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A,
0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xFC, 0xAA, 0xB8, 0xB7,
0xD3, 0xB5, 0xD8, 0x1E, 0xD6, 0xD1, 0xA5, 0x5B, 0x4B, 0x46, 0x10, 0xC1, 0x06, 0xA1, 0x29, 0x69, 0x25, 0xB7, 0xC2,
0xC6, 0x0C, 0x71, 0x26, 0x5E, 0x50, 0x15, 0x5D, 0xB6, 0x9C, 0x9E, 0x00, 0xC1, 0x67, 0x67, 0x4D, 0xCF, 0x84, 0xF7,
0x57, 0xA6, 0xDA, 0x08, 0x52, 0xF5, 0xE5, 0xBA, 0xB8, 0x7B, 0x5B, 0x68, 0xEE, 0x2F, 0x66, 0x89, 0xB7, 0xB7, 0x9B,
0xE5, 0x9C, 0x23, 0xAA, 0xFE, 0xF7, 0x68, 0x11, 0x9E, 0x5D, 0x18, 0x12, 0x48, 0x56, 0x5C, 0x9F, 0x57, 0xB6, 0xF8,
0x33, 0xF0, 0xE2, 0x5B, 0xFB, 0xCF, 0xF4, 0x51, 0x0C, 0xC6, 0x64, 0x30, 0x49, 0x71, 0x7C, 0xE2, 0x16, 0x90, 0x2E,
0xF4, 0x72, 0xEA, 0xE0, 0x34, 0x81, 0x77, 0x93, 0x9C, 0x07, 0xC2, 0x93, 0xB9, 0x61, 0xC0, 0xD3, 0xF0, 0x17, 0xC1,
0xBF, 0x03, 0xCD, 0xE1, 0xAD, 0x42, 0x3F, 0x0A, 0xD8, 0xC7, 0x2D, 0xAC, 0x36, 0xCB, 0xF6, 0x79, 0x22, 0x69, 0x84,
0xC4, 0x93, 0x29, 0x2A, 0x89, 0xB9, 0x00, 0x50, 0x62, 0x7C, 0xFF, 0x00, 0x16, 0xD9, 0x65, 0x00, 0x16, 0x65, 0x07,
0xFA, 0x5B, 0x36, 0xE2, 0xAC, 0xBF, 0x2D, 0x55, 0x79, 0x29, 0xB5, 0x26, 0x9B, 0xA9, 0x18, 0xB5, 0x15, 0x36, 0x9B,
0x53, 0xD1, 0xCA, 0x10, 0x93, 0x69, 0xB6, 0x94, 0x27, 0x16, 0xAF, 0x19, 0x29, 0xB8, 0xBB, 0x3F, 0x9A, 0xE1, 0x1E,
0x2A, 0xCB, 0x9E, 0x79, 0x4A, 0x92, 0x84, 0xDA, 0xD6, 0xED, 0xA4, 0xED, 0x7A, 0x72, 0xB5, 0xAC, 0xF6, 0xEB, 0xAD,
0xD5, 0x97, 0x91, 0xC9, 0x4D, 0xA4, 0xE9, 0xD7, 0x56, 0x45, 0x66, 0xD3, 0x9D, 0x85, 0xB1, 0x86, 0x59, 0x7E, 0xD9,
0x7E, 0xA0, 0xE0, 0xA8, 0x02, 0x17, 0x02, 0x3C, 0x36, 0x59, 0x18, 0xE2, 0x42, 0x1D, 0x40, 0x56, 0x0A, 0x77, 0x30,
0xAE, 0xFA, 0x56, 0xD4, 0xA4, 0x8A, 0xD9, 0xAD, 0xD6, 0x38, 0x1D, 0x7F, 0x77, 0x77, 0x1C, 0xB2, 0x4C, 0x22, 0x8C,
0x2B, 0xF3, 0x14, 0x64, 0x45, 0x85, 0x52, 0x47, 0x92, 0x7E, 0xF2, 0x0F, 0x34, 0x1D, 0xBC, 0x56, 0xDE, 0x85, 0xF0,
0x4B, 0xE1, 0x0A, 0xE9, 0xB0, 0xDB, 0x2D, 0x8C, 0x4C, 0xCD, 0x74, 0xF1, 0x42, 0x96, 0x93, 0xCF, 0xF6, 0x54, 0x0C,
0xB3, 0x44, 0x03, 0xAA, 0x6C, 0x49, 0x10, 0x87, 0x00, 0x88, 0xC2, 0xEE, 0x66, 0x6F, 0x9B, 0xAB, 0x57, 0xA4, 0x68,
0xDF, 0x0A, 0x3E, 0x1B, 0xDA, 0xE9, 0xDF, 0xD9, 0x96, 0xFE, 0x03, 0xB2, 0x8E, 0xD9, 0x8A, 0xDD, 0xC1, 0x77, 0x14,
0x97, 0x50, 0x2E, 0xE3, 0x20, 0x62, 0xA0, 0x33, 0x0C, 0xC6, 0x58, 0x48, 0x36, 0x6D, 0xC1, 0x20, 0xA2, 0x2B, 0x00,
0xB5, 0xF1, 0x99, 0xCF, 0x19, 0x65, 0x79, 0xAD, 0x0C, 0x3C, 0x2B, 0x61, 0xF9, 0x28, 0xD6, 0x52, 0x97, 0x35, 0x95,
0xD4, 0x61, 0x28, 0x5B, 0x99, 0xB8, 0xA9, 0xA9, 0x47, 0xD9, 0x3A, 0x69, 0xA9, 0xC6, 0x51, 0x52, 0x6A, 0x0D, 0x4A,
0x74, 0xA9, 0xC3, 0xFA, 0x7F, 0xC3, 0xFE, 0x25, 0xC1, 0x4E, 0xAE, 0x26, 0x2E, 0x13, 0x71, 0x5C, 0x8D, 0xFB, 0xB1,
0xD6, 0xEA, 0x6F, 0xF9, 0xAF, 0xBE, 0xB7, 0x4B, 0x55, 0x1D, 0x75, 0x4C, 0xF3, 0xDF, 0x05, 0xD9, 0xF8, 0x7B, 0xC3,
0xBA, 0xB5, 0xF1, 0xBC, 0x8D, 0x63, 0xDF, 0x1C, 0x32, 0x88, 0x1A, 0xF3, 0x74, 0x73, 0x89, 0x15, 0xC4, 0x8A, 0x25,
0x66, 0xDC, 0xAA, 0x98, 0x60, 0x06, 0x7F, 0xBD, 0xF2, 0x81, 0xF3, 0x36, 0xFE, 0x9D, 0xAF, 0x78, 0x8F, 0x54, 0xD3,
0xAD, 0xA6, 0x9E, 0xC5, 0xE5, 0x9C, 0x96, 0xDC, 0xB7, 0xE8, 0x84, 0x47, 0xFB, 0xB4, 0x54, 0x42, 0x0F, 0x03, 0xF8,
0xD7, 0xE5, 0x39, 0x60, 0xF8, 0x04, 0x79, 0x7B, 0xE3, 0xF4, 0x9F, 0x85, 0x5F, 0x08, 0xBE, 0x0D, 0x2C, 0x57, 0xCE,
0x6C, 0xA5, 0x2B, 0xA8, 0x08, 0x20, 0x82, 0x04, 0xB9, 0xB8, 0x82, 0x43, 0x28, 0x72, 0x55, 0xD5, 0x83, 0xB9, 0x05,
0xC2, 0x44, 0x55, 0x77, 0x8C, 0xFC, 0xEC, 0xD8, 0xCB, 0xE7, 0xBE, 0x9B, 0xE0, 0x5F, 0xC3, 0x9D, 0x32, 0xD4, 0x45,
0x26, 0x8B, 0x2C, 0x93, 0xC0, 0xF3, 0x95, 0x49, 0xEE, 0x2E, 0x0C, 0x26, 0x26, 0x31, 0xC9, 0xF3, 0xED, 0x95, 0x96,
0x32, 0xEC, 0x10, 0xE1, 0xB8, 0x60, 0x3A, 0x90, 0x1B, 0x3F, 0x9E, 0x66, 0x58, 0xFC, 0x14, 0x31, 0x15, 0xFD, 0x9C,
0x79, 0x57, 0x3A, 0x92, 0x84, 0x1C, 0x3D, 0xC9, 0xCD, 0x27, 0x6D, 0x12, 0xE5, 0xE7, 0xA6, 0xD5, 0x4D, 0x64, 0x93,
0xD1, 0x45, 0xC6, 0xAD, 0xE7, 0x2F, 0xE9, 0xEE, 0x17, 0xE3, 0x0C, 0x0A, 0xE1, 0x9C, 0x37, 0x34, 0x67, 0x2B, 0x73,
0x2B, 0xB4, 0xBA, 0x49, 0xF4, 0x72, 0x57, 0x51, 0xDD, 0x24, 0x9B, 0x5A, 0x36, 0x9A, 0x49, 0x1E, 0x15, 0xA7, 0x7E,
0xCE, 0xF6, 0x89, 0x6A, 0x96, 0x43, 0x55, 0xB5, 0xB3, 0xB6, 0xB8, 0x96, 0x2B, 0x9B, 0x78, 0xA0, 0xB4, 0x9E, 0xE5,
0x61, 0x90, 0xE0, 0xC2, 0xEC, 0x61, 0x91, 0x47, 0xCC, 0xDB, 0xF1, 0x80, 0xC0, 0x98, 0xD4, 0xE5, 0xDC, 0x64, 0xEC,
0x7C, 0x29, 0xFD, 0x98, 0x6C, 0xAF, 0xEC, 0xF5, 0x76, 0xB7, 0xF1, 0x36, 0x9A, 0x5D, 0x6F, 0x96, 0xE1, 0x74, 0xB5,
0xD3, 0x44, 0x91, 0xA8, 0x45, 0x0C, 0xE2, 0x3D, 0xAC, 0x85, 0xF9, 0x3B, 0x5B, 0x19, 0x8A, 0x43, 0x1F, 0xCA, 0x50,
0x9D, 0x87, 0xD2, 0x0F, 0xC2, 0x1F, 0x89, 0x4F, 0x6D, 0x04, 0x0F, 0xE1, 0x61, 0xAA, 0xEE, 0xB4, 0x8E, 0x56, 0xFB,
0x06, 0xAF, 0x04, 0xF3, 0x40, 0x9B, 0x55, 0x4E, 0xCB, 0x66, 0x91, 0xCB, 0x18, 0xC2, 0x88, 0xB6, 0xB1, 0x65, 0x60,
0xA4, 0x6E, 0xDC, 0xA4, 0x9E, 0xAF, 0xE1, 0xBF, 0xC2, 0x2F, 0x89, 0xA9, 0xE1, 0xFB, 0x8D, 0x6E, 0xC2, 0x3B, 0x4B,
0x30, 0xD7, 0x60, 0xDA, 0x41, 0x2D, 0xCC, 0x6C, 0x2E, 0x49, 0xF3, 0x1E, 0x42, 0x19, 0x5D, 0x58, 0x3C, 0x8A, 0x81,
0x82, 0xB8, 0x0C, 0xC9, 0xF2, 0xE5, 0x8F, 0x03, 0x8F, 0x36, 0xE3, 0x8A, 0x98, 0xCC, 0x14, 0xAB, 0x62, 0x2B, 0xC2,
0x34, 0xA2, 0xA5, 0x78, 0x37, 0x42, 0x12, 0x70, 0x8C, 0x62, 0xD2, 0x6D, 0x72, 0x7B, 0xAF, 0x9B, 0xDD, 0xFD, 0xE5,
0x4E, 0x79, 0x49, 0x45, 0x73, 0xA9, 0x72, 0x2F, 0xF0, 0x73, 0x83, 0xF8, 0xCE, 0xB4, 0x33, 0xE8, 0x3F, 0xAE, 0x42,
0xF7, 0x76, 0xB3, 0x83, 0x57, 0xE5, 0x95, 0xBE, 0xCE, 0x8F, 0x7B, 0xDE, 0xD7, 0x4B, 0x6B, 0x9C, 0x1E, 0x8D, 0xF0,
0x56, 0x7B, 0xDD, 0x26, 0x34, 0x5F, 0x1F, 0xDB, 0x9D, 0x37, 0x53, 0xB8, 0x96, 0xED, 0xA0, 0x16, 0xB1, 0xC9, 0x2C,
0x8B, 0xF2, 0x26, 0x58, 0x89, 0x37, 0xEE, 0x04, 0xB2, 0x97, 0x54, 0x2D, 0x88, 0xC0, 0x60, 0x0B, 0x03, 0x5E, 0x9B,
0x67, 0xFB, 0x36, 0xE9, 0x31, 0x2D, 0x9C, 0x37, 0x9E, 0x32, 0x84, 0x5D, 0x2C, 0x4A, 0xD6, 0xAD, 0x73, 0x63, 0x28,
0x36, 0xAA, 0x53, 0xF7, 0x90, 0x29, 0x8E, 0x5C, 0x92, 0xBE, 0x41, 0xDC, 0xC0, 0xB3, 0x2B, 0x46, 0x32, 0x4B, 0x96,
0x35, 0xAD, 0x1F, 0xC2, 0xDF, 0x89, 0x17, 0xF6, 0xB6, 0xFA, 0xB5, 0xED, 0xB8, 0xB7, 0x8E, 0x78, 0x66, 0x96, 0x09,
0x6C, 0x6E, 0x23, 0x32, 0x28, 0x08, 0xE8, 0xF2, 0xBA, 0xF9, 0x86, 0x48, 0xC0, 0x68, 0xFE, 0x63, 0xC8, 0x23, 0x82,
0xCC, 0xA5, 0x4A, 0xFA, 0x66, 0x8F, 0xF0, 0x37, 0xE2, 0x6D, 0xE5, 0xC9, 0x3A, 0x87, 0x86, 0xB4, 0xFB, 0x28, 0xAE,
0x61, 0x0F, 0x0B, 0x59, 0xDF, 0xC0, 0xD1, 0xAC, 0xE0, 0x24, 0x41, 0x42, 0xC4, 0x49, 0x1B, 0x10, 0x21, 0x50, 0xC0,
0x09, 0x11, 0x8B, 0xEE, 0x6E, 0x71, 0xE2, 0xE2, 0x78, 0xBB, 0x33, 0xFA, 0xC5, 0x1A, 0xB4, 0xEA, 0xC6, 0x74, 0xD2,
0x8C, 0x24, 0xA5, 0x2A, 0x3C, 0xB5, 0x2E, 0xB9, 0x9B, 0x71, 0x97, 0x34, 0xE0, 0xAA, 0x54, 0x93, 0x71, 0x56, 0x49,
0xB9, 0xC6, 0x4E, 0x9A, 0x9B, 0xD3, 0xFA, 0x7F, 0x81, 0xB8, 0xD6, 0xAC, 0xDD, 0x6F, 0x65, 0x8D, 0x82, 0x77, 0x85,
0x95, 0xE1, 0x7D, 0x1C, 0xD2, 0x8B, 0x7A, 0x27, 0xA2, 0xF4, 0xED, 0xEE, 0x9E, 0x67, 0xE0, 0x8F, 0x81, 0x37, 0xBA,
0xEF, 0x87, 0x67, 0xBF, 0x8F, 0xC4, 0xD2, 0xDD, 0x4F, 0x05, 0xA8, 0x61, 0x7B, 0x1E, 0x9E, 0xE1, 0x8C, 0xEA, 0xF3,
0x18, 0x99, 0xA3, 0xDE, 0x76, 0x66, 0x36, 0x2A, 0x30, 0xFB, 0x9B, 0x0F, 0xC3, 0x1C, 0xA3, 0x76, 0x4B, 0xFB, 0x3B,
0xE8, 0xBA, 0x66, 0xBF, 0x0E, 0x93, 0xFD, 0xAB, 0x23, 0xD9, 0xA5, 0xCE, 0x62, 0xF3, 0x34, 0xEB, 0x7F, 0x29, 0x0A,
0xA7, 0x04, 0x48, 0xF2, 0x79, 0x82, 0x42, 0x1D, 0x46, 0x4B, 0xA9, 0x1B, 0xD1, 0xBE, 0x52, 0x54, 0xD7, 0xA8, 0xF8,
0x33, 0xE1, 0xAF, 0xC4, 0xA8, 0x24, 0xD6, 0x6D, 0xED, 0xBC, 0x0A, 0xD3, 0x5A, 0x5C, 0xC4, 0xDF, 0x64, 0x17, 0x97,
0x36, 0xF0, 0x65, 0x52, 0x39, 0xD4, 0x23, 0x22, 0x9D, 0xD8, 0x64, 0x83, 0x04, 0x85, 0x38, 0xEF, 0xB8, 0x49, 0x91,
0xBB, 0xFF, 0x00, 0x0A, 0xDF, 0xE3, 0x77, 0x84, 0xAC, 0xD3, 0x4A, 0xBC, 0xF0, 0x3A, 0x5A, 0x5B, 0x4F, 0x72, 0x5E,
0xD7, 0x53, 0x8E, 0xE2, 0x12, 0x6D, 0xC6, 0x40, 0x20, 0x47, 0xE6, 0x3E, 0x71, 0x26, 0x70, 0x24, 0xE4, 0xB4, 0x63,
0x2A, 0xC0, 0xE0, 0x7E, 0x71, 0x98, 0x71, 0x35, 0x5C, 0x4E, 0x69, 0x5A, 0xBD, 0x7C, 0x7A, 0x9C, 0xE3, 0xC9, 0xCF,
0x38, 0x46, 0x8D, 0xA4, 0xDC, 0x63, 0x38, 0x4A, 0xE9, 0xED, 0x5A, 0x4E, 0x4A, 0x6D, 0xF3, 0xCA, 0x2D, 0xB9, 0x4D,
0xD3, 0x77, 0x8C, 0x7F, 0xA8, 0xF8, 0x53, 0x8D, 0x31, 0x32, 0xC8, 0xA9, 0xDB, 0x15, 0x04, 0x9F, 0xB4, 0xD2, 0xF4,
0xD3, 0xF8, 0xA5, 0x77, 0xCD, 0xB3, 0x49, 0x6C, 0xB4, 0xEA, 0x97, 0x46, 0x78, 0x45, 0xC4, 0x16, 0x5A, 0xDD, 0xA5,
0xA6, 0x8D, 0xA7, 0xEA, 0x77, 0x4A, 0x54, 0x47, 0x3C, 0x31, 0x41, 0x20, 0x06, 0x68, 0x00, 0x9A, 0x4F, 0x2A, 0x40,
0x5D, 0x5F, 0x69, 0xF3, 0x15, 0xCB, 0x32, 0x96, 0xDC, 0xC0, 0x90, 0xA5, 0xF0, 0xBD, 0xAF, 0xC3, 0x96, 0x83, 0x51,
0xB8, 0xD6, 0xA7, 0x48, 0xD2, 0xE8, 0x2B, 0x2C, 0x2D, 0x6B, 0xF6, 0xA4, 0x55, 0x8E, 0x45, 0x5F, 0xBA, 0x54, 0x2A,
0x36, 0x77, 0x12, 0x72, 0xE5, 0x9C, 0x14, 0x3B, 0x63, 0x6C, 0x62, 0x8A, 0x2B, 0xDE, 0xCD, 0x70, 0x74, 0xA7, 0x92,
0xE2, 0x31, 0x35, 0x2D, 0x29, 0x54, 0xBB, 0x97, 0xBB, 0x18, 0xF3, 0x38, 0x52, 0xA5, 0x38, 0xDF, 0x92, 0x31, 0x7E,
0xEB, 0x9B, 0x6A, 0xCD, 0x6B, 0xEF, 0x7C, 0x6E, 0x52, 0x7F, 0xE1, 0xF7, 0x0E, 0x54, 0x96, 0x1B, 0x3D, 0x52, 0x86,
0xF0, 0x73, 0x4B, 0xEE, 0x92, 0xD6, 0xD6, 0xBF, 0xC2, 0x8D, 0x9F, 0x0B, 0xDE, 0xEB, 0xF3, 0x69, 0x6B, 0x06, 0x9B,
0xA9, 0x5B, 0xE9, 0xF6, 0x6F, 0x76, 0x1E, 0x45, 0x8E, 0xE1, 0x54, 0xC8, 0xCD, 0xF3, 0x28, 0x8B, 0x1C, 0x9E, 0x25,
0x55, 0xDA, 0xA5, 0x58, 0x80, 0xCA, 0x46, 0x42, 0xA8, 0xF4, 0x1B, 0xEB, 0xBB, 0xBB, 0x8B, 0x69, 0xAF, 0xA5, 0x8E,
0xD0, 0x49, 0x25, 0x9C, 0x37, 0x73, 0xE9, 0xD7, 0xF2, 0x14, 0x8E, 0x55, 0x91, 0x64, 0x90, 0x46, 0xA8, 0xF9, 0x50,
0x57, 0x73, 0x2E, 0xE0, 0x55, 0xF6, 0x87, 0x45, 0x20, 0x2E, 0xC7, 0x28, 0xAF, 0xCD, 0x78, 0xA6, 0xA5, 0x5C, 0x26,
0x33, 0x01, 0x35, 0x2E, 0x67, 0x52, 0x35, 0xA0, 0xEE, 0x93, 0x56, 0x70, 0x4F, 0x48, 0xDB, 0x95, 0x6B, 0x76, 0xF4,
0xB4, 0xA5, 0x27, 0x29, 0x29, 0x35, 0x16, 0xBF, 0xA8, 0x7C, 0x3E, 0x92, 0xF6, 0x98, 0xA8, 0x5B, 0x4B, 0xC5, 0xDB,
0x5E, 0x8E, 0x4F, 0x7D, 0xFA, 0xDB, 0x57, 0xB2, 0x56, 0xD6, 0xED, 0xF7, 0x3E, 0x10, 0xB2, 0x82, 0xF6, 0xC2, 0xF2,
0xF3, 0x4E, 0xD4, 0xAF, 0xEE, 0x5A, 0xCA, 0xCE, 0xD2, 0xF1, 0xA7, 0xB8, 0x9D, 0xD4, 0x42, 0x8C, 0xF2, 0x3C, 0x8E,
0x42, 0x02, 0x24, 0xFD, 0xDA, 0x96, 0x60, 0xAD, 0xB0, 0xC9, 0x0E, 0x00, 0xC8, 0xC8, 0xEA, 0xEE, 0x12, 0x7B, 0x09,
0x6C, 0x6E, 0xCE, 0xA2, 0x0B, 0xAA, 0x32, 0xD9, 0xB9, 0x49, 0x66, 0xDF, 0x74, 0xCC, 0x31, 0x1B, 0xE4, 0xE6, 0x63,
0xF2, 0x91, 0x95, 0x42, 0x15, 0x8B, 0x7D, 0xE0, 0xAC, 0xB4, 0x51, 0x5F, 0x33, 0x3A, 0xD8, 0x8C, 0x46, 0x7A, 0x9C,
0xA6, 0xD7, 0x2C, 0x13, 0x49, 0x25, 0x6E, 0x6A, 0x73, 0xE4, 0x84, 0x9E, 0x9A, 0xC9, 0x29, 0x24, 0x9E, 0xE9, 0x42,
0x11, 0x8D, 0xA2, 0x9C, 0x5F, 0xF5, 0x37, 0x0D, 0x36, 0xF8, 0x7E, 0x9A, 0xEC, 0xE5, 0xE5, 0x7E, 0x67, 0x77, 0x7B,
0x7A, 0xBF, 0x5E, 0xB7, 0x3F, 0xFF, 0xD9,
};
// 8x8 progressive JPEG, 521 bytes
static const uint8_t JPEG_PROGRESSIVE[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10,
0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33,
0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67,
0x68, 0x67, 0x3E, 0x4D, 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11,
0x12, 0x12, 0x18, 0x15, 0x18, 0x2F, 0x1A, 0x1A, 0x2F, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC2, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0xC4, 0x00, 0x15, 0x01, 0x01, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xFF, 0xDA, 0x00, 0x0C, 0x03,
0x01, 0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01, 0x94, 0x05, 0x3F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08,
0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F,
0x01, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x01, 0x7F, 0xFF, 0xC4, 0x00,
0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
0x01, 0x00, 0x01, 0x3F, 0x21, 0x7F, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00,
0x10, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00,
0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
0x01, 0x00, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xD9,
};
class SourceImage : public RuntimeImage {
public:
explicit SourceImage(image::ImageType type = image::IMAGE_TYPE_RGB, int width = 0, int height = 0)
: RuntimeImage(JPEG, type, image::TRANSPARENCY_OPAQUE, nullptr, false, width, height) {}
size_t decoded_bytes() const { return this->decoded_bytes_; }
int scale() const { return static_cast<const JpegDecoder *>(this->decoder_.get())->get_scale(); }
};
/// The file in memory; fails reads once fail_at bytes have been read
class MemorySource : public DecodeSource {
public:
MemorySource(const uint8_t *data, size_t size, size_t fail_at = SIZE_MAX)
: data_(data), size_(size), fail_at_(fail_at) {}
int32_t read(uint8_t *buf, uint32_t len) override {
if (this->pos_ >= this->fail_at_)
return -1;
const size_t n = std::min<size_t>(len, this->size_ - this->pos_);
memcpy(buf, this->data_ + this->pos_, n);
this->pos_ += n;
this->reads++;
return static_cast<int32_t>(n);
}
bool seek(uint32_t position) override {
if (position > this->size_)
return false;
this->pos_ = position;
return true;
}
int reads{0};
protected:
const uint8_t *data_;
size_t size_;
size_t pos_{0};
size_t fail_at_;
};
/// Hands out only the bytes delivered so far, like a download that is still arriving
class TrickleSource : public DecodeSource {
public:
TrickleSource(const uint8_t *data, size_t size, size_t delivered) : data_(data), size_(size), delivered_(delivered) {}
int32_t read(uint8_t *buf, uint32_t len) override {
const size_t n = std::min<size_t>(len, this->delivered_ - this->pos_);
memcpy(buf, this->data_ + this->pos_, n);
this->pos_ += n;
return static_cast<int32_t>(n);
}
bool seek(uint32_t position) override {
if (position > this->delivered_)
return false;
this->pos_ = position;
return true;
}
void deliver(size_t len) { this->delivered_ = std::min(this->size_, this->delivered_ + len); }
protected:
const uint8_t *data_;
size_t size_;
size_t delivered_;
size_t pos_{0};
};
static bool decode_fed(RuntimeImage &img) {
std::vector<uint8_t> buffer(JPEG_ROWS, JPEG_ROWS + sizeof(JPEG_ROWS));
if (!img.begin_decode(buffer.size()))
return false;
if (img.feed_data(buffer.data(), buffer.size()) != static_cast<int>(buffer.size()))
return false;
return img.end_decode();
}
/// Decodes from a source a step at a time; returns the last result and counts the steps
static int decode_stepped(RuntimeImage &img, DecodeSource &source, int &steps) {
steps = 0;
if (!img.begin_decode(sizeof(JPEG_ROWS)) || !img.decoder_supports_source())
return DECODE_ERROR_UNSUPPORTED_FORMAT;
int result = img.begin_source(&source);
if (result != 0)
return result;
do {
result = img.decode_step();
steps++;
} while (result == DECODE_STEP_MORE);
if (result == DECODE_STEP_DONE && !img.end_decode())
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
return result;
}
static std::vector<uint8_t> pixels(const RuntimeImage &img) {
const uint8_t *start = img.get_data_start();
return std::vector<uint8_t>(start, start + img.get_width_stride() * img.get_height());
}
TEST(JpegSourceDecode, SteppedDecodeMatchesFedDecode) {
SourceImage fed;
ASSERT_TRUE(decode_fed(fed));
SourceImage stepped;
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
int steps;
ASSERT_EQ(decode_stepped(stepped, source, steps), DECODE_STEP_DONE);
EXPECT_EQ(steps, 3);
ASSERT_EQ(stepped.get_width(), 48);
ASSERT_EQ(stepped.get_height(), 40);
EXPECT_EQ(pixels(stepped), pixels(fed));
}
TEST(JpegSourceDecode, SteppedDirectRgb565MatchesFedDecode) {
// A fixed size that is not a whole multiple of the source exercises the scaling tables across steps
SourceImage fed(image::IMAGE_TYPE_RGB565, 30, 25);
ASSERT_TRUE(decode_fed(fed));
SourceImage stepped(image::IMAGE_TYPE_RGB565, 30, 25);
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
int steps;
ASSERT_EQ(decode_stepped(stepped, source, steps), DECODE_STEP_DONE);
EXPECT_EQ(pixels(stepped), pixels(fed));
}
// decodeBegin() is given the same reduced scale as decode(), and decodeRows() keeps it for every step
TEST(JpegSourceDecode, SteppedScaledDecodeMatchesFedDecode) {
for (auto type : {image::IMAGE_TYPE_RGB565, image::IMAGE_TYPE_RGB}) {
// 24x20 is half of 48x40, so JPEGDEC decodes at 1/2 size
SourceImage fed(type, 24, 20);
ASSERT_TRUE(decode_fed(fed));
ASSERT_EQ(fed.scale(), 2);
SourceImage stepped(type, 24, 20);
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
int steps;
ASSERT_EQ(decode_stepped(stepped, source, steps), DECODE_STEP_DONE);
ASSERT_EQ(stepped.scale(), 2);
EXPECT_EQ(pixels(stepped), pixels(fed)) << "image type " << type;
}
}
TEST(JpegSourceDecode, WaitsForDataThatHasNotArrived) {
SourceImage fed;
ASSERT_TRUE(decode_fed(fed));
// Enough for the headers (the scan starts at byte 609), then 50 bytes between steps
SourceImage img;
TrickleSource source(JPEG_ROWS, sizeof(JPEG_ROWS), 700);
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
ASSERT_EQ(img.begin_source(&source), 0);
int waits = 0;
int result;
while ((result = img.decode_step()) == DECODE_STEP_MORE || result == DECODE_STEP_NEED_DATA) {
if (result == DECODE_STEP_NEED_DATA)
waits++;
source.deliver(50);
ASSERT_LT(waits, 1000);
}
ASSERT_EQ(result, DECODE_STEP_DONE);
ASSERT_TRUE(img.end_decode());
EXPECT_GT(waits, 0);
EXPECT_EQ(pixels(img), pixels(fed));
}
TEST(JpegSourceDecode, ImageIsHiddenUntilTheLastStep) {
SourceImage img;
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
ASSERT_EQ(img.begin_source(&source), 0);
EXPECT_EQ(img.decode_step(), DECODE_STEP_MORE);
EXPECT_EQ(img.get_width(), 0);
EXPECT_EQ(img.get_data_start(), nullptr);
}
TEST(JpegSourceDecode, ReadFailureIsReportedAndTheDecoderRecovers) {
SourceImage img;
// The first read fills JPEGDEC's buffer, so the failure comes part way through the image data
MemorySource failing(JPEG_ROWS, sizeof(JPEG_ROWS), 2048);
int steps;
EXPECT_EQ(decode_stepped(img, failing, steps), DECODE_ERROR_READ);
EXPECT_GE(steps, 1);
img.release();
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
EXPECT_EQ(decode_stepped(img, source, steps), DECODE_STEP_DONE);
}
TEST(JpegSourceDecode, ReleasePartWayThroughLetsTheNextDecodeStart) {
SourceImage img;
MemorySource first(JPEG_ROWS, sizeof(JPEG_ROWS));
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
ASSERT_EQ(img.begin_source(&first), 0);
ASSERT_EQ(img.decode_step(), DECODE_STEP_MORE);
img.release();
EXPECT_FALSE(img.is_decoding());
MemorySource second(JPEG_ROWS, sizeof(JPEG_ROWS));
int steps;
EXPECT_EQ(decode_stepped(img, second, steps), DECODE_STEP_DONE);
EXPECT_EQ(steps, 3);
}
TEST(JpegSourceDecode, FailedFirstReadIsAReadError) {
SourceImage img;
MemorySource failing(JPEG_ROWS, sizeof(JPEG_ROWS), 0);
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
EXPECT_EQ(img.begin_source(&failing), DECODE_ERROR_READ);
}
TEST(JpegSourceDecode, FailedSeekToTheStartIsAReadError) {
class NoSeekSource : public MemorySource {
public:
using MemorySource::MemorySource;
bool seek(uint32_t position) override { return false; }
};
SourceImage img;
NoSeekSource source(JPEG_ROWS, sizeof(JPEG_ROWS));
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
EXPECT_EQ(img.begin_source(&source), DECODE_ERROR_READ);
EXPECT_EQ(img.decode_step(), DECODE_ERROR_INVALID_TYPE);
}
TEST(JpegSourceDecode, ProgressiveImageIsUnsupported) {
SourceImage img;
MemorySource source(JPEG_PROGRESSIVE, sizeof(JPEG_PROGRESSIVE));
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_PROGRESSIVE)));
EXPECT_EQ(img.begin_source(&source), DECODE_ERROR_UNSUPPORTED_FORMAT);
EXPECT_EQ(img.decode_step(), DECODE_ERROR_INVALID_TYPE);
}
TEST(JpegSourceDecode, CompletedDecodeCountsTheWholeImage) {
SourceImage img;
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
ASSERT_EQ(img.begin_source(&source), 0);
int result;
while ((result = img.decode_step()) == DECODE_STEP_MORE) {
EXPECT_EQ(img.decoded_bytes(), 0u);
}
ASSERT_EQ(result, DECODE_STEP_DONE);
EXPECT_EQ(img.decoded_bytes(), sizeof(JPEG_ROWS));
}
TEST(JpegSourceDecode, SourceIsReadAsTheDecodeNeedsIt) {
SourceImage img;
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
int steps;
ASSERT_EQ(decode_stepped(img, source, steps), DECODE_STEP_DONE);
EXPECT_GE(source.reads, 2);
}
TEST(JpegSourceDecode, DecodersThatCannotReadASourceSaySo) {
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
ASSERT_TRUE(img.begin_decode(100));
EXPECT_FALSE(img.decoder_supports_source());
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
EXPECT_EQ(img.begin_source(&source), DECODE_ERROR_UNSUPPORTED_FORMAT);
EXPECT_EQ(img.decode_step(), DECODE_ERROR_UNSUPPORTED_FORMAT);
}
TEST(JpegSourceDecode, SourceNeedsTheImageSize) {
SourceImage img;
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
// JPEGDEC needs the file size up front, and holds it in an int
ASSERT_TRUE(img.begin_decode(0));
EXPECT_EQ(img.begin_source(&source), DECODE_ERROR_INVALID_TYPE);
img.release();
ASSERT_TRUE(img.begin_decode(static_cast<size_t>(INT32_MAX) + 1));
EXPECT_EQ(img.begin_source(&source), DECODE_ERROR_INVALID_TYPE);
}
TEST(JpegSourceDecode, StepWithoutADecodeIsRefused) {
SourceImage img;
EXPECT_EQ(img.decode_step(), DECODE_ERROR_INVALID_TYPE);
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
// begin_source() was never called
EXPECT_EQ(img.decode_step(), DECODE_ERROR_INVALID_TYPE);
}
} // namespace esphome::runtime_image::testing
#endif // USE_RUNTIME_IMAGE_JPEG
@@ -0,0 +1,14 @@
# Common I2S audio bus configuration for ESP32-C6 IDF tests
# Provides a shared i2s_audio bus that speaker/microphone components can use
# Each consumer must give its speaker/microphone a unique data pin
substitutions:
i2s_bclk_pin: GPIO4
i2s_lrclk_pin: GPIO5
i2s_mclk_pin: GPIO6
i2s_audio:
- id: i2s_audio_bus
i2s_bclk_pin: ${i2s_bclk_pin}
i2s_lrclk_pin: ${i2s_lrclk_pin}
i2s_mclk_pin: ${i2s_mclk_pin}
+37 -8
View File
@@ -4,7 +4,7 @@ from ipaddress import IPv4Address
import pytest
from esphome.components.esp32 import const
from esphome.components.esp32 import KEY_ESP32, KEY_IDF_VERSION, const
from esphome.components.network import (
KEY_HIGH_PERFORMANCE_NETWORKING,
KEY_HIGH_PERFORMANCE_WIFI,
@@ -236,11 +236,19 @@ def wisp_core(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -
CORE.data,
KEY_CORE,
{
KEY_TARGET_PLATFORM: Platform.ESP32,
KEY_TARGET_PLATFORM: params.get("platform", Platform.ESP32),
KEY_TARGET_FRAMEWORK: params.get("framework", "esp-idf"),
KEY_FRAMEWORK_VERSION: params.get("idf", cv.Version(5, 4, 0)),
},
)
# The ESP-IDF underneath, which an Arduino build maps from its own version; only an
# ESP32 build has it
if params.get("platform", Platform.ESP32) == Platform.ESP32:
monkeypatch.setitem(
CORE.data,
KEY_ESP32,
{KEY_IDF_VERSION: params.get("idf", cv.Version(5, 4, 0))},
)
def _wisp_ap(**overrides: object) -> dict:
@@ -300,22 +308,43 @@ def test_dns_servers_limited_to_two() -> None:
@pytest.mark.parametrize(
("wisp_core", "valid"),
[({"idf": cv.Version(5, 5, 5)}, False), ({"idf": cv.Version(6, 0, 1)}, True)],
[
({"idf": cv.Version(5, 5, 5)}, False),
({"idf": cv.Version(6, 0, 1)}, True),
({"framework": "arduino", "idf": cv.Version(5, 5, 6)}, False),
({"framework": "arduino", "idf": cv.Version(6, 0, 1)}, True),
],
indirect=["wisp_core"],
)
def test_second_dns_server_requires_idf_6(wisp_core: None, valid: bool) -> None:
"""The ESP-IDF underneath counts, so an Arduino build on ESP-IDF 6.0 gets it too."""
ap = _wisp_ap(dns_servers=["1.1.1.1", "8.8.8.8"])
if valid:
wifi_network_ap(ap)
else:
with pytest.raises(cv.Invalid, match="6.0.0"):
with pytest.raises(cv.Invalid, match="ESP-IDF 6.0"):
wifi_network_ap(ap)
@pytest.mark.parametrize("wisp_core", [{"framework": "arduino"}], indirect=True)
def test_wisp_requires_esp_idf(wisp_core: None) -> None:
"""NAT is only available with the ESP-IDF framework."""
with pytest.raises(cv.Invalid, match="only available with framework"):
@pytest.mark.parametrize(
"wisp_core",
[{"platform": Platform.ESP8266, "framework": "arduino"}],
indirect=True,
)
def test_second_dns_server_elsewhere_is_a_config_error(wisp_core: None) -> None:
"""No ESP32 state to read a version from; the WISP platform check rejects it instead."""
with pytest.raises(cv.Invalid, match="only available on"):
wifi_network_ap(_wisp_ap(dns_servers=["1.1.1.1", "8.8.8.8"]))
@pytest.mark.parametrize(
"wisp_core",
[{"platform": Platform.ESP8266, "framework": "arduino"}],
indirect=True,
)
def test_wisp_requires_esp32(wisp_core: None) -> None:
"""The bridge is part of the ESP32 backend; other platforms have no NAT."""
with pytest.raises(cv.Invalid, match="only available on .*ESP32"):
wifi_network_ap(_wisp_ap())
+78 -4
View File
@@ -1536,13 +1536,87 @@ async def test_sub_entities_apply_errors(
@pytest.mark.asyncio
async def test_sub_entities_apply_needs_parent(
async def test_sub_entities_apply_without_parent_needs_namespaced_call(
setup_test_environment: list[str],
apply_functions: list[tuple[str, str, str]],
) -> None:
"""A hub method with no parent bound fails in Python, not as an undeclared name in C++."""
calls: list[tuple[Any, ...]] = []
conf = _apply_conf("k_id", _BUTTON_APPLY.cls)
sub = _apply_sub(calls, {"k": conf}, _BUTTON_APPLY, parent=None)
with pytest.raises(ValueError, match="namespaced free function"):
await sub("k", None, apply="restart()")
assert calls == []
assert apply_functions == []
@pytest.mark.asyncio
@pytest.mark.parametrize(
("apply_entity", "type_", "publish", "params", "body"),
[
(
_BUTTON_APPLY,
_BUTTON_APPLY.cls,
None,
"button::Button * entity",
"restart::restart_device();",
),
(
_SWITCH_APPLY,
_APPLY_SWITCH_CLS,
"after",
"switch_::Switch * entity, bool state",
"restart::restart_device();\nentity->publish_state(state);",
),
],
)
async def test_sub_entities_apply_without_parent(
monkeypatch: pytest.MonkeyPatch,
setup_test_environment: list[str],
apply_functions: list[tuple[str, str, str]],
apply_entity: entity_helpers.ApplyEntity,
type_: MockObjClass,
publish: str | None,
params: str,
body: str,
) -> None:
"""Without a parent the statement runs as written, with no parent lookup or deref."""
async def fail(*args: Any) -> None:
raise AssertionError("must not be called")
monkeypatch.setattr(entity_helpers.cg, "register_parented", fail)
monkeypatch.setattr(entity_helpers, "get_variable", fail)
calls: list[tuple[Any, ...]] = []
sub = _apply_sub(calls, {}, apply_entity, parent=None)
var = await sub.create(
_apply_conf("rst", type_),
None,
apply="restart::restart_device()",
publish=publish,
)
assert str(var) == "rst"
assert calls == [("rst", "<esphome__rst__fn>")]
assert apply_functions == [("esphome__rst__fn", params, body)]
assert setup_test_environment == []
@pytest.mark.asyncio
async def test_sub_entities_apply_without_parent_shares_equal_functions(
setup_test_environment: list[str], apply_functions: list[tuple[str, str, str]]
) -> None:
"""Parentless entities with the same statement share one function."""
calls: list[tuple[Any, ...]] = []
sub = _apply_sub(calls, {"k": _apply_conf("k_id")}, parent=None)
with pytest.raises(ValueError, match="needs a parent"):
await sub("k", None, apply="x()")
sub = _apply_sub(calls, {}, _BUTTON_APPLY, parent=None)
for name in ("a_btn", "b_btn"):
await sub.create(_apply_conf(name, _BUTTON_APPLY.cls), None, apply="x::go()")
await sub.create(_apply_conf("c_btn", _BUTTON_APPLY.cls), None, apply="x::stop()")
assert calls == [
("a_btn", "<esphome__a_btn__fn>"),
("b_btn", "<esphome__a_btn__fn>"),
("c_btn", "<esphome__c_btn__fn>"),
]
assert [body for _, _, body in apply_functions] == ["x::go();", "x::stop();"]
@pytest.mark.parametrize(
+33 -6
View File
@@ -1319,25 +1319,52 @@ async def test_apply_string_ref_literal_stays_in_flash_on_esp8266(
@pytest.mark.asyncio
@pytest.mark.parametrize("platform", ["esp32", "esp8266"])
@pytest.mark.parametrize(
"body", ["x *= 2;\nreturn {0x01, x};", "return std::vector<uint8_t>{++x};"]
)
async def test_apply_bytes_lambda_is_a_vector_in_a_block(
registries: tuple[Registry, Registry], mock_cg: MockCodegen, platform: str
registries: tuple[Registry, Registry],
mock_cg: MockCodegen,
platform: str,
body: str,
) -> None:
"""A lambda payload is called once into a scoped vector and passed as data and size."""
"""A lambda payload is called once into a scoped vector and passed as data and size;
it takes the trigger args by their own types and is never reduced, so it may modify them."""
fields = (ApplyBytes("data", "send({})"),)
config = {"data": Lambda("return {0x01, x};")}
config = {"data": Lambda(body)}
await _run_apply_action(
registries, fields, config, args=[(cg.uint8, "x")], platform=platform
)
text = _apply_definition(mock_cg)
# Parenthesized so a reduced `return a, b;` stays one initializer
assert "const std::vector<uint8_t> esphome__bytes = (" in text
assert "-> std::vector<uint8_t>" in text
assert text.startswith(
"static void esphome__obj_1__fn(const std::remove_cvref_t<uint8_t> & x) {"
)
assert (
"const std::vector<uint8_t> esphome__bytes = [](uint8_t x) -> std::vector<uint8_t> {"
in text
)
assert "}(x);" in text
assert (
f"::{PARENT_OBJ}->send(esphome__bytes.data(), esphome__bytes.size());" in text
)
assert "progmem_memcpy" not in text
@pytest.mark.asyncio
async def test_apply_bytes_keeps_a_mutable_reference_arg(
registries: tuple[Registry, Registry], mock_cg: MockCodegen
) -> None:
"""A reference trigger arg such as climate's `ClimateCall &` is passed through as is."""
fields = (ApplyBytes("data", "send({})"),)
config = {"data": Lambda("return {0x01};")}
call_ref = cg.MockObj("ClimateCall").operator("ref")
await _run_apply_action(registries, fields, config, args=[(call_ref, "x")])
text = _apply_definition(mock_cg)
assert text.startswith("static void esphome__obj_1__fn(ClimateCall & x) {")
assert "[](ClimateCall & x) -> std::vector<uint8_t> {" in text
assert "}(x);" in text
@pytest.mark.asyncio
async def test_apply_bytes_constant_is_a_shared_table(
registries: tuple[Registry, Registry], mock_cg: MockCodegen