跳转到内容

Display 显示组件

display 组件承载了 ESPHome 的图形渲染和显示引擎。它支持多种不同类型的显示屏,从简单的字符显示屏到具有完全可寻址像素的图形显示屏。

对于灵活性最高的图形显示屏,有两种显示内容的选项:

所有 display 组件都继承这些配置变量。

  • id (可选, ID):手动指定用于代码生成的 ID。如果有多个显示屏,则必须设置。
  • update_interval (可选, 时间):重新绘制屏幕的间隔。默认为 1s
  • lambda (可选, lambda):用于在显示屏上渲染内容的 lambda。 详见显示渲染引擎

所有图形显示屏还继承以下配置变量。

  • auto_clear_enabled (可选, 布尔值):是否在每次更新前清除显示屏。如果配置了 lambda 或页面,默认为 true,否则为 false。
  • show_test_card (可选, 布尔值):是否显示测试卡。默认为 false。如果设置,lambda 配置选项中的任何代码都将被忽略。
  • rotation (可选, 整数):显示屏的旋转角度,可选 0、90、180 或 270 度。默认为 0
  • pages (可选, 列表):页面配置 - 见下文。

ESPHome 自带的强大渲染引擎可以处理许多常见任务,如绘制基本形状、使用您选择的字体打印文本,甚至渲染图像。

为了实现所有这些灵活性,显示屏直接与 ESPHome 的 lambda 系统集成。 因此,当您想在屏幕上写入一些文本或传感器值时,您将使用 C++ 代码编写,使用的 API 设计目标是:

  • 简单易用,无需编程经验即可上手
  • 同时也足够灵活,可以处理更复杂的任务,如显示模拟时钟

在本节中,我们将讨论如何从 ESPHome 使用显示渲染引擎以及一些基本命令。请注意,这仅适用于可以单独控制每个像素的显示屏。

NOTE

显示硬件非常复杂,有时可能无法按预期工作。如果您在使用显示屏时遇到问题,请参阅下方的故障排除部分。

首先是一些基础知识:在 ESPHome 中设置显示屏平台时,会有一个名为 lambda: 的配置选项,每次 ESPHome 想要重新渲染显示屏时都会调用它。 在每个周期中,显示屏会在执行 lambda 之前自动清除。您可以通过设置 auto_clear_enabled: false 来禁用此行为。 在 lambda 中,您可以像在 ESPHome 中任何 lambda 一样编写代码。显示 lambda 还会传递一个名为 it 的变量,它代表渲染引擎对象。

display:
- platform: ...
# ...
lambda: |-
// 在此编写您的显示渲染代码
// 例如,从 [x=0,y=0] 到 [x=50,y=50] 绘制一条线
it.line(0, 0, 50, 50);

NOTE

Lambda 本质上只是 C++ 的轻度修改版本。所以不要忘记在每行末尾加上分号(;)。否则在编译阶段您会看到一长串错误消息。

如果您编译并上传上面的配置,您应该会看到一条黑色(或白色,取决于显示屏)的线,从左上角开始,以 45° 角向下延伸几像素。(如果在其他角落,请使用 rotation: 选项将显示屏旋转到您喜欢的方向)

这已经突出了在编写自定义显示代码之前必须学习的一点:左上角始终是像素坐标系的原点。此外,该坐标系中的所有点都是一对整数,如 50, 50,表示向右偏移和向下偏移。换句话说,x 始终代表水平轴(宽度),y 代表垂直轴(高度)。渲染引擎的惯例是始终先指定 x 坐标,然后是 y 坐标。

现在您对 ESPHome 的坐标系有了更多了解,让我们绘制一些基本形状,如线条、矩形、圆形甚至多边形:

display:
- platform: ...
# ...
lambda: |-
// 从 [0,0] 到 [100,50] 绘制一条线
it.line(0, 0, 100, 50);
// 绘制一个矩形的轮廓,左上角在 [5,20],宽度为 30,高度为 42
it.rectangle(5, 20, 30, 42);
// 绘制相同的矩形,相隔几像素,但这次是填充的
it.filled_rectangle(40, 40, 30, 42);
// 圆形!让我们绘制一个圆心在 [20,40],半径为 10 的圆
it.circle(20, 40, 10);
// ... 再次绘制填充的相同圆形
it.filled_circle(20, 75, 10);
// 圆环和半圆环。首先绘制带孔的圆
// 在 [75,75],内半径为 20,外半径为 30
it.filled_ring(75, 75, 30, 20);
// 和一个"仪表盘":部分填充的半圆环。
// 相同位置和大小,但从左到右填充 80%
it.filled_gauge(75, 75, 30, 20, 80);
// 三角形... 让我们绘制三角形轮廓,从其三个点的 [x,y] 坐标
// [25,5], [100,5], [80,25]
it.triangle(25, 5, 100, 5, 80, 25);
// 和一个填充的三角形!
it.filled_triangle(115, 5, 95, 25, 125, 70);
// 正多边形?让我们绘制一个填充的、尖顶的六边形,内接于圆
// 圆心在 [170,45],半径为 20
it.filled_regular_polygon(170, 45, 20, EDGES_HEXAGON);
// 和一个平顶的八边形轮廓围绕它!
it.regular_polygon(170, 45, 40, EDGES_OCTAGON, VARIATION_FLAT_TOP);
// 需要旋转多边形,或检索其顶点坐标?请查看 API!

所有上述方法都可以选择在末尾添加一个参数,指定绘制颜色。对于单色显示屏,只支持 COLOR_ON(如果未指定颜色则为默认值)和 COLOR_OFF

display:
- platform: ...
# ...
lambda: |-
// 打开整个显示屏
it.fill(COLOR_ON);
// 关闭整个显示屏
it.fill(COLOR_OFF);
// 关闭 [50,60] 处的单个像素
it.draw_pixel_at(50, 60, COLOR_OFF);

对于彩色显示屏(如 TFT 显示屏),您可以使用 Color 类。

display:
- platform: ...
# ...
lambda: |-
auto black = Color(0, 0, 0);
auto red = Color(255, 0, 0);
auto green = Color(0, 255, 0);
auto blue = Color(0, 0, 255);
auto white = Color(255, 255, 255);
it.filled_circle(20, 32, 15, black);
it.filled_circle(40, 32, 15, red);
it.filled_circle(60, 32, 15, green);
it.filled_circle(80, 32, 15, blue);
it.filled_circle(100, 32, 15, white);

此外,您还可以使用两个辅助方法来获取显示屏的宽度和高度:

display:
- platform: ...
# ...
lambda: |-
// 在显示屏中间绘制一个圆
it.filled_circle(it.get_width() / 2, it.get_height() / 2, 20);
// 关闭屏幕下半部分
it.filled_rectangle(0, it.get_height()/2, it.get_width(), it.get_height()/2, COLOR_OFF);

您可以在”另见”部分的”API 参考”中查看渲染引擎的完整 API 文档。

要能够显示文本,您需要准备一些字体。ESPHome 的字体渲染器允许您为文本使用 OpenType/TrueType/位图字体。这非常灵活,因为您可以准备各种不同大小的字体集,以及不同数量的字形,这在讨论闪存空间时非常方便。

在显示代码中,您可以通过引用字体并输入用双引号括起来的字符串来渲染静态文本:

display:
- platform: ...
# ...
lambda: |-
// 在 [0,10] 处打印字符串 "Hello World!"
it.print(0, 10, id(my_font), "Hello World!");

默认情况下,ESPHome 会在左上角对齐文本。这意味着如果您为文本输入坐标 [0,10],文本的左上角将位于 [0,10]。如果您想在显示屏右侧绘制一些文本,有时选择不同的文本对齐方式会很有用。 当您输入 [0,10] 时,您实际上是在告诉 ESPHome 应该将文本的锚点定位在 [0,10]。当使用不同的对齐方式(如 TOP_RIGHT)时,文本将定位在锚点的左侧,因此顾名思义,锚点位于文本的右上角

display:
- platform: ...
# ...
lambda: |-
// 默认左对齐
it.print(0, 0, id(my_font), "Left aligned");
// 在右边缘对齐
it.print(it.get_width(), 0, id(my_font), TextAlign::TOP_RIGHT, "Right aligned");

与基本形状一样,您也可以为文本指定颜色:

display:
- platform: ...
# ...
lambda: |-
// 语法始终是:it.print(<x>, <y>, <font>, [color=COLOR_ON], [align=TextAlign::TOP_LEFT], <text>);
it.print(0, 0, id(my_font), COLOR_ON, "Left aligned");

对于以较高位深渲染的字体,必须指定背景颜色以便抗锯齿正常工作:

display:
- platform: ...
# ...
lambda: |-
// 语法始终是:it.print(<x>, <y>, <font>, [color=COLOR_ON], [align], <text>, [color=COLOR_OFF]);
it.print(0, 0, id(my_font_with_icons), COLOR_ON, TextAlign::CENTER, "Just\U000f05d4here. Already\U000F02D1this.", COLOR_OFF);

静态文本本身并不令人印象深刻。我们真正想要的是在显示屏上显示动态内容,如传感器值!这就是 printf 的用武之地。printf 是来自 C 时代的格式化引擎,ESPHome 选择使用它是因为…嗯,我太懒了,不想创建一个完整的格式化引擎,而现有的东西文档更完善 :)

printf 可以做的事情可能比您需要的多得多,但对于基本内容来说也相当简单。例如,一个 printf 调用可能如下所示:

sensor:
- platform: ...
# ...
id: my_sensor
display:
- platform: ...
# ...
lambda: |-
it.printf(0, 0, id(my_font), "The sensor value is: %.1f", id(my_sensor).state);
// 如果传感器值为 30.02,结果将是:"The sensor value is: 30.0"

如您所见,当您调用 printf 时,大部分字符串按原样打印,但当遇到这个带有些东西的奇怪百分号时,它会被神奇地替换为格式后面的参数(这里是 id(my_sensor).state)。

每次在 printf 格式字符串中输入百分号 % 时,它会将后面的字母视为格式标签,直到遇到所谓的”说明符”(在本例中是 f)。您可以在 https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm 阅读更多相关信息,但对于 ESPHome,您只需要了解几件事。

让我们分解 %.1f

  • % - 启动格式字符串
  • .1 - 将十进制数舍入到小数点后 1 位。
  • f - 告诉 printf 参数数据类型的说明符。这里是 f(loat,浮点数)。

例如,如果您想以两位精度打印传感器值,您应该写 %.2f,以零位精度(不带小数)打印则写 %.0f

另一个有趣的格式字符串是 %7.2f,对于值 20.506,它会变成右对齐的字符串 " 20.51"

  • % - 启动格式

  • 7 - 表示数字将右对齐,如果结果短于 7 个字符,将在左侧用空格填充。

  • .2 - 将十进制数舍入到小数点后 2 位。

  • f - 说明符:f(loat,浮点数)。

您甚至可以在单个 printf 调用中使用任意数量的格式化项目。只需确保按正确顺序将参数放在格式字符串之后。

display:
- platform: ...
# ...
lambda: |-
// %% - 字面 % 符号
it.printf(0, 0, id(my_font), "Temperature: %.1f°C, Humidity: %.1f%%", id(temperature).state, id(humidity).state);

要显示来自 text_sensor 的文本字符串,请在变量末尾附加 .c_str()

display:
- platform: ...
# ...
lambda: |-
it.printf(0, 0, id(my_font), "Text to follow: %s", id(template_text).state.c_str());

使用抗锯齿字体时,您可能需要指定绘制字符的颜色,以及用于抗锯齿混合的背景颜色。这需要使用完整版本的 printf,例如:

display:
- platform: ...
# ...
lambda: |-
it.printf(10, 100, id(roboto), Color(0x123456), COLOR_OFF, display::TextAlign::BASELINE, "%f", id(heap_free).state);

这里要讨论的最后一个关于显示屏中 printf 使用的技巧是如何显示二进制传感器值。您当然可以像下面示例的前几行那样用 if 语句检查状态,但如果您想高效一点,也可以使用内联 if。使用 %s 打印说明符,您可以告诉它使用您传递的任何字符串,如 "ON""OFF"

binary_sensor:
- platform: ...
# ...
id: my_binary_sensor
display:
- platform: ...
# ...
lambda: |-
if (id(my_binary_sensor).state) {
it.print(0, 0, id(my_font), "state: ON");
} else {
it.print(0, 0, id(my_font), "state: OFF");
}
// 简写形式:
it.printf(0, 0, id(my_font), "State: %s", id(my_binary_sensor).state ? "ON" : "OFF");

NOTE

要在显示屏上显示外部数据,例如来自您的 Home Assistant 实例的数据,您可以使用 Mqtt Subscribe(请参阅那里的示例了解更多信息)。

您可以使用时间组件显示当前时间。请参阅时间文档中的示例。

当您只想显示图像的一部分,或确保在屏幕上绘制的内容不会超出屏幕上特定区域时,屏幕裁剪会很有用。

使用 start_clipping(left, top, right, bottom); 开始裁剪过程,当您在该区域绘制完成后,可以使用 end_clipping(); 结束裁剪过程。您可以根据需要嵌套任意数量的 start_clipping();,只要也相应地结束相同次数即可。

binary_sensor:
- platform: ...
# ...
id: my_binary_sensor
color:
- id: my_red
red: 100%
display:
- platform: ...
# ...
lambda: |-
if (id(my_binary_sensor).state) {
it.print(0, 0, id(my_font), "state: ON");
} else {
it.print(0, 0, id(my_font), "state: OFF");
}
// 简写形式:
it.start_clipping(40,0,140,20);
it.printf(0, 0, id(my_font), id(my_red), "State: %s", id(my_binary_sensor).state ? "ON" : "OFF");
it.end_clipping();

开始裁剪后,您可以使用 extend_clipping(left, top, right, bottom);shrink_clipping(left, top, right, bottom); 在先前设置的裁剪区域内操作区域。

使用 get_clipping(); 可以获取一个包含最新设置的裁剪区域的 Rect 对象。

class Rect {
int16_t x; ///< X/左坐标
int16_t y; ///< Y/上坐标
int16_t w; ///< 宽度
int16_t h; ///< 高度
int16_t x2(); ///< 右坐标
int16_t y2(); ///< 下坐标
};

使用 is_clipping(); 可以判断裁剪是否已激活。

在 ESPHome 中使用支持 RGB 的显示屏时,您可能希望使用自定义颜色。 color 组件正是为此目的而存在:

color:
- id: my_light_red
red: 100%
green: 20%
blue: 25%
white: 0%

或者,您可以使用 <color>_int 将颜色指定为整数值:

color:
- id: my_light_red
red_int: 255
green_int: 51
blue_int: 64
white_int: 0

或者,如果您更习惯十六进制值,可以使用 hex

color:
- id: my_light_red
hex: FF3340

配置变量:

  • red (可选, 百分比):红色分量的百分比。默认为 100%
  • red_int (可选, 整数):红色分量的亮度,范围 0255。默认为 255
  • green (可选, 百分比):绿色分量的百分比。默认为 100%
  • green_int (可选, 整数):绿色分量的亮度,范围 0255。默认为 255
  • blue (可选, 百分比):蓝色分量的百分比。默认为 100%
  • blue_int (可选, 整数):蓝色分量的亮度,范围 0255。默认为 255
  • white (可选, 百分比):白色分量的百分比。默认为 100%
  • white_int (可选, 整数):白色分量的亮度,范围 0255。默认为 255
  • hex (可选, 字符串):十六进制表示的颜色。默认为 FFFFFF

RGB 显示屏使用红、绿、蓝三色,而灰度显示屏可能使用白色。

某些显示屏类型还允许您显示”页面”。使用页面,您可以创建可以切换的绘制 lambda。例如,使用页面,您可以设置 3 个屏幕,每个屏幕有不同的内容,并在定时器上切换它们。

display:
- platform: ...
# ...
id: my_display
pages:
- id: page1
lambda: |-
it.print(0, 10, id(my_font), "This is page 1!");
- id: page2
lambda: |-
it.print(0, 10, id(my_font), "This is page 2!");

然后您可以使用三种不同的动作在它们之间切换:

display.page.show_next / display.page.show_previous 动作

Section titled “display.page.show_next / display.page.show_previous 动作”

显示下一页或上一页,在末尾循环。

on_...:
- display.page.show_next: my_display
- display.page.show_previous: my_display
# 例如在定时器上循环页面
interval:
- interval: 5s
then:
- display.page.show_next: my_display
- component.update: my_display

显示特定页面。

on_...:
- display.page.show: page1
# 模板化
- display.page.show: !lambda |-
if (id(my_binary_sensor).state) {
return id(page1);
} else {
return id(page2);
}

NOTE

要在页面显示后立即触发重绘,请使用 component.update 动作:

# 例如在定时器上循环页面
interval:
- interval: 5s
then:
- display.page.show_next: my_display
- component.update: my_display

当显示指定页面时,此条件返回 true。

# 在某个触发器中:
on_...:
- if:
condition:
display.is_displaying_page: page1
then:
...
- if:
condition:
display.is_displaying_page:
id: my_display
page_id: page2
then:
...

on_page_change:当显示的页面发生变化时,此自动化将被触发。

display:
- platform: ...
# ...
on_page_change:
- from: page1
to: page2
then:
lambda: |-
ESP_LOGD("display", "Page changed from 1 to 2");
  • from (可选, ID):页面 ID。如果设置,则仅从该页面切换时才触发自动化。默认为所有页面。
  • to (可选, ID):页面 ID。如果设置,则仅切换到该页面时才触发自动化。默认为所有页面。

此外,旧页面将作为变量 from 提供,新页面将作为变量 to 提供。

如果您在使用彩色显示屏时遇到问题,show_test_card: true 选项可以帮助您识别可能出了什么问题。

  • 它将显示红、绿、蓝的色条,渐变到黑色和白色。
  • 同时还会显示字母 “R”、“G” 和 “B” 以验证显示屏几何形状。
  • 显示屏角落周围会有一个矩形,在 0,0 角有一个标记,应该在屏幕左上角。

当以上所有点都正确显示时,显示屏就按预期工作了。 为了帮助图形显示团队确定帮助您的最佳方式,此选项结果的图片非常有帮助

如果您在 GitHub 上创建问题报告您的显示屏问题,请务必包含您购买显示屏的链接,以便我们可以验证您使用的配置。

NOTE

如果您在配置中设置 update_interval: never,您将看不到测试卡,因为 display: 组件不会用测试卡更新显示屏。如果您想查看测试卡,请将 update_interval: 设置为 never 以外的值。

NOTE

对于 8 位模式的显示屏,您将看到明显的色块而不是平滑的渐变。