Because I was lazy to set time each time on old clock after power outages (and natural slow deviations).
I decided to build "Smarter" clock that would sync time from internet automatically (NTP). With a posibility to add something more on it in future.
As I run HomeAssitant at home I wanted something compatible with it and not "too much" complicated.
After some research I found:
CrowPanel ESP32 3.5" Model: DIS05035H
It is ESP32 based and compatible with ESPhome. 3.5" was choosen as it would fit into same place as Old Clock was. Bigger would not fit.
I Ordered it in AliExpress
Initialization of values and boot (because I can :-)) screen:
esphome:
name: hnr-clock
friendly_name: hnr-clock
on_boot:
- delay: 5s
- lvgl.widget.hide: boot_screen # hide boot screen
- lvgl.page.show:
id: clock_page
- script.execute: script_update_seconds # make sure labels are updated on boot
- script.execute: script_update_time # make sure labels are updated on boot
- script.execute: script_update_date # make sure labels are updated on boot
i2c:
- sda: 22
scl: 21
spi:
- id: tft
clk_pin: 14
mosi_pin: 13
miso_pin: 33 #12 on v2.0
interface: hardware
output:
- id: gpio_backlight_pwm
platform: ledc
pin: 27
inverted: False
light:
- id: backlight
name: Backlight
platform: monochromatic
output: gpio_backlight_pwm
restore_mode: ALWAYS_ON
Additional Pins on manufacturer documentation (not used in my implementation):
Used for | Pins |
---|---|
SPK | IO26 |
UART | RX(IO3);TX(IO1) |
SD Card Slot(SPI) | MOSI(IO23); MISO(IO19); CLK(IO18); CS(IO5) |
- The board has GPIOD connector which I just defined as switches (for possible future use)
- I created 2 templated switches
- Antiburn - used to enable screen antiburn function
- Nigth mode - used to enable different style and backlight on screen
switch:
- platform: gpio # for possible future use
pin: GPIO25
id: GPIO25
# name: ""
- platform: gpio # for possible future use
pin: GPIO32
id: GPIO32
# name: ""
- platform: template
name: Antiburn
id: switch_antiburn
icon: mdi:television-shimmer
optimistic: true
entity_category: "config"
turn_on_action:
- logger.log: "Starting Antiburn"
- lvgl.pause:
show_snow: true
- light.turn_off: backlight
turn_off_action:
- logger.log: "Stopping Antiburn"
- if:
condition: lvgl.is_paused
then:
- lvgl.resume:
- lvgl.widget.redraw:
- light.turn_on: backlight
- platform: template
name: "Night mode"
id: switch_night
icon: mdi:lightbulb-night-outline
optimistic: true
entity_category: "config"
turn_on_action:
- lambda: |-
lv_obj_add_style(id(clock_label), id(time_style_night), LV_PART_MAIN);
- light.turn_on:
id: backlight
brightness: 0.5
transition_length: 3s
turn_off_action:
- lambda: |-
lv_obj_add_style(id(clock_label), id(time_style), LV_PART_MAIN);
- light.turn_on:
id: backlight
brightness: 1
transition_length: 3s
I used transformation to position screen correctly which is recomended way to do it as it is hardware based (rotation
option is not hw based).
display:
- id: main_display
platform: ili9xxx
model: ILI9488_A
dimensions:
height: 320
width: 480
transform:
mirror_y: true
mirror_x: true
swap_xy: true
spi_id: tft
cs_pin:
number: 15
ignore_strapping_warning: true
dc_pin:
number: 2
ignore_strapping_warning: true
invert_colors: false
data_rate: 20MHz
update_interval: never
auto_clear_enabled: false
- Time element used NTP as a source and each second updates LVGL elements.
- For displaying day of the week (in words) I had to use lambda to calculate visualization of it.
- Actions are done based on Second, Minute and Hour change
- on each second it updates
second
label - on each minute it updates
Hours and Minutes
label - on each hour it updates
date
label
- on each second it updates
There can be more efficient logic done for updating these time labels, but I thought maybe it is sufficient to leave like this
time:
- platform: sntp
id: time_comp
timezone: "Europe/Vilnius"
on_time:
- seconds: /1
then:
- script.execute: script_update_seconds
- seconds: 0
minutes: /1
then:
- script.execute: script_update_time
- seconds: 0
minutes: 0
hours: /1
then:
- script.execute: script_update_date
touchscreen:
- id: main_touchscreen
platform: xpt2046
cs_pin:
number: 12 #33 on v2.0
ignore_strapping_warning: true
interrupt_pin: 36
update_interval: 50ms
threshold: 400
calibration:
x_min: 280
x_max: 3860
y_min: 340
y_max: 3860
transform:
mirror_y: false
swap_xy: true
My defined style elements:
- Images - used as boot image, or any other image I will use in future
- Colors - multiple colors defined to be able to use them in Styles
- Fonts - For nice 7 segment style time show I found digital-dismay font
image:
- file: Images/hnr_logo.png
id: boot_logo
resize: 320x320
type: RGB565
transparency: alpha_channel
color:
- id: my_red
red: 100%
green: 0%
blue: 0%
- id: my_darkred
red: 50%
green: 0%
blue: 0%
- id: my_yellow
red: 100%
green: 100%
blue: 0%
- id: my_green
red: 0%
green: 100%
blue: 0%
- id: my_darkgreen
red: 0%
green: 70%
blue: 0%
- id: my_blue
red: 0%
green: 0%
blue: 100%
- id: my_gray
red: 50%
green: 50%
blue: 50%
- id: my_white
red: 100%
green: 100%
blue: 100%
- id: my_black
red: 0%
green: 0%
blue: 0%
font:
- file: "fonts/digital-dismay.regular.otf"
id: font_clock
size: 200
glyphs: ":0123456789"
- file: "fonts/digital-dismay.regular.otf"
id: font_clock_seconds
size: 32
glyphs: ":0123456789"
- file: "gfonts://Roboto"
id: roboto
size: 40
bpp: 4
glyphs: |-
!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzĄČĘĖĮŠŲŪŽąčęėįšųūž
- I wanted to use was styles, this allows better control of style used (easy to change)
- I wanted to use LVGL pages, as it makes easy in future to define additional page and even switch between them
- I found grid style organization fits me best. Main page was designe to have more grids just in case I want to display more icons there (future use). For curent setup it would have been enough to use one collumn.
- I implemented stop antiburn action when screen gets a
on_press
action (just in case I need to see time when it is inantiburn
mode).
lvgl:
buffer_size: 25%
disp_bg_color: 0x000000
style_definitions:
- id: time_style
text_color: my_green
text_font: font_clock
- id: time_style_night
text_color: my_darkgreen
text_font: font_clock
- id: seconds_style
text_color: my_green
text_font: font_clock_seconds
- id: date_style
text_color: my_green
text_font: roboto
pages:
- id: clock_page
bg_color: 0x000000
widgets:
- obj: # a properly placed coontainer object for all these controls
align: CENTER
width: 100%
height: 100%
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 1
layout: # enable the GRID layout for the children widgets
type: GRID # split the rows and the columns proportionally
grid_columns: [FR(1), FR(2), FR(2), FR(2), FR(2), FR(1)] # equal
grid_rows: [FR(15), FR(10), FR(50), FR(5), FR(15)] # like percents
widgets:
- label:
id: clock_label_date
grid_cell_column_pos: 1
grid_cell_row_pos: 0
grid_cell_column_span: 4
grid_cell_row_span: 1
grid_cell_x_align: CENTER
grid_cell_y_align: END
# border_color: 0xffffff
# border_width: 1
align: CENTER
styles: date_style
text: "1999-01-01"
- label:
id: clock_label
grid_cell_column_pos: 1
grid_cell_row_pos: 2
grid_cell_column_span: 4
grid_cell_row_span: 1
grid_cell_x_align: CENTER
grid_cell_y_align: CENTER
# border_color: 0xffffff
# border_width: 1
align: CENTER
styles: time_style
text: "99:99"
- label:
id: clock_label_seconds
grid_cell_column_pos: 5
grid_cell_row_pos: 3
grid_cell_column_span: 1
grid_cell_row_span: 1
grid_cell_x_align: END
grid_cell_y_align: END
# border_color: 0xffffff
# border_width: 1
align: CENTER
styles: seconds_style
text: "99s"
- label:
id: clock_label_dateofweek
grid_cell_column_pos: 1
grid_cell_row_pos: 4
grid_cell_column_span: 4
grid_cell_row_span: 1
grid_cell_x_align: CENTER
grid_cell_y_align: START
# border_color: 0xffffff
# border_width: 1
align: CENTER
styles: date_style
text: "Sunday"
on_press:
- switch.turn_off: switch_antiburn
top_layer:
widgets:
- obj:
id: boot_screen
x: 0
y: 0
width: 100%
height: 100%
bg_color: 0x000000
bg_opa: COVER
radius: 0
pad_all: 0
border_width: 0
widgets:
- image:
align: CENTER
src: boot_logo
- spinner:
align: CENTER
height: 200
width: 200
spin_time: 1s
arc_length: 60deg
arc_width: 8
indicator:
arc_color: 0x18bcf2
arc_width: 8
on_press:
- lvgl.widget.hide: boot_screen
I used scripts to update seconds, time(hour and minute), date (month day and day of the week) LVGL labels.
As I use then in multiple places thats why I use scripts. Scripts are used on boot that would set initial time after boot and not wait for time to come to update LVGL object.
script:
- id: script_update_seconds
then:
- lvgl.label.update:
id: clock_label_seconds
text:
format: "%02d"
args:
- id(time_comp).now().second
- id: script_update_time
then:
- lvgl.label.update:
id: clock_label
text:
format: "%02d:%02d"
args:
- id(time_comp).now().hour
- id(time_comp).now().minute
- id: script_update_date
then:
- lvgl.label.update:
id: clock_label_date
text:
format: "%04d-%02d-%02d"
args:
- id(time_comp).now().year
- id(time_comp).now().month
- id(time_comp).now().day_of_month
- lvgl.label.update:
id: clock_label_dateofweek
text: !lambda |-
auto day_of_week = id(time_comp).now().day_of_week;
//if (!time.is_valid()) return std::string("Invalid time");
const char* days[] = {"Sekmadienis","Pirmadienis", "Antradienis", "Trečiadienis", "Ketvirtadienis", "Penktadienis", "Šeštadienis"};
char buf[48];
snprintf(buf, sizeof(buf), "%s",
days[day_of_week-1]);
return std::string(buf);
Antiburn
switch enables antiburn of screen. Still thinking when to use it. Maybe during 3am-5am or when nobody is home (contorlled by HomeAssitant) or ...Night mode
switch can toggles night mode by applying different style, backlight or ...
The Manyfacturer has shared 3d STEP file for case. You can even buy screen with already made Acrylic case. It was not suitable for me as I need it to be standing.
I took it organized a bit (grouped properly) and modified (added holder) to be able to stand with 30° angle.
- Decide when/if to use
Antiburn
mode - Maybe add additional icons that would show HomeAssitant things
- Maybe create separate LVGL page that could show weather forecast