Compare commits

..
Author SHA1 Message Date
clyde 8a63166fe1 Merge branch 's-direct' into s-opaque 2026-10-23 14:55:09 +11:00
clydeandClaude Opus 5.5 6b391423f9 [runtime_image] Free both RGB565 tables when one cannot be allocated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:55:08 +11:00
clyde e48af57cc4 Merge branch 's-direct' into s-opaque 2026-10-23 13:21:59 +11:00
clyde 18bdd30824 Merge branch 's-bmpqoi' into s-direct
# Conflicts:
#	esphome/components/runtime_image/image_decoder.cpp
#	esphome/components/runtime_image/jpeg_decoder.cpp
2026-10-23 13:21:58 +11:00
clyde 91f951284f Merge branch 's-png' into s-bmpqoi 2026-10-23 13:21:08 +11:00
clyde 3856915568 Merge branch 's2-gray' into s-png
# Conflicts:
#	esphome/components/runtime_image/image_decoder.cpp
#	esphome/components/runtime_image/image_decoder.h
#	esphome/components/runtime_image/jpeg_decoder.cpp
#	tests/components/online_image/test_download_end.cpp
#	tests/components/runtime_image/test_decoder_reuse.cpp
2026-10-23 13:21:06 +11:00
clyde 4922b2b25f Merge branch 's2-draw' into s2-gray 2026-10-23 13:20:31 +11:00
clydeandClaude Opus 5.5 c43f039631 [runtime_image] Share one scale_boundary() between draw() and the JPEG RGB565 tables; test uneven scales
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:20:30 +11:00
clydeandClaude Opus 5.5 51d2df33a3 [runtime_image] Work out grayscale pixels with integer maths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:57:11 +10:00
clydeandClaude Opus 5.5 8d1476d585 [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 11:57:10 +10:00
clydeandClaude Opus 5.5 25b8c93ad6 [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 11:57:10 +10:00
clydeandClaude Opus 5.5 5b1642b721 [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 11:57:10 +10:00
clydeandClaude Opus 5.5 b62e12007f [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 11:57:10 +10:00
clydeandClaude Opus 5.5 68d24cb215 [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 11:57:10 +10:00
clyde 07f71ec053 Merge branch 's-direct' into s-opaque 2026-10-23 12:53:14 +11:00
clydeandClaude Opus 5.5 201e64eae4 [runtime_image] Draw pixel by pixel when the RGB565 tables cannot be allocated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:53:13 +11:00
clyde 9c0f94cdcc Merge branch 's-bmpqoi' into s-direct 2026-10-23 12:49:22 +11:00
clydeandClaude Opus 5.5 31d54f13ad [runtime_image] Stop drawing BMP pixels after the last row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:49:16 +11:00
clyde a8602f55bd Merge branch 's-png' into s-bmpqoi
# Conflicts:
#	esphome/components/runtime_image/png_decoder.cpp
2026-10-23 12:46:05 +11:00
clydeandClaude Opus 5.5 f8894856b3 [runtime_image] Keep feeding PNG pieces past chunk boundaries; make the time limit tests independent of host speed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:45:35 +11:00
clyde 47432e4a1f Merge branch 's-direct' into s-opaque 2026-10-23 09:15:42 +11:00
clyde 9c822bd991 Merge branch 's-bmpqoi' into s-direct 2026-10-23 09:15:42 +11:00
clyde 5229e1bc2c Merge branch 's-png' into s-bmpqoi 2026-10-23 09:15:42 +11:00
clyde 7fae88cf12 Merge branch 's-gray' into s-png 2026-10-23 09:15:41 +11:00
clyde d18c64c257 Merge branch 's-draw' into s-gray 2026-10-23 09:15:41 +11:00
clyde e4a7223b6f Merge branch 's-scale' into s-draw 2026-10-23 09:15:41 +11:00
clydeandClaude Opus 5.5 1219cac32d [runtime_image] Check the scaled RGB decode against the scaled RGB565 one
JPEGDEC's x86 SSE code writes full size RGB8888 with red and blue swapped, so a full size decode is no reference
there. Integer comparisons also keep clang-tidy's bugprone-integer-division quiet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:15:40 +11:00
clyde 75daf90ad6 Merge branch 's-direct' into s-opaque
# Conflicts:
#	tests/components/runtime_image/test_decoder_reuse.cpp
2026-10-23 08:52:41 +11:00
clydeandClaude Opus 5.5 c909b09941 [runtime_image] Build a JPEG's drawing tables once, for the decoded size
set_size() built them for the full image size before start_() picked the scale and built them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:52:30 +11:00
clyde 115ba5ee6c Merge branch 's-png' into s-bmpqoi 2026-10-23 08:50:37 +11:00
clyde 305a1bf3fa Merge branch 's-gray' into s-png 2026-10-23 08:50:37 +11:00
clyde d00820fae7 Merge branch 's-draw' into s-gray 2026-10-23 08:50:37 +11:00
clyde 2503dc49e3 Merge branch 's-scale' into s-draw 2026-10-23 08:50:37 +11:00
clyde ff329537bf Merge branch 's-bmpqoi' into s-direct 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 973913d05d Merge branch 's-direct' into s-opaque 2026-10-23 08:23:14 +11:00
clyde cfbaa907bc Merge branch 's-png' into s-bmpqoi 2026-10-23 08:23:14 +11:00
clyde e5a51a9521 Merge branch 's-bmpqoi' into s-direct 2026-10-23 08:23:14 +11:00
clyde 679283c28f Merge branch 's-gray' into s-png 2026-10-23 08:23:13 +11:00
clyde 4dc829e091 Merge branch 's-draw' into s-gray 2026-10-23 08:23:13 +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 60d6170465 Merge branch 's-direct' into s-opaque 2026-10-23 08:01:28 +11:00
clyde 2e62c7a70b Merge branch 's-bmpqoi' into s-direct 2026-10-23 08:01:28 +11:00
clyde ff1a9dbb86 Merge branch 's-png' into s-bmpqoi 2026-10-23 08:01:27 +11:00
clyde 95fad9aaf5 Merge branch 's-gray' into s-png 2026-10-23 08:01:27 +11:00
clyde e31de889ef Merge branch 's-draw' into s-gray 2026-10-23 08:01:27 +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
clyde 4bff5c9cec Merge branch 's-direct' into s-opaque 2026-10-23 07:25:22 +11:00
clyde 8cd83ec51b Merge branch 's-bmpqoi' into s-direct 2026-10-23 07:25:22 +11:00
clyde e0f3eb8677 Merge branch 's-png' into s-bmpqoi 2026-10-23 07:25:21 +11:00
clyde e84efbb934 Merge branch 's-gray' into s-png
# Conflicts:
#	esphome/components/online_image/online_image.cpp
#	tests/components/online_image/test_download_end.cpp
2026-10-23 07:25:20 +11:00
clyde f7b2bb8c19 Merge branch 's-draw' into s-gray 2026-10-23 07:24:51 +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 066e95077a Merge branch 's-draw' into s-gray 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 9ce82f98cc [runtime_image] Work out grayscale pixels with integer maths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:49 +10: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
clydeandClaude Opus 5.5 533523e0da [runtime_image] Draw BMP pixels opaque
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:48:26 +11:00
clydeandClaude Opus 5.5 a03730dd26 [runtime_image] Write any opaque RGB565 image straight into the buffer
Move the JPEG decoder's boundary tables into ImageDecoder, so draw() writes opaque RGB565 pixels for BMP, PNG and
QOI directly, and draw each QOI run once per row it covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:07:11 +11:00
clyde e0fb0d06a9 Merge branch 'image-png-time-limit' into runtime-image-bmp-qoi-time-limit 2026-10-22 22:03:59 +11:00
clyde a453dd66c6 Merge branch 'runtime-image-gray-integer' into image-png-time-limit 2026-10-22 22:03:59 +11:00
clyde 52c4c0310d Merge branch 'runtime-image-integer-draw' into runtime-image-gray-integer 2026-10-22 22:03:59 +11:00
clyde 547dbf18d4 Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 22:03:58 +11:00
clyde ba3af07a23 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 22:03:58 +11:00
clyde 9c6ae9c615 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 22:03:58 +11:00
clyde de02ef8555 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 22:03:57 +11:00
clydeandClaude Opus 5.5 38b23152f4 [runtime_image] Put the is_finished() doc back above is_finished()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:03:51 +11:00
clydeandClaude Opus 5.5 e9c6d4341b [runtime_image] Limit how long one BMP or QOI decode call runs
Count the bytes QOI uses before a chunk split across two calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:00:06 +11:00
clyde 0bc55bcdc6 Merge branch 'runtime-image-gray-integer' into image-png-time-limit
# Conflicts:
#	tests/components/online_image/test_download_end.cpp
2026-10-22 21:57:14 +11:00
clydeandClaude Opus 5.5 0849e4b412 [runtime_image] Work out grayscale pixels with integer maths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:56:41 +11:00
clyde 375e8bc39b Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 21:44:49 +11:00
clyde 16a91ab760 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 21:44:44 +11:00
clydeandClaude Opus 5.5 2456e8d686 [online_image] Use emplace_back in the download started test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:44:39 +11:00
clyde 0c8e1c3f1c Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 21:35:06 +11:00
clyde a3c575d643 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 21:31:12 +11:00
clydeandClaude Opus 5.5 0573c8b568 [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-22 21:24:05 +11:00
clyde d01205fa62 Merge branch 'runtime-image-jpeg-steps' into runtime-image-jpeg-scale 2026-10-22 21:17:21 +11:00
clyde de7ea89d0b Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 21:17:19 +11:00
clyde 0077f68158 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 21:17:18 +11:00
clydeandClaude Opus 5.5 7458f8c7d8 [runtime_image] Pin JPEGDEC to 1.8.4-esphome.3, built with -O2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:17:13 +11:00
clyde 0e5e0a8ff6 Merge remote-tracking branch 'upstream/dev' into runtime-image-jpeg-steps 2026-10-22 21:15:57 +11:00
clydeandClaude Opus 5.5 752c230405 [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-22 21:14:33 +11:00
clydeandClaude Opus 5.5 cfc92657a2 [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-22 20:51:14 +11:00
clydeandClaude Opus 5.5 32f32f144d [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-22 20:44:48 +11:00
clydeandClaude Opus 5.5 9b60e7cff2 [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-22 20:19:44 +11:00
clydeandClaude Opus 5.5 02b8db1f3f [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-22 20:17:27 +11:00
clyde f860c8a8d9 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 20:13:39 +11:00
clyde bb7a060138 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 20:13:38 +11:00
clydeandClaude Opus 5.5 84839507bd [runtime_image] Pin JPEGDEC to 1.8.4-esphome.2, with the ESP32-S3 SIMD under ESP-IDF
Full-size JPEG decodes are 12-19% faster on an ESP32-S3 with ESP-IDF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:13:33 +11:00
clydeandClaude Opus 5.5 1de1794574 [runtime_image] Let one PNG decode call run for up to 15 ms
Closer to the 16 ms loop time, so a large PNG needs fewer passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:10:55 +11:00
clydeandClaude Opus 5.5 84638cf2b6 [runtime_image] Use longer names in the PNG time limit test for clang-tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:59:50 +11:00
clydeandClaude Opus 5.5 4df1b53d85 [runtime_image] Feed pngle 64 bytes at a time
Flat areas of an image compress so well that 256 bytes could still
hold 160 ms of drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:54:28 +11:00
clydeandClaude Opus 5.5 bac443840c [runtime_image] Limit how long one PNG decode call runs
A few KB of compressed PNG data can hold many rows of pixels, so one
feed_data() call could hold up the main loop for hundreds of
milliseconds. Feed pngle 256 bytes at a time and return after 10 ms,
leaving the rest for the next call.

online_image now passes data the decoder has not used to it again
when nothing new arrives, instead of treating it as data the decoder
could not use once the download ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:48:22 +11:00
clyde ef54eb14d8 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 16:29:02 +11:00
clydeandClaude Opus 5.5 60aa3e24f1 [online_image] Brace the whole-file fallback for clang-tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:29:01 +11:00
clyde 63566999bd Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 16:28:45 +11:00
clydeandClaude Opus 5.5 1728d3326b [runtime_image] Log a failed open from a source at debug level, as the caller may recover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:28:44 +11:00
clyde 2b99c56754 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 16:21:09 +11:00
clydeandClaude Opus 5.5 d79cd9a7eb [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-22 16:21:08 +11:00
clyde fa18994fab Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 16:19:20 +11:00
clydeandClaude Opus 5.5 1090706216 [runtime_image] Count the bytes of a stepped decode, and say when begin_source() can be called
Report any failed read of the source as DECODE_ERROR_READ from one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:19:01 +11:00
clydeandClaude Opus 5.5 ced497995d [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-22 14:35:37 +11:00
clydeandClaude Opus 5.5 228c553f60 [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-22 14:34:15 +11:00
clydeandClaude Opus 5.5 563e197c6d [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-22 14:29:07 +11:00
clyde 9d07f1f400 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 14:27:47 +11:00
clydeandClaude Opus 5.5 846fc70bdf [runtime_image] Report a progressive JPEG as an unsupported format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:27:46 +11:00
clyde 74212d8d04 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:41:53 +11:00
clydeandClaude Opus 5.5 101c9cd840 [runtime_image] Take JPEGDEC from esphome-libs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:41:48 +11:00
clyde 8152dcf224 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:37:13 +11:00
clydeandClaude Opus 5.5 f2d50b4aa7 [runtime_image] Drop the trailing underscore from the static JPEGDEC callbacks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:37:13 +11:00
clydeandClaude Opus 5.5 f75b3d6ded [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-22 13:17:00 +11:00
clyde 3cf29bf4fb Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:14:54 +11:00
clydeandClaude Opus 5.5 f752e3b150 [runtime_image] Report a failed seek to the start as a read error
Also limit the JPEGDEC pin comment to the fixes ESPHome uses: it never
crops, and refuses progressive images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:14:51 +11:00
clyde a5bc7070d1 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:06:41 +11:00
clydeandClaude Opus 5.5 b2eebfb010 [runtime_image] Pin JPEGDEC to the 1.8.4-esphome.1 release
Adds the upstream fix that skips building AC Huffman tables for
progressive images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:06:37 +11:00
clyde a596d1d3c8 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 09:09:13 +11:00
clydeandClaude Opus 5.5 6701aedae2 [runtime_image] Pin JPEGDEC with the grayscale, crop and progressive fixes
A grayscale JPEG decoded for any image type but direct RGB565 came out garbled:
JPEGDEC wrote RGB565 words where it was asked for RGB8888. The new pin fixes
that, along with cropping and a crash on progressive images with skipped blocks,
which ESPHome does not use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:09:09 +11:00
clydeandClaude Opus 5.5 6fddbd1d48 [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-22 07:23:28 +11:00
clydeandClaude Opus 5.5 03aaa00d75 [runtime_image] Refuse a source decode without a usable image size
JPEGDEC needs the file size when it opens a source, and holds it in an int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:21:14 +11:00
clydeandClaude Opus 5.5 b612eb2929 [runtime_image] Let a source-reading decoder wait for data that has not arrived
A source that is still arriving, such as a download, may have nothing to give
when the decoder asks for more. The pinned JPEGDEC now pauses before an MCU when
less input is buffered than it needs and the source has no more yet, and
decode_step() reports that as DECODE_STEP_NEED_DATA; the next call carries on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:04:54 +11:00
clydeandClaude Opus 5.5 63c041a95b [runtime_image] Pin JPEGDEC with the ARM NEON clipped MCU fix
Upstream's fix for the NEON path writing past the end of a row on clipped MCUs,
which overflows a framebuffer whose width is not a multiple of 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:36:44 +11:00
clydeandClaude Opus 5.5 f8d5096093 [runtime_image] Pin JPEGDEC with the ARM NEON colour order fix
The NEON 4:2:0 RGB8888 path swapped red and blue (JPEGDEC PR 121), which shows on
host builds on 64-bit ARM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:26:39 +11:00
clydeandClaude Opus 5.5 f835600fa6 [runtime_image] Let a decoder read its input itself and decode JPEG a row at a time
JPEGDEC decodes a whole image in one call, from a copy of the whole file in RAM.
A JPEGDEC that can stop between rows (1.8.4 plus decodeBegin() and decodeRows(),
pinned from a fork for now) lets the JPEG decoder read a DecodeSource itself,
through JPEGDEC's own 2 KB buffer, and decode one row of MCUs per decode_step().
A component that can read its input on demand, such as a file, no longer needs a
buffer for the whole file or a main loop pass long enough to decode the image.

Decoders that cannot read a source say so, and the fed decode() path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:08:01 +11:00
131 changed files with 1397 additions and 4234 deletions

No files matched your search

+1 -10
View File
@@ -175,11 +175,6 @@ esphome/components/ds2484/* @mrk-its
esphome/components/ds248x/* @tomwellnitz
esphome/components/ds3231/* @linkedupbits
esphome/components/dsmr/* @glmnet @PolarGoose
esphome/components/duco/* @kokx
esphome/components/duco/number/* @kokx
esphome/components/duco/select/* @kokx
esphome/components/duco/sensor/* @kokx
esphome/components/duco/text_sensor/* @kokx
esphome/components/duty_time/* @dudanov
esphome/components/ecocomfort2/* @gledian
esphome/components/ee895/* @Stock-M
@@ -379,8 +374,7 @@ esphome/components/m5stack_8angle/* @rnauber
esphome/components/mapping/* @clydebarrow
esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell
esphome/components/max17048/* @B48D81EFCC @ryansch
esphome/components/max170xx/* @ryansch
esphome/components/max17048/* @B48D81EFCC
esphome/components/max31865/* @DAVe3283
esphome/components/max31888/* @JiriPrchal
esphome/components/max44009/* @berfenger
@@ -468,8 +462,6 @@ esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_interface/* @clydebarrow
esphome/components/packet_interface/packet_transport/* @clydebarrow
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
@@ -690,7 +682,6 @@ esphome/components/tuya/water_heater/* @iago-veiga
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_interface/* @clydebarrow
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/uart_tcp/* @Bascht74
+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_();
}
-87
View File
@@ -1,87 +0,0 @@
import esphome.codegen as cg
from esphome.components import time, uart
import esphome.config_validation as cv
from esphome.const import CONF_DISCOVERY, CONF_ID, CONF_TIME, CONF_TIME_ID
from esphome.core import ID
from esphome.types import ConfigType
CODEOWNERS = ["@kokx"]
DEPENDENCIES = ["uart"]
DOMAIN = "duco"
MULTI_CONF = True
CONF_DISABLE_CRC = "disable_crc"
CONF_DUCO_ID = "duco_id"
duco_ns = cg.esphome_ns.namespace("duco")
Duco = duco_ns.class_("Duco", cg.Component, uart.UARTDevice)
# Base of every class that sends requests through the hub
DucoDevice = duco_ns.class_(
"DucoDevice", cg.PollingComponent, cg.Parented.template(Duco)
)
DucoDiscovery = duco_ns.class_("DucoDiscovery", DucoDevice)
DucoTime = duco_ns.class_("DucoTime", cg.PollingComponent, cg.Parented.template(Duco))
# Entities of the platforms take the hub with this
DUCO_COMPONENT_SCHEMA = cv.Schema({cv.GenerateID(CONF_DUCO_ID): cv.use_id(Duco)})
DISCOVERY_SCHEMA = cv.Schema({cv.GenerateID(): cv.declare_id(DucoDiscovery)}).extend(
cv.COMPONENT_SCHEMA
)
TIME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DucoTime),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
).extend(cv.polling_component_schema("15min"))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Duco),
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
cv.Optional(CONF_DISCOVERY): DISCOVERY_SCHEMA,
cv.Optional(CONF_TIME): TIME_SCHEMA,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
DOMAIN,
baud_rate=57600,
require_tx=True,
require_rx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
_request_device_slot = cg.slot_counter("DUCO_DEVICE_COUNT")
async def register_duco_device(var: cg.MockObj, config: ConfigType, hub_id: ID) -> None:
"""Register a device that sends requests, and give it an open request slot on its hub."""
await cg.register_component(var, config)
hub = await cg.get_variable(hub_id)
await cg.register_parented(var, hub)
_request_device_slot(str(hub_id))
cg.add(hub.register_device(var))
async def to_code(config: ConfigType) -> None:
var = await uart.new_uart_component(config)
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
if (discovery := config.get(CONF_DISCOVERY)) is not None:
discovery_var = cg.new_Pvariable(discovery[CONF_ID])
await register_duco_device(discovery_var, discovery, config[CONF_ID])
if (time_config := config.get(CONF_TIME)) is not None:
clock = await cg.get_variable(time_config[CONF_TIME_ID])
time_var = cg.new_Pvariable(time_config[CONF_ID], clock)
await cg.register_component(time_var, time_config)
await cg.register_parented(time_var, var)
-273
View File
@@ -1,273 +0,0 @@
#include "duco.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::duco {
ESPHOME_LOG_TAG(TAG, "duco");
static constexpr uint8_t HEADER_START = 0xAA;
static constexpr uint8_t HEADER_END = 0x55;
static constexpr uint8_t STUFFING = 0x01; // follows an 0xAA inside a message, not part of it
static constexpr uint32_t FRAME_GAP_MS = 50;
static constexpr uint32_t DISCOVERY_START_ID = 0;
static constexpr uint32_t DISCOVERY_STEP_ID = 1;
static constexpr uint32_t DISCOVERY_START_MS = 16000;
static constexpr uint32_t DISCOVERY_STEP_MS = 1600;
static constexpr uint8_t DISCOVERY_MAX_ATTEMPTS = 3;
void Duco::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Read first: after a slow loop pass the rest of a message is already waiting, which is not silence
this->read_available([this, now](uint8_t byte) {
if (this->parse_byte_(byte)) {
this->last_byte_ = now;
} else {
this->reset_frame_();
}
});
// The last message of a burst has no header after it, so it ends on silence
if (this->rx_size_ != 0 && now - this->last_byte_ > FRAME_GAP_MS) {
this->finalize_message_();
this->reset_frame_();
}
}
void Duco::reset_frame_() {
this->rx_size_ = 0;
this->escaped_ = false;
}
void Duco::close_request(DucoDevice *device) {
#ifdef DUCO_DEVICE_COUNT
for (auto &pending : this->pending_) {
if (pending.device == device) {
pending.function = 0;
pending.missed = false;
this->update_warning_();
return;
}
}
#endif
}
bool Duco::parse_byte_(uint8_t byte) {
const size_t at = this->rx_size_;
if (at >= MAX_FRAME_SIZE) {
ESP_LOGV(TAG, "Message longer than %u bytes dropped", static_cast<unsigned>(MAX_FRAME_SIZE));
return false;
}
const uint8_t prev = at > 0 ? this->rx_buffer_[at - 1] : 0;
if (at == 1 && prev == HEADER_START && byte == HEADER_END)
return false; // header of the next message
if (at >= 2) {
const bool escaped = this->escaped_;
this->escaped_ = false;
if (prev == HEADER_START && !escaped) {
if (byte == STUFFING) {
this->escaped_ = true;
return true;
}
if (at >= 3 && byte == HEADER_END) {
this->rx_size_--; // drop the 0xAA of the header
this->finalize_message_();
return false;
}
}
}
this->rx_buffer_[this->rx_size_++] = byte;
return true;
}
void Duco::finalize_message_() {
const uint8_t *raw = this->rx_buffer_.data();
// length (function, id and data), function, id, data, CRC low, CRC high
if (this->rx_size_ < 5 || raw[0] < 2 || this->rx_size_ < raw[0] + 3u) {
ESP_LOGV(TAG, "Malformed message dropped: %u bytes, length byte %u", this->rx_size_, raw[0]);
return;
}
const uint8_t length = raw[0];
const uint16_t computed_crc = crc16(raw, length + 1);
const uint16_t remote_crc = encode_uint16(raw[length + 2], raw[length + 1]);
if (computed_crc != remote_crc) {
if (!this->disable_crc_) {
ESP_LOGV(TAG, "CRC check failed: %04X != %04X", computed_crc, remote_crc);
return;
}
ESP_LOGV(TAG, "CRC check failed, ignored: %04X != %04X", computed_crc, remote_crc);
}
#ifdef DUCO_DEVICE_COUNT
// The box first acknowledges a request with function + 1, then answers with function + 2
const DucoMessage message{raw[2], raw + 3, static_cast<uint8_t>(length - 2)};
for (auto &pending : this->pending_) {
if (pending.function != 0 && pending.id == message.id && pending.function + 2 == raw[1]) {
pending.function = 0;
pending.missed = false;
this->update_warning_();
pending.device->receive_response(message);
return;
}
}
// Acknowledgements (odd function codes) are expected and never match
if ((raw[1] & 1) == 0) {
ESP_LOGV(TAG, "Answer %02X with id %u matches no open request", raw[1], message.id);
}
#endif
}
#ifdef DUCO_DEVICE_COUNT
void Duco::update_warning_() {
for (const auto &pending : this->pending_) {
if (pending.missed) {
this->status_set_warning(LOG_STR("No answer from the box"));
return;
}
}
this->status_clear_warning();
}
#endif
void Duco::dump_config() {
ESP_LOGCONFIG(TAG,
"Duco:\n"
" CRC Disabled: %s",
YESNO(this->disable_crc_));
}
uint8_t Duco::next_id_() {
do {
this->last_id_++;
} while (this->last_id_ == HEADER_START || this->last_id_ == HEADER_END);
return this->last_id_;
}
void Duco::send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data) {
if (data.size() > MAX_REQUEST_DATA) {
ESP_LOGE(TAG, "Request of %u bytes is too long", static_cast<unsigned>(data.size()));
return;
}
// Not byte stuffed, as in the author's original and no capture shows a request with 0xAA. An 0xAA followed by any
// byte but 0x01 or 0x55 reads the same with or without unstuffing on the box, so only those rare pairs are
// unverified.
const size_t size = data.size();
const uint8_t id = this->next_id_();
uint8_t frame[5 + MAX_REQUEST_DATA + 2] = {HEADER_START, HEADER_END, static_cast<uint8_t>(size + 2), function, id};
std::copy_n(data.begin(), size, frame + 5);
const uint16_t crc = crc16(frame + 2, size + 3);
frame[size + 5] = crc & 0xFF;
frame[size + 6] = crc >> 8;
this->write_array(frame, size + 7);
this->flush();
#ifdef DUCO_DEVICE_COUNT
for (auto &pending : this->pending_) {
if (pending.device == device) {
pending.missed = pending.function != 0;
if (pending.missed) {
ESP_LOGV(TAG, "No answer to request %02X with id %u", pending.function, pending.id);
this->update_warning_();
}
pending.id = id;
pending.function = function;
break;
}
}
#endif
ESP_LOGV_HEX(TAG, frame + 2, size + 5, sizeof(frame), "Sent %s");
}
static const LogString *node_type_name(uint8_t type) {
switch (type) {
case 8:
return LOG_STR("UCBAT");
case 9:
return LOG_STR("UC");
case 10:
return LOG_STR("UCRH");
case 12:
return LOG_STR("UCCO2");
case 13:
return LOG_STR("VLV");
case 17:
return LOG_STR("BOX");
case 18:
return LOG_STR("SWITCH");
default:
return LOG_STR("UNKNOWN");
}
}
void DucoDiscovery::setup() {
// Give the box time to start before walking its node addresses
this->set_timeout(DISCOVERY_START_ID, DISCOVERY_START_MS, [this]() {
this->set_interval(DISCOVERY_STEP_ID, DISCOVERY_STEP_MS, [this]() { this->update(); });
});
}
void DucoDiscovery::update() {
if (this->attempts_ == DISCOVERY_MAX_ATTEMPTS) {
ESP_LOGV(TAG, "No answer for node %u", this->next_node_);
this->next_node_++;
this->attempts_ = 0;
}
if (this->next_node_ >= NODE_COUNT) {
this->cancel_interval(DISCOVERY_STEP_ID);
this->parent_->close_request(this);
return;
}
// Without an answer by the next step the same node is asked again; a retry is not a missed answer
if (this->attempts_ > 0)
this->parent_->close_request(this);
ESP_LOGV(TAG, "Discovering node %u", this->next_node_);
this->attempts_++;
this->parent_->send(this, FUNCTION_NODE, {0x01, this->next_node_});
}
void DucoDiscovery::receive_response(const DucoMessage &message) {
// Only the latest attempt is open, so a late answer to an earlier one never arrives here
if (message.size >= 1 && this->next_node_ < NODE_COUNT) {
if (message.data[0] != 0) {
ESP_LOGV(TAG, "Found node %u: type %u", this->next_node_, message.data[0]);
}
this->node_types_[this->next_node_++] = message.data[0];
this->attempts_ = 0;
}
}
void DucoDiscovery::dump_config() {
ESP_LOGCONFIG(TAG, "Discovered nodes:");
for (uint8_t node = 0; node < NODE_COUNT; node++) {
const uint8_t type = this->node_types_[node];
if (type != 0) {
ESP_LOGCONFIG(TAG, " Node %u: type %u (%s)", node, type, LOG_STR_ARG(node_type_name(type)));
}
}
if (this->next_node_ < NODE_COUNT) {
ESP_LOGCONFIG(TAG, " Still discovering, at node %u", this->next_node_);
}
}
#ifdef USE_TIME
void DucoTime::update() {
const ESPTime now = this->clock_->now();
if (!now.is_valid()) {
ESP_LOGV(TAG, "Time is not valid yet");
return;
}
const uint32_t ts = now.timestamp;
this->parent_->send(nullptr, FUNCTION_PARAMETER,
{0x05, static_cast<uint8_t>(ts), static_cast<uint8_t>(ts >> 8), static_cast<uint8_t>(ts >> 16),
static_cast<uint8_t>(ts >> 24), 0x00, 0x00, 0x00, 0x00});
}
#endif
} // namespace esphome::duco
-113
View File
@@ -1,113 +0,0 @@
#pragma once
#include <array>
#include <initializer_list>
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome::duco {
// Function codes of the requests; the box answers a request with function + 2
static constexpr uint8_t FUNCTION_NODE = 0x0c; // discovery, status and mode of a node
static constexpr uint8_t FUNCTION_NODE_DATA = 0x10; // a block of a node's data (sensors, serial number)
static constexpr uint8_t FUNCTION_PARAMETER = 0x24; // read or write a box parameter, set the clock
/// A received message; data points into the receive buffer and is valid only during the callback.
struct DucoMessage {
uint8_t id;
const uint8_t *data;
uint8_t size;
};
class DucoDevice;
/// The open request of one device: the answer to it is function + 2 with the same message id.
struct DucoPendingRequest {
DucoDevice *device;
uint8_t id;
uint8_t function; // 0 while nothing is open
bool missed; // the request before this one got no answer; sits in padding
};
class Duco : public uart::UARTDevice, public Component {
public:
void loop() override;
void dump_config() override;
void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; }
#ifdef DUCO_DEVICE_COUNT
void register_device(DucoDevice *device) { this->pending_.push_back({device, 0, 0, false}); }
#endif
/// Forget device's open request, for a device that will not send again.
void close_request(DucoDevice *device);
/// Send a request; its answer goes to device, or nowhere if nullptr. A device's new request replaces its
/// open one, so an answer is never lost to another device's traffic.
void send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data);
protected:
// The longest answer to a request sent here is 25 bytes (node status), the longest request 9
static constexpr size_t MAX_FRAME_SIZE = 32;
static constexpr size_t MAX_REQUEST_DATA = 9;
bool parse_byte_(uint8_t byte);
void reset_frame_();
#ifdef DUCO_DEVICE_COUNT
void update_warning_(); // warn while any device's previous request went unanswered
#endif
void finalize_message_();
uint8_t next_id_();
#ifdef DUCO_DEVICE_COUNT
StaticVector<DucoPendingRequest, DUCO_DEVICE_COUNT> pending_;
#endif
std::array<uint8_t, MAX_FRAME_SIZE> rx_buffer_{};
uint32_t last_byte_{0};
uint8_t rx_size_{0};
uint8_t last_id_{10};
bool escaped_{false}; // the last 0xAA was followed by a stuffing byte, so it starts no header
bool disable_crc_{false};
};
/// Every entity polls the box; deriving from PollingComponent shares its vtable.
class DucoDevice : public PollingComponent, public Parented<Duco> {
public:
virtual void receive_response(const DucoMessage &message) = 0;
};
/// Walks the node addresses once after boot; the nodes found are listed in the config dump.
class DucoDiscovery : public DucoDevice {
public:
void setup() override;
void dump_config() override;
void update() override; // one discovery step, run by the step interval
void receive_response(const DucoMessage &message) override;
protected:
static constexpr uint8_t NODE_COUNT = 0x45;
std::array<uint8_t, NODE_COUNT> node_types_{}; // 0 when no node answered at that address
uint8_t next_node_{0};
uint8_t attempts_{0};
};
#ifdef USE_TIME
class DucoTime : public PollingComponent, public Parented<Duco> {
public:
explicit DucoTime(time::RealTimeClock *clock) : clock_(clock) {}
void update() override;
protected:
time::RealTimeClock *clock_;
};
#endif
} // namespace esphome::duco
@@ -1,34 +0,0 @@
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
DucoComfortTemperature = duco_ns.class_(
"DucoComfortTemperature", DucoDevice, number.Number
)
CONFIG_SCHEMA = (
number.number_schema(
DucoComfortTemperature,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
)
.extend(DUCO_COMPONENT_SCHEMA)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config: ConfigType) -> None:
var = await number.new_number(config, min_value=10.0, max_value=25.5, step=0.1)
await register_duco_device(var, config, config[CONF_DUCO_ID])
-26
View File
@@ -1,26 +0,0 @@
#include "number.h"
#include <cmath>
namespace esphome::duco {
static constexpr uint8_t PARAMETER_COMFORT_TEMPERATURE = 0x12;
static constexpr uint8_t GROUP_COMFORT_TEMPERATURE = 0x0a;
void DucoComfortTemperature::update() {
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE});
}
void DucoComfortTemperature::receive_response(const DucoMessage &message) {
// Reads and writes get the same answer: one byte in tenths of a degree, below 10.0 is invalid
if (message.size >= 4 && message.data[3] >= 100)
this->publish_state(message.data[3] / 10.0f);
}
void DucoComfortTemperature::control(float value) {
const auto tenths = static_cast<uint8_t>(lroundf(value * 10.0f));
this->parent_->send(this, FUNCTION_PARAMETER,
{0x01, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE, tenths, 0x00, 0x00, 0x00});
}
} // namespace esphome::duco
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../duco.h"
namespace esphome::duco {
class DucoComfortTemperature : public DucoDevice, public number::Number {
public:
void update() override;
void receive_response(const DucoMessage &message) override;
protected:
void control(float value) override;
};
} // namespace esphome::duco
@@ -1,45 +0,0 @@
from esphome.components import select
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
# Same order as MODE_CODES in select.cpp
DUCO_MODE_OPTIONS = [
"AUTO",
"MAN1",
"MAN2",
"MAN3",
"EMPT",
"CNT1",
"CNT2",
"CNT3",
"MAN1x2",
"MAN2x2",
"MAN3x2",
"MAN1x3",
"MAN2x3",
"MAN3x3",
]
DucoSelect = duco_ns.class_("DucoSelect", DucoDevice, select.Select)
CONFIG_SCHEMA = (
select.select_schema(DucoSelect)
.extend(cv.polling_component_schema("3s"))
.extend(DUCO_COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await select.new_select(config, options=DUCO_MODE_OPTIONS)
await register_duco_device(var, config, config[CONF_DUCO_ID])
-47
View File
@@ -1,47 +0,0 @@
#include "select.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::duco {
ESPHOME_LOG_TAG(TAG, "duco.select");
// Box code of each option, in the order of DUCO_MODE_OPTIONS in __init__.py
static constexpr uint8_t MODE_CODES[] PROGMEM = {
0x00, // AUTO
0x04, // MAN1
0x05, // MAN2
0x06, // MAN3
0x07, // EMPT
0x08, // CNT1
0x09, // CNT2
0x0a, // CNT3
0x84, // MAN1x2
0x85, // MAN2x2
0x86, // MAN3x2
0xc4, // MAN1x3
0xc5, // MAN2x3
0xc6, // MAN3x3
};
void DucoSelect::update() { this->parent_->send(this, FUNCTION_NODE, {0x02, 0x01}); }
void DucoSelect::receive_response(const DucoMessage &message) {
if (message.size < 1)
return;
for (size_t i = 0; i < sizeof(MODE_CODES); i++) {
if (progmem_read_byte(&MODE_CODES[i]) == message.data[0]) {
this->publish_state(i);
return;
}
}
ESP_LOGV(TAG, "Unknown mode %02X", message.data[0]);
}
void DucoSelect::control(size_t index) {
// The next update reads the new mode back
this->parent_->send(nullptr, FUNCTION_NODE, {0x04, 0x01, progmem_read_byte(&MODE_CODES[index])});
}
} // namespace esphome::duco
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../duco.h"
namespace esphome::duco {
class DucoSelect : public DucoDevice, public select::Select {
public:
void update() override;
void receive_response(const DucoMessage &message) override;
protected:
void control(size_t index) override;
};
} // namespace esphome::duco
-156
View File
@@ -1,156 +0,0 @@
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_CO2,
CONF_HUMIDITY,
CONF_TEMPERATURE,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PARTS_PER_MILLION,
UNIT_PERCENT,
UNIT_SECOND,
)
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
UNIT_DAYS = "d"
CONF_BYPASS = "bypass"
CONF_FILTER_REMAINING = "filter_remaining"
CONF_FLOW_LEVEL = "flow_level"
CONF_TEMPERATURE_EHA = "temperature_eha"
CONF_TEMPERATURE_ETA = "temperature_eta"
CONF_TEMPERATURE_ODA = "temperature_oda"
CONF_TEMPERATURE_SUP = "temperature_sup"
CONF_TIME_REMAINING = "time_remaining"
DucoSensor = duco_ns.class_("DucoSensor", DucoDevice, sensor.Sensor)
DucoReading = duco_ns.enum("DucoReading", is_class=True)
BOX_NODE = 1
def _schema(update_interval: str = "60s", **kwargs) -> cv.Schema:
return sensor.sensor_schema(
DucoSensor, state_class=STATE_CLASS_MEASUREMENT, **kwargs
).extend(cv.polling_component_schema(update_interval))
def _node_schema(**kwargs) -> cv.All:
"""A list of sensors, one per node address."""
return cv.ensure_list(
_schema(**kwargs).extend({cv.Required(CONF_ADDRESS): cv.int_range(0, 68)})
)
_TEMPERATURE = {
"unit_of_measurement": UNIT_CELSIUS,
"accuracy_decimals": 1,
"device_class": DEVICE_CLASS_TEMPERATURE,
}
# key: (what is read, request argument or None for the node address, schema)
SENSORS = {
CONF_CO2: (
DucoReading.DUCO_READING_CO2,
None,
_node_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
),
),
CONF_TEMPERATURE: (
DucoReading.DUCO_READING_NODE_TEMPERATURE,
None,
_node_schema(**_TEMPERATURE),
),
CONF_HUMIDITY: (
DucoReading.DUCO_READING_HUMIDITY,
None,
_node_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
),
),
CONF_FILTER_REMAINING: (
DucoReading.DUCO_READING_FILTER_REMAINING,
0x30,
_schema(
"1200s",
unit_of_measurement=UNIT_DAYS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
),
),
CONF_FLOW_LEVEL: (
DucoReading.DUCO_READING_FLOW_LEVEL,
BOX_NODE,
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
),
CONF_TIME_REMAINING: (
DucoReading.DUCO_READING_TIME_REMAINING,
BOX_NODE,
_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
),
),
CONF_TEMPERATURE_ODA: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x00,
_schema(**_TEMPERATURE),
),
CONF_TEMPERATURE_SUP: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x01,
_schema(**_TEMPERATURE),
),
CONF_TEMPERATURE_ETA: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x02,
_schema(**_TEMPERATURE),
),
CONF_TEMPERATURE_EHA: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x03,
_schema(**_TEMPERATURE),
),
CONF_BYPASS: (
DucoReading.DUCO_READING_BYPASS,
0x10,
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
),
}
CONFIG_SCHEMA = DUCO_COMPONENT_SCHEMA.extend(
{cv.Optional(key): schema for key, (_, _, schema) in SENSORS.items()}
)
async def to_code(config: ConfigType) -> None:
for key, (reading, argument, _) in SENSORS.items():
if (confs := config.get(key)) is None:
continue
for conf in confs if isinstance(confs, list) else [confs]:
var = await sensor.new_sensor(
conf, reading, conf.get(CONF_ADDRESS, argument)
)
await register_duco_device(var, conf, config[CONF_DUCO_ID])
-78
View File
@@ -1,78 +0,0 @@
#include "sensor.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::duco {
ESPHOME_LOG_TAG(TAG, "duco.sensor");
enum class DucoValueFormat : uint8_t {
DUCO_VALUE_FORMAT_UINT8,
DUCO_VALUE_FORMAT_UINT16,
DUCO_VALUE_FORMAT_INT16,
};
/// A little endian field at offset in the answer. Values outside [minimum, maximum] are dropped;
/// the box reports those for sensors that are missing or still starting.
struct ReadingLayout {
uint8_t function;
uint8_t offset;
DucoValueFormat format;
uint8_t divisor;
int16_t minimum;
uint16_t maximum;
};
// Indexed by DucoReading
static constexpr ReadingLayout READINGS[] PROGMEM = {
{FUNCTION_NODE_DATA, 4, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 300, 10000}, // CO2
{FUNCTION_NODE_DATA, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 10, 0, 1000}, // node temperature
{FUNCTION_NODE_DATA, 6, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 100, 0, 10000}, // humidity
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // filter remaining
{FUNCTION_NODE, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // flow level
{FUNCTION_NODE, 12, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 0, 65535}, // time remaining
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_INT16, 10, -999, 1000}, // box temperature
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 100}, // bypass
};
static ReadingLayout layout_of(DucoReading reading) {
ReadingLayout layout;
progmem_memcpy(&layout, &READINGS[static_cast<uint8_t>(reading)], sizeof(layout));
return layout;
}
void DucoSensor::update() {
switch (layout_of(this->reading_).function) {
case FUNCTION_NODE_DATA:
this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, this->argument_, 0x00, 0x49, 0x04});
break;
case FUNCTION_PARAMETER:
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, this->argument_, 0x09});
break;
case FUNCTION_NODE:
this->parent_->send(this, FUNCTION_NODE, {0x02, this->argument_});
break;
}
}
void DucoSensor::receive_response(const DucoMessage &message) {
const ReadingLayout layout = layout_of(this->reading_);
const bool wide = layout.format != DucoValueFormat::DUCO_VALUE_FORMAT_UINT8;
if (message.size < layout.offset + (wide ? 2 : 1)) {
ESP_LOGV(TAG, "Answer of %u bytes is too short", message.size);
return;
}
const uint8_t *field = message.data + layout.offset;
int32_t value = field[0];
if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_UINT16) {
value = encode_value_le<uint16_t>(field);
} else if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_INT16) {
value = encode_value_le<int16_t>(field);
}
if (value >= layout.minimum && value <= layout.maximum)
this->publish_state(static_cast<float>(value) / layout.divisor);
}
} // namespace esphome::duco
-34
View File
@@ -1,34 +0,0 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "../duco.h"
namespace esphome::duco {
/// What a sensor reads; the request and where the value is in the answer are in READINGS in sensor.cpp.
enum class DucoReading : uint8_t {
DUCO_READING_CO2,
DUCO_READING_NODE_TEMPERATURE,
DUCO_READING_HUMIDITY,
DUCO_READING_FILTER_REMAINING,
DUCO_READING_FLOW_LEVEL,
DUCO_READING_TIME_REMAINING,
DUCO_READING_BOX_TEMPERATURE,
DUCO_READING_BYPASS,
};
class DucoSensor : public DucoDevice, public sensor::Sensor {
public:
/// argument is the node address, or the parameter number for a box parameter.
DucoSensor(DucoReading reading, uint8_t argument) : reading_(reading), argument_(argument) {}
void update() override;
void receive_response(const DucoMessage &message) override;
protected:
DucoReading reading_;
uint8_t argument_;
};
} // namespace esphome::duco
@@ -1,27 +0,0 @@
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
DucoSerial = duco_ns.class_("DucoSerial", DucoDevice, text_sensor.TextSensor)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(DucoSerial)
.extend(cv.polling_component_schema("300s"))
.extend(DUCO_COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
await register_duco_device(var, config, config[CONF_DUCO_ID])
@@ -1,17 +0,0 @@
#include "serial.h"
#include <cstring>
namespace esphome::duco {
void DucoSerial::update() { this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, 0x01, 0x00, 0x1a, 0x10}); }
void DucoSerial::receive_response(const DucoMessage &message) {
if (message.size > 2) {
// The serial number starts at byte 2 and is padded with zeros
const char *serial = reinterpret_cast<const char *>(message.data + 2);
this->publish_state(serial, strnlen(serial, message.size - 2));
}
}
} // namespace esphome::duco
@@ -1,15 +0,0 @@
#pragma once
#include "esphome/components/text_sensor/text_sensor.h"
#include "../duco.h"
namespace esphome::duco {
class DucoSerial : public DucoDevice, public text_sensor::TextSensor {
public:
void update() override;
void receive_response(const DucoMessage &message) override;
};
} // namespace esphome::duco
+8 -64
View File
@@ -1,5 +1,4 @@
#include "epaper_spi.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
@@ -38,42 +37,6 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
return true;
}
bool EPaperBase::init_sent_frame_(size_t length) {
if (!this->is_using_partial_update_())
return false;
if (this->sent_.init(length))
return true;
ESP_LOGW(TAG, "No memory for the previous frame; partial updates will send the whole drawn area");
return false;
}
uint32_t EPaperBase::buffer_hash_() const {
uint32_t hash = FNV1_OFFSET_BASIS;
size_t index = 0;
while (index != this->buffer_.size()) {
size_t length;
const uint8_t *data = this->buffer_.get_span(index, length);
for (size_t i = 0; i != length; i++)
hash = fnv1a_hash_extend(hash, data[i]);
index += length;
}
return hash;
}
void EPaperBase::set_full_bounds_() {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaperBase::reset_bounds_() {
this->x_low_ = this->width_;
this->x_high_ = 0;
this->y_low_ = this->height_;
this->y_high_ = 0;
}
void EPaperBase::setup_pins_() const {
for (auto *pin : this->enable_pins_) {
pin->setup();
@@ -270,28 +233,17 @@ void EPaperBase::process_state_() {
// Refresh the whole panel even if nothing was drawn
this->full_update_requested_ = false;
this->update_count_ = 0;
this->set_full_bounds_();
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) {
this->set_state_(EPaperState::IDLE);
return;
}
if (this->is_using_partial_update_()) {
const uint32_t frame_hash = this->buffer_hash_();
if (this->update_count_ != 0 && this->shown_hash_valid_ && frame_hash == this->shown_hash_) {
ESP_LOGD(TAG, "Frame unchanged, refresh skipped");
this->reset_bounds_();
this->set_state_(EPaperState::IDLE);
return;
}
this->shown_hash_ = frame_hash;
}
this->drawn_since_hash_ = false;
// Valid again only once this frame is on the panel, so an abandoned update leaves nothing to match
this->shown_hash_valid_ = false;
// A full update resets update_count_ to zero, so a non-zero count marks a partial one
this->partial_ = this->update_count_ != 0;
this->sent_count_ = 0;
this->set_state_(EPaperState::RESET);
break;
case EPaperState::INITIALISE:
@@ -304,15 +256,10 @@ void EPaperBase::process_state_() {
if (!this->transfer_data()) {
return; // Not done yet, come back next loop
}
if (this->sent_.is_valid() && this->sent_count_ == this->sent_.size())
this->sent_valid_ = true;
// A draw during the chunked push may not have reached the panel, so the next update sends the whole frame
this->sent_hash_intact_ = this->is_using_partial_update_() && !this->drawn_since_hash_;
if (this->drawn_since_hash_) {
this->set_full_bounds_();
} else {
this->reset_bounds_();
}
this->x_low_ = this->width_;
this->x_high_ = 0;
this->y_low_ = this->height_;
this->y_high_ = 0;
this->set_state_(EPaperState::POWER_ON);
break;
case EPaperState::POWER_ON:
@@ -333,7 +280,6 @@ void EPaperBase::process_state_() {
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
this->shown_hash_valid_ = this->sent_hash_intact_;
this->status_clear_warning();
this->set_state_(EPaperState::IDLE);
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
@@ -419,7 +365,6 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
this->drawn_since_hash_ = true;
return true;
}
@@ -541,7 +486,6 @@ void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_star
this->x_high_ = clamp_at_least(this->x_high_, x_high);
this->y_low_ = clamp_at_most(this->y_low_, y_low);
this->y_high_ = clamp_at_least(this->y_high_, y_high);
this->drawn_since_hash_ = true;
}
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
+4 -27
View File
@@ -99,7 +99,10 @@ class EPaperBase : public Display,
// We store 8 pixels per byte
this->buffer_.fill(pixel_color);
this->mark_all_drawn_();
this->x_high_ = this->width_;
this->y_high_ = this->height_;
this->x_low_ = 0;
this->y_low_ = 0;
}
void clear() override {
@@ -136,25 +139,6 @@ class EPaperBase : public Display,
void wait_for_idle_(bool should_wait);
void abandon_update_();
bool init_buffer_(size_t buffer_length);
// Allocates the copy of the previous frame; only drivers that diff against it (needs_previous_frame) call this
bool init_sent_frame_(size_t length);
void record_sent_(size_t index, const uint8_t *data, size_t length) {
if (this->sent_.is_valid()) {
this->sent_.write(index, data, length);
this->sent_count_ += length;
}
}
// Whether the driver reads the previous frame (sent_) for its differential refresh
virtual bool needs_previous_frame() const { return false; }
// FNV-1a hash of the current frame buffer, to detect an unchanged frame without a full copy
uint32_t buffer_hash_() const;
void reset_bounds_();
void set_full_bounds_();
// For a draw that writes the whole buffer without going through rotate_coordinates_()
void mark_all_drawn_() {
this->set_full_bounds_();
this->drawn_since_hash_ = true;
}
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, bool big_endian,
@@ -216,13 +200,6 @@ class EPaperBase : public Display,
size_t buffer_length_;
size_t current_data_index_{}; // used by data transfer to track progress
split_buffer::SplitBuffer buffer_{};
split_buffer::SplitBuffer sent_{}; // the previous frame, kept only by drivers that diff against it
bool sent_valid_{}; // sent_ holds a whole frame
size_t sent_count_{}; // bytes recorded into sent_ by the update in progress
uint32_t shown_hash_{}; // hash of the frame on the panel, to skip an unchanged update
bool shown_hash_valid_{}; // shown_hash_ holds the hash of a frame whose refresh completed
bool sent_hash_intact_{}; // the buffer was unchanged by the end of the push of shown_hash_
bool drawn_since_hash_{}; // something was drawn after shown_hash_ was taken
GPIOPin *dc_pin_{};
GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{};
@@ -18,7 +18,11 @@ void EPaper4bpp::fill(Color color) {
// We store 2 pixels per byte
this->buffer_.fill(pixel_color + (pixel_color << 4));
this->mark_all_drawn_();
// Whole buffer just changed; mark the entire canvas dirty.
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaper4bpp::clear() {
@@ -71,7 +71,10 @@ void EPaperInkplate2::fill(Color color) {
for (size_t i = half_buffer; i < this->buffer_length_; i++)
this->buffer_[i] = red_byte;
this->mark_all_drawn_();
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaperInkplate2::clear() { this->fill(COLOR_ON); }
@@ -30,7 +30,6 @@ void EPaperJD79660::fill(Color color) {
// We store 4 pixels per byte
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
this->drawn_since_hash_ = true;
}
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
@@ -7,12 +7,6 @@
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.mono");
void EPaperMono::setup() {
EPaperBase::setup();
if (!this->is_failed() && this->needs_previous_frame())
this->init_sent_frame_(this->sent_frame_length());
}
void EPaperMono::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
@@ -72,8 +66,7 @@ bool HOT EPaperMono::transfer_data() {
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
size_t data_idx = row_start;
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->send_red_ && !this->send_red_as_image_ ? 0 : this->buffer_[data_idx++];
}
@@ -12,8 +12,6 @@ class EPaperMono : public EPaperBase {
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
void setup() override;
protected:
void refresh_screen(bool partial) override;
void power_on() override {}
@@ -21,8 +19,6 @@ class EPaperMono : public EPaperBase {
void deep_sleep() override;
bool reset() override;
virtual void set_window();
// Bytes in the comparison frame; the buffer's size unless a subclass sends the frame in another layout
virtual size_t sent_frame_length() const { return this->buffer_length_; }
bool transfer_data() override;
bool send_red_{true};
bool software_reset_{true}; // send 0x12 after the hardware reset
@@ -6,6 +6,21 @@
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1677");
void EPaperSSD1677::setup() {
EPaperMono::setup();
if (!this->is_failed())
this->init_comparison_frame_();
}
void EPaperSSD1677::init_comparison_frame_() {
if (!this->is_using_partial_update_())
return;
if (!this->sent_.init(this->plane_row_length_() * this->height_)) {
ESP_LOGW(TAG, "No memory for the comparison frame; partial updates will degrade unchanged areas");
}
}
void EPaperSSD1677::plane_row(size_t y, uint8_t *out) {
const size_t row_length = this->plane_row_length_();
@@ -67,7 +82,7 @@ bool HOT EPaperSSD1677::transfer_data() {
}
this->write_array(data, length);
if (this->plane_ == 1)
this->record_sent_(this->current_data_index_, data, length);
this->sent_.write(this->current_data_index_, data, length);
this->current_data_index_ += length;
if (this->current_data_index_ != plane_length && millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
@@ -26,12 +26,12 @@ class EPaperSSD1677 : public EPaperMono {
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
: EPaperMono(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
void setup() override;
protected:
// Reads the previous frame to copy it into the other RAM bank on a full refresh
bool needs_previous_frame() const override { return true; }
// Allocates the comparison frame as setup() would, for tests that run without a bus.
void init_comparison_frame_() { this->init_sent_frame_(this->sent_frame_length()); }
size_t sent_frame_length() const override { return this->plane_row_length_() * this->height_; }
// Allocates the comparison frame when partial updates are enabled. Separate from setup() so it
// can run without a bus.
void init_comparison_frame_();
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
@@ -39,7 +39,8 @@ class EPaperSSD1677 : public EPaperMono {
bool reset() override;
bool transfer_data() override;
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
split_buffer::SplitBuffer sent_{}; // the frame last sent to 0x24, i.e. what the panel shows
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
};
} // namespace esphome::epaper_spi
@@ -29,7 +29,10 @@ void EPaperSSD1677Gray4::fill(Color color) {
}
const uint8_t level = color_to_gray4(color);
this->buffer_.fill((uint8_t) (level | (level << 2) | (level << 4) | (level << 6)));
this->mark_all_drawn_();
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void HOT EPaperSSD1677Gray4::draw_pixel_at(int x, int y, Color color) {
@@ -80,14 +83,14 @@ bool HOT EPaperSSD1677Gray4::transfer_data() {
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
const size_t src_row = this->current_data_index_ * this->row_width_ + this->x_low_ / 4;
for (size_t i = 0; i != plane_row_length; i++)
bytes_to_send[i] = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
// What the next partial update compares against: the high bits, as a black-and-white frame.
if (first_pass)
this->record_sent_(this->current_data_index_ * plane_row_length, bytes_to_send, plane_row_length);
// The OTP grayscale waveform treats data as inverted relative to monochrome
for (size_t i = 0; i != plane_row_length; i++)
bytes_to_send[i] = (uint8_t) ~bytes_to_send[i];
for (size_t i = 0; i != plane_row_length; i++) {
const uint8_t plane = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
// The OTP grayscale waveform treats data as inverted relative to monochrome
bytes_to_send[i] = (uint8_t) ~plane;
// What the next partial update compares against: the high bits, as a black-and-white frame.
if (first_pass && this->sent_.is_valid())
this->sent_[this->current_data_index_ * plane_row_length + i] = plane;
}
++this->current_data_index_;
this->write_array(bytes_to_send, plane_row_length);
if (millis() - start_time > MAX_TRANSFER_TIME) {
@@ -1,50 +1,10 @@
#include "epaper_spi_ssd1681.h"
#include <algorithm>
#include <cstring>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1681");
void EPaperSSD1681::bounds_from_changes_() {
if (!this->sent_valid_ || this->sent_.size() != this->buffer_.size())
return;
bool changed = false;
uint16_t row_low = this->height_, row_high = 0, col_low = this->row_width_, col_high = 0;
for (uint16_t row = 0; row != this->height_; row++) {
const size_t base = row * this->row_width_;
const size_t end = base + this->row_width_;
size_t index = base;
while (index != end) {
size_t new_length, old_length;
const uint8_t *new_bytes = this->buffer_.get_span(index, new_length);
const uint8_t *old_bytes = this->sent_.get_span(index, old_length);
const size_t length = std::min({new_length, old_length, end - index});
if (std::memcmp(new_bytes, old_bytes, length) != 0) {
for (size_t i = 0; i != length; i++) {
if (new_bytes[i] == old_bytes[i])
continue;
const auto col = static_cast<uint16_t>(index + i - base);
changed = true;
row_low = std::min(row_low, row);
row_high = std::max<uint16_t>(row_high, row + 1);
col_low = std::min(col_low, col);
col_high = std::max<uint16_t>(col_high, col + 1);
}
}
index += length;
}
}
if (!changed)
return;
this->x_low_ = col_low * 8;
this->x_high_ = std::min<uint16_t>(col_high * 8, this->width_);
this->y_low_ = row_low;
this->y_high_ = row_high;
}
bool HOT EPaperSSD1681::transfer_data() {
if (this->current_data_index_ == 0 && this->send_red_) {
if (!this->partial_) {
@@ -55,7 +15,6 @@ bool HOT EPaperSSD1681::transfer_data() {
this->y_high_ = this->height_;
} else {
// 0x26 already holds the image on the panel: the controller copies 0x24 into it after each refresh
this->bounds_from_changes_();
this->set_window();
this->send_red_ = false;
}
@@ -16,10 +16,6 @@ class EPaperSSD1681 : public EPaperSSD1683 {
}
protected:
// Reads the previous frame to compute the changed window for a partial refresh
bool needs_previous_frame() const override { return true; }
// Shrinks the update bounds to the bytes that differ from the frame last sent
void bounds_from_changes_();
bool transfer_data() override;
void refresh_screen(bool partial) override;
};
@@ -71,15 +71,12 @@ bool HOT EPaperSSD1683::transfer_data() {
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_;
size_t data_idx = row_start;
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->buffer_[data_idx++];
}
++this->current_data_index_;
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (!this->send_red_)
this->record_sent_(row_start, &bytes_to_send.front(), row_length);
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
@@ -232,7 +232,6 @@ void EPaperT133A01::fill(Color color) {
}
auto pixel_color = color_to_index(color);
this->buffer_.fill(pixel_color + (pixel_color << 4));
this->drawn_since_hash_ = true;
}
void EPaperT133A01::draw_pixel_at(int x, int y, Color color) {
@@ -1,5 +1,4 @@
#include "epaper_waveshare_bwr.h"
#include "esphome/core/helpers.h"
#include <algorithm>
@@ -37,9 +36,17 @@ void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
const auto bwr = color_to_bwr(color);
assign_bits(this->buffer_[pos], bit, bwr == BwrState::BWR_BLACK);
if (bwr == BwrState::BWR_BLACK) {
this->buffer_[pos] |= bit;
} else {
this->buffer_[pos] &= ~bit;
}
assign_bits(this->buffer_[red_offset + pos], bit, bwr == BwrState::BWR_RED);
if (bwr == BwrState::BWR_RED) {
this->buffer_[red_offset + pos] |= bit;
} else {
this->buffer_[red_offset + pos] &= ~bit;
}
}
void EPaperWaveshareBWR::fill(Color color) {
+5 -2
View File
@@ -1,7 +1,6 @@
#include "es8156.h"
#include "es8156_const.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -101,7 +100,11 @@ bool ES8156::set_mute_state_(bool mute_state) {
ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13);
assign_bits(reg13, BIT(1) | BIT(2), mute_state);
if (mute_state) {
reg13 |= BIT(1) | BIT(2);
} else {
reg13 &= ~(BIT(1) | BIT(2));
}
ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state));
return this->write_byte(ES8156_REG13_DAC_MUTE, reg13);
+10 -3
View File
@@ -1,7 +1,6 @@
#include "es8311.h"
#include "es8311_const.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -155,7 +154,11 @@ bool ES8311::configure_clock_() {
// Register 0x06
uint8_t reg06;
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, &reg06));
assign_bits(reg06, BIT(5), this->sclk_inverted_);
if (this->sclk_inverted_) {
reg06 |= BIT(5);
} else {
reg06 &= ~BIT(5);
}
reg06 &= 0xE0;
if (coefficient->bclk_div < 19) {
reg06 |= (coefficient->bclk_div - 1) << 0;
@@ -220,7 +223,11 @@ bool ES8311::set_mute_state_(bool mute_state) {
return false;
}
assign_bits(reg31, BIT(6) | BIT(5), mute_state);
if (mute_state) {
reg31 |= BIT(6) | BIT(5);
} else {
reg31 &= ~(BIT(6) | BIT(5));
}
return this->write_byte(ES8311_REG31_DAC, reg31);
}
+5 -2
View File
@@ -2,7 +2,6 @@
#include <cinttypes>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::es8388 {
@@ -185,7 +184,11 @@ bool ES8388::set_mute_state_(bool mute_state) {
// DAC settings that must be preserved. Previously muting overwrote the whole
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
// could not be unmuted again.
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
if (mute_state) {
value |= ES8388_DACCONTROL3_DAC_MUTE;
} else {
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
}
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
return this->write_byte(ES8388_DACCONTROL3, value);
+5 -2
View File
@@ -1,6 +1,5 @@
#include "gree.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
namespace esphome::gree {
@@ -25,7 +24,11 @@ void GreeClimate::set_model(Model model) {
}
void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
assign_bits(this->mode_bits_, bit_mask, enabled);
if (enabled) {
this->mode_bits_ |= bit_mask;
} else {
this->mode_bits_ &= ~bit_mask;
}
this->transmit_state();
}
@@ -1,5 +1,4 @@
#include "hitachi_ac344.h"
#include "esphome/core/helpers.h"
namespace esphome::hitachi_ac344 {
@@ -17,6 +16,15 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
*dst |= ((data & mask) << offset);
}
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
uint8_t mask = 1 << position;
if (on) {
*data |= mask;
} else {
*data &= ~mask;
}
}
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
for (uint16_t i = 1; i < length; i += 2) {
// Code done this way to avoid a compiler warning bug.
@@ -116,7 +124,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC3
void HitachiClimate::set_swing_v_(bool on) {
set_swing_v_toggle_(on); // Set the button value.
assign_bits(this->remote_state_[HITACHI_AC344_SWINGV_BYTE], 1 << HITACHI_AC344_SWINGV_OFFSET, on);
set_bit(&remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET, on);
}
bool HitachiClimate::get_swing_v_() {
@@ -1,5 +1,4 @@
#include "hitachi_ac424.h"
#include "esphome/core/helpers.h"
namespace esphome::hitachi_ac424 {
@@ -17,6 +16,15 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
*dst |= ((data & mask) << offset);
}
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
uint8_t mask = 1 << position;
if (on) {
*data |= mask;
} else {
*data &= ~mask;
}
}
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
for (uint16_t i = 1; i < length; i += 2) {
// Code done this way to avoid a compiler warning bug.
@@ -116,7 +124,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC4
void HitachiClimate::set_swing_v_(bool on) {
set_swing_v_toggle_(on); // Set the button value.
assign_bits(this->remote_state_[HITACHI_AC424_SWINGV_BYTE], 1 << HITACHI_AC424_SWINGV_OFFSET, on);
set_bit(&remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET, on);
}
bool HitachiClimate::get_swing_v_() {
+10 -12
View File
@@ -45,15 +45,14 @@ void HmacSHA256::get_hex(char *output) {
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
return constant_time_equals(this->digest_, expected, SHA256_DIGEST_SIZE);
return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0;
}
bool HmacSHA256::equals_hex(const char *expected) {
uint8_t parsed[SHA256_DIGEST_SIZE];
if (parse_hex(expected, SHA256_DIGEST_SIZE * 2, parsed, SHA256_DIGEST_SIZE) != SHA256_DIGEST_SIZE * 2) {
return false;
}
return this->equals_bytes(parsed);
char hex_output[SHA256_DIGEST_SIZE * 2 + 1];
this->get_hex(hex_output);
hex_output[SHA256_DIGEST_SIZE * 2] = '\0';
return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0;
}
#elif defined(USE_HMAC_SHA256_MBEDTLS)
@@ -78,15 +77,14 @@ void HmacSHA256::get_hex(char *output) {
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
return constant_time_equals(this->digest_, expected, SHA256_DIGEST_SIZE);
return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0;
}
bool HmacSHA256::equals_hex(const char *expected) {
uint8_t parsed[SHA256_DIGEST_SIZE];
if (parse_hex(expected, SHA256_DIGEST_SIZE * 2, parsed, SHA256_DIGEST_SIZE) != SHA256_DIGEST_SIZE * 2) {
return false;
}
return this->equals_bytes(parsed);
char hex_output[SHA256_DIGEST_SIZE * 2 + 1];
this->get_hex(hex_output);
hex_output[SHA256_DIGEST_SIZE * 2] = '\0';
return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0;
}
#else
+55 -15
View File
@@ -3,36 +3,67 @@
namespace esphome::max17043 {
// MAX17043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
// Consult the datasheet at https://www.analog.com/en/products/max17043.html
ESPHOME_LOG_TAG(TAG, "max17043");
void MAX17043Component::update() {
if (!this->ready_for_update_())
return;
static const uint8_t MAX17043_VCELL = 0x02;
static const uint8_t MAX17043_SOC = 0x04;
static const uint8_t MAX17043_CONFIG = 0x0c;
uint16_t raw;
bool ok = true;
static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C;
static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4)
static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080;
void MAX17043Component::update() {
uint16_t raw_voltage, raw_percent;
if (this->voltage_sensor_ != nullptr) {
if (this->read_byte_16(max170xx::REG_VCELL, &raw)) {
// 12 bits in the top of the word at 1.25 mV
this->voltage_sensor_->publish_state((raw >> 4) * 1.25e-3f);
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL"));
} else {
ok = false;
float voltage = (1.25f * (float) (raw_voltage >> 4)) / 1000.0f;
this->voltage_sensor_->publish_state(voltage);
this->status_clear_warning();
}
}
if (this->battery_remaining_sensor_ != nullptr) {
if (this->read_byte_16(max170xx::REG_SOC, &raw)) {
float percent = (float) ((raw >> 8) + 0.003906f * (raw & 0x00ff));
this->battery_remaining_sensor_->publish_state(percent);
if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) {
this->status_set_warning(LOG_STR("Unable to read MAX17043_SOC"));
} else {
ok = false;
float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff));
this->battery_remaining_sensor_->publish_state(percent);
this->status_clear_warning();
}
}
}
this->status_comm_result(ok, LOG_STR("Register read failed"));
void MAX17043Component::setup() {
uint16_t config_reg;
if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
if (this->read(reinterpret_cast<uint8_t *>(&config_reg), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK;
ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg);
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
this->mark_failed(LOG_STR("Device does not appear to be a MAX17043"));
return;
}
// need to write back to config register to reset the sleep bit
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
this->mark_failed(LOG_STR("sleep reset failed"));
return;
}
}
void MAX17043Component::dump_config() {
@@ -43,4 +74,13 @@ void MAX17043Component::dump_config() {
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
}
void MAX17043Component::sleep_mode() {
if (!this->is_failed()) {
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
this->status_set_warning();
}
}
}
} // namespace esphome::max17043
+7 -4
View File
@@ -1,18 +1,21 @@
#pragma once
#include "esphome/components/max170xx/max170xx.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::max17043 {
class MAX17043Component final : public max170xx::MAX170xxComponent<max170xx::Model::MODEL_MAX17043> {
class MAX17043Component final : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void update() override;
void sleep_mode();
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
this->battery_remaining_sensor_ = battery_remaining_sensor;
battery_remaining_sensor_ = battery_remaining_sensor;
}
protected:
-1
View File
@@ -16,7 +16,6 @@ from esphome.const import (
)
from esphome.types import ConfigType
AUTO_LOAD = ["max170xx"]
DEPENDENCIES = ["i2c"]
max17043_ns = cg.esphome_ns.namespace("max17043")
+1 -1
View File
@@ -1,2 +1,2 @@
CODEOWNERS = ["@B48D81EFCC", "@ryansch"]
CODEOWNERS = ["@B48D81EFCC"]
DOMAIN = "max17048"
+19 -5
View File
@@ -8,27 +8,41 @@ namespace esphome::max17048 {
ESPHOME_LOG_TAG(TAG, "max17048");
static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed
static constexpr uint8_t REG_VCELL = 0x02; // cell voltage
static constexpr uint8_t REG_SOC = 0x04; // state of charge
static constexpr uint8_t REG_VERSION = 0x08; // 0x001x
static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed
static constexpr uint16_t VERSION_MASK = 0xFFF0;
static constexpr uint16_t VERSION_FAMILY = 0x0010;
static constexpr float VCELL_VOLTS_PER_LSB = 78.125e-6f;
static constexpr float SOC_PERCENT_PER_LSB = 1.0f / 256.0f;
static constexpr float CRATE_PERCENT_PER_HOUR_PER_LSB = 0.208f;
void MAX17048Component::update() {
if (!this->ready_for_update_())
void MAX17048Component::setup() {
uint16_t version;
if (!this->read_byte_16(REG_VERSION, &version)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if ((version & VERSION_MASK) != VERSION_FAMILY) {
ESP_LOGW(TAG, "Unexpected version 0x%04X", version);
}
}
void MAX17048Component::update() {
bool ok = true;
uint16_t raw;
if (this->battery_voltage_sensor_ != nullptr) {
if (this->read_byte_16(max170xx::REG_VCELL, &raw)) {
if (this->read_byte_16(REG_VCELL, &raw)) {
this->battery_voltage_sensor_->publish_state(raw * VCELL_VOLTS_PER_LSB);
} else {
ok = false;
}
}
if (this->battery_level_sensor_ != nullptr) {
if (this->read_byte_16(max170xx::REG_SOC, &raw)) {
if (this->read_byte_16(REG_SOC, &raw)) {
this->battery_level_sensor_->publish_state(raw * SOC_PERCENT_PER_LSB);
} else {
ok = false;
+4 -2
View File
@@ -1,16 +1,18 @@
#pragma once
#include "esphome/components/max170xx/max170xx.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::max17048 {
class MAX17048Component final : public max170xx::MAX170xxComponent<max170xx::Model::MODEL_MAX17048> {
class MAX17048Component final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(battery_voltage)
SUB_SENSOR(battery_level)
SUB_SENSOR(rate)
public:
void setup() override;
void dump_config() override;
void update() override;
};
-16
View File
@@ -1,12 +1,9 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_BATTERY_VOLTAGE,
CONF_ID,
CONF_RATE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_VOLTAGE,
@@ -17,7 +14,6 @@ from esphome.const import (
)
from esphome.types import ConfigType
AUTO_LOAD = ["max170xx"]
DEPENDENCIES = ["i2c"]
UNIT_PERCENT_PER_HOUR = "%/h"
@@ -65,15 +61,3 @@ async def to_code(config: ConfigType) -> None:
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage_sensor)
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
await sensors(CONF_RATE, var.set_rate_sensor)
MAX17048_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(MAX17048Component),
}
)
automation.register_apply_action(
"max17048.sleep_mode", MAX17048_ACTION_SCHEMA, automation.ApplyCall("sleep_mode()")
)
-3
View File
@@ -1,3 +0,0 @@
CODEOWNERS = ["@ryansch"]
DEPENDENCIES = ["i2c"]
DOMAIN = "max170xx"
-87
View File
@@ -1,87 +0,0 @@
#include "max170xx.h"
#include "esphome/core/log.h"
namespace esphome::max170xx {
ESPHOME_LOG_TAG(TAG, "max170xx");
template<Model MODEL> void MAX170xxComponent<MODEL>::setup() {
// A silent bus is retried from update(): some boards gate the I2C rail until after setup.
// An early on_boot may already have configured the gauge and put it to sleep, so this must not configure again.
this->ready_for_update_();
}
template<Model MODEL> bool MAX170xxComponent<MODEL>::ready_for_update_() {
if (this->configured_ || this->configure_())
return true;
this->status_set_warning(LOG_STR("Gauge did not answer"));
return false;
}
template<Model MODEL> bool MAX170xxComponent<MODEL>::configure_() {
uint16_t config;
if (!this->read_byte_16(REG_CONFIG, &config))
return false;
const bool was_asleep = (config & CONFIG_SLEEP) != 0;
// RCOMP and ATHD survive on the battery, so an earlier firmware's values are reset here, not treated as a fault
if ((config & CONFIG_SAFE_MASK) != CONFIG_POWER_UP_DEFAULT) {
ESP_LOGW(TAG, "CONFIG reads 0x%04X, expected 0x%04X; resetting it", config & CONFIG_SAFE_MASK,
CONFIG_POWER_UP_DEFAULT);
}
uint16_t version;
if (this->read_byte_16(REG_VERSION, &version)) {
ESP_LOGCONFIG(TAG, "IC version: 0x%04X", version);
}
// Write back the power-up value to clear a stale sleep bit; a failed write is retried like a silent bus
if (!this->write_byte_16(REG_CONFIG, CONFIG_POWER_UP_DEFAULT))
return false;
this->configured_ = true;
// Waking from sleep raises a false low-battery alert; a real one at an ordinary boot is left alone
if (was_asleep)
this->set_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID, WAKE_ALERT_CLEAR_DELAY_MS, [this]() { this->clear_wake_alert_(); });
return true;
}
template<Model MODEL> void MAX170xxComponent<MODEL>::clear_wake_alert_() {
uint16_t config;
if (!this->read_byte_16(REG_CONFIG, &config) || !this->write_byte_16(REG_CONFIG, config & ~CONFIG_ALRT)) {
this->status_set_warning(LOG_STR("Wake alert clear failed"));
return;
}
if constexpr (MODEL == Model::MODEL_MAX17048) {
uint16_t status;
if (!this->read_byte_16(REG_STATUS, &status) || !this->write_byte_16(REG_STATUS, status & ~STATUS_HD))
this->status_set_warning(LOG_STR("Wake alert clear failed"));
}
}
template<Model MODEL> void MAX170xxComponent<MODEL>::sleep_mode() {
if (this->is_failed())
return;
// The deferred configuration clears the sleep bit, so it has to run before the gauge is put to sleep
if (!this->ready_for_update_())
return;
// A clear still pending would otherwise run against a sleeping gauge
if (this->cancel_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID))
this->clear_wake_alert_();
if constexpr (MODEL == Model::MODEL_MAX17048) {
if (!this->write_byte_16(REG_MODE, MODE_EN_SLEEP)) {
this->status_set_warning(LOG_STR("Unable to enable sleep mode in the mode register"));
return;
}
}
if (!this->write_byte_16(REG_CONFIG, CONFIG_POWER_UP_DEFAULT | CONFIG_SLEEP))
this->status_set_warning(LOG_STR("Unable to write the sleep bit to config register"));
}
template class MAX170xxComponent<Model::MODEL_MAX17043>;
template class MAX170xxComponent<Model::MODEL_MAX17048>;
} // namespace esphome::max170xx
-61
View File
@@ -1,61 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::max170xx {
// Shared base for the ModelGauge fuel gauges.
// MAX17043: https://www.analog.com/en/products/max17043.html
// MAX17048: https://www.analog.com/en/products/max17048.html
// The MAX17048 is register-compatible with the MAX17043, and adds a STATUS register and a MODE.EnSleep gate in
// front of CONFIG.SLEEP.
static constexpr uint8_t REG_VCELL = 0x02;
static constexpr uint8_t REG_SOC = 0x04;
static constexpr uint8_t REG_MODE = 0x06;
static constexpr uint8_t REG_VERSION = 0x08;
static constexpr uint8_t REG_CONFIG = 0x0C;
static constexpr uint8_t REG_STATUS = 0x1A;
static constexpr uint16_t CONFIG_POWER_UP_DEFAULT = 0x971C;
// Mask out SLEEP (7), ALRT (5), and bit 6, which is a don't-care on the MAX17043 but ALSC on the MAX17048.
static constexpr uint16_t CONFIG_SAFE_MASK = 0xFF1F;
static constexpr uint16_t CONFIG_SLEEP = 0x0080;
static constexpr uint16_t CONFIG_ALRT = 0x0020;
// CONFIG.SLEEP is ignored unless MODE.EnSleep (bit 13) is set first.
static constexpr uint16_t MODE_EN_SLEEP = 0x2000;
static constexpr uint16_t STATUS_HD = 0x1000;
static constexpr uint32_t WAKE_ALERT_CLEAR_TIMEOUT_ID = 0;
// The gauge raises its false wake alert some time after the wake write; on hardware it rose 0.8 to 1.0 s later
static constexpr uint32_t WAKE_ALERT_CLEAR_DELAY_MS = 2000;
enum class Model : uint8_t {
MODEL_MAX17043,
MODEL_MAX17048,
};
/// Defined in max170xx.cpp, which instantiates both models.
template<Model MODEL> class MAX170xxComponent : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void sleep_mode();
protected:
/// True once the gauge is configured; otherwise retries the configuration and warns if the gauge did not answer.
/// Call first in update().
bool ready_for_update_();
/// Verify and wake the gauge; false if it did not answer.
bool configure_();
/// Clear the false alert raised on waking, leaving every other CONFIG and STATUS bit as it is.
void clear_wake_alert_();
bool configured_{false};
};
extern template class MAX170xxComponent<Model::MODEL_MAX17043>;
extern template class MAX170xxComponent<Model::MODEL_MAX17048>;
} // namespace esphome::max170xx
+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);
@@ -231,30 +231,47 @@ void OnlineImage::loop() {
this->end_connection_();
this->download_error_callback_.call();
return;
} else if (this->downloader_->is_read_complete()) {
// The response has ended, so data left in the buffer can never be completed
size_t unread = this->download_buffer_.unread();
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
if (this->is_decode_finished() || !this->decoder_knows_end()) {
this->finish_download_();
} else {
// Nothing new arrived. The decoder works for a limited time per call, so it may still have data to use.
if (this->download_buffer_.unread() != 0) {
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
if (consumed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", esphome::runtime_image::decode_error_to_string(consumed));
this->end_connection_();
this->download_error_callback_.call();
return;
}
if (consumed > 0) {
this->download_buffer_.read(consumed);
this->last_data_time_ = App.get_loop_component_start_time();
return;
}
ESP_LOGE(TAG, "Download ended before the image was complete");
} else if (unread == 0) {
ESP_LOGE(TAG, "Server sent an empty response");
} else {
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
}
this->end_connection_();
this->download_error_callback_.call();
return;
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
// ESP-IDF keeps returning 0 after the server closes the connection early
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->end_connection_();
this->download_error_callback_.call();
return;
if (this->downloader_->is_read_complete()) {
// The response has ended, so data left in the buffer can never be completed
size_t unread = this->download_buffer_.unread();
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
if (this->is_decode_finished() || !this->decoder_knows_end()) {
this->finish_download_();
return;
}
ESP_LOGE(TAG, "Download ended before the image was complete");
} else if (unread == 0) {
ESP_LOGE(TAG, "Server sent an empty response");
} else {
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
}
this->end_connection_();
this->download_error_callback_.call();
return;
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
// ESP-IDF keeps returning 0 after the server closes the connection early
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->end_connection_();
this->download_error_callback_.call();
return;
}
}
} else {
// Buffer is full, need to decode some data first
+16 -16
View File
@@ -33,22 +33,22 @@ uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; }
int16_t parse_s16(OpenthermData &data) { return data.get_s16(); }
float parse_f88(OpenthermData &data) { return data.get_f88(); }
void write_flag8_lb_0(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 0, value); }
void write_flag8_lb_1(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 1, value); }
void write_flag8_lb_2(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 2, value); }
void write_flag8_lb_3(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 3, value); }
void write_flag8_lb_4(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 4, value); }
void write_flag8_lb_5(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 5, value); }
void write_flag8_lb_6(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 6, value); }
void write_flag8_lb_7(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 7, value); }
void write_flag8_hb_0(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 0, value); }
void write_flag8_hb_1(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 1, value); }
void write_flag8_hb_2(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 2, value); }
void write_flag8_hb_3(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 3, value); }
void write_flag8_hb_4(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 4, value); }
void write_flag8_hb_5(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 5, value); }
void write_flag8_hb_6(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 6, value); }
void write_flag8_hb_7(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 7, value); }
void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); }
void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); }
void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); }
void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); }
void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); }
void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); }
void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); }
void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); }
void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); }
void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); }
void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); }
void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); }
void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); }
void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); }
void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); }
void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); }
void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; }
void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; }
void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; }
+8
View File
@@ -20,6 +20,14 @@ namespace esphome::opentherm {
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
}
enum OperationMode {
IDLE = 0, // no operation
@@ -1,56 +0,0 @@
from typing import Any
import esphome.codegen as cg
from esphome.components.const import CONF_MAX_PACKET_SIZE
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@clydebarrow"]
DOMAIN = "packet_interface"
IS_PLATFORM_COMPONENT = True
CONF_PACKET_INTERFACE = "packet_interface"
packet_interface_ns = cg.esphome_ns.namespace("packet_interface")
PacketInterface = packet_interface_ns.class_("PacketInterface", cg.Component)
__all__ = [
"CONF_MAX_PACKET_SIZE",
"CONF_PACKET_INTERFACE",
"PacketInterface",
"new_packet_interface",
"packet_interface_ns",
"packet_interface_schema",
]
def packet_interface_schema(
class_: MockObjClass,
default_max_packet_size: int,
max_packet_size_limit: int = 4096,
) -> cv.Schema:
"""Schema for a link platform."""
return cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.Optional(
CONF_MAX_PACKET_SIZE, default=default_max_packet_size
): cv.int_range(16, max_packet_size_limit),
}
).extend(cv.COMPONENT_SCHEMA)
async def new_packet_interface(config: ConfigType, *args: Any) -> MockObj:
"""Create the link layer.
The link's constructor takes its maximum packet size, usually as a std::span over a buffer that code generation
sized from the configuration, so the two cannot differ.
Returns the link layer; applications reach the top layer with PacketInterface::top().
"""
var = cg.new_Pvariable(config[CONF_ID], *args)
cg.add(var.set_name(config[CONF_ID].id))
await cg.register_component(var, config)
return var
@@ -1,173 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <vector>
namespace esphome::packet_interface {
/**
* A buffer class that efficiently represents a composite buffer with three sections:
* - pre_buffer: A pointer to uint8_t data with a length
* - mid_buffer: A reference to another PacketBuffer (optional)
* - post_buffer: A pointer to uint8_t data with a length
*
* This allows efficient zero-copy composition of buffers without allocating new memory.
*
* WARNING: PacketBuffer holds raw pointers to data. The user must ensure that the data remains valid
* for the lifetime of the PacketBuffer instance. Storing PacketBuffer instances beyond the scope of the data they
* reference will lead to dangling pointers and undefined behavior, so to keep a packet, copy it with copy_to() into a
* buffer allocated in setup().
*/
class PacketBuffer {
public:
PacketBuffer() = default;
// Disable copy semantics - PacketBuffer contains pointers and copying would create dangling references
PacketBuffer(const PacketBuffer &) = delete;
PacketBuffer &operator=(const PacketBuffer &) = delete;
// Enable move semantics
PacketBuffer(PacketBuffer &&) = default;
PacketBuffer &operator=(PacketBuffer &&) = default;
/**
* Construct from a single buffer.
*/
PacketBuffer(const uint8_t *data, size_t length) : pre_buffer_(data), pre_length_(length) {}
/**
* Construct from a vector.
*/
explicit PacketBuffer(const std::vector<uint8_t> &vec) : pre_buffer_(vec.data()), pre_length_(vec.size()) {}
// A temporary vector would leave the buffer dangling
explicit PacketBuffer(std::vector<uint8_t> &&) = delete;
/**
* Construct with pre and post buffers.
*/
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const uint8_t *post_data, size_t post_length)
: pre_buffer_(pre_data), pre_length_(pre_length), post_buffer_(post_data), post_length_(post_length) {}
/**
* Construct with pre, mid, and post buffers.
*/
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const PacketBuffer *mid_buffer, const uint8_t *post_data,
size_t post_length)
: pre_buffer_(pre_data),
pre_length_(pre_length),
mid_buffer_(mid_buffer),
post_buffer_(post_data),
post_length_(post_length) {}
/**
* Get the total size of the buffer.
*/
size_t size() const {
size_t total = this->pre_length_ + this->post_length_;
if (this->mid_buffer_ != nullptr) {
total += this->mid_buffer_->size();
}
return total;
}
/**
* Check if the buffer is empty.
*/
bool empty() const { return this->size() == 0; }
/**
* Check if all the data is held in the pre-buffer, so it can be read as one span.
*/
bool is_contiguous() const { return this->mid_buffer_ == nullptr && this->post_length_ == 0; }
/**
* Get the data as one span. Only valid when is_contiguous() is true.
*/
std::span<const uint8_t> as_span() const { return {this->pre_buffer_, this->pre_length_}; }
/**
* Access a byte at a specific position.
* @param index The position to access
* @return The byte at the specified position, or 0 past the end
*/
uint8_t operator[](size_t index) const {
if (index < this->pre_length_) {
return this->pre_buffer_[index];
}
index -= this->pre_length_;
if (this->mid_buffer_ != nullptr) {
size_t mid_size = this->mid_buffer_->size();
if (index < mid_size) {
return (*this->mid_buffer_)[index];
}
index -= mid_size;
}
return index < this->post_length_ ? this->post_buffer_[index] : 0;
}
/**
* Copy data from the buffer to a destination pointer.
* @param dest Destination pointer
* @param count Number of bytes to copy
* @param offset Offset in the source buffer to start copying from
* @return Number of bytes actually copied
*/
size_t copy_to(uint8_t *dest, size_t count, size_t offset = 0) const {
size_t total_size = this->size();
if (offset >= total_size) {
return 0;
}
size_t available = total_size - offset;
size_t to_copy = count < available ? count : available;
size_t copied = 0;
// Copy from pre_buffer
if (offset < this->pre_length_) {
size_t pre_offset = offset;
size_t pre_available = this->pre_length_ - pre_offset;
size_t pre_copy = to_copy < pre_available ? to_copy : pre_available;
std::memcpy(dest, this->pre_buffer_ + pre_offset, pre_copy);
copied += pre_copy;
dest += pre_copy;
offset += pre_copy;
}
// Copy from mid_buffer
size_t mid_size = this->mid_buffer_ != nullptr ? this->mid_buffer_->size() : 0;
if (copied < to_copy && offset < this->pre_length_ + mid_size) {
size_t mid_offset = offset - this->pre_length_;
size_t mid_available = mid_size - mid_offset;
size_t mid_copy = (to_copy - copied) < mid_available ? (to_copy - copied) : mid_available;
this->mid_buffer_->copy_to(dest, mid_copy, mid_offset);
copied += mid_copy;
dest += mid_copy;
offset += mid_copy;
}
// Copy from post_buffer
if ((copied < to_copy) && (this->post_length_ > 0)) {
size_t post_offset = offset - this->pre_length_ - mid_size;
size_t post_available = this->post_length_ - post_offset;
size_t post_copy = (to_copy - copied) < post_available ? (to_copy - copied) : post_available;
std::memcpy(dest, this->post_buffer_ + post_offset, post_copy);
copied += post_copy;
}
return copied;
}
private:
const uint8_t *pre_buffer_{nullptr};
size_t pre_length_{0};
const PacketBuffer *mid_buffer_{nullptr};
const uint8_t *post_buffer_{nullptr};
size_t post_length_{0};
};
} // namespace esphome::packet_interface
@@ -1,39 +0,0 @@
#include "packet_interface.h"
#include "esphome/core/log.h"
namespace esphome::packet_interface {
ESPHOME_LOG_TAG(TAG, "packet_interface");
// Packets in a row that cannot be understood before the mismatch is logged
static constexpr uint8_t MISMATCH_COUNT = 10;
void PacketInterface::dump_config() {
ESP_LOGCONFIG(TAG,
"Packet Interface '%s':\n"
" Max Packet Size: %zu",
this->name_, this->get_max_packet_size());
}
void PacketInterface::on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
static constexpr size_t LOG_BYTES = 24;
uint8_t head[LOG_BYTES];
size_t len = data.copy_to(head, LOG_BYTES);
ESP_LOGV_HEX(TAG, head, len, LOG_BYTES, "%s: received %zu bytes %s", this->name_, data.size());
#endif
this->callback_.call(data, meta_data);
}
void PacketInterface::note_received_(bool understood) {
if (understood) {
this->not_understood_count_ = 0;
} else if (this->not_understood_count_ < MISMATCH_COUNT) {
this->not_understood_count_++;
if (this->not_understood_count_ == MISMATCH_COUNT) {
ESP_LOGW(TAG, "%s: %u packets in a row could not be understood; check that both ends have the same layers",
this->name_, MISMATCH_COUNT);
}
}
}
} // namespace esphome::packet_interface
@@ -1,149 +0,0 @@
#pragma once
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <utility>
#include "esphome/core/string_ref.h"
#include "esphome/core/helpers.h"
#include "esphome/core/component.h"
#include "packet_buffer.h"
namespace esphome::packet_interface {
/// The room left for a packet of \p size bytes after \p used bytes, or 0 if it is too small.
constexpr size_t remaining_packet_size(size_t size, size_t used) { return size > used ? size - used : 0; }
/// What a stack of layers provides, as bits of PacketInterface::get_capabilities().
enum Capability : uint8_t {
CAPABILITY_ENCRYPTION = 1 << 0,
CAPABILITY_RELIABILITY = 1 << 1,
CAPABILITY_SESSION = 1 << 2,
CAPABILITY_ADDRESSING = 1 << 3,
CAPABILITY_FRAGMENTATION = 1 << 4,
};
/**
* A struct to hold metadata about a packet.
* @param info Additional information about the packet
* @param source_address With an addressing layer, the sender of a received packet; 0 if not known
* @param destination_address With an addressing layer, the destination of a packet; when sending, 0 uses the
* configured destination
* @param rssi The RSSI of the packet
* @param snr The SNR of the packet
* @param mac_address The MAC address of the sender/receiver
*/
class PacketMetaData {
public:
StringRef info{};
uint16_t source_address{0};
uint16_t destination_address{0};
float rssi{NAN};
float snr{NAN};
uint8_t mac_address[MAC_ADDRESS_SIZE]{};
};
/**
* A class that will transport data to and from an abstract channel.
*
* The operations defined are:
*
* Transmit: the function send_to_interface() takes a PacketBuffer of bytes, and returns when all data is
* written or buffered for transmission.
*
* Receive: The callbacks for an instance of this class are called with currently received data.
*/
class PacketInterface : public Component {
public:
/**
*
* Method to be implemented by child classes to send data to the interface.
*
* @param data The data to be sent
* @param meta_data Metadata about the packet to be sent
* @return True if the data was queued for transmission, false if it could not be sent
*/
virtual bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) = 0;
/// The largest packet this layer accepts; fixed when the layer is constructed.
size_t get_max_packet_size() const { return this->max_packet_size_; }
/// How many packets send_to_interface() would accept now, so a layer above can tell before sending a series.
virtual size_t get_send_capacity() const { return SIZE_MAX; }
/// The capabilities of this layer and every layer below it.
uint8_t get_capabilities() const { return this->capabilities_; }
bool has_capability(Capability capability) const { return (this->capabilities_ & capability) != 0; }
/**
* With CAPABILITY_SESSION, the callback is called with true when the peer can be reached, including after it
* restarts, and with false when it is lost. The session layer is always the top layer, so register on top().
* A reliability layer calls it with false when it resets the link and discards data, which the session layer above
* reports as the session lost.
*/
template<typename F> void add_on_session_state_callback(F &&callback) {
this->session_callback_.add(std::forward<F>(callback));
}
/// The topmost layer stacked on this one, or this one if nothing is stacked on it.
PacketInterface *top() {
PacketInterface *layer = this;
while (layer->upper_ != nullptr)
layer = layer->upper_;
return layer;
}
/// Name shown in log messages; a layer takes the name of the link it is stacked on.
void set_name(const char *name) { this->name_ = name; }
const char *get_name() const { return this->name_; }
/**
* Add a listener that will be called when data is received.
*
* @param callback The callback to be called when data is received.
*/
template<typename F> void add_packet_interface_listener(F &&callback) {
this->callback_.add(std::forward<F>(callback));
}
void dump_config() override;
protected:
/// @param max_packet_size The largest packet the layer accepts, set from the layer below or the layer's own buffer
explicit PacketInterface(size_t max_packet_size) : max_packet_size_(max_packet_size) {}
virtual ~PacketInterface() = default;
/**
* This function should be called by implementing classes with received data to be passed to any listeners.
*
* @param data The received data
* @param meta_data Metadata about the received packet
*/
void on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data);
/**
* Called by a layer for each received packet it understood, or could not make sense of. A run of packets that
* cannot be understood usually means the peer has different layers configured, which is logged once per run.
*/
void note_received_(bool understood);
/// Called by a layer's constructor to record itself as the layer above \p lower, adding its own capability.
void stack_on_(PacketInterface *lower, Capability capability) {
lower->upper_ = this;
this->inherit_from_(lower, capability);
}
/// Takes the name and capabilities of `lower` without becoming its top, for one of several stacks on a link.
void inherit_from_(PacketInterface *lower, Capability capability) {
this->name_ = lower->name_;
this->capabilities_ = lower->capabilities_ | capability;
// A link that has a session of its own, such as TCP, reports it to whatever is stacked above, as does a
// reliability layer that discards data; the session layer handles the latter itself
if ((lower->has_capability(CAPABILITY_SESSION) || lower->has_capability(CAPABILITY_RELIABILITY)) &&
capability != CAPABILITY_SESSION) {
lower->add_on_session_state_callback([this](bool connected) { this->session_callback_.call(connected); });
}
}
LazyCallbackManager<void(const PacketBuffer &, const PacketMetaData &)> callback_;
LazyCallbackManager<void(bool)> session_callback_;
PacketInterface *upper_{nullptr};
const char *name_{""};
const size_t max_packet_size_;
uint8_t capabilities_{0};
uint8_t not_understood_count_{0};
};
} // namespace esphome::packet_interface
@@ -1,28 +0,0 @@
import esphome.codegen as cg
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import CONF_PACKET_INTERFACE, PacketInterface, packet_interface_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["packet_interface"]
PacketInterfaceTransport = packet_interface_ns.class_(
"PacketInterfaceTransport", PacketTransport
)
CONFIG_SCHEMA = transport_schema(PacketInterfaceTransport).extend(
{
cv.GenerateID(CONF_PACKET_INTERFACE): cv.use_id(PacketInterface),
}
)
async def to_code(config: ConfigType) -> None:
interface = await cg.get_variable(config[CONF_PACKET_INTERFACE])
await new_packet_transport(config, interface)
@@ -1,29 +0,0 @@
#include "packet_interface_transport.h"
#include "esphome/core/log.h"
namespace esphome::packet_interface {
ESPHOME_LOG_TAG(TAG, "packet_interface.transport");
void PacketInterfaceTransport::setup() {
PacketTransport::setup();
this->interface_ = this->interface_->top();
this->interface_->add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &) { this->receive_(data); });
}
void PacketInterfaceTransport::receive_(const PacketBuffer &data) {
// Packet interface layers deliver packets in one piece
if (!data.is_contiguous()) {
ESP_LOGW(TAG, "Packet split across buffers is not supported");
return;
}
this->process_(data.as_span());
}
void PacketInterfaceTransport::send_packet(const std::vector<uint8_t> &buf) const {
if (!this->interface_->send_to_interface(PacketBuffer(buf), {}))
ESP_LOGD(TAG, "Failed to send packet of %zu bytes", buf.size());
}
} // namespace esphome::packet_interface
@@ -1,29 +0,0 @@
#pragma once
#include "esphome/components/packet_interface/packet_interface.h"
#include "esphome/components/packet_transport/packet_transport.h"
namespace esphome::packet_interface {
/**
* A transport protocol adapter that bridges PacketInterface to PacketTransport.
* This allows packet_transport functionality to be used with any PacketInterface implementation.
*/
class PacketInterfaceTransport : public packet_transport::PacketTransport {
public:
explicit PacketInterfaceTransport(PacketInterface *interface) : interface_(interface) {}
void setup() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
protected:
// implementing PacketTransport virtual methods
size_t get_max_packet_size() override { return this->interface_->top()->get_max_packet_size(); }
void send_packet(const std::vector<uint8_t> &buf) const override;
void receive_(const PacketBuffer &data);
PacketInterface *interface_;
};
} // namespace esphome::packet_interface
@@ -205,10 +205,8 @@ async def register_packet_transport(var: MockObj, config: ConfigType) -> set[str
return providers
async def new_packet_transport(
config: ConfigType, *args: Any
) -> tuple[MockObj, set[str]]:
var = cg.new_Pvariable(config[CONF_ID], *args)
async def new_packet_transport(config: ConfigType) -> tuple[MockObj, set[str]]:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_platform_name(config[CONF_PLATFORM]))
providers = await register_packet_transport(var, config)
return var, providers
@@ -547,11 +547,9 @@ void PacketTransport::dump_config() {
ESP_LOGCONFIG(TAG,
"Packet Transport:\n"
" Platform: %s\n"
" Max Packet Size: %zu\n"
" Encrypted: %s\n"
" Ping-pong: %s",
this->platform_name_, this->get_max_packet_size(), YESNO(this->is_encrypted_()),
YESNO(this->ping_pong_enable_));
this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_));
#ifdef USE_SENSOR
for (const auto &sensor : this->sensors_) {
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id);
+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
@@ -90,7 +90,11 @@ void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) {
uint16_t pos = x + (y / 8) * this->get_width_internal();
uint8_t subpos = y % 8;
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
if (color.is_on()) {
this->buffer_[pos] |= (1 << subpos);
} else {
this->buffer_[pos] &= ~(1 << subpos);
}
}
void PCD8544::dump_config() {
+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;
@@ -3,6 +3,7 @@
#ifdef USE_RUNTIME_IMAGE_BMP
#include "esphome/components/display/display.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -12,6 +13,9 @@ namespace esphome::runtime_image {
ESPHOME_LOG_TAG(TAG, "image_decoder.bmp");
// BMP has no alpha. Every pixel is drawn opaque, or a transparent image would hide it
static constexpr Color OPAQUE_BLACK(0, 0, 0, 0xFF);
void BmpDecoder::reset() {
ImageDecoder::reset();
this->bits_per_pixel_ = 0;
@@ -142,23 +146,31 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
this->current_index_ = this->data_offset_;
index = this->data_offset_;
}
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
const uint32_t start = millis();
uint32_t steps = 0;
auto out_of_time = [start, &steps]() {
return ++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS;
};
// Bytes after the last pixel are used up without drawing anything
const size_t total_pixels = static_cast<size_t>(this->width_) * this->height_;
switch (this->bits_per_pixel_) {
case 1: {
size_t width = static_cast<size_t>(this->width_);
while (index < size) {
while (index < size && this->paint_index_ < total_pixels) {
size_t x = this->paint_index_ % width;
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
size_t remaining_in_row = width - x;
uint8_t pixels_in_byte = std::min<size_t>(remaining_in_row, 8);
bool end_of_row = remaining_in_row <= 8;
size_t needed = 1 + (end_of_row ? this->padding_bytes_ : 0);
if (index + needed > size) {
if (index + needed > size || out_of_time()) {
this->decoded_bytes_ += index;
return index;
}
uint8_t current_byte = buffer[index];
for (uint8_t i = 0; i < pixels_in_byte; i++) {
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : OPAQUE_BLACK;
this->draw(x + i, y, 1, 1, c);
}
this->paint_index_ += pixels_in_byte;
@@ -175,11 +187,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
case 8: {
size_t width = static_cast<size_t>(this->width_);
size_t last_col = width - 1;
while (index < size) {
while (index < size && this->paint_index_ < total_pixels) {
size_t x = this->paint_index_ % width;
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
size_t needed = 1 + ((x == last_col) ? this->padding_bytes_ : 0);
if (index + needed > size) {
if (index + needed > size || out_of_time()) {
this->decoded_bytes_ += index;
return index;
}
@@ -194,7 +206,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
uint8_t b = rgb & 0xff;
uint8_t g = (rgb >> 8) & 0xff;
uint8_t r = (rgb >> 16) & 0xff;
this->draw(x, y, 1, 1, Color(r, g, b));
this->draw(x, y, 1, 1, Color(r, g, b, 0xFF));
this->paint_index_++;
this->current_index_++;
index++;
@@ -208,18 +220,18 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
case 24: {
size_t width = static_cast<size_t>(this->width_);
size_t last_col = width - 1;
while (index < size) {
while (index < size && this->paint_index_ < total_pixels) {
size_t x = this->paint_index_ % width;
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
size_t needed = 3 + ((x == last_col) ? this->padding_bytes_ : 0);
if (index + needed > size) {
if (index + needed > size || out_of_time()) {
this->decoded_bytes_ += index;
return index;
}
uint8_t b = buffer[index];
uint8_t g = buffer[index + 1];
uint8_t r = buffer[index + 2];
this->draw(x, y, 1, 1, Color(r, g, b));
this->draw(x, y, 1, 1, Color(r, g, b, 0xFF));
this->paint_index_++;
this->current_index_ += 3;
index += 3;
@@ -1,29 +1,106 @@
#include "image_decoder.h"
#include "runtime_image.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
namespace esphome::runtime_image {
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;
return success;
if (!this->resize_image_(width, height))
return false;
this->prepare_draw_(width, height);
return true;
}
bool ImageDecoder::resize_image_(int width, int height) {
this->size_valid_ = this->image_->resize(width, height) > 0;
return this->size_valid_;
}
void ImageDecoder::prepare_draw_(int width, int height) {
this->draw_width_ = width;
this->draw_height_ = height;
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
if (!this->direct_rgb565_)
return;
if ((this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) ||
(this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1))) {
ESP_LOGW(TAG, "No memory to draw straight into the buffer; drawing pixel by pixel");
this->direct_rgb565_ = false;
// Free the table that did fit, as nothing uses it
this->x_boundaries_ = FixedVector<uint16_t>();
this->y_boundaries_ = FixedVector<uint16_t>();
return;
}
this->x_boundaries_.clear();
this->y_boundaries_.clear();
const int buffer_width = this->image_->get_buffer_width();
const int buffer_height = this->image_->get_buffer_height();
// The same boundaries as draw() uses, so both paths paint the same pixels
for (int x = 0; x <= width; x++) {
this->x_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(x, width, buffer_width)));
}
for (int y = 0; y <= height; y++) {
this->y_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(y, height, buffer_height)));
}
}
void ImageDecoder::draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565) {
const int x_end = std::min(x + w, this->draw_width_);
const int y_end = std::min(y + h, this->draw_height_);
x = std::max(x, 0);
y = std::max(y, 0);
if (x >= x_end || y >= y_end)
return;
// A block of source pixels lands on one block of the buffer
const uint16_t x1 = this->x_boundaries_[x];
const uint16_t x2 = this->x_boundaries_[x_end];
const uint16_t y1 = this->y_boundaries_[y];
const uint16_t y2 = this->y_boundaries_[y_end];
const bool big_endian = this->image_is_big_endian_();
const uint8_t first = big_endian ? rgb565 >> 8 : rgb565 & 0xFF;
const uint8_t second = big_endian ? rgb565 & 0xFF : rgb565 >> 8;
uint8_t *const buffer = this->image_buffer_();
const uint32_t buffer_width = this->image_->get_buffer_width();
for (uint32_t row = y1; row < y2; row++) {
uint8_t *destination = buffer + (row * buffer_width + x1) * 2;
for (uint32_t col = x1; col < x2; col++) {
destination[0] = first;
destination[1] = second;
destination += 2;
}
}
}
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++) {
if (this->direct_rgb565_) {
this->draw_rgb565_(x, y, w, h, display::ColorUtil::color_to_565(color));
return;
}
// When shrinking, 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 = scale_boundary(x, this->draw_width_, buffer_width);
y1 = scale_boundary(y, this->draw_height_, buffer_height);
x2 = scale_boundary(x + w, this->draw_width_, buffer_width);
y2 = scale_boundary(y + h, this->draw_height_, buffer_height);
}
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);
}
}
@@ -2,10 +2,20 @@
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
#include "image_format.h"
namespace esphome::runtime_image {
/// The first of `target` pixels that pixel n of `source` paints, rounded up: ceil(n * target / source). The spans of
/// neighbouring pixels meet without overlapping, so a shrunk image paints each target pixel once.
inline int scale_boundary(int n, int source, int target) {
if (source <= 0)
return 0;
return static_cast<int>((static_cast<uint32_t>(n) * static_cast<uint32_t>(target) + source - 1) /
static_cast<uint32_t>(source));
}
enum DecodeError : int {
DECODE_ERROR_INVALID_TYPE = -1,
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
@@ -38,6 +48,11 @@ constexpr const char *decode_error_to_string(int error) {
}
}
// The longest one decode() call works before it returns, leaving the rest of the data for the next call
static constexpr uint32_t DECODE_TIME_LIMIT_MS = 15;
// Pixel loops read the clock once per this many steps
static constexpr uint32_t DECODE_TIME_CHECK_STEPS = 64;
class RuntimeImage;
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
@@ -83,8 +98,9 @@ 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;
this->direct_rgb565_ = false;
}
/**
@@ -179,19 +195,37 @@ class ImageDecoder {
/// Whether is_finished() can tell a complete image from a cut-off one
virtual bool knows_end() const { return this->expected_size_ > 0; }
/// The image is opaque RGB565, so draw() writes it straight into the buffer
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
protected:
/// Resize the image for a width x height source, without preparing to draw. prepare_draw_() must follow.
bool resize_image_(int width, int height);
/**
* Draw a source of width x height over the whole buffer from now on. For an opaque RGB565 image this builds the
* tables of where each source column and row lands; without the memory for them it draws pixel by pixel instead.
*/
void prepare_draw_(int width, int height);
/// Paint a block of source pixels in one color straight into the buffer. Only while uses_direct_rgb565().
void draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565);
/// The image's pixel buffer, for decoders that write their output straight into it
uint8_t *image_buffer_() const;
bool image_is_big_endian_() const;
double x_scale_ = 1.0;
double y_scale_ = 1.0;
// Size of the image as the decoder draws it, which draw() spreads over the buffer
int draw_width_ = 0;
int draw_height_ = 0;
// Boundary n is the first buffer column, or row, that source column, or row, n paints
FixedVector<uint16_t> x_boundaries_{};
FixedVector<uint16_t> y_boundaries_{};
RuntimeImage *image_;
size_t expected_size_ = 0; // Expected data size (0 if unknown)
size_t decoded_bytes_ = 0; // Bytes processed so far
const ImageFormat format_;
bool active_ = false; // A decoding session is in progress
bool size_valid_ = true; // Last set_size() result; draw() no-ops while false
bool direct_rgb565_ = false;
};
} // namespace esphome::runtime_image
@@ -16,32 +16,6 @@ ESPHOME_LOG_TAG(TAG, "image_decoder.jpeg");
namespace esphome::runtime_image {
bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
if (this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) {
return false;
}
if (this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1)) {
return false;
}
this->source_width_ = width;
this->source_height_ = height;
this->x_boundaries_.clear();
this->y_boundaries_.clear();
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
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));
}
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
}
return true;
}
void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels) {
if (pixels == nullptr) {
return;
@@ -52,14 +26,14 @@ void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels)
const uint8_t first = this->image_is_big_endian_() ? 1 : 0;
for (int source_y = 0; source_y < h; source_y++) {
const int absolute_y = y + source_y;
if (absolute_y < 0 || absolute_y >= this->source_height_) {
if (absolute_y < 0 || absolute_y >= this->draw_height_) {
continue;
}
const uint16_t y1 = this->y_boundaries_[absolute_y];
const uint16_t y2 = this->y_boundaries_[absolute_y + 1];
for (int source_x = 0; source_x < w; source_x++) {
const int absolute_x = x + source_x;
if (absolute_x < 0 || absolute_x >= this->source_width_) {
if (absolute_x < 0 || absolute_x >= this->draw_width_) {
continue;
}
const uint16_t x1 = this->x_boundaries_[absolute_x];
@@ -157,17 +131,10 @@ int JpegDecoder::start_() {
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
this->jpeg_.setUserPointer(this);
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
if (this->direct_rgb565_) {
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
} else {
this->jpeg_.setPixelType(RGB8888);
}
const int width = this->jpeg_.getWidth();
const int height = this->jpeg_.getHeight();
if (!this->set_size(width, height)) {
// The tables for drawing are only built once the decoded size is known, below
if (!this->resize_image_(width, height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
// 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
@@ -181,13 +148,16 @@ int JpegDecoder::start_() {
}
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
this->y_scale_ = static_cast<double>(buffer_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;
this->prepare_draw_(decoded_width, decoded_height);
if (this->direct_rgb565_) {
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
} else {
this->jpeg_.setPixelType(RGB8888);
}
return 0;
}
@@ -30,13 +30,11 @@ class JpegDecoder : public ImageDecoder {
/// 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
@@ -49,13 +47,8 @@ class JpegDecoder : public ImageDecoder {
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};
};
@@ -6,10 +6,15 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
ESPHOME_LOG_TAG(TAG, "image_decoder.png");
namespace esphome::runtime_image {
// Compressed bytes handed to pngle at a time
static constexpr size_t FEED_PIECE_SIZE = 64;
/**
* @brief Callback method that will be called by the PNGLE engine when the basic
* data of the image is received (i.e. width and height);
@@ -93,16 +98,26 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
ESP_LOGD(TAG, "Waiting for more data");
return 0;
}
auto fed = pngle_feed(this->pngle_, buffer, size);
if (fed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
} else if (!this->size_valid_) {
return DECODE_ERROR_OUT_OF_MEMORY;
} else {
// A few compressed bytes can hold many rows of pixels, so feed small pieces and stop once the call has run for
// DECODE_TIME_LIMIT_MS; the caller passes the rest again later
const uint32_t start = millis();
size_t total = 0;
while (total < size) {
const size_t piece = std::min(FEED_PIECE_SIZE, size - total);
const int fed = pngle_feed(this->pngle_, buffer + total, piece);
if (fed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
if (!this->size_valid_)
return DECODE_ERROR_OUT_OF_MEMORY;
this->decoded_bytes_ += fed;
total += fed;
// pngle takes less than the piece when a chunk header or CRC runs past its end; that is no reason to stop
if (fed == 0 || millis() - start >= DECODE_TIME_LIMIT_MS)
break;
}
return fed;
return total;
}
} // namespace esphome::runtime_image
@@ -3,9 +3,11 @@
#ifdef USE_RUNTIME_IMAGE_QOI
#include "esphome/components/display/display.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cinttypes>
namespace esphome::runtime_image {
@@ -102,12 +104,17 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
Color color;
const size_t total_pixels = this->width_ * this->height_;
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
const uint32_t start = millis();
uint32_t steps = 0;
while (index < size && this->paint_index_ < total_pixels) {
if (++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS)
break;
color = this->last_pixel_;
uint8_t byte = buffer[index];
if (byte == QOI_OP_RGB) {
if (size < index + QOI_RGB_CHUNK_SIZE) {
return index; // Need more data for RGB chunk
break; // Need more data for RGB chunk
}
index++;
color.r = buffer[index++];
@@ -117,7 +124,7 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
this->paint_index_++;
} else if (byte == QOI_OP_RGBA) {
if (size < index + QOI_RGBA_CHUNK_SIZE) {
return index; // Need more data for RGBA chunk
break; // Need more data for RGBA chunk
}
index++;
color.r = buffer[index++];
@@ -129,15 +136,18 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
} else if ((byte & QOI_MASK_OP) == QOI_OP_RUN) {
// QOI run chunk
size_t run_length = (byte & QOI_MASK_VALUE) + 1; // run length is encoded in the lower 6 bits, plus one
for (size_t i = 0; i < run_length; i++) {
// TODO: optimize by drawing runs of pixels at once instead of one by one
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
this->paint_index_++;
// Draw the part of the run in each row it covers at once
while (run_length > 0) {
const size_t x = this->paint_index_ % this->width_;
const size_t count = std::min(run_length, this->width_ - x);
this->draw(x, this->paint_index_ / this->width_, count, 1, color);
this->paint_index_ += count;
run_length -= count;
}
index++;
} else if ((byte & QOI_MASK_OP) == QOI_OP_LUMA) {
if (size < index + QOI_LUMA_CHUNK_SIZE) {
return index; // Need more data for LUMA chunk
break; // Need more data for LUMA chunk
}
index++;
uint8_t byte2 = buffer[index++];
@@ -165,8 +175,10 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
this->last_pixel_ = color;
this->color_table_[qoi_color_table_index(color)] = color;
}
this->decoded_bytes_ += size;
return size;
if (this->paint_index_ >= total_pixels)
index = size; // All that follows the last pixel is the end marker
this->decoded_bytes_ += index;
return index;
}
} // namespace esphome::runtime_image
@@ -1,6 +1,5 @@
#include "runtime_image.h"
#include "image_decoder.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cstdint>
@@ -92,7 +91,11 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
auto on = is_color_on(color);
if (this->has_transparency() && color.w < 0x80)
on = false;
assign_bits(this->buffer_[pos], bitno, on);
if (on) {
this->buffer_[pos] |= bitno;
} else {
this->buffer_[pos] &= ~bitno;
}
break;
}
case image::IMAGE_TYPE_RGB111: {
@@ -113,7 +116,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
}
case image::IMAGE_TYPE_GRAYSCALE: {
uint32_t pos = this->get_position_(x, y);
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
// 0.2125, 0.7154 and 0.0721 in 1/256ths, summing to 256 so white stays 255. Integer maths: the S3 has no
// double FPU
auto gray = static_cast<uint8_t>((54 * color.r + 183 * color.g + 19 * color.b) >> 8);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
gray = 0;
@@ -342,7 +342,11 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) {
uint16_t pos = x + (y / 8) * this->get_width_internal();
uint8_t subpos = y & 0x07;
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
if (color.is_on()) {
this->buffer_[pos] |= (1 << subpos);
} else {
this->buffer_[pos] &= ~(1 << subpos);
}
}
void SSD1306::fill(Color color) {
// If clipping is active, fall back to base implementation
+26 -15
View File
@@ -53,13 +53,6 @@ MODELS = DriverChip.get_models()
DEFAULT_MODEL = "WAVESHARE-400X300"
def _model_key(model: DriverChip, key: str) -> cv.Optional | cv.Required:
"""Default the key from the model, or require it when the model has none."""
if model.get_default(key, None) is None:
return cv.Required(key)
return model.option(key)
def _model_schema(model: DriverChip) -> cv.Schema:
iseqconf = (
cv.Required(CONF_INIT_SEQUENCE)
@@ -79,12 +72,12 @@ def _model_schema(model: DriverChip) -> cv.Schema:
{
cv.GenerateID(): cv.declare_id(ST7305),
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
_model_key(model, CONF_CS_PIN): pins.gpio_output_pin_schema,
_model_key(model, CONF_DC_PIN): pins.gpio_output_pin_schema,
model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
_model_key(model, CONF_WIDTH): cv.int_range(min=8, max=1024),
_model_key(model, CONF_HEIGHT): cv.int_range(min=8, max=1024),
_model_key(model, CONF_PORTRAIT): cv.boolean,
cv.Optional(CONF_WIDTH): cv.int_range(min=8, max=1024),
cv.Optional(CONF_HEIGHT): cv.int_range(min=8, max=1024),
cv.Optional(CONF_PORTRAIT): cv.boolean,
cv.Optional(CONF_COLUMN_START): cv.int_range(min=0, max=255),
cv.Optional(CONF_COLUMN_END): cv.int_range(min=0, max=255),
cv.Optional(CONF_ROW_START): cv.int_range(min=0, max=255),
@@ -96,9 +89,23 @@ def _model_schema(model: DriverChip) -> cv.Schema:
def _resolve(config: ConfigType, model: DriverChip) -> None:
"""Derive and validate the address window, and default the init sequence."""
width = config[CONF_WIDTH]
height = config[CONF_HEIGHT]
"""Fill in model defaults and validate the geometry and address window."""
if (width := config.get(CONF_WIDTH)) is None:
if (width := model.get_default(CONF_WIDTH, None)) is None:
raise cv.Invalid("'width' is required for this model", path=[CONF_WIDTH])
config[CONF_WIDTH] = width
if (height := config.get(CONF_HEIGHT)) is None:
if (height := model.get_default(CONF_HEIGHT, None)) is None:
raise cv.Invalid("'height' is required for this model", path=[CONF_HEIGHT])
config[CONF_HEIGHT] = height
if config.get(CONF_PORTRAIT) is None:
if (default := model.get_default(CONF_PORTRAIT, None)) is None:
raise cv.Invalid(
"'portrait' is required for this model", path=[CONF_PORTRAIT]
)
config[CONF_PORTRAIT] = default
if config[CONF_PORTRAIT]:
if width % 12 or height % 2:
raise cv.Invalid(
@@ -145,6 +152,10 @@ def _resolve(config: ConfigType, model: DriverChip) -> None:
if config.get(CONF_INIT_SEQUENCE) is None:
config[CONF_INIT_SEQUENCE] = model.initsequence
for key, label in ((CONF_CS_PIN, "cs_pin"), (CONF_DC_PIN, "dc_pin")):
if key not in config:
raise cv.Invalid(f"'{label}' is required for this model", path=[key])
def model_schema(config: ConfigType) -> cv.Schema:
"""Return the schema for the model selected in the config."""
+7 -2
View File
@@ -1,8 +1,8 @@
#include "st7305.h"
#include <algorithm>
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::st7305 {
@@ -27,6 +27,7 @@ bool ST7305::init_buffer_() {
this->mark_failed(LOG_STR("Buffer allocation failed"));
return false;
}
memset(this->buffer_, 0x00, this->buffer_length_);
this->clear_dirty_();
return true;
}
@@ -190,7 +191,11 @@ void HOT ST7305::draw_absolute_pixel_internal(int x, int y, Color color) {
// The init sequence enables INVON, so a set bit is white and a clear bit is black.
uint8_t &value = this->buffer_[index];
const uint8_t previous = value;
assign_bits(value, mask, color.is_on());
if (color.is_on()) {
value |= mask;
} else {
value &= ~mask;
}
if (value == previous)
return;
this->extend_dirty_(x, y, x, y);
@@ -123,7 +123,11 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
uint16_t pos = x + (y / 8) * this->get_width_internal();
uint8_t subpos = y & 0x07;
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
if (color.is_on()) {
this->buffer_[pos] |= (1 << subpos);
} else {
this->buffer_[pos] &= ~(1 << subpos);
}
}
void ST7567::fill(Color color) {
+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
+4 -49
View File
@@ -19,52 +19,6 @@ static constexpr uint8_t LOG_LEVEL_TO_SYSLOG_SEVERITY[] PROGMEM = {
7 // VERY_VERBOSE
};
static constexpr char SECRET_BEGIN[] = ESPHOME_LOG_SECRET_BEGIN;
static constexpr char SECRET_END[] = ESPHOME_LOG_SECRET_END;
static constexpr size_t SECRET_BEGIN_LEN = sizeof(SECRET_BEGIN) - 1;
static constexpr size_t SECRET_END_LEN = sizeof(SECRET_END) - 1;
static bool starts_with_marker(const char *message, size_t remaining, const char *marker, size_t marker_len) {
return message[0] == '\033' && remaining >= marker_len && memcmp(message, marker, marker_len) == 0;
}
static size_t append_bytes(char *buf, size_t size, size_t pos, const char *data, size_t len) {
if (pos >= size) {
return size;
}
size_t n = std::min(len, size - pos - 1);
memcpy(buf + pos, data, n);
pos += n;
buf[pos] = '\0';
return pos;
}
size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *message, size_t len) {
if (memchr(message, '\033', len) == nullptr) {
return append_bytes(buf, size, pos, message, len);
}
size_t i = 0;
while (i < len) {
if (starts_with_marker(message + i, len - i, SECRET_BEGIN, SECRET_BEGIN_LEN)) {
pos = buf_append_str(buf, size, pos, "[redacted]");
i += SECRET_BEGIN_LEN;
while (i < len && !starts_with_marker(message + i, len - i, SECRET_END, SECRET_END_LEN)) {
i++;
}
if (i < len) {
i += SECRET_END_LEN;
}
continue;
}
size_t start = i;
while (i < len && !starts_with_marker(message + i, len - i, SECRET_BEGIN, SECRET_BEGIN_LEN)) {
i++;
}
pos = append_bytes(buf, size, pos, message + start, i - start);
}
return pos;
}
void Syslog::setup() {
logger::global_logger->add_log_callback(
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
@@ -117,15 +71,16 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
} else {
offset = buf_append_str(packet, sizeof(packet), offset, "-");
}
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - ", App.get_name().c_str(), tag);
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - %.*s", App.get_name().c_str(), tag,
(int) len, message);
} else {
// RFC 3164 has no NILVALUE. If the clock is invalid, omit TIMESTAMP so a relay can add it.
if (now.is_valid()) {
offset += now.strftime(packet + offset, sizeof(packet) - offset, "%b %e %H:%M:%S ");
}
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: ", App.get_name().c_str(), tag);
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: %.*s", App.get_name().c_str(), tag, (int) len,
message);
}
offset = append_redacted_message(packet, sizeof(packet), offset, message, len);
// Clamp to exclude null terminator position if buffer was filled
if (offset >= sizeof(packet)) {
offset = sizeof(packet) - 1;
@@ -12,10 +12,6 @@ enum SyslogFormat : uint8_t {
SYSLOG_FORMAT_RFC5424,
};
/// Append len bytes of message to buf at pos, replacing every LOG_SECRET() region with "[redacted]".
/// Returns the new position, capped at size like the other buf_append helpers.
size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *message, size_t len);
class Syslog final : public Component, public Parented<udp::UDPComponent> {
public:
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
+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"));
@@ -1,36 +0,0 @@
import esphome.codegen as cg
from esphome.components import packet_interface, uart
from esphome.components.packet_interface import (
CONF_MAX_PACKET_SIZE,
packet_interface_schema,
)
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import uart_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["uart"]
UartPacketInterface = uart_ns.class_(
"UartPacketInterface",
packet_interface.PacketInterface,
uart.UARTDevice,
)
CONFIG_SCHEMA = packet_interface_schema(UartPacketInterface, 508).extend(
uart.UART_DEVICE_SCHEMA
)
async def to_code(config: ConfigType) -> None:
# The receive buffer's size is known here, so it is a static array rather than allocated at run time
rx_buffer = f"{config[CONF_ID].id}__rx_buffer"
cg.add_global(
cg.RawStatement(f"static uint8_t {rx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
)
var = await packet_interface.new_packet_interface(
config, cg.RawExpression(rx_buffer)
)
await uart.register_uart_device(var, config)
@@ -1,82 +0,0 @@
#include "esphome/core/log.h"
#include "uart_packet_interface.h"
namespace esphome::uart {
using packet_interface::PacketBuffer;
using packet_interface::PacketMetaData;
ESPHOME_LOG_TAG(TAG, "uart.packet_interface");
static constexpr uint8_t FLAG_BYTE = 0x7E;
static constexpr uint8_t CONTROL_BYTE = 0x7D;
static constexpr uint8_t ESCAPE_XOR = 0x20;
// Raw bytes escaped and written per UART write
static constexpr size_t TX_CHUNK_SIZE = 32;
void UartPacketInterface::loop() {
this->read_available([this](uint8_t byte) { this->handle_rx_byte_(byte); });
}
void UartPacketInterface::handle_rx_byte_(uint8_t byte) {
if (byte == FLAG_BYTE) {
// A flag that ends a frame also starts the next, so an unfinished frame cannot hide the one after it
if (this->rx_started_ && this->rx_length_ != 0)
this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_.data(), this->rx_length_), {});
this->rx_started_ = true;
this->rx_length_ = 0;
this->rx_control_ = false;
return;
}
if (!this->rx_started_)
return;
if (byte == CONTROL_BYTE) {
this->rx_control_ = true;
return;
}
if (this->rx_control_) {
byte ^= ESCAPE_XOR;
this->rx_control_ = false;
}
if (this->rx_length_ >= this->rx_buffer_.size()) {
ESP_LOGD(TAG, "Packet too large, discarding");
this->rx_started_ = false;
this->rx_length_ = 0;
return;
}
this->rx_buffer_[this->rx_length_++] = byte;
}
bool UartPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
size_t size = data.size();
if (size == 0)
return true;
if (size > this->max_packet_size_) {
ESP_LOGW(TAG, "Packet of %zu bytes is larger than %zu", size, this->max_packet_size_);
return false;
}
uint8_t raw[TX_CHUNK_SIZE]{};
// Every byte may be escaped, plus a flag byte at each end
uint8_t out[TX_CHUNK_SIZE * 2 + 2];
size_t out_len = 0;
out[out_len++] = FLAG_BYTE;
for (size_t offset = 0; offset < size;) {
size_t count = data.copy_to(raw, TX_CHUNK_SIZE, offset);
offset += count;
for (size_t i = 0; i != count; i++) {
uint8_t byte = raw[i];
if (byte == FLAG_BYTE || byte == CONTROL_BYTE) {
out[out_len++] = CONTROL_BYTE;
byte ^= ESCAPE_XOR;
}
out[out_len++] = byte;
}
if (offset == size)
out[out_len++] = FLAG_BYTE;
this->write_array(out, out_len);
out_len = 0;
}
return true;
}
} // namespace esphome::uart
@@ -1,36 +0,0 @@
#pragma once
#include <span>
#include "esphome/components/packet_interface/packet_interface.h"
#include "esphome/components/uart/uart.h"
namespace esphome::uart {
/**
* A packet interface for sending and receiving framed packets over a UART connection.
* Uses simple byte-stuffing framing with FLAG_BYTE and CONTROL_BYTE.
* The protocol wraps data between FLAG_BYTEs.
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
class UartPacketInterface : public packet_interface::PacketInterface, public UARTDevice {
public:
/// @param rx_buffer Holds the unescaped data being received; its size is the largest packet, and it must outlive
/// the link
explicit UartPacketInterface(std::span<uint8_t> rx_buffer)
: packet_interface::PacketInterface(rx_buffer.size()), rx_buffer_(rx_buffer) {}
void loop() override;
bool send_to_interface(const packet_interface::PacketBuffer &data,
const packet_interface::PacketMetaData &meta_data) override;
protected:
void handle_rx_byte_(uint8_t byte);
const std::span<uint8_t> rx_buffer_;
size_t rx_length_{0};
bool rx_started_{};
bool rx_control_{};
};
} // namespace esphome::uart
@@ -6,10 +6,6 @@ namespace esphome::uart {
ESPHOME_LOG_TAG(TAG, "uart_transport");
static constexpr uint16_t MAX_PACKET_SIZE = 508;
static constexpr uint8_t FLAG_BYTE = 0x7E;
static constexpr uint8_t CONTROL_BYTE = 0x7D;
void UARTTransport::loop() {
PacketTransport::loop();
@@ -82,5 +78,4 @@ void UARTTransport::send_packet(const std::vector<uint8_t> &buf) const {
this->parent_->write_byte(FLAG_BYTE);
}
size_t UARTTransport::get_max_packet_size() { return MAX_PACKET_SIZE; }
} // namespace esphome::uart
@@ -16,6 +16,9 @@ namespace esphome::uart {
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
static const uint16_t MAX_PACKET_SIZE = 508;
static const uint8_t FLAG_BYTE = 0x7E;
static const uint8_t CONTROL_BYTE = 0x7D;
class UARTTransport final : public packet_transport::PacketTransport, public UARTDevice {
public:
@@ -26,7 +29,7 @@ class UARTTransport final : public packet_transport::PacketTransport, public UAR
void write_byte_(uint8_t byte) const;
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; };
size_t get_max_packet_size() override;
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
std::vector<uint8_t> receive_buffer_{};
bool rx_started_{};
bool rx_control_{};
@@ -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_));
-1
View File
@@ -75,7 +75,6 @@
#define DFROBOT_SEN0465_COMPENSATION_RANGES 3
#define USE_DISPLAY
#define DISPLAY_PAGE_CHANGE_CALLBACK_COUNT 1
#define DUCO_DEVICE_COUNT 4
#define USE_EMONTX_VERSION
#define USE_EMONTX_VERSION_TEXT_SENSOR
#define USE_ENTITY_DEVICE_CLASS
+3 -5
View File
@@ -29,14 +29,12 @@ class HashBase {
void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); }
/// Compare the hash against a provided byte-encoded hash
bool equals_bytes(const uint8_t *expected) { return constant_time_equals(this->digest_, expected, this->get_size()); }
bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; }
/// Compare the hash against a provided hex-encoded hash of get_size() * 2 characters, which need not be
/// null-terminated
/// Compare the hash against a provided hex-encoded hash
bool equals_hex(const char *expected) {
uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes)
const size_t size = this->get_size();
if (parse_hex(expected, size * 2, parsed, size) != size * 2) {
if (!parse_hex(expected, parsed, this->get_size())) {
return false;
}
return this->equals_bytes(parsed);
-10
View File
@@ -1112,16 +1112,6 @@ template<typename T> constexpr T convert_little_endian(T val) {
#endif
}
/// Compare len bytes of a and b in a time that does not depend on where they first differ.
/// Use this for digests, passwords and tokens, where memcmp's early exit leaks how many bytes matched.
inline bool constant_time_equals(const uint8_t *a, const uint8_t *b, size_t len) {
volatile uint8_t diff = 0;
for (size_t i = 0; i < len; i++) {
diff = diff | (a[i] ^ b[i]);
}
return diff == 0;
}
///@}
/// @name Strings
-5
View File
@@ -188,8 +188,6 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
The ``esphome:`` key is special: its sub-keys are merged into the
existing esphome config (e.g. to add ``areas:`` or ``devices:``).
A list, such as the entries of a platform component, is extended, so
several directories can each add their own entries to one key.
Keys already present in the base config (e.g. ``host:``, ``logger:``)
are protected by ``setdefault`` in the caller.
@@ -215,9 +213,6 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
for sub_key, sub_value in value.items():
esphome_extra.setdefault(sub_key, sub_value)
continue
if isinstance(value, list) and isinstance(merged.get(key), list):
merged[key] = merged[key] + value
continue
merged.setdefault(key, value)
return merged
@@ -9,7 +9,6 @@ from esphome.components.st7305.display import (
CONF_PORTRAIT,
CONF_ROW_END,
MODELS,
_model_schema,
_resolve,
)
from esphome.const import CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_WIDTH
@@ -73,22 +72,3 @@ def test_portrait_rejects_inner_not_multiple_of_12(width: int) -> None:
def test_window_ends_must_fit_in_a_byte() -> None:
with pytest.raises(cv.Invalid, match="between 0 and 255"):
_resolve(_config(**{CONF_WIDTH: 1024, CONF_HEIGHT: 300}), MODELS["CUSTOM"])
MODEL_KEYS = (CONF_WIDTH, CONF_HEIGHT, CONF_PORTRAIT, CONF_CS_PIN, CONF_DC_PIN)
def _schema_key(model: str, key: str) -> cv.Optional | cv.Required:
return next(k for k in _model_schema(MODELS[model]).schema if k == key)
@pytest.mark.parametrize("key", MODEL_KEYS)
def test_custom_requires_model_keys(key: str) -> None:
assert isinstance(_schema_key("CUSTOM", key), cv.Required)
@pytest.mark.parametrize("key", MODEL_KEYS)
def test_model_defaults_model_keys(key: str) -> None:
schema_key = _schema_key("WAVESHARE-400X300", key)
assert not isinstance(schema_key, cv.Required)
assert schema_key.default() == MODELS["WAVESHARE-400X300"].get_default(key)
@@ -1,39 +0,0 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/core/helpers.h"
namespace esphome {
TEST(ConstantTimeEquals, EqualBuffers) {
const uint8_t a[] = {0x00, 0x01, 0x7F, 0x80, 0xFF};
const uint8_t b[] = {0x00, 0x01, 0x7F, 0x80, 0xFF};
EXPECT_TRUE(constant_time_equals(a, b, sizeof(a)));
}
TEST(ConstantTimeEquals, FirstByteDiffers) {
const uint8_t a[] = {0x00, 0x01, 0x02, 0x03};
const uint8_t b[] = {0x10, 0x01, 0x02, 0x03};
EXPECT_FALSE(constant_time_equals(a, b, sizeof(a)));
}
TEST(ConstantTimeEquals, LastByteDiffers) {
const uint8_t a[] = {0x00, 0x01, 0x02, 0x03};
const uint8_t b[] = {0x00, 0x01, 0x02, 0x13};
EXPECT_FALSE(constant_time_equals(a, b, sizeof(a)));
}
TEST(ConstantTimeEquals, SingleBitDifference) {
const uint8_t a[] = {0xAA, 0xAA};
const uint8_t b[] = {0xAA, 0xAB};
EXPECT_FALSE(constant_time_equals(a, b, sizeof(a)));
}
TEST(ConstantTimeEquals, ZeroLengthIsEqual) {
const uint8_t a[] = {0x01};
const uint8_t b[] = {0x02};
EXPECT_TRUE(constant_time_equals(a, b, 0));
}
} // namespace esphome
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more