From 4c612fc462b882a763f3ea4960d6920926042c49 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Wed, 25 May 2022 02:06:52 +0200 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=F0=9F=8E=89=20add=20first=20try?= =?UTF-8?q?=20at=20an=20esphome=20component=20for=20this=20wordclock=20fir?= =?UTF-8?q?mware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code is loosely based on https://github.com/leinich/ha-wordclock-esphome --- wordclock.h | 222 +++++++++++++++++++++++++++++++++++++++++++++++++ wordclock.yaml | 61 ++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 wordclock.h create mode 100644 wordclock.yaml diff --git a/wordclock.h b/wordclock.h new file mode 100644 index 0000000..758ef0b --- /dev/null +++ b/wordclock.h @@ -0,0 +1,222 @@ +#include "esphome.h" +#include + +// By now only loosely based on https://github.com/leinich/ha-wordclock-esphome + +// esphome dependencies: +// needs: esphome time --> id: current_time +// needs: esphome fastled --> id: fastledlight + +///// Random Stuff ///// + +#ifndef WORDCLOCK_NUM_LEDS +#define WORDCLOCK_NUM_LEDS 198 +#endif + +#ifndef WORDCLOCK_DATA_PIN +#define WORDCLOCK_DATA_PIN 22 +#endif + +/* NOTE: +This section is about mapping LED indices to +positions in the grid. + +On EVEN rows, the LED strip is running along the +matrix indices, but the physical positions of the +LEDs on the strip did not align with the letter +cutouts. + +On ODD rows, the string is running backwards, because +it is snaking its way back and forth. */ +int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17}; +int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0}; +int map_coords_to_strip(int row, int column) { + if (column % 2) { + return (10 - column) * 18 + mapping_odd[row]; + } else { + return (10 - column) * 18 + mapping_even [row]; + } +} + +///// Word Table ///// +int WORD_IT_IS[5][2] = {{4,0}, {0,0}, {0,1}, {0,3}, {0,4}}; + +int WORD_QUARTER[8][2] = {{7,0}, {1,2}, {1,3}, {1,4}, {1,5}, {1,6}, {1,7}, {1,8}}; +int WORD_TWENTY[7][2] = {{6,0}, {2,0}, {2,1}, {2,2}, {2,3}, {2,4}, {2,5}}; +int WORD_FIVE_MINUTES[5][2] = {{4,0}, {2,6}, {2,7}, {2,8}, {2,9}}; +int WORD_HALF[5][2] = {{4,0}, {3,0}, {3,1}, {3,2}, {3,3}}; +int WORD_TEN_MINUTES[4][2] = {{3,0}, {3,5}, {3,6}, {3,7}}; + +int WORD_TO[3][2] = {{2,0}, {3,9}, {3,10}}; +int WORD_PAST[5][2] = {{4,0}, {4,0}, {4,1}, {4,2}, {4,3}}; + +int WORD_NINE[5][2] = {{4,0}, {4,7}, {4,8}, {4,9}, {4,10}}; +int WORD_ONE[4][2] = {{3,0}, {5,0}, {5,1}, {5,2}}; +int WORD_SIX[4][2] = {{3,0}, {5,3}, {5,4}, {5,5}}; +int WORD_THREE[6][2] = {{5,0}, {5,6}, {5,7}, {5,8}, {5,9}, {5,10}}; +int WORD_FOUR[5][2] = {{4,0}, {6,0}, {6,1}, {6,2}, {6,3}}; +int WORD_FIVE[5][2] = {{4,0}, {6,4}, {6,5}, {6,6}, {6,7}}; +int WORD_TWO[4][2] = {{3,0}, {6,8}, {6,9}, {6,10}}; +int WORD_EIGHT[6][2] = {{5,0}, {7,0}, {7,1}, {7,2}, {7,3}, {7,4}}; +int WORD_ELEVEN[7][2] = {{6,0}, {7,5}, {7,6}, {7,7}, {7,8}, {7,9}, {7,10}}; +int WORD_SEVEN[6][2] = {{5,0}, {8,0}, {8,1}, {8,2}, {8,3}, {8,4}}; +int WORD_TWELFE[7][2] = {{6,0}, {8,5}, {8,6}, {8,7}, {8,8}, {8,9}, {8,10}}; +int WORD_TEN[4][2] = {{3,0}, {9,0}, {9,1}, {9,2}}; + +int WORD_OCLOCK[7][2] = {{6,0}, {9,5}, {9,6}, {9,7}, {9,8}, {9,9}, {9,10}}; + +// TODO: It's probably unnecessary to have the "API" in CustomAPIDevice, since this +// component doesn't actually register any services anymore. +class Wordclock : public Component, public CustomAPIDevice { + public: + CRGB leds[WORDCLOCK_NUM_LEDS]; + int hour = -1; + int minute = -1; + int red = 124; + int green = 124; + int blue = 124; + int brightness = 50; + + void setup() override { + FastLED.addLeds(leds, WORDCLOCK_NUM_LEDS); + FastLED.setBrightness(brightness); + clear_all_leds(); + FastLED.show(); + // TODO: Set up some kind of initialization sequence. But it should be based on an effect or similarly supporting the + // cooperative multithreading. delay() calls are uncalled for. ^^ + } + + void clear_all_leds() { + for(int i = 0; i < WORDCLOCK_NUM_LEDS; i++) { + leds[i].setRGB(0, 0, 0); + } + } + + void display_word(const int word[][2], const CRGB& c) { + for (int i=1; i < word[0][0] + 1; i++) { + leds[map_coords_to_strip(word[i][0], word[i][1])].setRGB(c.r, c.g, c.b); + } + } + + void display_minutes(int minutes, const CRGB& color) { + int five_minute_chunk = minutes / 5; + + switch (five_minute_chunk) + { + case 0: // sharp + display_word(WORD_OCLOCK, color); ESP_LOGD("minute", "oclock "); break; + case 1: // five past + display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five past "); break; + case 2: // ten past + display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten past "); break; + case 3: // quarter past + display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter past "); break; + case 4: // twenty past + display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty past "); break; + case 5: // twenty five past + display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color); + ESP_LOGD("minute", "twenty five past "); break; + case 6: // half past + display_word(WORD_HALF, color); ESP_LOGD("minute", "half past "); break; + case 7: // twenty five to + display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color); + ESP_LOGD("minute", "twenty five to "); break; + case 8: // twenty to + display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty to "); break; + case 9: // quarter to + display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter to "); break; + case 10: // ten to + display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten to "); break; + case 11: // five to + display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five to "); break; + default: + break; + } + if (five_minute_chunk > 6) { + display_word(WORD_TO, color); + } else if (five_minute_chunk > 0) { + display_word(WORD_PAST, color); + } + } + + void display_hour(int hour, int minutes, const CRGB& color) { + int five_minute_chunk = minutes / 5; + if (five_minute_chunk > 6) { + hour += 1; + } + + switch (hour % 12) + { + case 0: // twelve + display_word(WORD_TWELFE, color); ESP_LOGD("hour", "twelve "); break; + case 1: // one + display_word(WORD_ONE, color); ESP_LOGD("hour", "one "); break; + case 2: // two + display_word(WORD_TWO, color); ESP_LOGD("hour", "two "); break; + case 3: // three + display_word(WORD_THREE, color); ESP_LOGD("hour", "three "); break; + case 4: // four + display_word(WORD_FOUR, color); ESP_LOGD("hour", "four "); break; + case 5: // five + display_word(WORD_FIVE, color); ESP_LOGD("hour", "five "); break; + case 6: // six + display_word(WORD_SIX, color); ESP_LOGD("hour", "six "); break; + case 7: // seven + display_word(WORD_SEVEN, color); ESP_LOGD("hour", "seven "); break; + case 8: // eight + display_word(WORD_EIGHT, color); ESP_LOGD("hour", "eight "); break; + case 9: // nine + display_word(WORD_NINE, color); ESP_LOGD("hour", "nine "); break; + case 10: // ten + display_word(WORD_TEN, color); ESP_LOGD("hour", "ten "); break; + case 11: // eleven + display_word(WORD_ELEVEN, color); ESP_LOGD("hour", "eleven "); break; + default: + break; + } + } + + void display_time(int hour, int minutes, const CRGB& color) { + display_word(WORD_IT_IS, color); + display_hour(hour, minutes, color); + display_minutes(minutes, color); + } + + void loop() override { + auto time = id(current_time).now(); + // https://www.esphome.io/api/classesphome_1_1light_1_1_light_color_values.html LightColorValues Class + auto fastledlight2 = id(fastledlight).current_values; + //convert float 0.0 till 1.0 into int 0 till 255 + red = (int) (fastledlight2.get_red() * 125); + green = (int) (fastledlight2.get_green() * 125); + blue = (int) (fastledlight2.get_blue() * 125); + brightness = 0; + //check if light is on and set brightness + if (fastledlight2.get_state() > 0 ) { + brightness = (int) (fastledlight2.get_brightness()*125); + } else { + ESP_LOGD("loop", "fastledlight state off - b: %i rgb %i %i %i", brightness, red, green, blue); delay(100); + } + + FastLED.setBrightness(brightness); + FastLED.show(); + //check if valid time. Blink red,green,blue until valid time is present + if (!time.is_valid() == false) { + // do something to show that the clock isn't dead. Maybe I can instantiate and effect and use that for this. + } + else { + if(time.hour != hour || time.minute != minute) { + hour = time.hour; + minute = time.minute; + if (hour >= 0 && time.is_valid() == true){ + + clear_all_leds(); + display_time(time.hour, time.minute, CRGB(red, green, blue)); + FastLED.show(); + + ESP_LOGE("loop", "Update Time: %i:%i Brightness: %i RGB: %i-%i-%i", time.hour, time.minute, brightness, red, green, blue); + } + } + } + } +}; diff --git a/wordclock.yaml b/wordclock.yaml new file mode 100644 index 0000000..cd6027e --- /dev/null +++ b/wordclock.yaml @@ -0,0 +1,61 @@ +esphome: + name: "${devicename}" + platformio_options: + build_flags: + - -DWORDCLOCK_DATA_PIN=22 + - -DWORDCLOCK_NUM_LEDS=198 + includes: + - wordclock.h + +esp32: + board: wemos_d1_mini32 + framework: + type: arduino + +substitutions: + devicename: wordclock + friendly_name: "Wordclock" + light_friendly_name: "Wordclock Light" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "${devicename}" + password: !secret ap_password + manual_ip: + static_ip: !secret manualip_static_ip + gateway: !secret manualip_gateway + subnet: !secret manualip_subnet + +api: + +ota: + password: "${devicename}" + +logger: + # esp8266_store_log_strings_in_flash: false +web_server: + port: 80 + +light: + - platform: fastled_clockless + id: fastledlight + chipset: WS2812 + pin: 23 + num_leds: 198 + rgb_order: GRB + name: ${light_friendly_name} + restore_mode: ALWAYS_ON + +time: + - platform: sntp + id: current_time + timezone: !secret timezone + +custom_component: + - lambda: |- + auto wordclock = new Wordclock(); + return {wordclock}; + components: + - id: wordclock From 5b0930d616b64e1154a62bf52cc341f95b6556de Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Wed, 3 May 2023 20:55:22 +0200 Subject: [PATCH 02/16] feat: add external component and some test configurations --- .gitignore | 7 ++ components/wordcl/__init__.py | 53 ++++++++++++++++ components/wordcl/desky.cpp | 116 ++++++++++++++++++++++++++++++++++ components/wordcl/desky.h | 52 +++++++++++++++ display_test.yaml | 66 +++++++++++++++++++ flake.nix | 67 ++++++++++++++++++++ secrets.example.yaml | 7 ++ wordclock.h | 12 +++- wordclock.yaml | 19 +++++- wordclock8266.yaml | 89 ++++++++++++++++++++++++++ wordclock_new.yaml | 29 +++++++++ 11 files changed, 511 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 components/wordcl/__init__.py create mode 100644 components/wordcl/desky.cpp create mode 100644 components/wordcl/desky.h create mode 100644 display_test.yaml create mode 100644 flake.nix create mode 100644 secrets.example.yaml create mode 100644 wordclock8266.yaml create mode 100644 wordclock_new.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..844caea --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml +/secrets.*.yaml +!/secrets.example.yaml diff --git a/components/wordcl/__init__.py b/components/wordcl/__init__.py new file mode 100644 index 0000000..de1ab14 --- /dev/null +++ b/components/wordcl/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import light +from esphome.components import uart +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_HEIGHT, CONF_TIMEOUT, ICON_GAUGE + +DEPENDENCIES = ['time'] +AUTO_LOAD = ['light'] + +wordclock_ns = cg.esphome_ns.namespace('wordcl') + +Wordclock = wordclock_ns.class_('Wordclock', cg.Component, light.) +Desky = desky_ns.class_('Desky', cg.Component, uart.UARTDevice) + +CONF_UP = "up" +CONF_DOWN = "down" +CONF_REQUEST = "request" +CONF_STOPPING_DISTANCE = "stopping_distance" + + + +CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(Desky), + cv.Optional(CONF_UP): pins.gpio_output_pin_schema, + cv.Optional(CONF_DOWN): pins.gpio_output_pin_schema, + cv.Optional(CONF_REQUEST): pins.gpio_output_pin_schema, + cv.Optional(CONF_HEIGHT): sensor.sensor_schema(icon=ICON_GAUGE, accuracy_decimals=0), + cv.Optional(CONF_STOPPING_DISTANCE, default=15): cv.positive_int, + cv.Optional(CONF_TIMEOUT): cv.time_period, +}).extend(uart.UART_DEVICE_SCHEMA) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + if CONF_UP in config: + pin = await cg.gpio_pin_expression(config[CONF_UP]) + cg.add(var.set_up_pin(pin)) + if CONF_DOWN in config: + pin = await cg.gpio_pin_expression(config[CONF_DOWN]) + cg.add(var.set_down_pin(pin)) + if CONF_REQUEST in config: + pin = await cg.gpio_pin_expression(config[CONF_REQUEST]) + cg.add(var.set_request_pin(pin)) + if CONF_HEIGHT in config: + sens = await sensor.new_sensor(config[CONF_HEIGHT]) + cg.add(var.set_height_sensor(sens)) + cg.add(var.set_stopping_distance(config[CONF_STOPPING_DISTANCE])) + if CONF_TIMEOUT in config: + cg.add(var.set_timeout(config[CONF_TIMEOUT].total_milliseconds)) + diff --git a/components/wordcl/desky.cpp b/components/wordcl/desky.cpp new file mode 100644 index 0000000..bcd6159 --- /dev/null +++ b/components/wordcl/desky.cpp @@ -0,0 +1,116 @@ +#include "desky.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace desky { + +static const char *TAG = "desky"; + +const char *desky_operation_to_str(DeskyOperation op) { + switch (op) { + case DESKY_OPERATION_IDLE: + return "IDLE"; + case DESKY_OPERATION_RAISING: + return "RAISING"; + case DESKY_OPERATION_LOWERING: + return "LOWERING"; + default: + return "UNKNOWN"; + } +} + +void Desky::setup() { + if (this->up_pin_ != nullptr) + this->up_pin_->digital_write(false); + if (this->down_pin_ != nullptr) + this->down_pin_->digital_write(false); + if (this->request_pin_ != nullptr) { + this->request_pin_->digital_write(true); + this->request_time_ = millis(); + } +} + +void Desky::loop() { + static int state = 0; + static uint8_t high_byte; + + while (this->available()) { + uint8_t c; + int value; + this->read_byte(&c); + switch (state) { + case 0: + if (c == 1) + state = 1; + break; + case 1: + if (c == 1) + state = 2; + else + state = 0; + break; + case 2: + high_byte = c; + state = 3; + break; + case 3: + value = (high_byte << 8) + c; + this->current_pos_ = value; + if (this->height_sensor_ != nullptr) + this->height_sensor_->publish_state(value); + state = 0; + break; + } + } + + if (this->target_pos_ >= 0) { + if (abs(this->target_pos_ - this->current_pos_) < this->stopping_distance_) + this->stop(); + if ((this->timeout_ >= 0) && (millis() - this->start_time_ >= this->timeout_)) + this->stop(); + } + + if ((this->request_time_ > 0) && (millis() - this->request_time_ >= 100)) { + this->request_pin_->digital_write(false); + this->request_time_ = 0; + } +} + +void Desky::dump_config() { + ESP_LOGCONFIG(TAG, "Desky desk:"); + LOG_SENSOR("", "Height", this->height_sensor_); + LOG_PIN("Up pin: ", this->up_pin_); + LOG_PIN("Down pin: ", this->down_pin_); + LOG_PIN("Request pin: ", this->request_pin_); +} + +void Desky::move_to(int target_pos) { + if (abs(target_pos - this->current_pos_) < this->stopping_distance_) + return; + if (target_pos > this->current_pos_) { + if (this->up_pin_ == nullptr) + return; + this->up_pin_->digital_write(true); + this->current_operation = DESKY_OPERATION_RAISING; + } else { + if (this->down_pin_ == nullptr) + return; + this->down_pin_->digital_write(true); + this->current_operation = DESKY_OPERATION_LOWERING; + } + this->target_pos_ = target_pos; + if (this->timeout_ >= 0) + this->start_time_ = millis(); +} + +void Desky::stop() { + this->target_pos_ = -1; + if (this->up_pin_ != nullptr) + this->up_pin_->digital_write(false); + if (this->down_pin_ != nullptr) + this->down_pin_->digital_write(false); + this->current_operation = DESKY_OPERATION_IDLE; +} + +} // namespace desky +} // namespace esphome diff --git a/components/wordcl/desky.h b/components/wordcl/desky.h new file mode 100644 index 0000000..fc9062c --- /dev/null +++ b/components/wordcl/desky.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace desky { + +enum DeskyOperation : uint8_t { + DESKY_OPERATION_IDLE = 0, + DESKY_OPERATION_RAISING, + DESKY_OPERATION_LOWERING, +}; + +const char *desky_operation_to_str(DeskyOperation op); + +class Desky : public Component, public sensor::Sensor, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::LATE; } + void setup() override; + void loop() override; + void dump_config() override; + + void set_height_sensor(sensor::Sensor *sensor) { this->height_sensor_ = sensor; } + void set_up_pin(GPIOPin *pin) { this->up_pin_ = pin; } + void set_down_pin(GPIOPin *pin) { this->down_pin_ = pin; } + void set_request_pin(GPIOPin *pin) { this->request_pin_ = pin; } + void set_stopping_distance(int distance) { this->stopping_distance_ = distance; } + void set_timeout(int timeout) { this->timeout_ = timeout; } + + void move_to(int height); + void stop(); + + DeskyOperation current_operation{DESKY_OPERATION_IDLE}; + + protected: + sensor::Sensor *height_sensor_{nullptr}; + GPIOPin *up_pin_{nullptr}; + GPIOPin *down_pin_{nullptr}; + GPIOPin *request_pin_{nullptr}; + int stopping_distance_; + int current_pos_{0}; + int target_pos_{-1}; + int timeout_{-1}; + uint64_t start_time_; + uint64_t request_time_{0}; +}; + +} // namespace desky +} // namespace esphome diff --git a/display_test.yaml b/display_test.yaml new file mode 100644 index 0000000..837eacf --- /dev/null +++ b/display_test.yaml @@ -0,0 +1,66 @@ +esphome: + name: "display_thing" + +esp8266: + board: d1_mini + framework: + version: recommended + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "${devicename}" + password: !secret ap_password + manual_ip: + static_ip: !secret manualip_static_ip + gateway: !secret manualip_gateway + subnet: !secret manualip_subnet + dns1: 1.1.1.1 + dns2: 1.0.0.1 + +api: + +ota: + password: "${devicename}" + +logger: + # esp8266_store_log_strings_in_flash: false +web_server: + port: 80 + +light: + - name: NeoPixel Strip 1 + id: neopixel_strip_1 + platform: neopixelbus + type: GRB + variant: WS2812 + pin: GPIO3 + num_leds: 200 + # default_transition_length: 0.2s + method: + type: esp8266_dma + +display: + - platform: addressable_light + id: led_matrix_display + addressable_light_id: neopixel_strip_1 + width: 18 + height: 11 + rotation: 0° + update_interval: 16ms + lambda: |- + // Draw a bulls-eye pattern + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, 18, 11, red); + it.rectangle(1, 1, 17, 10, green); + it.rectangle(2, 2, 16, 9, blue); + it.rectangle(3, 3, 15, 8, red); + + +time: + - platform: sntp + id: current_time + timezone: !secret timezone diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d696ee7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,67 @@ +{ + description = "EspHome Word Clock"; + inputs = { + systems.url = "github:nix-systems/x86_64-linux"; + # nixpkgs.url = "github:NixOS/nixpkgs/unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/22.11"; + utils.url = "github:numtide/flake-utils"; + utils.inputs.systems.follows = "systems"; + }; + outputs = { self, nixpkgs, utils, ... }: utils.lib.eachDefaultSystem ( + system: + let pkgs = import nixpkgs { inherit system; }; + in { + + # This block here is used when running `nix develop` + devShells.default = pkgs.mkShell rec { + # Update the name to something that suites your project. + name = "esphome-wordclock"; + + # build environment dependencies + packages = with pkgs; [ + esphome + ]; + + # Setting up the environment variables you need during development. + + # Todo figure out why I can't use clang on Asahi but can on Darwin + # Use "clang++" for most systems but OSX Asahi requires g++ for some reason or a runtime error occurs + shellHook = let + # This is for an icon that is used below for the command line input below + icon = "f121"; + in '' + export PS1="$(echo -e '\u${icon}') {\[$(tput sgr0)\]\[\033[38;5;228m\]\w\[$(tput sgr0)\]\[\033[38;5;15m\]} (${name}) \\$ \[$(tput sgr0)\]" + # export COMPILER="clang++" + #export COMPILER="g++" + echo "test"; + ''; + }; + # This is used when running `nix build` + # packages.default = pkgs.llvmPackages_14.stdenv.mkDerivation rec { + packages.default = pkgs.esphome.stdenv.mkDerivation rec { + name = "esphome-wordclock"; + version = "2.0.0"; + src = self; + + # buildInputs = [ pkgs.esphome ]; + + # buildPhase = "COMPILER='clang++' make"; + + # installPhase = '' + # mkdir -p $out/bin; + # install -t $out/bin worm + # ''; + + # meta = with inputs.utils.lib; { + # homepage = "https://github.com/icecreammatt/ssu-cs315-worm"; + # description = '' + # Terminal CLI Worm Game + # ''; + # }; + }; + # packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello; + + # packages.x86_64-linux.default = self.packages.x86_64-linux.hello; + + }); +} diff --git a/secrets.example.yaml b/secrets.example.yaml new file mode 100644 index 0000000..a6373f1 --- /dev/null +++ b/secrets.example.yaml @@ -0,0 +1,7 @@ +wifi_password: secret +wifi_ssid: example +manualip_static_ip: 192.168.1.177 +manualip_gateway: 192.168.1.1 +manualip_subnet: 255.255.255.0 +timezone: Europe/Zurich +ap_password: "password for internal fallback accesspoint" diff --git a/wordclock.h b/wordclock.h index 758ef0b..95feeb5 100644 --- a/wordclock.h +++ b/wordclock.h @@ -72,6 +72,7 @@ class Wordclock : public Component, public CustomAPIDevice { CRGB leds[WORDCLOCK_NUM_LEDS]; int hour = -1; int minute = -1; + int second = -1; int red = 124; int green = 124; int blue = 124; @@ -83,7 +84,7 @@ class Wordclock : public Component, public CustomAPIDevice { clear_all_leds(); FastLED.show(); // TODO: Set up some kind of initialization sequence. But it should be based on an effect or similarly supporting the - // cooperative multithreading. delay() calls are uncalled for. ^^ + // cooperative multithreading. delay() calls are uncalled for. } void clear_all_leds() { @@ -195,16 +196,21 @@ class Wordclock : public Component, public CustomAPIDevice { if (fastledlight2.get_state() > 0 ) { brightness = (int) (fastledlight2.get_brightness()*125); } else { - ESP_LOGD("loop", "fastledlight state off - b: %i rgb %i %i %i", brightness, red, green, blue); delay(100); + // ESP_LOGD("loop", "fastledlight state off - b: %i rgb %i %i %i", brightness, red, green, blue); delay(100); } FastLED.setBrightness(brightness); FastLED.show(); //check if valid time. Blink red,green,blue until valid time is present - if (!time.is_valid() == false) { + if (time.is_valid() == false) { + ESP_LOGD("loop", "time is not valid"); // do something to show that the clock isn't dead. Maybe I can instantiate and effect and use that for this. } else { + if (time.second != second) { + second = time.second; + ESP_LOGD("loop", "time is now [%02i:%02i:%02i]", time.hour, time.minute, time.second); + } if(time.hour != hour || time.minute != minute) { hour = time.hour; minute = time.minute; diff --git a/wordclock.yaml b/wordclock.yaml index cd6027e..d3250e1 100644 --- a/wordclock.yaml +++ b/wordclock.yaml @@ -7,10 +7,21 @@ esphome: includes: - wordclock.h -esp32: - board: wemos_d1_mini32 +external_components: + - source: + type: local + path: components + components: [ wordcl ] + +esp8266: + board: d1_mini framework: - type: arduino + version: recommended + +# esp32: +# board: ttgo-t7-v13-mini32 +# framework: +# type: arduino substitutions: devicename: wordclock @@ -27,6 +38,8 @@ wifi: static_ip: !secret manualip_static_ip gateway: !secret manualip_gateway subnet: !secret manualip_subnet + dns1: 1.1.1.1 + dns2: 1.0.0.1 api: diff --git a/wordclock8266.yaml b/wordclock8266.yaml new file mode 100644 index 0000000..5986c77 --- /dev/null +++ b/wordclock8266.yaml @@ -0,0 +1,89 @@ +esphome: + name: "wordclock" + +esp8266: + board: d1_mini + framework: + version: recommended + +external_components: + - source: + type: local + path: components + components: [ wordcl ] + +# substitutions: +# devicename: wordclock +# friendly_name: "Wordclock" +# light_friendly_name: "Wordclock Light" + +# wifi: +# ssid: !secret wifi_ssid +# password: !secret wifi_password +# ap: +# ssid: "${devicename}" +# password: !secret ap_password +# manual_ip: +# static_ip: !secret manualip_static_ip +# gateway: !secret manualip_gateway +# subnet: !secret manualip_subnet +# dns1: 1.1.1.1 +# dns2: 1.0.0.1 + +# api: + +# ota: +# password: "${devicename}" + +# logger: +# # esp8266_store_log_strings_in_flash: false +# web_server: +# port: 80 + +wordclock: + +light: + - name: neopixel + id: neopixel_1 + platform: neopixelbus + type: GRB + variant: WS2812 + pin: GPIO3 + num_leds: 30 + default_transition_length: 0.2s + # restore_mode: RESTORE_DEFAULT_ON + method: + type: esp8266_dma + on_turn_on: + then: + - light.turn_on: + id: neopixel_1 + brightness: 35% + effect: rainbow + effects: + # - random: + # - pulse: + # - strobe: + # - flicker: + - addressable_rainbow: + name: rainbow + # - addressable_color_wipe: + # - addressable_scan: + # - addressable_twinkle: + # - addressable_random_twinkle: + # - addressable_fireworks: + # - addressable_flicker: + # - wled: + + + + +# time: +# - platform: sntp +# id: current_time +# timezone: !secret timezone + +# - light.turn_on: +# id: light_1 +# brightness: 100% +# effect: addressable_rainbow \ No newline at end of file diff --git a/wordclock_new.yaml b/wordclock_new.yaml new file mode 100644 index 0000000..cdaf34e --- /dev/null +++ b/wordclock_new.yaml @@ -0,0 +1,29 @@ +esphome: + name: wordclock-livingroom + +esp32: + board: ttgo-t7-v13-mini32 + framework: + type: arduino + +# Enable logging +logger: + +# Enable Home Assistant API +api: + password: "" + +ota: + password: "" + +wifi: + ssid: "wifithing" + password: "lostandfound1" + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Wordclock-Livingroom" + password: "wGfGBPnJcbzE" + +captive_portal: + \ No newline at end of file From 7f091409cf957f2d844e978787764057c20ca72c Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Tue, 9 May 2023 20:01:36 +0200 Subject: [PATCH 03/16] feat: wordclock works! --- .gitignore | 3 + components/wordclock/__init__.py | 447 +++++++++++++++++++++++++++++ components/wordclock/wordclock.cpp | 305 ++++++++++++++++++++ components/wordclock/wordclock.h | 227 +++++++++++++++ display_test.yaml | 56 +++- image_test.yaml | 154 ++++++++++ image_test_3leds.yaml | 225 +++++++++++++++ test.png | Bin 0 -> 780 bytes 8 files changed, 1405 insertions(+), 12 deletions(-) create mode 100644 components/wordclock/__init__.py create mode 100644 components/wordclock/wordclock.cpp create mode 100644 components/wordclock/wordclock.h create mode 100644 image_test.yaml create mode 100644 image_test_3leds.yaml create mode 100644 test.png diff --git a/.gitignore b/.gitignore index 844caea..08f0921 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ /secrets.yaml /secrets.*.yaml !/secrets.example.yaml +/.pio +/.vscode +__pycache__ diff --git a/components/wordclock/__init__.py b/components/wordclock/__init__.py new file mode 100644 index 0000000..3192cf8 --- /dev/null +++ b/components/wordclock/__init__.py @@ -0,0 +1,447 @@ +import logging + +from esphome import core +from esphome.components import display, font, time, light +import esphome.config_validation as cv +import esphome.codegen as cg +import esphome.cpp_generator as cpp +from esphome.const import ( + CONF_ID, + CONF_NAME, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_TIME_ID, + CONF_SEGMENTS, + CONF_ADDRESSABLE_LIGHT_ID, + CONF_HOUR, + CONF_HOURS, + CONF_MINUTE, + CONF_MINUTES, +) +from esphome.core import CORE, HexInt + +_LOGGER = logging.getLogger(__name__) + +CONF_DISPLAY_ID = "display_id" + +CONF_LINE_START_X = "x1" +CONF_LINE_START_Y = "y1" + +CONF_LINE_END_X = "x2" +CONF_LINE_END_Y = "y2" + +CONF_LINE = "line" + +CONF_WORDCLOCK_STATIC_SEGMENTS = "static_segments" +CONF_HOUR_OFFSET = "hour_offset" + +# CONF_HOURS_MIDNIGHT = "0" +# CONF_HOURS_ONE = "1" +# CONF_HOURS_TWO = "2" +# CONF_HOURS_THREE = "3" +# CONF_HOURS_FOUR = "4" +# CONF_HOURS_FIVE = "5" +# CONF_HOURS_SIX = "6" +# CONF_HOURS_SEVEN = "7" +# CONF_HOURS_EIGHT = "8" +# CONF_HOURS_NINE = "9" +# CONF_HOURS_TEN = "10" +# CONF_HOURS_ELEVEN = "11" +# CONF_HOURS_TWELVE = "12" +# CONF_HOURS_THIRTEEN = "13" +# CONF_HOURS_FOURTEEN = "14" +# CONF_HOURS_FIFTEEN = "15" +# CONF_HOURS_SIXTEEN = "16" +# CONF_HOURS_SEVENTEEN = "17" +# CONF_HOURS_EIGHTTEEN = "18" +# CONF_HOURS_NINETEEN = "19" +# CONF_HOURS_TWENTY = "20" +# CONF_HOURS_TWENTYONE = "21" +# CONF_HOURS_TWENTYTWO = "22" +# CONF_HOURS_TWENTYTHREE = "23" + +# CONF_HOURS_MIDNIGHT = "midnight" +# CONF_HOURS_ONE = "one" +# CONF_HOURS_TWO = "two" +# CONF_HOURS_THREE = "three" +# CONF_HOURS_FOUR = "four" +# CONF_HOURS_FIVE = "five" +# CONF_HOURS_SIX = "six" +# CONF_HOURS_SEVEN = "seven" +# CONF_HOURS_EIGHT = "eight" +# CONF_HOURS_NINE = "nine" +# CONF_HOURS_TEN = "ten" +# CONF_HOURS_ELEVEN = "eleven" +# CONF_HOURS_TWELVE = "twelve" +# CONF_HOURS_THIRTEEN = "thirteen" +# CONF_HOURS_FOURTEEN = "fourteen" +# CONF_HOURS_FIFTEEN = "fifteen" +# CONF_HOURS_SIXTEEN = "sixteen" +# CONF_HOURS_SEVENTEEN = "seventeen" +# CONF_HOURS_EIGHTTEEN = "eightteen" +# CONF_HOURS_NINETEEN = "nineteen" +# CONF_HOURS_TWENTY = "twenty" +# CONF_HOURS_TWENTYONE = "twentyone" +# CONF_HOURS_TWENTYTWO = "twentytwo" +# CONF_HOURS_TWENTYTHREE = "twentythree" + +# CONF_MINUTES_SHARP = "0" +# CONF_MINUTES_FIVE = "5" +# CONF_MINUTES_TEN = "10" +# CONF_MINUTES_FIFTEEN = "15" +# CONF_MINUTES_TWENTY = "20" +# CONF_MINUTES_TWENTYFIVE = "25" +# CONF_MINUTES_THIRTY = "30" +# CONF_MINUTES_THIRTYFIVE = "35" +# CONF_MINUTES_FORTY = "40" +# CONF_MINUTES_FORTYFIVE = "45" +# CONF_MINUTES_FIFTY = "50" +# CONF_MINUTES_FIFTYFIVE = "55" + + +DEPENDENCIES = ["display", "time"] +MULTI_CONF = False + + +wordclock_ns = cg.esphome_ns.namespace("wordclock") +SegmentCoords = wordclock_ns.struct("SegmentCoords") +Wordclock = wordclock_ns.class_( + "Wordclock", cg.PollingComponent +) + +WORDCLOCK_SEGMENT_SCHEMA = { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_LINE): { + cv.Required(CONF_LINE_START_X): cv.int_, + cv.Required(CONF_LINE_START_Y): cv.int_, + cv.Required(CONF_LINE_END_X): cv.int_, + cv.Optional(CONF_LINE_END_Y): cv.int_, + }, +} + +WORDCLOCK_HOUR_SCHEMA = { + cv.Required(CONF_HOUR): cv.uint16_t, + cv.Required(CONF_SEGMENTS): cv.ensure_list(cv.string_strict), +} + +WORDCLOCK_MINUTE_SCHEMA = { + cv.Required(CONF_MINUTE): cv.uint16_t, + cv.Required(CONF_HOUR_OFFSET): cv.int_range(-1,1), + cv.Required(CONF_SEGMENTS): cv.ensure_list(cv.string_strict), +} + +# WORDCLOCK_CONFIG_SCHEMA = { +# cv.Optional(CONF_WORDCLOCK_STATIC_TEXT): cv.ensure_list(cv.string_strict), +# # cv.Required(CONF_LAMBDA): cv.lambda_, +# cv.Required(CONF_SEGMENTS): cv.ensure_list(WORDCLOCK_SEGMENT_SCHEMA), +# cv.Required(CONF_MINUTES): cv.ensure_list(WORDCLOCK_MINUTE_SCHEMA), +# cv.Required(CONF_HOURS): cv.ensure_list(WORDCLOCK_HOUR_SCHEMA), +# } + +DATA1 = "data1" +DATA_X1 = "data_x1" +DATA_X2 = "data_x2" +DATA_Y1 = "data_y1" +DATA_Y2 = "data_y2" +DATA_SEGMENT_COORDS = "data_segment_coords" +DATA_MINUTES = "data_minutes" +DATA_HOURS = "data_hours" +DATA_SEGMENTS_HOUR = "data_segments_hour" +DATA_SEGMENTS_MINUTE = "data_segments_minute" +DATA3 = "data3" +DATA4 = "data4" +DATA_VECTOR_SEGMENTS_HOUR = "data_vector_segments_hour" +DATA_VECTOR_SEGMENTS_MINUTE = "data_vector_segments_minute" + +int8 = cg.global_ns.namespace("int8_t") +uint16_ptr = cg.global_ns.namespace("uint16_t *") +WORDCLOCK_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Wordclock), + cv.Required(CONF_DISPLAY_ID): cv.use_id( + display.DisplayBuffer + ), + cv.Required(CONF_TIME_ID): cv.use_id( + time.RealTimeClock + ), + cv.Required(CONF_ADDRESSABLE_LIGHT_ID): cv.use_id( + light.AddressableLightState + ), + # cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_WORDCLOCK_STATIC_SEGMENTS): cv.ensure_list(cv.string_strict), + # cv.Required(CONF_LAMBDA): cv.lambda_, + cv.Required(CONF_SEGMENTS): cv.ensure_list(WORDCLOCK_SEGMENT_SCHEMA), + cv.Required(CONF_MINUTES): cv.ensure_list(WORDCLOCK_MINUTE_SCHEMA), + cv.Required(CONF_HOURS): cv.ensure_list(WORDCLOCK_HOUR_SCHEMA), + cv.GenerateID(DATA_X1): cv.declare_id(uint16_ptr), + cv.GenerateID(DATA_X2): cv.declare_id(cg.uint8), + cv.GenerateID(DATA_Y1): cv.declare_id(cg.uint8), + cv.GenerateID(DATA_Y2): cv.declare_id(cg.uint8), + cv.GenerateID(DATA_SEGMENT_COORDS): cv.declare_id(SegmentCoords), + + # cv.GenerateID(DATA_MINUTES): cv.declare_id(int8), + # cv.GenerateID(DATA_HOURS): cv.declare_id(int8), + # cv.GenerateID(DATA_MINUTES): cv.declare_id(uint16_ptr), + # cv.GenerateID(DATA_HOURS): cv.declare_id(uint16_ptr), + # cv.GenerateID(DATA_SEGMENTS_MINUTE): cv.declare_id(cg.std_vector.template(cg.uint16)), + # cv.GenerateID(DATA_SEGMENTS_HOUR): cv.declare_id(cg.std_vector.template(cg.uint16)), + + cv.GenerateID(DATA_VECTOR_SEGMENTS_HOUR): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), + cv.GenerateID(DATA_VECTOR_SEGMENTS_MINUTE): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), + # cv.GenerateID(DATA3): cv.declare_id(cg.uint8), + # cv.GenerateID(DATA4): cv.declare_id(cg.std_vector.template(cg.int32)), + # cv.Required(CONF_WORDCLOCK_CONFIG): cv.ensure_list(WORDCLOCK_CONFIG_SCHEMA), + + # cv.Required(CONF_ID): cv.declare_id(Wordclock_), + # cv.Required(CONF_FILE): cv.file_, + # cv.Optional(CONF_RESIZE): cv.dimensions, + # cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( + # "NONE", "FLOYDSTEINBERG", upper=True + # ), + # cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + +# CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, WORDCLOCK_SCHEMA) +CONFIG_SCHEMA = WORDCLOCK_SCHEMA + +async def to_code(config): + wrapped_display = await cg.get_variable(config[CONF_DISPLAY_ID]) + wrapped_time = await cg.get_variable(config[CONF_TIME_ID]) + wrapped_light_state = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID]) + var = cg.new_Pvariable(config[CONF_ID], wrapped_time, wrapped_display, wrapped_light_state) + + SEGMENT_MAP = dict() + for idx, segm in enumerate(config[CONF_SEGMENTS]): + print(segm[CONF_NAME]) + SEGMENT_MAP[segm[CONF_NAME]] = idx + + line_start_x = segm[CONF_LINE][CONF_LINE_START_X] + line_end_x = segm[CONF_LINE][CONF_LINE_END_X] + line_start_y = segm[CONF_LINE][CONF_LINE_START_Y] + line_end_y = segm[CONF_LINE].get(CONF_LINE_END_Y, line_start_y) + + exp = cg.StructInitializer( + SegmentCoords, + ("x1", line_start_x), + ("x2", line_end_x), + ("y1", line_start_y), + ("y2", line_end_y), + ) + cg.add(var.add_segment(exp)) + + + if CONF_WORDCLOCK_STATIC_SEGMENTS in config: + + # cg.static_const_array(config[DATA_SEGMENT_COORDS], accumulator) + # print(SEGMENT_MAP) + for segment_name in config[CONF_WORDCLOCK_STATIC_SEGMENTS]: + cg.add(var.add_static(SEGMENT_MAP[segment_name])) + # static_segment_ids = config[CONF_WORDCLOCK_STATIC_SEGMENTS] + # exp = cg.std_vector.template(cg.uint16)(static_segment_ids) + # cg.add(var.add_static(hour[CONF_HOUR], cpp.UnaryOpExpression("&", exp))) + + + + hours = [] + for idx, hour in enumerate(config[CONF_HOURS]): + segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] + exp = cg.std_vector.template(cg.uint16)(segment_ids) + hours.append(exp) + hours_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_HOUR], cg.std_vector.template(cg.std_vector.template(cg.uint16))(hours)) + + minutes = [] + for idx, hour in enumerate(config[CONF_MINUTES]): + segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] + exp = cg.std_vector.template(cg.uint16)(segment_ids) + minutes.append(exp) + minutes_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_MINUTE], cg.std_vector.template(cg.std_vector.template(cg.uint16))(minutes)) + + for idx, hour in enumerate(config[CONF_HOURS]): + exp = cg.std_vector.template(cg.uint16)(segment_ids) + cg.add(var.add_hour(hour[CONF_HOUR], cpp.UnaryOpExpression("&", hours_array[idx]))) + + # minutes = [[],] * 60 + for idx, minute in enumerate(config[CONF_MINUTES]): + exp = cg.std_vector.template(cg.uint16)(segment_ids) + cg.add(var.add_minute(minute[CONF_MINUTE], cpp.UnaryOpExpression("&", minutes_array[idx]))) + # segment_ids = [SEGMENT_MAP[a] for a in minute[CONF_SEGMENTS]] + # minutes[minute[CONF_MINUTE]] = cg.std_vector.template(cg.int32)() + # foo = [] + # cg.ArrayInitializer() + '''exp = cg.std_vector.template(cg.uint16)(segment_ids)''' + # exp = cg.ArrayInitializer(minute[CONF_SEGMENTS]) + '''cg.add(var.add_minute(minute[CONF_MINUTE], exp))''' + + # for segment_str in minute[CONF_SEGMENTS]: + # foo.append(SEGMENT_MAP[segment_str]) + # # minutes[minute[CONF_MINUTE]].push_back(SEGMENT_MAP[segment_str]) + # minutes[minute[CONF_MINUTE]] = foo + # print(minute[CONF_MINUTE]) + # SEGMENT_MAP[i[CONF_NAME]] = idx + # accumulator.append(idx) + for idx, minute in enumerate(config[CONF_MINUTES]): + cg.add(var.add_hour_offset(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET])) + + + await cg.register_component(var, config) + """ + accumulator = [] + SEGMENT_MAP = dict() + for idx, segm in enumerate(config[CONF_SEGMENTS]): + print(segm[CONF_NAME]) + SEGMENT_MAP[segm[CONF_NAME]] = idx + # x1.append() + # x2.append() + line_start_x = segm[CONF_LINE][CONF_LINE_START_X] + line_end_x = segm[CONF_LINE][CONF_LINE_END_X] + line_start_y = segm[CONF_LINE][CONF_LINE_START_Y] + line_end_y = segm[CONF_LINE].get(CONF_LINE_END_Y, line_start_y) + # print(line_start_y) + # print(line_end_y) + # y1.append(segm[CONF_LINE][CONF_LINE_START_Y]) + # y2.append(line_end_y) + + exp = cg.StructInitializer( + SegmentCoords, + ("x1", line_start_x), + ("x2", line_end_x), + ("y1", line_start_y), + ("y2", line_end_y), + ) + accumulator.append(exp) + + cg.static_const_array(config[DATA_SEGMENT_COORDS], accumulator) + print(SEGMENT_MAP) + + + minutes = [[],] * 60 + for idx, minute in enumerate(config[CONF_MINUTES]): + # minutes[minute[CONF_MINUTE]] = cg.std_vector.template(cg.int32)() + foo = [] + cg.ArrayInitializer() + for segment_str in minute[CONF_SEGMENTS]: + foo.append(SEGMENT_MAP[segment_str]) + # minutes[minute[CONF_MINUTE]].push_back(SEGMENT_MAP[segment_str]) + minutes[minute[CONF_MINUTE]] = foo + print(minute[CONF_MINUTE]) + # SEGMENT_MAP[i[CONF_NAME]] = idx + # accumulator.append(idx) + print(minutes) + cg.static_const_array(config[DATA_MINUTES], minutes) + """ + """ + hours = [cg.std_vector.template(cg.uint16)()] * 24 + cg.static_const_array(config[DATA_HOURS], hours) + + for idx, i in enumerate(config[CONF_HOURS]): + # for segment_str in i[CONF_SEGMENTS]: + # print(config[DATA_HOURS].type) + # push = config[DATA_HOURS].get(i[CONF_HOUR]).push_back(SEGMENT_MAP[segment_str]) + # cg.add(push) + # hours[i[CONF_HOUR]] = cg.std_vector.template(cg.int32)() + + print(i[CONF_HOUR]) + # SEGMENT_MAP[i[CONF_NAME]] = idx + # accumulator.append(idx) + print(hours) + # cg.static_const_array(config[DATA_HOURS], hours) + # cg.static_const_array(config[DATA3], hours) + # cg.static_const_array(config[DATA4], hours) + """ + + + # prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + # cg.add_global(cg.Statement(f"const int {i[CONF_NAME]} = 0;")) + # cg.add_global(f""" + # /* + # {global_consts} + # */ + # """) + + # from PIL import Image + + # path = CORE.relative_config_path(config[CONF_FILE]) + # try: + # image = Image.open(path) + # except Exception as e: + # raise core.EsphomeError(f"Could not load image file {path}: {e}") + + # width, height = image.size + + # if CONF_RESIZE in config: + # image.thumbnail(config[CONF_RESIZE]) + # width, height = image.size + # else: + # if width > 500 or height > 500: + # _LOGGER.warning( + # "The image you requested is very big. Please consider using" + # " the resize parameter." + # ) + + # dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG + # if config[CONF_TYPE] == "GRAYSCALE": + # image = image.convert("L", dither=dither) + # pixels = list(image.getdata()) + # data = [0 for _ in range(height * width)] + # pos = 0 + # for pix in pixels: + # data[pos] = pix + # pos += 1 + + # elif config[CONF_TYPE] == "RGB24": + # image = image.convert("RGB") + # pixels = list(image.getdata()) + # data = [0 for _ in range(height * width * 3)] + # pos = 0 + # for pix in pixels: + # data[pos] = pix[0] + # pos += 1 + # data[pos] = pix[1] + # pos += 1 + # data[pos] = pix[2] + # pos += 1 + + # elif config[CONF_TYPE] == "RGB565": + # image = image.convert("RGB") + # pixels = list(image.getdata()) + # data = [0 for _ in range(height * width * 3)] + # pos = 0 + # for pix in pixels: + # R = pix[0] >> 3 + # G = pix[1] >> 2 + # B = pix[2] >> 3 + # rgb = (R << 11) | (G << 5) | B + # data[pos] = rgb >> 8 + # pos += 1 + # data[pos] = rgb & 255 + # pos += 1 + + # elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"): + # image = image.convert("1", dither=dither) + # width8 = ((width + 7) // 8) * 8 + # data = [0 for _ in range(height * width8 // 8)] + # for y in range(height): + # for x in range(width): + # if image.getpixel((x, y)): + # continue + # pos = x + y * width8 + # data[pos // 8] |= 0x80 >> (pos % 8) + + # elif config[CONF_TYPE] == "TRANSPARENT_IMAGE": + # image = image.convert("RGBA") + # width8 = ((width + 7) // 8) * 8 + # data = [0 for _ in range(height * width8 // 8)] + # for y in range(height): + # for x in range(width): + # if not image.getpixel((x, y))[3]: + # continue + # pos = x + y * width8 + # data[pos // 8] |= 0x80 >> (pos % 8) + + # rhs = [HexInt(x) for x in data] + # prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + # cg.new_Pvariable( + # config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] + # ) diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp new file mode 100644 index 0000000..e40a452 --- /dev/null +++ b/components/wordclock/wordclock.cpp @@ -0,0 +1,305 @@ +#include "wordclock.h" + +namespace esphome { +namespace wordclock { + +void Wordclock::setup() { + // this->time->update(); + + + // this->light_state->add_effects({this->randomTwinkle}); + // this->start_idle_animation(); +} + +void Wordclock::start_idle_animation() { + this->display->set_enabled(false); + // auto call = + this->light_state->turn_on().set_effect("random_twinkle").perform(); + // call.set_effect("random_twinkle"); + // call.perform(); +} + +void Wordclock::end_idle_animation() { + this->light_state->turn_off().perform(); + this->display->set_enabled(true); + + + // this->light->clear_effect_data(); + // this->display->get_light()->set_effect_active(false); + // auto call1 = this->light_state->turn_on(); + // call1.set_effect("None"); + // call1.perform(); + // this->light->all().set(Color(0xF0FF00)); + + // this->display->fill(Color(0)); + + // this->light_state->turn_off().perform(); + // call2.perform(); +} + +void Wordclock::update() { + + // esphome::addressable_light::AddressableLightDisplay it = *(this->display); + // ESP_LOGD("loop", "beep"); + // ESP_LOGD("loop", "time is now [%02i:%02i:%02i]", this->time->now().hour, this->time->now().minute, this->time->now().second); + // ESP_LOGE("wordclock.cpp", "display_ptr: 0x%x", it); + // ESP_LOGE("wordclock.cpp", "this: 0x%x", (this)); + // ESP_LOGE("wordclock.cpp", "this->display: 0x%x", (this->display)); + // ESP_LOGE("wordclock.cpp", "&this->display: 0x%x", &(this->display)); + // ESP_LOGE("loop", "time_ptr: %i", this->time); + // it.line(0,0,0,0, Color(0x00FF00)); + + esphome::time::ESPTime time = this->time->now(); + this->find_hour(9); + this->find_minute(31); + + if (time.is_valid() == false) { + if (this->valid_time) { + ESP_LOGD("loop", "time is not valid [%02i:%02i:%02i]", time.hour, time.minute, time.second); + this->start_idle_animation(); + this->valid_time = false; + return; + } + } + + else { + if (!this->valid_time) { + this->end_idle_animation(); + this->valid_time = true; + ESP_LOGD("wordclock.cpp", "time is now valid [%02i:%02i:%02i]", time.hour, time.minute, time.second); + return; + } + // for (uint8_t idx = 0;idx < ; idx++) { + + // } + + // std::vector *minute = this->find_minute(time.minute); + // std::vector *hour = this->find_hour(time.hour); + this->display->fill(Color(0x000000)); + + int8_t minute = this->find_minute(time.minute); + int8_t hour = this->find_hour((time.hour + this->hour_offsets->at(minute)) % 24); + + for (uint16_t segment_idx : *this->static_segments){ + this->draw_segment(segment_idx); + } + + for (uint16_t segment_idx : this->minutes->at(minute)){ + this->draw_segment(segment_idx); + } + + for (uint16_t segment_idx : this->hours->at(hour)){ + this->draw_segment(segment_idx); + } + // this->draw_segment(0); + // this->draw_segment(1); + // this->draw_segment(4); + // this->draw_segment(10); + // this->draw_segment(19); + // this->light->range(0, 10).fade_to_white(100); + // this->display->get_light()->range(18, 35).set(Color(0xf0ff0f)); + + // SegmentCoords s = this->segments->at(time); + + + // ESP_LOGD("wordclock.cpp", "time is now [%02i:%02i:%02i]", time.hour, time.minute, time.second); + // ESP_LOGD("wordclock.cpp", "x1: %i, y1: %i, x2: %i, y2: %i", s.x1, s.y1, s.x2, s.y2); + + + // this->display->draw_pixel_at(0, 0, Color(0xFF0000)); + // this->display->draw_pixel_at(1, 0, Color(0x00FF00)); + // this->display->draw_pixel_at(2, 0, Color(0x0000FF)); + // if (time.second != second) { + // second = time.second; + // ESP_LOGD("loop", "time is now [%02i:%02i:%02i]", time.hour, time.minute, time.second); + // } + // if(time.hour != hour || time.minute != minute) { + // hour = time.hour; + // minute = time.minute; + // if (hour >= 0 && time.is_valid() == true){ + + // display_time(time.hour, time.minute, CRGB(red, green, blue)); + + // ESP_LOGE("loop", "Update Time: %i:%i Brightness: %i RGB: %i-%i-%i", time.hour, time.minute, brightness, red, green, blue); + // } + // } + // } + // if (!this->reading_ && !mode_funcs_.empty()) { + // this->reading_ = true; + // this->read_mode_(0); + } +} + +void Wordclock::draw_segment(uint16_t segment_id) { + SegmentCoords s = this->segments->at(segment_id); + // ESP_LOGD("wordclock.cpp", "x1: %i, y1: %i, x2: %i, y2: %i", s.x1, s.y1, s.x2, s.y2); + this->display->line(s.x1, s.y1, s.x2, s.y2, esphome::Color(0xFFFFFF)); +} +// void Wordclock::set_writer(display_writer_t &&writer) { +// this->writer_ = writer; +// } + +void Wordclock::add_segment(SegmentCoords segment) { + // if (!this->segments) { + + // } + + this->segments->push_back(segment); + // this->writer_ = writer; +} + +// std::vector * Wordclock::find_hour(uint8_t hour) { +// std::vector *empty_vector; // = new std::vector(); +// uint16_t last_defined_hour = -1; +// for (int i = 0; i < this->hours->size(); i++) { +// if (this->hours->at(i).size()) { +// if (hour == i) { +// return &(this->hours->at(i)); +// } +// else { +// last_defined_hour = i; +// } +// } +// else { +// empty_vector = &(this->hours->at(i)); +// if (hour == i) { +// if (last_defined_hour == -1) return empty_vector; +// return &(this->hours->at(last_defined_hour)); +// } +// } +// } +// return empty_vector; +// } + +int8_t Wordclock::find_hour(uint8_t hour) { + uint16_t last_defined_hour = -1; + for (int i = 0; i < this->hours->size(); i++) { + if (this->hours->at(i).size()) { + last_defined_hour = i; + if (hour == i) { + return last_defined_hour; + } + } + if (hour == i) break; + } + return last_defined_hour; +} + +int8_t Wordclock::find_minute(uint8_t minute) { + uint16_t last_defined_minute = -1; + for (int i = 0; i < this->minutes->size(); i++) { + if (this->minutes->at(i).size()) { + last_defined_minute = i; + if (minute == i) { + return last_defined_minute; + } + } + if (minute == i) break; + } + return last_defined_minute; +} +// std::vector * Wordclock::find_minute(uint8_t minute) { +// std::vector *empty_vector;// = new std::vector(); +// uint16_t last_defined_minute = -1; +// for (int i = 0; i < this->minutes->size(); i++) { +// if (this->minutes->at(i).size()) { +// if (minute == i) { +// return &(this->minutes->at(i)); +// } +// else { +// last_defined_minute = i; +// } +// } +// else { +// empty_vector = &(this->minutes->at(i)); +// if (minute == i) { +// if (last_defined_minute == -1) return empty_vector; +// return &(this->minutes->at(last_defined_minute)); +// } +// } +// } +// return empty_vector; +// } + + +void Wordclock::add_hour_offset(uint8_t index, int8_t offset) { + (*this->hour_offsets)[index] = offset; +} + +void Wordclock::add_hour(uint8_t hour, std::vector *segments) { + for (uint16_t i : *segments){ + this->hours->at(hour).push_back(i); + } +} + +void Wordclock::add_minute(uint8_t minute, std::vector *segments) { + for (uint16_t i : *segments){ + this->minutes->at(minute).push_back(i); + } +} + +void Wordclock::add_static(uint16_t segment_id) { + this->static_segments->push_back(segment_id); +} + +// uint16_t ** + +// Wordclock::Wordclock(std::vector *minutes, std::vector *hours, SegmentCoords *segments) +// Wordclock::Wordclock(uint16_t **minutes, uint16_t **hours, SegmentCoords *segments) +// : PollingComponent(1000) { +// // this->minutes = minutes; +// // this->hours = hours; +// // this->segments = segments; +// // std::vector minutes[60]; +// // std::vector hours[24]; +// } + Wordclock::Wordclock() + : PollingComponent(1000) { + // this->minutes = std::vector>(); + // // for (int i=0; i<60; i++) this->minutes.push_back(std::vector()); + + // this->hours = std::vector>(); + // // for (int i=0; i<24; i++) this->minutes.push_back(std::vector()); + + // this->segments = std::vector(); + } + + Wordclock::Wordclock(esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, esphome::light::AddressableLightState *light_state) + : PollingComponent(16) { + // ESP_LOGE("wordclock.cpp", "this: 0x%x", (this)); + // ESP_LOGE("wordclock.cpp", "display: 0x%x", (display)); + // ESP_LOGE("wordclock.cpp", "&display: 0x%x", (&display)); + + this->time = time; + this->display = display; + this->light = this->display->get_light(); + this->light_state = light_state; + // light::AddressableLight *light = this->display->get_light(); + + // light::AddressableRainbowLightEffect *light_addressablerainbowlighteffect; + // light::AddressableTwinkleEffect *light_addressabletwinkleeffect; + // light::AddressableRandomTwinkleEffect *light_addressablerandomtwinkleeffect; + + // this->rainbow = new light::AddressableRainbowLightEffect("rainbow"); + // this->rainbow->set_speed(10); + // this->rainbow->set_width(50); + // this->twinkle = new light::AddressableTwinkleEffect("twinkle"); + // this->twinkle->set_twinkle_probability(0.05f); + // this->twinkle->set_progress_interval(4); + // this->randomTwinkle = new light::AddressableRandomTwinkleEffect("random_twinkle"); + // this->randomTwinkle->set_twinkle_probability(0.05f); + // this->randomTwinkle->set_progress_interval(32); + + this->hour_offsets = new std::vector(60, 0); + + this->minutes = new std::vector>(60, std::vector()); + // for (int i=0; i<60; i++) this->minutes->push_back(std::vector()); + + this->hours = new std::vector>(24, std::vector()); + // for (int i=0; i<24; i++) this->hours->push_back(std::vector()); + + this->segments = new std::vector(); + this->static_segments = new std::vector(); + } +} // namespace wordclock +} // namespace esphome diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h new file mode 100644 index 0000000..42b98df --- /dev/null +++ b/components/wordclock/wordclock.h @@ -0,0 +1,227 @@ +#pragma once + +#include "esphome.h" +// By now only loosely based on https://github.com/leinich/ha-wordclock-esphome + +// esphome dependencies: +// needs: esphome time --> id: current_time +// needs: esphome fastled --> id: fastledlight + +// #ifdef USE_ESP32 + +namespace esphome { +namespace wordclock { + +///// Word Table ///// +// .line(0,0,1,0, color); +// .line(2,0,3,0, color); +// int WORD_IT_IS[5][2] = {{4,0}, {0,0}, {0,1}, {0,3}, {0,4}}; + +// // .line(2,1,8,1, color); +// int WORD_QUARTER[8][2] = {{7,0}, {1,2}, {1,3}, {1,4}, {1,5}, {1,6}, {1,7}, {1,8}}; +// // .line(0,2, 5,2, color); +// int WORD_TWENTY[7][2] = {{6,0}, {2,0}, {2,1}, {2,2}, {2,3}, {2,4}, {2,5}}; +// // .line(6,2, 9,2, color); +// int WORD_FIVE_MINUTES[5][2] = {{4,0}, {2,6}, {2,7}, {2,8}, {2,9}}; +// // .line(0,3, 3,3, color); +// int WORD_HALF[5][2] = {{4,0}, {3,0}, {3,1}, {3,2}, {3,3}}; +// // .line(5,3, 7,3, color); +// int WORD_TEN_MINUTES[4][2] = {{3,0}, {3,5}, {3,6}, {3,7}}; +// // .line(9,3, 10,3, color); +// int WORD_TO[3][2] = {{2,0}, {3,9}, {3,10}}; +// // .line(0,4, 3,4, color); +// int WORD_PAST[5][2] = {{4,0}, {4,0}, {4,1}, {4,2}, {4,3}}; + +// int WORD_NINE[5][2] = {{4,0}, {4,7}, {4,8}, {4,9}, {4,10}}; +// int WORD_ONE[4][2] = {{3,0}, {5,0}, {5,1}, {5,2}}; +// int WORD_SIX[4][2] = {{3,0}, {5,3}, {5,4}, {5,5}}; +// int WORD_THREE[6][2] = {{5,0}, {5,6}, {5,7}, {5,8}, {5,9}, {5,10}}; +// int WORD_FOUR[5][2] = {{4,0}, {6,0}, {6,1}, {6,2}, {6,3}}; +// int WORD_FIVE[5][2] = {{4,0}, {6,4}, {6,5}, {6,6}, {6,7}}; +// int WORD_TWO[4][2] = {{3,0}, {6,8}, {6,9}, {6,10}}; +// int WORD_EIGHT[6][2] = {{5,0}, {7,0}, {7,1}, {7,2}, {7,3}, {7,4}}; +// int WORD_ELEVEN[7][2] = {{6,0}, {7,5}, {7,6}, {7,7}, {7,8}, {7,9}, {7,10}}; +// int WORD_SEVEN[6][2] = {{5,0}, {8,0}, {8,1}, {8,2}, {8,3}, {8,4}}; +// int WORD_TWELFE[7][2] = {{6,0}, {8,5}, {8,6}, {8,7}, {8,8}, {8,9}, {8,10}}; +// int WORD_TEN[4][2] = {{3,0}, {9,0}, {9,1}, {9,2}}; +// int WORD_OCLOCK[7][2] = {{6,0}, {9,5}, {9,6}, {9,7}, {9,8}, {9,9}, {9,10}}; + + + +// using display_writer_t = std::function; + + +struct SegmentCoords { + uint16_t x1; + uint16_t x2; + uint16_t y1; + uint16_t y2; +}; + + +class Wordclock : public esphome::PollingComponent { + public: + Wordclock(); + Wordclock(esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, esphome::light::AddressableLightState *light_state); + void add_segment(SegmentCoords segment); + void add_hour(uint8_t index, std::vector *segments); + void add_minute(uint8_t index, std::vector *segments); + void add_static(uint16_t segment_id); + void add_hour_offset(uint8_t index, int8_t offset); + void setup(); + void update(); + // void setup() override { + + // } + + // void set_writer(display_writer_t &&writer); + + // void display_word(const int word[][2], const CRGB& c) { + // for (int i=1; i < word[0][0] + 1; i++) { + // leds[map_coords_to_strip(word[i][0], word[i][1])].setRGB(c.r, c.g, c.b); + // } + // } + + // void display_minutes(int minutes, const CRGB& color) { + // int five_minute_chunk = minutes / 5; + + // switch (five_minute_chunk) + // { + // case 0: // sharp + // display_word(WORD_OCLOCK, color); ESP_LOGD("minute", "oclock "); break; + // case 1: // five past + // display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five past "); break; + // case 2: // ten past + // display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten past "); break; + // case 3: // quarter past + // display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter past "); break; + // case 4: // twenty past + // display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty past "); break; + // case 5: // twenty five past + // display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color); + // ESP_LOGD("minute", "twenty five past "); break; + // case 6: // half past + // display_word(WORD_HALF, color); ESP_LOGD("minute", "half past "); break; + // case 7: // twenty five to + // display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color); + // ESP_LOGD("minute", "twenty five to "); break; + // case 8: // twenty to + // display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty to "); break; + // case 9: // quarter to + // display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter to "); break; + // case 10: // ten to + // display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten to "); break; + // case 11: // five to + // display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five to "); break; + // default: + // break; + // } + // if (five_minute_chunk > 6) { + // display_word(WORD_TO, color); + // } else if (five_minute_chunk > 0) { + // display_word(WORD_PAST, color); + // } + // } + + // void display_hour(int hour, int minutes, const CRGB& color) { + // int five_minute_chunk = minutes / 5; + // if (five_minute_chunk > 6) { + // hour += 1; + // } + + // switch (hour % 12) + // { + // case 0: // twelve + // display_word(WORD_TWELFE, color); ESP_LOGD("hour", "twelve "); break; + // case 1: // one + // display_word(WORD_ONE, color); ESP_LOGD("hour", "one "); break; + // case 2: // two + // display_word(WORD_TWO, color); ESP_LOGD("hour", "two "); break; + // case 3: // three + // display_word(WORD_THREE, color); ESP_LOGD("hour", "three "); break; + // case 4: // four + // display_word(WORD_FOUR, color); ESP_LOGD("hour", "four "); break; + // case 5: // five + // display_word(WORD_FIVE, color); ESP_LOGD("hour", "five "); break; + // case 6: // six + // display_word(WORD_SIX, color); ESP_LOGD("hour", "six "); break; + // case 7: // seven + // display_word(WORD_SEVEN, color); ESP_LOGD("hour", "seven "); break; + // case 8: // eight + // display_word(WORD_EIGHT, color); ESP_LOGD("hour", "eight "); break; + // case 9: // nine + // display_word(WORD_NINE, color); ESP_LOGD("hour", "nine "); break; + // case 10: // ten + // display_word(WORD_TEN, color); ESP_LOGD("hour", "ten "); break; + // case 11: // eleven + // display_word(WORD_ELEVEN, color); ESP_LOGD("hour", "eleven "); break; + // default: + // break; + // } + // } + + // void display_time(int hour, int minutes, const CRGB& color) { + // // clear_all_leds(); + // display_word(WORD_IT_IS, color); + // display_hour(hour, minutes, color); + // display_minutes(minutes, color); + // // FastLED.show(); + // } + + + + // void loop() override { + // // auto time = id(current_time).now(); + // // https://www.esphome.io/api/classesphome_1_1light_1_1_light_color_values.html LightColorValues Class + // // auto fastledlight2 = id(fastledlight).current_values; + // //convert float 0.0 till 1.0 into int 0 till 255 + // red = (int) (fastledlight2.get_red() * 125); + // green = (int) (fastledlight2.get_green() * 125); + // blue = (int) (fastledlight2.get_blue() * 125); + // brightness = 0; + // //check if light is on and set brightness + // if (fastledlight2.get_state() > 0 ) { + // brightness = (int) (fastledlight2.get_brightness()*125); + // } else { + // // ESP_LOGD("loop", "fastledlight state off - b: %i rgb %i %i %i", brightness, red, green, blue); delay(100); + // } + + // FastLED.setBrightness(brightness); + // FastLED.show(); + // //check if valid time. Blink red,green,blue until valid time is present + + // } + protected: + void draw_segment(uint16_t segment_id); + int8_t find_hour(uint8_t hour); + int8_t find_minute(uint8_t minute); + // std::vector * find_hour(uint8_t hour); + // std::vector * find_minute(uint8_t minute); + std::vector> *minutes; + std::vector> *hours; + std::vector *static_segments; + std::vector *hour_offsets; + + void start_idle_animation(); + void end_idle_animation(); + + // uint16_t **minutes; + // uint16_t **hours; + + bool valid_time{true}; + std::vector *segments; + esphome::time::RealTimeClock *time; + esphome::light::AddressableLight *light; + esphome::light::AddressableLightState *light_state; + esphome::addressable_light::AddressableLightDisplay *display; + light::AddressableRainbowLightEffect *rainbow; + light::AddressableTwinkleEffect *twinkle; + light::AddressableRandomTwinkleEffect *randomTwinkle; + +}; + + + +} // namespace wordclock +} // namespace esphome + diff --git a/display_test.yaml b/display_test.yaml index 837eacf..9a0f27f 100644 --- a/display_test.yaml +++ b/display_test.yaml @@ -1,5 +1,5 @@ esphome: - name: "display_thing" + name: "display-thing" esp8266: board: d1_mini @@ -26,8 +26,8 @@ ota: logger: # esp8266_store_log_strings_in_flash: false -web_server: - port: 80 +# web_server: +# port: 80 light: - name: NeoPixel Strip 1 @@ -36,8 +36,10 @@ light: type: GRB variant: WS2812 pin: GPIO3 - num_leds: 200 - # default_transition_length: 0.2s + num_leds: 199 + restore_mode: ALWAYS_ON + default_transition_length: 0s + color_correct: [100%, 100%, 100%] method: type: esp8266_dma @@ -47,18 +49,48 @@ display: addressable_light_id: neopixel_strip_1 width: 18 height: 11 - rotation: 0° + rotation: 270° update_interval: 16ms + pixel_mapper: |- + int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16}; + int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16}; + if (x > 9) return -1; + if (y % 2 == 0) { + return (y * 18 + mapping_even[x]); // mapping_even[y]; + } else { + return (y * 18 + mapping_odd[x]); // mapping_odd[y]; + //return (y * 18 + 17 - x); // mapping_odd[y]; + } + //if (x % 2 == 0) { + // return (x * 8) + y; + //} + //return (x * 8) + (7 - y); lambda: |- - // Draw a bulls-eye pattern Color red = Color(0xFF0000); Color green = Color(0x00FF00); Color blue = Color(0x0000FF); - it.rectangle(0, 0, 18, 11, red); - it.rectangle(1, 1, 17, 10, green); - it.rectangle(2, 2, 16, 9, blue); - it.rectangle(3, 3, 15, 8, red); - + Color turquoise = Color(0x00FFFF); + it.draw_pixel_at(0,0, green); + it.draw_pixel_at(10,0, red); + it.draw_pixel_at(0,9, turquoise); + it.draw_pixel_at(10,9, blue); + //it.line(0,0, 11,0, red); + //it.line(0,1, 11,1, green); + //it.line(0,2, 11,2, blue); + //it.line(0,3, 11,3, red); + //it.line(0,4, 11,4, green); + //it.line(0,5, 11,5, blue); + //it.line(0,6, 11,6, red); + //it.line(0,7, 11,7, green); + //it.line(0,8, 11,8, blue); + //it.line(0,9, 11,9, red); + //it.line(0,10, 9,10, green); + //it.line(0,11, 9,11, blue); + // it.line(5,14, 10,14, green); + //it.rectangle(0, 0, 17, 10, red); + //it.rectangle(1, 1, 16, 9, green); + //it.rectangle(2, 2, 15, 8, blue); + //it.rectangle(3, 3, 14, 7, red); time: - platform: sntp diff --git a/image_test.yaml b/image_test.yaml new file mode 100644 index 0000000..f3662f4 --- /dev/null +++ b/image_test.yaml @@ -0,0 +1,154 @@ +esphome: + name: "display-thing" + +esp8266: + board: d1_mini + framework: + version: recommended + +external_components: + - source: + type: local + path: components + components: [ wordclock, addressable_light ] + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "${devicename}" + password: !secret ap_password + manual_ip: + static_ip: !secret manualip_static_ip + gateway: !secret manualip_gateway + subnet: !secret manualip_subnet + dns1: 1.1.1.1 + dns2: 1.0.0.1 + +api: + +# ota: +# password: "${devicename}" + +logger: + # esp8266_store_log_strings_in_flash: false +web_server: + port: 80 + +light: + - name: NeoPixel Strip 1 + id: neopixel_strip_1 + platform: neopixelbus + type: GRB + variant: WS2812 + pin: GPIO3 + num_leds: 199 + restore_mode: ALWAYS_ON + default_transition_length: 0s + color_correct: [100%, 100%, 100%] + method: + type: esp8266_dma + +display: + - platform: addressable_light + id: led_matrix_display + addressable_light_id: neopixel_strip_1 + width: 18 + height: 11 + rotation: 270° + update_interval: 16ms + auto_clear_enabled: false + pixel_mapper: |- + int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16}; + int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16}; + if (x > 9) return -1; + if (y % 2 == 0) { + return (y * 18 + mapping_even[x]); // mapping_even[y]; + } else { + return (y * 18 + mapping_odd[x]); // mapping_odd[y]; + } + # lambda: |- + # it.fill(Color(0)); + # it.image(0, 0, id(foo)); + # //it.line(0,0,5,5, Color(0x00FF00)); + +# image: +# - id: foo +# file: ./test.png +# type: RGB24 +# resize: 18x11 + + +time: + - platform: sntp + id: current_time + timezone: !secret timezone + + +wordclock: + time_id: current_time + display_id: led_matrix_display + static_segments: ["IT", "IS"] + segments: + - {name: "IT", line: {x1: 0, x2: 1, y1: 0}} + - {name: "IS", line: {x1: 3, x2: 4, y1: 0}} + - {name: "AM", line: {x1: 7, x2: 8, y1: 0}} + - {name: "PM", line: {x1: 9, x2: 10, y1: 0}} + - {name: "QUARTER", line: {x1: 2, x2: 8, y1: 1}} + - {name: "TWENTY", line: {x1: 0, x2: 5, y1: 2}} + - {name: "FIVE_MINUTES", line: {x1: 6, x2: 9, y1: 2}} + - {name: "HALF", line: {x1: 0, x2: 3, y1: 3}} + - {name: "TEN_MINUTES", line: {x1: 5, x2: 7, y1: 3}} + - {name: "TO", line: {x1: 9, x2: 10, y1: 3}} + - {name: "PAST", line: {x1: 0, x2: 3, y1: 4}} + - {name: "NINE", line: {x1: 7, x2: 10, y1: 4}} + - {name: "ONE", line: {x1: 0, x2: 2, y1: 5}} + - {name: "SIX", line: {x1: 3, x2: 5, y1: 5}} + - {name: "THREE", line: {x1: 6, x2: 10, y1: 5}} + - {name: "FOUR", line: {x1: 0, x2: 3, y1: 6}} + - {name: "FIVE", line: {x1: 4, x2: 7, y1: 6}} + - {name: "TWO", line: {x1: 8, x2: 10, y1: 6}} + - {name: "EIGHT", line: {x1: 0, x2: 4, y1: 7}} + - {name: "ELEVEN", line: {x1: 5, x2: 10, y1: 7}} + - {name: "SEVEN", line: {x1: 0, x2: 4, y1: 8}} + - {name: "TWELVE", line: {x1: 5, x2: 10, y1: 8}} + - {name: "TEN", line: {x1: 0, x2: 2, y1: 9}} + - {name: "OCLOCK", line: {x1: 5, x2: 10, y1: 9}} + minutes: + - {minute: 0, segments: ["OCLOCK"]} + - {minute: 5, segments: ["FIVE_MINUTES", "PAST"]} + - {minute: 10, segments: ["TEN_MINUTES", "PAST"]} + - {minute: 15, segments: ["QUARTER", "PAST"]} + - {minute: 20, segments: ["TWENTY", "PAST"]} + - {minute: 25, segments: ["TWENTY", "FIVE_MINUTES", "PAST"]} + - {minute: 30, segments: ["HALF", "PAST"]} + - {minute: 35, segments: ["TWENTY", "FIVE_MINUTES", "TO"]} + - {minute: 40, segments: ["TWENTY", "TO"]} + - {minute: 45, segments: ["QUARTER", "TO"]} + - {minute: 50, segments: ["TEN_MINUTES", "TO"]} + - {minute: 55, segments: ["FIVE_MINUTES", "TO"]} + hours: + - {hour: 0, segments: ["TWELVE", "AM"]} + - {hour: 1, segments: ["ONE", "AM"]} + - {hour: 2, segments: ["TWO", "AM"]} + - {hour: 3, segments: ["THREE", "AM"]} + - {hour: 4, segments: ["FOUR", "AM"]} + - {hour: 5, segments: ["FIVE", "AM"]} + - {hour: 6, segments: ["SIX", "AM"]} + - {hour: 7, segments: ["SEVEN", "AM"]} + - {hour: 8, segments: ["EIGHT", "AM"]} + - {hour: 9, segments: ["NINE", "AM"]} + - {hour: 10, segments: ["TEN", "AM"]} + - {hour: 11, segments: ["ELEVEN", "AM"]} + - {hour: 12, segments: ["TWELVE", "PM"]} + - {hour: 13, segments: ["ONE", "PM"]} + - {hour: 14, segments: ["TWO", "PM"]} + - {hour: 15, segments: ["THREE", "PM"]} + - {hour: 16, segments: ["FOUR", "PM"]} + - {hour: 17, segments: ["FIVE", "PM"]} + - {hour: 18, segments: ["SIX", "PM"]} + - {hour: 19, segments: ["SEVEN", "PM"]} + - {hour: 20, segments: ["EIGHT", "PM"]} + - {hour: 21, segments: ["NINE", "PM"]} + - {hour: 22, segments: ["TEN", "PM"]} + - {hour: 23, segments: ["ELEVEN", "PM"]} \ No newline at end of file diff --git a/image_test_3leds.yaml b/image_test_3leds.yaml new file mode 100644 index 0000000..f9d53b1 --- /dev/null +++ b/image_test_3leds.yaml @@ -0,0 +1,225 @@ +esphome: + name: "display-thing" + # on_boot: + # priority: 600 + # # ... + # then: + # - light.turn_on: + # id: neopixel_strip_1 + # brightness: 100% + # effect: random_twinkle + +esp8266: + board: d1_mini + framework: + version: recommended + +external_components: + - source: + type: local + path: components + components: [ wordclock ] + # components: [ wordclock, addressable_light ] + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "WordClock Configuration" + password: !secret ap_password + manual_ip: + static_ip: !secret manualip_static_ip + gateway: !secret manualip_gateway + subnet: !secret manualip_subnet + dns1: 1.1.1.1 + dns2: 1.0.0.1 + +captive_portal: + +api: + +ota: + password: "wordclock" + +logger: + # esp8266_store_log_strings_in_flash: false +# web_server: +# port: 80 + +time: + - platform: sntp + id: current_time + timezone: !secret timezone + on_time_sync: + then: + - logger.log: "synced system clock" + # - light.turn_on: + # id: neopixel_strip_1 + # effect: "None" + # - light.turn_off: + # id: neopixel_strip_1 +light: + - name: NeoPixel Strip 1 + id: neopixel_strip_1 + platform: neopixelbus + type: GRB + variant: WS2812 + pin: GPIO3 + num_leds: 198 + restore_mode: ALWAYS_OFF + # default_transition_length: 0.7s + # color_correct: [100%, 100%, 100%] + method: + type: esp8266_dma + + effects: + # - random: + # - pulse: + # - strobe: + # - flicker: + - addressable_rainbow: + name: "rainbow" + # - addressable_color_wipe: + # - addressable_scan: + - addressable_twinkle: + name: "twinkle" + - addressable_random_twinkle: + name: "random_twinkle" + # - addressable_fireworks: + # - addressable_flicker: + # - wled: + + +display: + - platform: addressable_light + id: led_matrix_display + addressable_light_id: neopixel_strip_1 + width: 18 + height: 11 + rotation: 270° + update_interval: 16ms + auto_clear_enabled: false + pixel_mapper: |- + int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16}; + int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16}; + if (x > 9) return -1; + if (y % 2 == 0) { + return (y * 18 + mapping_even[x]); // mapping_even[y]; + } else { + return (y * 18 + mapping_odd[x]); // mapping_odd[y]; + } + # lambda: |- + # //ESP_LOGE("lambda", "display_ptr: %i", &it); + # //it.fill(Color(0xff00ff)); + # //it.line(0,0,0,0, Color(0x00FF00)); + + +wordclock: + time_id: current_time + display_id: led_matrix_display + addressable_light_id: neopixel_strip_1 + static_segments: ["IT", "IS"] + segments: + - {name: "IT", line: {x1: 0, x2: 1, y1: 0}} + - {name: "IS", line: {x1: 3, x2: 4, y1: 0}} + - {name: "AM", line: {x1: 7, x2: 8, y1: 0}} + - {name: "PM", line: {x1: 9, x2: 10, y1: 0}} + - {name: "QUARTER", line: {x1: 2, x2: 8, y1: 1}} + - {name: "TWENTY", line: {x1: 0, x2: 5, y1: 2}} + - {name: "FIVE_MINUTES", line: {x1: 6, x2: 9, y1: 2}} + - {name: "HALF", line: {x1: 0, x2: 3, y1: 3}} + - {name: "TEN_MINUTES", line: {x1: 5, x2: 7, y1: 3}} + - {name: "TO", line: {x1: 9, x2: 10, y1: 3}} + - {name: "PAST", line: {x1: 0, x2: 3, y1: 4}} + - {name: "NINE", line: {x1: 7, x2: 10, y1: 4}} + - {name: "ONE", line: {x1: 0, x2: 2, y1: 5}} + - {name: "SIX", line: {x1: 3, x2: 5, y1: 5}} + - {name: "THREE", line: {x1: 6, x2: 10, y1: 5}} + - {name: "FOUR", line: {x1: 0, x2: 3, y1: 6}} + - {name: "FIVE", line: {x1: 4, x2: 7, y1: 6}} + - {name: "TWO", line: {x1: 8, x2: 10, y1: 6}} + - {name: "EIGHT", line: {x1: 0, x2: 4, y1: 7}} + - {name: "ELEVEN", line: {x1: 5, x2: 10, y1: 7}} + - {name: "SEVEN", line: {x1: 0, x2: 4, y1: 8}} + - {name: "TWELVE", line: {x1: 5, x2: 10, y1: 8}} + - {name: "TEN", line: {x1: 0, x2: 2, y1: 9}} + - {name: "OCLOCK", line: {x1: 5, x2: 10, y1: 9}} + minutes: + - {minute: 0, hour_offset: 0, segments: ["OCLOCK"]} + - {minute: 5, hour_offset: 0, segments: ["FIVE_MINUTES", "PAST"]} + - {minute: 10, hour_offset: 0, segments: ["TEN_MINUTES", "PAST"]} + - {minute: 15, hour_offset: 0, segments: ["QUARTER", "PAST"]} + - {minute: 20, hour_offset: 0, segments: ["TWENTY", "PAST"]} + - {minute: 25, hour_offset: 0, segments: ["TWENTY", "FIVE_MINUTES", "PAST"]} + - {minute: 30, hour_offset: 0, segments: ["HALF", "PAST"]} + - {minute: 35, hour_offset: 1, segments: ["TWENTY", "FIVE_MINUTES", "TO"]} + - {minute: 40, hour_offset: 1, segments: ["TWENTY", "TO"]} + - {minute: 45, hour_offset: 1, segments: ["QUARTER", "TO"]} + - {minute: 50, hour_offset: 1, segments: ["TEN_MINUTES", "TO"]} + - {minute: 55, hour_offset: 1, segments: ["FIVE_MINUTES", "TO"]} + hours: + - {hour: 0, segments: "TWELVE"} + - {hour: 1, segments: "ONE"} + - {hour: 2, segments: "TWO"} + - {hour: 3, segments: "THREE"} + - {hour: 4, segments: "FOUR"} + - {hour: 5, segments: "FIVE"} + - {hour: 6, segments: "SIX"} + - {hour: 7, segments: "SEVEN"} + - {hour: 8, segments: "EIGHT"} + - {hour: 9, segments: "NINE"} + - {hour: 10, segments: "TEN"} + - {hour: 11, segments: "ELEVEN"} + - {hour: 12, segments: "TWELVE"} + - {hour: 13, segments: "ONE"} + - {hour: 14, segments: "TWO"} + - {hour: 15, segments: "THREE"} + - {hour: 16, segments: "FOUR"} + - {hour: 17, segments: "FIVE"} + - {hour: 18, segments: "SIX"} + - {hour: 19, segments: "SEVEN"} + - {hour: 20, segments: "EIGHT"} + - {hour: 21, segments: "NINE"} + - {hour: 22, segments: "TEN"} + - {hour: 23, segments: "ELEVEN"} + # hours: + # - {hour: 0, segments: ["TWELVE", "AM"]} + # - {hour: 1, segments: ["ONE", "AM"]} + # - {hour: 2, segments: ["TWO", "AM"]} + # - {hour: 3, segments: ["THREE", "AM"]} + # - {hour: 4, segments: ["FOUR", "AM"]} + # - {hour: 5, segments: ["FIVE", "AM"]} + # - {hour: 6, segments: ["SIX", "AM"]} + # - {hour: 7, segments: ["SEVEN", "AM"]} + # - {hour: 8, segments: ["EIGHT", "AM"]} + # - {hour: 9, segments: ["NINE", "AM"]} + # - {hour: 10, segments: ["TEN", "AM"]} + # - {hour: 11, segments: ["ELEVEN", "AM"]} + # - {hour: 12, segments: ["TWELVE", "PM"]} + # - {hour: 13, segments: ["ONE", "PM"]} + # - {hour: 14, segments: ["TWO", "PM"]} + # - {hour: 15, segments: ["THREE", "PM"]} + # - {hour: 16, segments: ["FOUR", "PM"]} + # - {hour: 17, segments: ["FIVE", "PM"]} + # - {hour: 18, segments: ["SIX", "PM"]} + # - {hour: 19, segments: ["SEVEN", "PM"]} + # - {hour: 20, segments: ["EIGHT", "PM"]} + # - {hour: 21, segments: ["NINE", "PM"]} + # - {hour: 22, segments: ["TEN", "PM"]} + # - {hour: 23, segments: ["ELEVEN", "PM"]} + # minutes: + # - {minute: 0, segments: ["A"]} + # - {minute: 5, segments: ["B"]} + # - {minute: 10, segments: ["A"]} + # - {minute: 15, segments: ["B"]} + # - {minute: 20, segments: ["A"]} + # - {minute: 25, segments: ["B"]} + # - {minute: 30, segments: ["A"]} + # - {minute: 35, segments: ["B"]} + # - {minute: 40, segments: ["A"]} + # - {minute: 45, segments: ["B"]} + # - {minute: 50, segments: ["A"]} + # - {minute: 55, segments: ["B"]} + # hours: + # - {hour: 0, segments: ["C"]} + \ No newline at end of file diff --git a/test.png b/test.png new file mode 100644 index 0000000000000000000000000000000000000000..a6acecdaa7dc443432df140828544fecdafbd024 GIT binary patch literal 780 zcmV+n1M~ceP)EX>4Tx04R}tkv&MmKpe$iQ%glE4ptCx$k003rHVM#DionYs1;guFuC*#nzSS- zE{=k0!NHHks)LKOt`4q(Aou~|>f)s6A|?JWDYS_3;J6>}?mh0_0Ya)s#Kpat9cFs>_D#NPe0^u?W1M(KqFR;agx}&F!tTkJASrOI@XJfP+I| zqD0y29`Ek&?d{()o&J6Ryr6Q#A*=r;00009a7bBm001r{001r{0eGc9b^rhX2XskI zMF-~v0~IGI=v~?X0000PbVXQnLvL+uWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbN z_DMuRR2b6*!COwkP!t8weIB=XA*H+$_|P~fN9H&f1s^63#26AasS%n&?_;}`d_IjV z5C@BpgdsHg0M!B}6wqIvzb7XYiM&p{k&VeJXsk#ha-x4c<`_j4B&5KTK=G5yVj;Vo zDh% zrB;TZAqW7HemwSVt7Z^EjM{aU61&b0hp^p-zR&*tHmRW!1kDhF)XFBvd@gM(mp}T; zZ=AqfFo0JnR76#;xMilwO%>@^QxQ-|K`}ByX2^TqziJw&4gLYPNQg-XcMY@v0000< KMNUMnLSTZox>ckA literal 0 HcmV?d00001 From dd1408c5cbcd4015f6a4fddff1d9d4d12beca118 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Fri, 12 May 2023 05:07:21 +0200 Subject: [PATCH 04/16] feat: add transitions and clean up code. --- components/wordcl/__init__.py | 53 --- components/wordcl/desky.cpp | 116 ----- components/wordcl/desky.h | 52 --- components/wordclock/__init__.py | 424 ++++------------- components/wordclock/wordclock.cpp | 430 +++++++----------- components/wordclock/wordclock.h | 383 ++++++++-------- flake.lock | 64 +++ ...test_3leds.yaml => susannes_wordclock.yaml | 110 ++--- 8 files changed, 533 insertions(+), 1099 deletions(-) delete mode 100644 components/wordcl/__init__.py delete mode 100644 components/wordcl/desky.cpp delete mode 100644 components/wordcl/desky.h create mode 100644 flake.lock rename image_test_3leds.yaml => susannes_wordclock.yaml (61%) diff --git a/components/wordcl/__init__.py b/components/wordcl/__init__.py deleted file mode 100644 index de1ab14..0000000 --- a/components/wordcl/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome import pins -from esphome.components import light -from esphome.components import uart -from esphome.components import sensor -from esphome.const import CONF_ID, CONF_HEIGHT, CONF_TIMEOUT, ICON_GAUGE - -DEPENDENCIES = ['time'] -AUTO_LOAD = ['light'] - -wordclock_ns = cg.esphome_ns.namespace('wordcl') - -Wordclock = wordclock_ns.class_('Wordclock', cg.Component, light.) -Desky = desky_ns.class_('Desky', cg.Component, uart.UARTDevice) - -CONF_UP = "up" -CONF_DOWN = "down" -CONF_REQUEST = "request" -CONF_STOPPING_DISTANCE = "stopping_distance" - - - -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(Desky), - cv.Optional(CONF_UP): pins.gpio_output_pin_schema, - cv.Optional(CONF_DOWN): pins.gpio_output_pin_schema, - cv.Optional(CONF_REQUEST): pins.gpio_output_pin_schema, - cv.Optional(CONF_HEIGHT): sensor.sensor_schema(icon=ICON_GAUGE, accuracy_decimals=0), - cv.Optional(CONF_STOPPING_DISTANCE, default=15): cv.positive_int, - cv.Optional(CONF_TIMEOUT): cv.time_period, -}).extend(uart.UART_DEVICE_SCHEMA) - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await uart.register_uart_device(var, config) - if CONF_UP in config: - pin = await cg.gpio_pin_expression(config[CONF_UP]) - cg.add(var.set_up_pin(pin)) - if CONF_DOWN in config: - pin = await cg.gpio_pin_expression(config[CONF_DOWN]) - cg.add(var.set_down_pin(pin)) - if CONF_REQUEST in config: - pin = await cg.gpio_pin_expression(config[CONF_REQUEST]) - cg.add(var.set_request_pin(pin)) - if CONF_HEIGHT in config: - sens = await sensor.new_sensor(config[CONF_HEIGHT]) - cg.add(var.set_height_sensor(sens)) - cg.add(var.set_stopping_distance(config[CONF_STOPPING_DISTANCE])) - if CONF_TIMEOUT in config: - cg.add(var.set_timeout(config[CONF_TIMEOUT].total_milliseconds)) - diff --git a/components/wordcl/desky.cpp b/components/wordcl/desky.cpp deleted file mode 100644 index bcd6159..0000000 --- a/components/wordcl/desky.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "desky.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace desky { - -static const char *TAG = "desky"; - -const char *desky_operation_to_str(DeskyOperation op) { - switch (op) { - case DESKY_OPERATION_IDLE: - return "IDLE"; - case DESKY_OPERATION_RAISING: - return "RAISING"; - case DESKY_OPERATION_LOWERING: - return "LOWERING"; - default: - return "UNKNOWN"; - } -} - -void Desky::setup() { - if (this->up_pin_ != nullptr) - this->up_pin_->digital_write(false); - if (this->down_pin_ != nullptr) - this->down_pin_->digital_write(false); - if (this->request_pin_ != nullptr) { - this->request_pin_->digital_write(true); - this->request_time_ = millis(); - } -} - -void Desky::loop() { - static int state = 0; - static uint8_t high_byte; - - while (this->available()) { - uint8_t c; - int value; - this->read_byte(&c); - switch (state) { - case 0: - if (c == 1) - state = 1; - break; - case 1: - if (c == 1) - state = 2; - else - state = 0; - break; - case 2: - high_byte = c; - state = 3; - break; - case 3: - value = (high_byte << 8) + c; - this->current_pos_ = value; - if (this->height_sensor_ != nullptr) - this->height_sensor_->publish_state(value); - state = 0; - break; - } - } - - if (this->target_pos_ >= 0) { - if (abs(this->target_pos_ - this->current_pos_) < this->stopping_distance_) - this->stop(); - if ((this->timeout_ >= 0) && (millis() - this->start_time_ >= this->timeout_)) - this->stop(); - } - - if ((this->request_time_ > 0) && (millis() - this->request_time_ >= 100)) { - this->request_pin_->digital_write(false); - this->request_time_ = 0; - } -} - -void Desky::dump_config() { - ESP_LOGCONFIG(TAG, "Desky desk:"); - LOG_SENSOR("", "Height", this->height_sensor_); - LOG_PIN("Up pin: ", this->up_pin_); - LOG_PIN("Down pin: ", this->down_pin_); - LOG_PIN("Request pin: ", this->request_pin_); -} - -void Desky::move_to(int target_pos) { - if (abs(target_pos - this->current_pos_) < this->stopping_distance_) - return; - if (target_pos > this->current_pos_) { - if (this->up_pin_ == nullptr) - return; - this->up_pin_->digital_write(true); - this->current_operation = DESKY_OPERATION_RAISING; - } else { - if (this->down_pin_ == nullptr) - return; - this->down_pin_->digital_write(true); - this->current_operation = DESKY_OPERATION_LOWERING; - } - this->target_pos_ = target_pos; - if (this->timeout_ >= 0) - this->start_time_ = millis(); -} - -void Desky::stop() { - this->target_pos_ = -1; - if (this->up_pin_ != nullptr) - this->up_pin_->digital_write(false); - if (this->down_pin_ != nullptr) - this->down_pin_->digital_write(false); - this->current_operation = DESKY_OPERATION_IDLE; -} - -} // namespace desky -} // namespace esphome diff --git a/components/wordcl/desky.h b/components/wordcl/desky.h deleted file mode 100644 index fc9062c..0000000 --- a/components/wordcl/desky.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/uart/uart.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace desky { - -enum DeskyOperation : uint8_t { - DESKY_OPERATION_IDLE = 0, - DESKY_OPERATION_RAISING, - DESKY_OPERATION_LOWERING, -}; - -const char *desky_operation_to_str(DeskyOperation op); - -class Desky : public Component, public sensor::Sensor, public uart::UARTDevice { - public: - float get_setup_priority() const override { return setup_priority::LATE; } - void setup() override; - void loop() override; - void dump_config() override; - - void set_height_sensor(sensor::Sensor *sensor) { this->height_sensor_ = sensor; } - void set_up_pin(GPIOPin *pin) { this->up_pin_ = pin; } - void set_down_pin(GPIOPin *pin) { this->down_pin_ = pin; } - void set_request_pin(GPIOPin *pin) { this->request_pin_ = pin; } - void set_stopping_distance(int distance) { this->stopping_distance_ = distance; } - void set_timeout(int timeout) { this->timeout_ = timeout; } - - void move_to(int height); - void stop(); - - DeskyOperation current_operation{DESKY_OPERATION_IDLE}; - - protected: - sensor::Sensor *height_sensor_{nullptr}; - GPIOPin *up_pin_{nullptr}; - GPIOPin *down_pin_{nullptr}; - GPIOPin *request_pin_{nullptr}; - int stopping_distance_; - int current_pos_{0}; - int target_pos_{-1}; - int timeout_{-1}; - uint64_t start_time_; - uint64_t request_time_{0}; -}; - -} // namespace desky -} // namespace esphome diff --git a/components/wordclock/__init__.py b/components/wordclock/__init__.py index 3192cf8..242f2b6 100644 --- a/components/wordclock/__init__.py +++ b/components/wordclock/__init__.py @@ -1,10 +1,11 @@ import logging from esphome import core -from esphome.components import display, font, time, light +from esphome.components import display, font, time, light, color import esphome.config_validation as cv import esphome.codegen as cg import esphome.cpp_generator as cpp + from esphome.const import ( CONF_ID, CONF_NAME, @@ -17,6 +18,8 @@ from esphome.const import ( CONF_HOURS, CONF_MINUTE, CONF_MINUTES, + CONF_BRIGHTNESS, + CONF_UPDATE_INTERVAL, ) from esphome.core import CORE, HexInt @@ -32,77 +35,21 @@ CONF_LINE_END_Y = "y2" CONF_LINE = "line" +CONF_COLOR_ON = "color_on" +CONF_COLOR_OFF = "color_off" + +DATA_VECTOR_SEGMENTS_HOUR = "data_vector_segments_hour" +DATA_VECTOR_SEGMENTS_MINUTE = "data_vector_segments_minute" +DATA_VECTOR_SEGMENTS = "data_vector_segments_ptr" + CONF_WORDCLOCK_STATIC_SEGMENTS = "static_segments" CONF_HOUR_OFFSET = "hour_offset" - -# CONF_HOURS_MIDNIGHT = "0" -# CONF_HOURS_ONE = "1" -# CONF_HOURS_TWO = "2" -# CONF_HOURS_THREE = "3" -# CONF_HOURS_FOUR = "4" -# CONF_HOURS_FIVE = "5" -# CONF_HOURS_SIX = "6" -# CONF_HOURS_SEVEN = "7" -# CONF_HOURS_EIGHT = "8" -# CONF_HOURS_NINE = "9" -# CONF_HOURS_TEN = "10" -# CONF_HOURS_ELEVEN = "11" -# CONF_HOURS_TWELVE = "12" -# CONF_HOURS_THIRTEEN = "13" -# CONF_HOURS_FOURTEEN = "14" -# CONF_HOURS_FIFTEEN = "15" -# CONF_HOURS_SIXTEEN = "16" -# CONF_HOURS_SEVENTEEN = "17" -# CONF_HOURS_EIGHTTEEN = "18" -# CONF_HOURS_NINETEEN = "19" -# CONF_HOURS_TWENTY = "20" -# CONF_HOURS_TWENTYONE = "21" -# CONF_HOURS_TWENTYTWO = "22" -# CONF_HOURS_TWENTYTHREE = "23" - -# CONF_HOURS_MIDNIGHT = "midnight" -# CONF_HOURS_ONE = "one" -# CONF_HOURS_TWO = "two" -# CONF_HOURS_THREE = "three" -# CONF_HOURS_FOUR = "four" -# CONF_HOURS_FIVE = "five" -# CONF_HOURS_SIX = "six" -# CONF_HOURS_SEVEN = "seven" -# CONF_HOURS_EIGHT = "eight" -# CONF_HOURS_NINE = "nine" -# CONF_HOURS_TEN = "ten" -# CONF_HOURS_ELEVEN = "eleven" -# CONF_HOURS_TWELVE = "twelve" -# CONF_HOURS_THIRTEEN = "thirteen" -# CONF_HOURS_FOURTEEN = "fourteen" -# CONF_HOURS_FIFTEEN = "fifteen" -# CONF_HOURS_SIXTEEN = "sixteen" -# CONF_HOURS_SEVENTEEN = "seventeen" -# CONF_HOURS_EIGHTTEEN = "eightteen" -# CONF_HOURS_NINETEEN = "nineteen" -# CONF_HOURS_TWENTY = "twenty" -# CONF_HOURS_TWENTYONE = "twentyone" -# CONF_HOURS_TWENTYTWO = "twentytwo" -# CONF_HOURS_TWENTYTHREE = "twentythree" - -# CONF_MINUTES_SHARP = "0" -# CONF_MINUTES_FIVE = "5" -# CONF_MINUTES_TEN = "10" -# CONF_MINUTES_FIFTEEN = "15" -# CONF_MINUTES_TWENTY = "20" -# CONF_MINUTES_TWENTYFIVE = "25" -# CONF_MINUTES_THIRTY = "30" -# CONF_MINUTES_THIRTYFIVE = "35" -# CONF_MINUTES_FORTY = "40" -# CONF_MINUTES_FORTYFIVE = "45" -# CONF_MINUTES_FIFTY = "50" -# CONF_MINUTES_FIFTYFIVE = "55" - +CONF_FADE_LENGTH = "fade_length" DEPENDENCIES = ["display", "time"] MULTI_CONF = False - +int8 = cg.global_ns.namespace("int8_t") wordclock_ns = cg.esphome_ns.namespace("wordclock") SegmentCoords = wordclock_ns.struct("SegmentCoords") Wordclock = wordclock_ns.class_( @@ -130,31 +77,7 @@ WORDCLOCK_MINUTE_SCHEMA = { cv.Required(CONF_SEGMENTS): cv.ensure_list(cv.string_strict), } -# WORDCLOCK_CONFIG_SCHEMA = { -# cv.Optional(CONF_WORDCLOCK_STATIC_TEXT): cv.ensure_list(cv.string_strict), -# # cv.Required(CONF_LAMBDA): cv.lambda_, -# cv.Required(CONF_SEGMENTS): cv.ensure_list(WORDCLOCK_SEGMENT_SCHEMA), -# cv.Required(CONF_MINUTES): cv.ensure_list(WORDCLOCK_MINUTE_SCHEMA), -# cv.Required(CONF_HOURS): cv.ensure_list(WORDCLOCK_HOUR_SCHEMA), -# } -DATA1 = "data1" -DATA_X1 = "data_x1" -DATA_X2 = "data_x2" -DATA_Y1 = "data_y1" -DATA_Y2 = "data_y2" -DATA_SEGMENT_COORDS = "data_segment_coords" -DATA_MINUTES = "data_minutes" -DATA_HOURS = "data_hours" -DATA_SEGMENTS_HOUR = "data_segments_hour" -DATA_SEGMENTS_MINUTE = "data_segments_minute" -DATA3 = "data3" -DATA4 = "data4" -DATA_VECTOR_SEGMENTS_HOUR = "data_vector_segments_hour" -DATA_VECTOR_SEGMENTS_MINUTE = "data_vector_segments_minute" - -int8 = cg.global_ns.namespace("int8_t") -uint16_ptr = cg.global_ns.namespace("uint16_t *") WORDCLOCK_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Wordclock), @@ -167,43 +90,28 @@ WORDCLOCK_SCHEMA = cv.Schema( cv.Required(CONF_ADDRESSABLE_LIGHT_ID): cv.use_id( light.AddressableLightState ), - # cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_ON): cv.use_id(color.ColorStruct), + cv.Optional(CONF_COLOR_OFF): cv.use_id(color.ColorStruct), cv.Optional(CONF_WORDCLOCK_STATIC_SEGMENTS): cv.ensure_list(cv.string_strict), - # cv.Required(CONF_LAMBDA): cv.lambda_, cv.Required(CONF_SEGMENTS): cv.ensure_list(WORDCLOCK_SEGMENT_SCHEMA), cv.Required(CONF_MINUTES): cv.ensure_list(WORDCLOCK_MINUTE_SCHEMA), cv.Required(CONF_HOURS): cv.ensure_list(WORDCLOCK_HOUR_SCHEMA), - cv.GenerateID(DATA_X1): cv.declare_id(uint16_ptr), - cv.GenerateID(DATA_X2): cv.declare_id(cg.uint8), - cv.GenerateID(DATA_Y1): cv.declare_id(cg.uint8), - cv.GenerateID(DATA_Y2): cv.declare_id(cg.uint8), - cv.GenerateID(DATA_SEGMENT_COORDS): cv.declare_id(SegmentCoords), - - # cv.GenerateID(DATA_MINUTES): cv.declare_id(int8), - # cv.GenerateID(DATA_HOURS): cv.declare_id(int8), - # cv.GenerateID(DATA_MINUTES): cv.declare_id(uint16_ptr), - # cv.GenerateID(DATA_HOURS): cv.declare_id(uint16_ptr), - # cv.GenerateID(DATA_SEGMENTS_MINUTE): cv.declare_id(cg.std_vector.template(cg.uint16)), - # cv.GenerateID(DATA_SEGMENTS_HOUR): cv.declare_id(cg.std_vector.template(cg.uint16)), - - cv.GenerateID(DATA_VECTOR_SEGMENTS_HOUR): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), - cv.GenerateID(DATA_VECTOR_SEGMENTS_MINUTE): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), - # cv.GenerateID(DATA3): cv.declare_id(cg.uint8), - # cv.GenerateID(DATA4): cv.declare_id(cg.std_vector.template(cg.int32)), - # cv.Required(CONF_WORDCLOCK_CONFIG): cv.ensure_list(WORDCLOCK_CONFIG_SCHEMA), - - # cv.Required(CONF_ID): cv.declare_id(Wordclock_), - # cv.Required(CONF_FILE): cv.file_, - # cv.Optional(CONF_RESIZE): cv.dimensions, - # cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), - # cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( - # "NONE", "FLOYDSTEINBERG", upper=True - # ), - # cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + # cv.Optional( + # CONF_FADE_LENGTH, default="700ms" + # ): cv., + cv.Optional( + CONF_FADE_LENGTH, default="700ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_UPDATE_INTERVAL, default="16ms" + ): cv.positive_time_period_milliseconds, + # cv.GenerateID(DATA_VECTOR_SEGMENTS_HOUR): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), + # cv.GenerateID(DATA_VECTOR_SEGMENTS_MINUTE): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), + cv.GenerateID(DATA_VECTOR_SEGMENTS): cv.declare_id(cg.std_vector.template(cg.uint16)), } ) -# CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, WORDCLOCK_SCHEMA) CONFIG_SCHEMA = WORDCLOCK_SCHEMA async def to_code(config): @@ -212,236 +120,84 @@ async def to_code(config): wrapped_light_state = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID]) var = cg.new_Pvariable(config[CONF_ID], wrapped_time, wrapped_display, wrapped_light_state) + if CONF_COLOR_ON in config: + wrapped_color_on = await cg.get_variable(config[CONF_COLOR_ON]) + cg.add(var.set_on_color(wrapped_color_on)) + if CONF_COLOR_OFF in config: + wrapped_color_off = await cg.get_variable(config[CONF_COLOR_OFF]) + cg.add(var.set_off_color(wrapped_color_off)) + + # segments_vector_ptr = cg.new_Pvariable(config[DATA_VECTOR_SEGMENTS]) +# void Wordclock::setup_transitions(uint32_t milliseconds) { +# this->on_transformer->setup(0, this->brightness, milliseconds); +# this->off_transformer->setup(this->brightness, 0, milliseconds); +# } + cg.add(var.setup_transitions(config[CONF_FADE_LENGTH])) + cg.add(var.set_brightness(int(config[CONF_BRIGHTNESS] * 255))) + + + + SEGMENT_MAP = dict() - for idx, segm in enumerate(config[CONF_SEGMENTS]): - print(segm[CONF_NAME]) - SEGMENT_MAP[segm[CONF_NAME]] = idx + for idx, segment in enumerate(config[CONF_SEGMENTS]): + SEGMENT_MAP[segment[CONF_NAME]] = idx + line = segment[CONF_LINE] - line_start_x = segm[CONF_LINE][CONF_LINE_START_X] - line_end_x = segm[CONF_LINE][CONF_LINE_END_X] - line_start_y = segm[CONF_LINE][CONF_LINE_START_Y] - line_end_y = segm[CONF_LINE].get(CONF_LINE_END_Y, line_start_y) + x1 = line[CONF_LINE_START_X] + y1 = line[CONF_LINE_START_Y] + x2 = line[CONF_LINE_END_X] + y2 = line.get(CONF_LINE_END_Y, y1) - exp = cg.StructInitializer( - SegmentCoords, - ("x1", line_start_x), - ("x2", line_end_x), - ("y1", line_start_y), - ("y2", line_end_y), - ) + exp = cg.StructInitializer(SegmentCoords, ("x1", x1), ("y1", y1), ("x2", x2), ("y2", y2),) cg.add(var.add_segment(exp)) if CONF_WORDCLOCK_STATIC_SEGMENTS in config: - - # cg.static_const_array(config[DATA_SEGMENT_COORDS], accumulator) - # print(SEGMENT_MAP) for segment_name in config[CONF_WORDCLOCK_STATIC_SEGMENTS]: cg.add(var.add_static(SEGMENT_MAP[segment_name])) - # static_segment_ids = config[CONF_WORDCLOCK_STATIC_SEGMENTS] - # exp = cg.std_vector.template(cg.uint16)(static_segment_ids) - # cg.add(var.add_static(hour[CONF_HOUR], cpp.UnaryOpExpression("&", exp))) - - - - hours = [] - for idx, hour in enumerate(config[CONF_HOURS]): - segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] - exp = cg.std_vector.template(cg.uint16)(segment_ids) - hours.append(exp) - hours_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_HOUR], cg.std_vector.template(cg.std_vector.template(cg.uint16))(hours)) - - minutes = [] - for idx, hour in enumerate(config[CONF_MINUTES]): - segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] - exp = cg.std_vector.template(cg.uint16)(segment_ids) - minutes.append(exp) - minutes_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_MINUTE], cg.std_vector.template(cg.std_vector.template(cg.uint16))(minutes)) for idx, hour in enumerate(config[CONF_HOURS]): - exp = cg.std_vector.template(cg.uint16)(segment_ids) - cg.add(var.add_hour(hour[CONF_HOUR], cpp.UnaryOpExpression("&", hours_array[idx]))) + segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] + cg.add(var.add_hour(hour[CONF_HOUR], cg.std_vector.template(cg.uint16).new(segment_ids))) + # segments_vector_ptr = segments_vector_ptr.new(segment_ids) + # foo = await cg.get_variable(config[DATA_VECTOR_SEGMENTS]) + # foo = cg.std_vector.template(cg.uint16).new(segment_ids) + + # del foo - # minutes = [[],] * 60 for idx, minute in enumerate(config[CONF_MINUTES]): - exp = cg.std_vector.template(cg.uint16)(segment_ids) - cg.add(var.add_minute(minute[CONF_MINUTE], cpp.UnaryOpExpression("&", minutes_array[idx]))) - # segment_ids = [SEGMENT_MAP[a] for a in minute[CONF_SEGMENTS]] - # minutes[minute[CONF_MINUTE]] = cg.std_vector.template(cg.int32)() - # foo = [] - # cg.ArrayInitializer() - '''exp = cg.std_vector.template(cg.uint16)(segment_ids)''' - # exp = cg.ArrayInitializer(minute[CONF_SEGMENTS]) - '''cg.add(var.add_minute(minute[CONF_MINUTE], exp))''' - - # for segment_str in minute[CONF_SEGMENTS]: - # foo.append(SEGMENT_MAP[segment_str]) - # # minutes[minute[CONF_MINUTE]].push_back(SEGMENT_MAP[segment_str]) - # minutes[minute[CONF_MINUTE]] = foo - # print(minute[CONF_MINUTE]) - # SEGMENT_MAP[i[CONF_NAME]] = idx - # accumulator.append(idx) - for idx, minute in enumerate(config[CONF_MINUTES]): - cg.add(var.add_hour_offset(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET])) - - + segment_ids = [SEGMENT_MAP[a] for a in minute[CONF_SEGMENTS]] + cg.add(var.add_minute(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET], cg.std_vector.template(cg.uint16).new(segment_ids))) + # segments_vector_ptr = segments_vector_ptr.new(segment_ids) + # segments_vector_ptr = cg.new_Pvariable(config[DATA_VECTOR_SEGMENTS], segment_ids) + # foo = await cg.get_variable(config[DATA_VECTOR_SEGMENTS]) + # foo = cg.std_vector.template(cg.uint16).new(segment_ids) + # segments_vector = cg.new_Pvariable(core.ID(None, type=cg.std_vector.template(cg.uint16)), segment_ids) + # del foo await cg.register_component(var, config) - """ - accumulator = [] - SEGMENT_MAP = dict() - for idx, segm in enumerate(config[CONF_SEGMENTS]): - print(segm[CONF_NAME]) - SEGMENT_MAP[segm[CONF_NAME]] = idx - # x1.append() - # x2.append() - line_start_x = segm[CONF_LINE][CONF_LINE_START_X] - line_end_x = segm[CONF_LINE][CONF_LINE_END_X] - line_start_y = segm[CONF_LINE][CONF_LINE_START_Y] - line_end_y = segm[CONF_LINE].get(CONF_LINE_END_Y, line_start_y) - # print(line_start_y) - # print(line_end_y) - # y1.append(segm[CONF_LINE][CONF_LINE_START_Y]) - # y2.append(line_end_y) - exp = cg.StructInitializer( - SegmentCoords, - ("x1", line_start_x), - ("x2", line_end_x), - ("y1", line_start_y), - ("y2", line_end_y), - ) - accumulator.append(exp) + # hours = [] + # for idx, hour in enumerate(config[CONF_HOURS]): + # segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] + # exp = cg.std_vector.template(cg.uint16)(segment_ids) + # hours.append(exp) + # hours_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_HOUR], cg.std_vector.template(cg.std_vector.template(cg.uint16))(hours)) - cg.static_const_array(config[DATA_SEGMENT_COORDS], accumulator) - print(SEGMENT_MAP) + # minutes = [] + # for idx, hour in enumerate(config[CONF_MINUTES]): + # segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] + # exp = cg.std_vector.template(cg.uint16)(segment_ids) + # minutes.append(exp) + # minutes_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_MINUTE], cg.std_vector.template(cg.std_vector.template(cg.uint16))(minutes)) + # for idx, hour in enumerate(config[CONF_HOURS]): + # exp = cg.std_vector.template(cg.uint16)(segment_ids) + # cg.add(var.add_hour(hour[CONF_HOUR], cpp.UnaryOpExpression("&", hours_array[idx]))) - minutes = [[],] * 60 - for idx, minute in enumerate(config[CONF_MINUTES]): - # minutes[minute[CONF_MINUTE]] = cg.std_vector.template(cg.int32)() - foo = [] - cg.ArrayInitializer() - for segment_str in minute[CONF_SEGMENTS]: - foo.append(SEGMENT_MAP[segment_str]) - # minutes[minute[CONF_MINUTE]].push_back(SEGMENT_MAP[segment_str]) - minutes[minute[CONF_MINUTE]] = foo - print(minute[CONF_MINUTE]) - # SEGMENT_MAP[i[CONF_NAME]] = idx - # accumulator.append(idx) - print(minutes) - cg.static_const_array(config[DATA_MINUTES], minutes) - """ - """ - hours = [cg.std_vector.template(cg.uint16)()] * 24 - cg.static_const_array(config[DATA_HOURS], hours) + # for idx, minute in enumerate(config[CONF_MINUTES]): + # exp = cg.std_vector.template(cg.uint16)(segment_ids) + # cg.add(var.add_minute(minute[CONF_MINUTE], cpp.UnaryOpExpression("&", minutes_array[idx]))) + + # for idx, minute in enumerate(config[CONF_MINUTES]): + # cg.add(var.add_hour_offset(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET])) - for idx, i in enumerate(config[CONF_HOURS]): - # for segment_str in i[CONF_SEGMENTS]: - # print(config[DATA_HOURS].type) - # push = config[DATA_HOURS].get(i[CONF_HOUR]).push_back(SEGMENT_MAP[segment_str]) - # cg.add(push) - # hours[i[CONF_HOUR]] = cg.std_vector.template(cg.int32)() - - print(i[CONF_HOUR]) - # SEGMENT_MAP[i[CONF_NAME]] = idx - # accumulator.append(idx) - print(hours) - # cg.static_const_array(config[DATA_HOURS], hours) - # cg.static_const_array(config[DATA3], hours) - # cg.static_const_array(config[DATA4], hours) - """ - - - # prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - # cg.add_global(cg.Statement(f"const int {i[CONF_NAME]} = 0;")) - # cg.add_global(f""" - # /* - # {global_consts} - # */ - # """) - - # from PIL import Image - - # path = CORE.relative_config_path(config[CONF_FILE]) - # try: - # image = Image.open(path) - # except Exception as e: - # raise core.EsphomeError(f"Could not load image file {path}: {e}") - - # width, height = image.size - - # if CONF_RESIZE in config: - # image.thumbnail(config[CONF_RESIZE]) - # width, height = image.size - # else: - # if width > 500 or height > 500: - # _LOGGER.warning( - # "The image you requested is very big. Please consider using" - # " the resize parameter." - # ) - - # dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG - # if config[CONF_TYPE] == "GRAYSCALE": - # image = image.convert("L", dither=dither) - # pixels = list(image.getdata()) - # data = [0 for _ in range(height * width)] - # pos = 0 - # for pix in pixels: - # data[pos] = pix - # pos += 1 - - # elif config[CONF_TYPE] == "RGB24": - # image = image.convert("RGB") - # pixels = list(image.getdata()) - # data = [0 for _ in range(height * width * 3)] - # pos = 0 - # for pix in pixels: - # data[pos] = pix[0] - # pos += 1 - # data[pos] = pix[1] - # pos += 1 - # data[pos] = pix[2] - # pos += 1 - - # elif config[CONF_TYPE] == "RGB565": - # image = image.convert("RGB") - # pixels = list(image.getdata()) - # data = [0 for _ in range(height * width * 3)] - # pos = 0 - # for pix in pixels: - # R = pix[0] >> 3 - # G = pix[1] >> 2 - # B = pix[2] >> 3 - # rgb = (R << 11) | (G << 5) | B - # data[pos] = rgb >> 8 - # pos += 1 - # data[pos] = rgb & 255 - # pos += 1 - - # elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"): - # image = image.convert("1", dither=dither) - # width8 = ((width + 7) // 8) * 8 - # data = [0 for _ in range(height * width8 // 8)] - # for y in range(height): - # for x in range(width): - # if image.getpixel((x, y)): - # continue - # pos = x + y * width8 - # data[pos // 8] |= 0x80 >> (pos % 8) - - # elif config[CONF_TYPE] == "TRANSPARENT_IMAGE": - # image = image.convert("RGBA") - # width8 = ((width + 7) // 8) * 8 - # data = [0 for _ in range(height * width8 // 8)] - # for y in range(height): - # for x in range(width): - # if not image.getpixel((x, y))[3]: - # continue - # pos = x + y * width8 - # data[pos // 8] |= 0x80 >> (pos % 8) - - # rhs = [HexInt(x) for x in data] - # prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - # cg.new_Pvariable( - # config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] - # ) diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp index e40a452..848b636 100644 --- a/components/wordclock/wordclock.cpp +++ b/components/wordclock/wordclock.cpp @@ -4,302 +4,202 @@ namespace esphome { namespace wordclock { void Wordclock::setup() { - // this->time->update(); - - - // this->light_state->add_effects({this->randomTwinkle}); - // this->start_idle_animation(); -} - -void Wordclock::start_idle_animation() { - this->display->set_enabled(false); - // auto call = - this->light_state->turn_on().set_effect("random_twinkle").perform(); - // call.set_effect("random_twinkle"); - // call.perform(); -} - -void Wordclock::end_idle_animation() { - this->light_state->turn_off().perform(); - this->display->set_enabled(true); - - - // this->light->clear_effect_data(); - // this->display->get_light()->set_effect_active(false); - // auto call1 = this->light_state->turn_on(); - // call1.set_effect("None"); - // call1.perform(); - // this->light->all().set(Color(0xF0FF00)); - - // this->display->fill(Color(0)); - - // this->light_state->turn_off().perform(); - // call2.perform(); + this->valid_time = this->time->now().is_valid(); + if (!this->valid_time) { + this->start_idle_animation(); + } + // this->valid_time = true; } void Wordclock::update() { - - // esphome::addressable_light::AddressableLightDisplay it = *(this->display); - // ESP_LOGD("loop", "beep"); - // ESP_LOGD("loop", "time is now [%02i:%02i:%02i]", this->time->now().hour, this->time->now().minute, this->time->now().second); - // ESP_LOGE("wordclock.cpp", "display_ptr: 0x%x", it); - // ESP_LOGE("wordclock.cpp", "this: 0x%x", (this)); - // ESP_LOGE("wordclock.cpp", "this->display: 0x%x", (this->display)); - // ESP_LOGE("wordclock.cpp", "&this->display: 0x%x", &(this->display)); - // ESP_LOGE("loop", "time_ptr: %i", this->time); - // it.line(0,0,0,0, Color(0x00FF00)); - esphome::time::ESPTime time = this->time->now(); - this->find_hour(9); - this->find_minute(31); - if (time.is_valid() == false) { + if (!time.is_valid()) { if (this->valid_time) { - ESP_LOGD("loop", "time is not valid [%02i:%02i:%02i]", time.hour, time.minute, time.second); + ESP_LOGD("wordclock.cpp", "time is not valid [%02i:%02i:%02i]", time.hour, time.minute, time.second); this->start_idle_animation(); this->valid_time = false; return; } } - else { if (!this->valid_time) { + ESP_LOGD("wordclock.cpp", "time is valid [%02i:%02i:%02i]", time.hour, time.minute, time.second); this->end_idle_animation(); this->valid_time = true; - ESP_LOGD("wordclock.cpp", "time is now valid [%02i:%02i:%02i]", time.hour, time.minute, time.second); return; } - // for (uint8_t idx = 0;idx < ; idx++) { - - // } - - // std::vector *minute = this->find_minute(time.minute); - // std::vector *hour = this->find_hour(time.hour); - this->display->fill(Color(0x000000)); - + bool dirty = false; + int8_t minute = this->find_minute(time.minute); - int8_t hour = this->find_hour((time.hour + this->hour_offsets->at(minute)) % 24); + int8_t hour = this->find_hour((time.hour + this->minutes->at(minute).hour_offset) % 24); - for (uint16_t segment_idx : *this->static_segments){ - this->draw_segment(segment_idx); + if (hour != this->current_hour) { + this->current_hour = hour; + dirty = true; + } + if (minute != this->current_minute) { + this->current_minute = minute; + dirty = true; } - for (uint16_t segment_idx : this->minutes->at(minute)){ - this->draw_segment(segment_idx); + if (dirty) { + this->previous_segments->clear(); + for (uint16_t segment_idx : *this->current_segments) { + this->previous_segments->push_back(segment_idx); + } + // std::sort(this->previous_segments->begin(), this->previous_segments->end()); + + // std::sort(s.begin(), s.end(), [](int a, int b) + // { + // return a > b; + // }); + + this->current_segments->clear(); + for (uint16_t segment_idx : *this->static_segments) { + this->current_segments->push_back(segment_idx); + // this->draw_segment(segment_idx); + } + + for (uint16_t segment_idx : *(this->minutes->at(minute).segments)) { + this->current_segments->push_back(segment_idx); + // this->draw_segment(segment_idx); + } + + for (uint16_t segment_idx : *(this->hours->at(hour).segments)) { + this->current_segments->push_back(segment_idx); + // this->draw_segment(segment_idx); + } + std::sort(this->current_segments->begin(), this->current_segments->end()); + + this->find_difference(this->previous_segments, this->current_segments); + // ESP_LOGD("wordclock.cpp", "reset"); + this->on_transformer->reset(); + this->off_transformer->reset(); } - for (uint16_t segment_idx : this->hours->at(hour)){ - this->draw_segment(segment_idx); + // Color on = this->on_color * this->on_transformer->apply().value_or(255); + // Color off = this->off_color * this->off_transformer->apply().value_or(255); + uint8_t transition_progress = this->on_transformer->apply().value_or(255); + // uint8_t on_progress = this->on_transformer->apply().value_or(255); + // uint8_t off_progress = this->off_transformer->apply().value_or(255); + // ESP_LOGD("wordclock.cpp", "off progress [%d]", off_progress); + Color added_color = this->off_color.gradient(this->on_color, transition_progress); + Color removed_color = this->on_color.gradient(this->off_color, transition_progress); + // ESP_LOGD("wordclock.cpp", "transition progress [%d], added [0x%06x], removed [0x%06x]", transition_progress, added_color.raw_32, removed_color.raw_32); + for (uint16_t segment_idx : *this->added_segments) { + // ESP_LOGD("wordclock.cpp", "on [%d]", segment_idx); + this->draw_segment(segment_idx, added_color); + // this->draw_segment(segment_idx, on_progress); + } + for (uint16_t segment_idx : *this->removed_segments) { + // ESP_LOGD("wordclock.cpp", "off [%d]", segment_idx); + this->draw_segment(segment_idx, removed_color); + // this->draw_segment(segment_idx, off_progress); } - // this->draw_segment(0); - // this->draw_segment(1); - // this->draw_segment(4); - // this->draw_segment(10); - // this->draw_segment(19); - // this->light->range(0, 10).fade_to_white(100); - // this->display->get_light()->range(18, 35).set(Color(0xf0ff0f)); - - // SegmentCoords s = this->segments->at(time); - - - // ESP_LOGD("wordclock.cpp", "time is now [%02i:%02i:%02i]", time.hour, time.minute, time.second); - // ESP_LOGD("wordclock.cpp", "x1: %i, y1: %i, x2: %i, y2: %i", s.x1, s.y1, s.x2, s.y2); - - - // this->display->draw_pixel_at(0, 0, Color(0xFF0000)); - // this->display->draw_pixel_at(1, 0, Color(0x00FF00)); - // this->display->draw_pixel_at(2, 0, Color(0x0000FF)); - // if (time.second != second) { - // second = time.second; - // ESP_LOGD("loop", "time is now [%02i:%02i:%02i]", time.hour, time.minute, time.second); - // } - // if(time.hour != hour || time.minute != minute) { - // hour = time.hour; - // minute = time.minute; - // if (hour >= 0 && time.is_valid() == true){ - - // display_time(time.hour, time.minute, CRGB(red, green, blue)); - - // ESP_LOGE("loop", "Update Time: %i:%i Brightness: %i RGB: %i-%i-%i", time.hour, time.minute, brightness, red, green, blue); - // } - // } - // } - // if (!this->reading_ && !mode_funcs_.empty()) { - // this->reading_ = true; - // this->read_mode_(0); } } -void Wordclock::draw_segment(uint16_t segment_id) { - SegmentCoords s = this->segments->at(segment_id); - // ESP_LOGD("wordclock.cpp", "x1: %i, y1: %i, x2: %i, y2: %i", s.x1, s.y1, s.x2, s.y2); - this->display->line(s.x1, s.y1, s.x2, s.y2, esphome::Color(0xFFFFFF)); +void Wordclock::find_difference(std::vector *a_vec, std::vector *b_vec) { + // for (uint16_t segment_idx : a) { + // this->current_segments->push_back(segment_idx); + // } + this->added_segments->clear(); + this->removed_segments->clear(); + + auto a = a_vec->begin(); + auto b = b_vec->begin(); + + while (!(a == a_vec->end() && b == b_vec->end())) { + if (a == a_vec->end()) { this->added_segments-> push_back(*b); b++; } + else if (b == b_vec->end()) { this->removed_segments->push_back(*a); a++; } + else if (*a > *b) { this->added_segments-> push_back(*b); b++; } + else if (*a < *b) { this->removed_segments->push_back(*a); a++; } + else if (*a == *b) { a++; b++; } + } } -// void Wordclock::set_writer(display_writer_t &&writer) { -// this->writer_ = writer; + +int8_t Wordclock::find_hour(uint8_t target_value) { + std::vector *elements = this->hours; + for (int i = 0; i < elements->size(); i++) { + if (elements->at(i).hour == target_value) { + return i; + } else if (elements->at(i).hour > target_value) { + return i - 1; + } + } + return elements->size() - 1; + // uint16_t last_defined_index = -1; + // for (int i = 0; i < elements->size(); i++) { + // if (elements->at(i).size()) { + // last_defined_index = i; + // } + // if (i >= target_value) break; + // } + // return last_defined_index; + +} + +int8_t Wordclock::find_minute(uint8_t target_value) { + std::vector *elements = this->minutes; + for (int i = 0; i < elements->size(); i++) { + if (elements->at(i).minute == target_value) { + return i; + } else if (elements->at(i).minute > target_value) { + return i - 1; + } + } + return elements->size() - 1; +} + +void Wordclock::setup_transitions(uint32_t milliseconds) { + this->on_transformer->setup(0, this->brightness, milliseconds); + this->off_transformer->setup(this->brightness, 0, milliseconds); +} + + + +// Wordclock::Wordclock() : PollingComponent(1000) { // } -void Wordclock::add_segment(SegmentCoords segment) { - // if (!this->segments) { - - // } +Wordclock::Wordclock( + esphome::time::RealTimeClock *time, + esphome::addressable_light::AddressableLightDisplay *display, + esphome::light::AddressableLightState *light_state) { - this->segments->push_back(segment); - // this->writer_ = writer; + this->time = time; + this->display = display; + this->light_state = light_state; + + this->light_state->set_default_transition_length(1); + + this->minutes = new std::vector(); + this->hours = new std::vector(); + + this->segments = new std::vector(); + this->static_segments = new std::vector(); + + this->previous_segments = new std::vector(); + this->current_segments = new std::vector(); + + this->added_segments = new std::vector(); + this->removed_segments = new std::vector(); + + this->on_transformer = new BrightnessTransitionTransformer(); + // this->on_transformer->setup(0, this->brightness, 700); + this->off_transformer = new BrightnessTransitionTransformer(); + // this->off_transformer->setup(this->brightness, 0, 700); + +// wordclock_wordclock->add_segment(wordclock::SegmentCoords{ +// .x1 = 0, +// .x2 = 4, +// .y1 = 8, +// .y2 = 8, +// }); + + // this->hour_offsets = new std::vector(60, 0); + // this->minutes = new std::vector>(60, std::vector()); + // this->hours = new std::vector>(24, std::vector()); } - -// std::vector * Wordclock::find_hour(uint8_t hour) { -// std::vector *empty_vector; // = new std::vector(); -// uint16_t last_defined_hour = -1; -// for (int i = 0; i < this->hours->size(); i++) { -// if (this->hours->at(i).size()) { -// if (hour == i) { -// return &(this->hours->at(i)); -// } -// else { -// last_defined_hour = i; -// } -// } -// else { -// empty_vector = &(this->hours->at(i)); -// if (hour == i) { -// if (last_defined_hour == -1) return empty_vector; -// return &(this->hours->at(last_defined_hour)); -// } -// } -// } -// return empty_vector; -// } - -int8_t Wordclock::find_hour(uint8_t hour) { - uint16_t last_defined_hour = -1; - for (int i = 0; i < this->hours->size(); i++) { - if (this->hours->at(i).size()) { - last_defined_hour = i; - if (hour == i) { - return last_defined_hour; - } - } - if (hour == i) break; - } - return last_defined_hour; -} - -int8_t Wordclock::find_minute(uint8_t minute) { - uint16_t last_defined_minute = -1; - for (int i = 0; i < this->minutes->size(); i++) { - if (this->minutes->at(i).size()) { - last_defined_minute = i; - if (minute == i) { - return last_defined_minute; - } - } - if (minute == i) break; - } - return last_defined_minute; -} -// std::vector * Wordclock::find_minute(uint8_t minute) { -// std::vector *empty_vector;// = new std::vector(); -// uint16_t last_defined_minute = -1; -// for (int i = 0; i < this->minutes->size(); i++) { -// if (this->minutes->at(i).size()) { -// if (minute == i) { -// return &(this->minutes->at(i)); -// } -// else { -// last_defined_minute = i; -// } -// } -// else { -// empty_vector = &(this->minutes->at(i)); -// if (minute == i) { -// if (last_defined_minute == -1) return empty_vector; -// return &(this->minutes->at(last_defined_minute)); -// } -// } -// } -// return empty_vector; -// } - - -void Wordclock::add_hour_offset(uint8_t index, int8_t offset) { - (*this->hour_offsets)[index] = offset; -} - -void Wordclock::add_hour(uint8_t hour, std::vector *segments) { - for (uint16_t i : *segments){ - this->hours->at(hour).push_back(i); - } -} - -void Wordclock::add_minute(uint8_t minute, std::vector *segments) { - for (uint16_t i : *segments){ - this->minutes->at(minute).push_back(i); - } -} - -void Wordclock::add_static(uint16_t segment_id) { - this->static_segments->push_back(segment_id); -} - -// uint16_t ** - -// Wordclock::Wordclock(std::vector *minutes, std::vector *hours, SegmentCoords *segments) -// Wordclock::Wordclock(uint16_t **minutes, uint16_t **hours, SegmentCoords *segments) -// : PollingComponent(1000) { -// // this->minutes = minutes; -// // this->hours = hours; -// // this->segments = segments; -// // std::vector minutes[60]; -// // std::vector hours[24]; -// } - Wordclock::Wordclock() - : PollingComponent(1000) { - // this->minutes = std::vector>(); - // // for (int i=0; i<60; i++) this->minutes.push_back(std::vector()); - - // this->hours = std::vector>(); - // // for (int i=0; i<24; i++) this->minutes.push_back(std::vector()); - - // this->segments = std::vector(); - } - - Wordclock::Wordclock(esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, esphome::light::AddressableLightState *light_state) - : PollingComponent(16) { - // ESP_LOGE("wordclock.cpp", "this: 0x%x", (this)); - // ESP_LOGE("wordclock.cpp", "display: 0x%x", (display)); - // ESP_LOGE("wordclock.cpp", "&display: 0x%x", (&display)); - - this->time = time; - this->display = display; - this->light = this->display->get_light(); - this->light_state = light_state; - // light::AddressableLight *light = this->display->get_light(); - - // light::AddressableRainbowLightEffect *light_addressablerainbowlighteffect; - // light::AddressableTwinkleEffect *light_addressabletwinkleeffect; - // light::AddressableRandomTwinkleEffect *light_addressablerandomtwinkleeffect; - - // this->rainbow = new light::AddressableRainbowLightEffect("rainbow"); - // this->rainbow->set_speed(10); - // this->rainbow->set_width(50); - // this->twinkle = new light::AddressableTwinkleEffect("twinkle"); - // this->twinkle->set_twinkle_probability(0.05f); - // this->twinkle->set_progress_interval(4); - // this->randomTwinkle = new light::AddressableRandomTwinkleEffect("random_twinkle"); - // this->randomTwinkle->set_twinkle_probability(0.05f); - // this->randomTwinkle->set_progress_interval(32); - - this->hour_offsets = new std::vector(60, 0); - - this->minutes = new std::vector>(60, std::vector()); - // for (int i=0; i<60; i++) this->minutes->push_back(std::vector()); - - this->hours = new std::vector>(24, std::vector()); - // for (int i=0; i<24; i++) this->hours->push_back(std::vector()); - - this->segments = new std::vector(); - this->static_segments = new std::vector(); - } } // namespace wordclock } // namespace esphome diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index 42b98df..226efc0 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -1,227 +1,218 @@ #pragma once - #include "esphome.h" -// By now only loosely based on https://github.com/leinich/ha-wordclock-esphome - -// esphome dependencies: -// needs: esphome time --> id: current_time -// needs: esphome fastled --> id: fastledlight - -// #ifdef USE_ESP32 namespace esphome { namespace wordclock { -///// Word Table ///// -// .line(0,0,1,0, color); -// .line(2,0,3,0, color); -// int WORD_IT_IS[5][2] = {{4,0}, {0,0}, {0,1}, {0,3}, {0,4}}; - -// // .line(2,1,8,1, color); -// int WORD_QUARTER[8][2] = {{7,0}, {1,2}, {1,3}, {1,4}, {1,5}, {1,6}, {1,7}, {1,8}}; -// // .line(0,2, 5,2, color); -// int WORD_TWENTY[7][2] = {{6,0}, {2,0}, {2,1}, {2,2}, {2,3}, {2,4}, {2,5}}; -// // .line(6,2, 9,2, color); -// int WORD_FIVE_MINUTES[5][2] = {{4,0}, {2,6}, {2,7}, {2,8}, {2,9}}; -// // .line(0,3, 3,3, color); -// int WORD_HALF[5][2] = {{4,0}, {3,0}, {3,1}, {3,2}, {3,3}}; -// // .line(5,3, 7,3, color); -// int WORD_TEN_MINUTES[4][2] = {{3,0}, {3,5}, {3,6}, {3,7}}; -// // .line(9,3, 10,3, color); -// int WORD_TO[3][2] = {{2,0}, {3,9}, {3,10}}; -// // .line(0,4, 3,4, color); -// int WORD_PAST[5][2] = {{4,0}, {4,0}, {4,1}, {4,2}, {4,3}}; - -// int WORD_NINE[5][2] = {{4,0}, {4,7}, {4,8}, {4,9}, {4,10}}; -// int WORD_ONE[4][2] = {{3,0}, {5,0}, {5,1}, {5,2}}; -// int WORD_SIX[4][2] = {{3,0}, {5,3}, {5,4}, {5,5}}; -// int WORD_THREE[6][2] = {{5,0}, {5,6}, {5,7}, {5,8}, {5,9}, {5,10}}; -// int WORD_FOUR[5][2] = {{4,0}, {6,0}, {6,1}, {6,2}, {6,3}}; -// int WORD_FIVE[5][2] = {{4,0}, {6,4}, {6,5}, {6,6}, {6,7}}; -// int WORD_TWO[4][2] = {{3,0}, {6,8}, {6,9}, {6,10}}; -// int WORD_EIGHT[6][2] = {{5,0}, {7,0}, {7,1}, {7,2}, {7,3}, {7,4}}; -// int WORD_ELEVEN[7][2] = {{6,0}, {7,5}, {7,6}, {7,7}, {7,8}, {7,9}, {7,10}}; -// int WORD_SEVEN[6][2] = {{5,0}, {8,0}, {8,1}, {8,2}, {8,3}, {8,4}}; -// int WORD_TWELFE[7][2] = {{6,0}, {8,5}, {8,6}, {8,7}, {8,8}, {8,9}, {8,10}}; -// int WORD_TEN[4][2] = {{3,0}, {9,0}, {9,1}, {9,2}}; -// int WORD_OCLOCK[7][2] = {{6,0}, {9,5}, {9,6}, {9,7}, {9,8}, {9,9}, {9,10}}; - - - -// using display_writer_t = std::function; +struct Minute { + uint8_t minute; + uint8_t hour_offset; + std::vector *segments; +}; +struct Hour { + uint8_t hour; + std::vector *segments; +}; struct SegmentCoords { uint16_t x1; - uint16_t x2; uint16_t y1; + uint16_t x2; uint16_t y2; }; +class BrightnessTransformer { +public: + virtual ~BrightnessTransformer() = default; + + void setup(const uint8_t start_values, const uint8_t target_values, uint32_t length) { + this->start_time_ = millis(); + this->length_ = length; + this->start_values_ = start_values; + this->target_values_ = target_values; + this->start(); + } + + /// Indicates whether this transformation is finished. + virtual bool is_finished() { return this->get_progress_() >= 1.0f; } + + /// This will be called before the transition is started. + virtual void start() {} + + /// This will be called while the transformer is active to apply the transition to the light. Can either write to the + /// light directly, or return LightColorValues that will be applied. + virtual optional apply() = 0; + + /// This will be called after transition is finished. + virtual void stop() {} + + void reset() { + this->start_time_ = millis(); + } + + const uint8_t &get_start_values() const { return this->start_values_; } + const uint8_t &get_target_values() const { return this->target_values_; } + +protected: + /// The progress of this transition, on a scale of 0 to 1. + float get_progress_() { + uint32_t now = esphome::millis(); + if (now < this->start_time_) + return 0.0f; + if (now >= this->start_time_ + this->length_) + return 1.0f; + return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); + } + + uint32_t start_time_; + uint32_t length_; + uint8_t start_values_; + uint8_t target_values_; +}; + +class BrightnessTransitionTransformer : public BrightnessTransformer { +public: + optional apply() override { + float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_()); + return esphome::lerp(v, this->start_values_, this->target_values_); + } +protected: + // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like + // transition from 0 to 1 on x = [0, 1] + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } +}; + class Wordclock : public esphome::PollingComponent { - public: - Wordclock(); - Wordclock(esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, esphome::light::AddressableLightState *light_state); - void add_segment(SegmentCoords segment); - void add_hour(uint8_t index, std::vector *segments); - void add_minute(uint8_t index, std::vector *segments); - void add_static(uint16_t segment_id); - void add_hour_offset(uint8_t index, int8_t offset); - void setup(); - void update(); - // void setup() override { - // } +public: + // Wordclock(); + Wordclock( + esphome::time::RealTimeClock *time, + esphome::addressable_light::AddressableLightDisplay *display, + esphome::light::AddressableLightState *light_state + ); + + void setup(); + void update(); + // void setup_color(uint8_t brightness, Color on_color, Color off_color); + void set_brightness(uint8_t brightness) { +this->brightness = brightness; + } + void set_on_color(Color on_color) { + this->on_color = on_color; + } + void set_off_color(Color off_color) { + this->off_color = off_color; + } + void setup_transitions(uint32_t milliseconds); - // void set_writer(display_writer_t &&writer); + void add_segment(SegmentCoords segment) { + this->segments->push_back(segment); + } - // void display_word(const int word[][2], const CRGB& c) { - // for (int i=1; i < word[0][0] + 1; i++) { - // leds[map_coords_to_strip(word[i][0], word[i][1])].setRGB(c.r, c.g, c.b); - // } - // } + void add_static(uint16_t segment_id) { + this->static_segments->push_back(segment_id); + } - // void display_minutes(int minutes, const CRGB& color) { - // int five_minute_chunk = minutes / 5; + void add_hour(uint8_t hour, std::vector *segments_ptr) { + this->hours->push_back(Hour{ + .hour = hour, + .segments = segments_ptr, + }); + } - // switch (five_minute_chunk) - // { - // case 0: // sharp - // display_word(WORD_OCLOCK, color); ESP_LOGD("minute", "oclock "); break; - // case 1: // five past - // display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five past "); break; - // case 2: // ten past - // display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten past "); break; - // case 3: // quarter past - // display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter past "); break; - // case 4: // twenty past - // display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty past "); break; - // case 5: // twenty five past - // display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color); - // ESP_LOGD("minute", "twenty five past "); break; - // case 6: // half past - // display_word(WORD_HALF, color); ESP_LOGD("minute", "half past "); break; - // case 7: // twenty five to - // display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color); - // ESP_LOGD("minute", "twenty five to "); break; - // case 8: // twenty to - // display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty to "); break; - // case 9: // quarter to - // display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter to "); break; - // case 10: // ten to - // display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten to "); break; - // case 11: // five to - // display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five to "); break; - // default: - // break; - // } - // if (five_minute_chunk > 6) { - // display_word(WORD_TO, color); - // } else if (five_minute_chunk > 0) { - // display_word(WORD_PAST, color); - // } - // } + void add_minute(uint8_t minute, uint8_t hour_offset, std::vector *segments_ptr) { + this->minutes->push_back(Minute{ + .minute = minute, + .hour_offset = hour_offset, + .segments = segments_ptr, + }); + } - // void display_hour(int hour, int minutes, const CRGB& color) { - // int five_minute_chunk = minutes / 5; - // if (five_minute_chunk > 6) { - // hour += 1; - // } + // void add_hour_segment(uint8_t index, uint16_t segment_id) { + // this->hours->at(index).segments->push_back(segment_id); + // } + // void add_minute_segment(uint8_t index, uint16_t segment_id) { + // this->minutes->at(index).segments->push_back(segment_id); + // } + // void add_hour(uint8_t index, std::vector *segments); + // void add_minute(uint8_t index, std::vector *segments); + // void add_hour_offset(uint8_t index, int8_t offset) { + // this->minutes->at(index).hour_offset = offset; + // } +protected: + // ESPHome Components + esphome::time::RealTimeClock *time; + esphome::light::AddressableLightState *light_state; + esphome::addressable_light::AddressableLightDisplay *display; - // switch (hour % 12) - // { - // case 0: // twelve - // display_word(WORD_TWELFE, color); ESP_LOGD("hour", "twelve "); break; - // case 1: // one - // display_word(WORD_ONE, color); ESP_LOGD("hour", "one "); break; - // case 2: // two - // display_word(WORD_TWO, color); ESP_LOGD("hour", "two "); break; - // case 3: // three - // display_word(WORD_THREE, color); ESP_LOGD("hour", "three "); break; - // case 4: // four - // display_word(WORD_FOUR, color); ESP_LOGD("hour", "four "); break; - // case 5: // five - // display_word(WORD_FIVE, color); ESP_LOGD("hour", "five "); break; - // case 6: // six - // display_word(WORD_SIX, color); ESP_LOGD("hour", "six "); break; - // case 7: // seven - // display_word(WORD_SEVEN, color); ESP_LOGD("hour", "seven "); break; - // case 8: // eight - // display_word(WORD_EIGHT, color); ESP_LOGD("hour", "eight "); break; - // case 9: // nine - // display_word(WORD_NINE, color); ESP_LOGD("hour", "nine "); break; - // case 10: // ten - // display_word(WORD_TEN, color); ESP_LOGD("hour", "ten "); break; - // case 11: // eleven - // display_word(WORD_ELEVEN, color); ESP_LOGD("hour", "eleven "); break; - // default: - // break; - // } - // } + // Time related state + bool valid_time{false}; + int8_t current_hour{-1}; + int8_t current_minute{-1}; - // void display_time(int hour, int minutes, const CRGB& color) { - // // clear_all_leds(); - // display_word(WORD_IT_IS, color); - // display_hour(hour, minutes, color); - // display_minutes(minutes, color); - // // FastLED.show(); - // } + // Segments Configuration + std::vector *segments; + std::vector *static_segments; + std::vector *minutes; + std::vector *hours; + + BrightnessTransitionTransformer *off_transformer; + BrightnessTransitionTransformer *on_transformer; + + std::vector *added_segments; + std::vector *removed_segments; + std::vector *current_segments; + std::vector *previous_segments; + + // Color + // Color get_on_color() { + // return this->on_color * this->brightness; + // } + // Color get_off_color() { + // return this->off_color * this->brightness; + // } + Color on_color {Color(0xFFFFFF)}; + Color off_color {Color(0x000000)}; + uint8_t brightness{255}; + + // Utils + int8_t find_hour(uint8_t target_value); + int8_t find_minute(uint8_t target_value); + void find_difference(std::vector *a_vec, std::vector *b_vec); + // void draw_segment(uint16_t segment_id) { + // this->draw_segment(segment_id, this->brightness); + // } + void draw_segment(uint16_t segment_id, uint8_t brightness) { + this->draw_segment(segment_id, this->on_color * brightness); + } + void draw_segment(uint16_t segment_id, Color color) { + SegmentCoords s = this->segments->at(segment_id); + // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32); + this->display->line(s.x1, s.y1, s.x2, s.y2, color); + } + void start_idle_animation() { + this->display->fill(this->off_color); + this->display->set_enabled(false); + this->light_state->turn_on().set_effect("random_twinkle").perform(); + } + + void end_idle_animation() { + this->light_state->turn_off().perform(); + this->display->set_enabled(true); + this->display->fill(this->off_color); + this->previous_segments->clear(); + this->current_segments->clear(); + } - - // void loop() override { - // // auto time = id(current_time).now(); - // // https://www.esphome.io/api/classesphome_1_1light_1_1_light_color_values.html LightColorValues Class - // // auto fastledlight2 = id(fastledlight).current_values; - // //convert float 0.0 till 1.0 into int 0 till 255 - // red = (int) (fastledlight2.get_red() * 125); - // green = (int) (fastledlight2.get_green() * 125); - // blue = (int) (fastledlight2.get_blue() * 125); - // brightness = 0; - // //check if light is on and set brightness - // if (fastledlight2.get_state() > 0 ) { - // brightness = (int) (fastledlight2.get_brightness()*125); - // } else { - // // ESP_LOGD("loop", "fastledlight state off - b: %i rgb %i %i %i", brightness, red, green, blue); delay(100); - // } - - // FastLED.setBrightness(brightness); - // FastLED.show(); - // //check if valid time. Blink red,green,blue until valid time is present - - // } - protected: - void draw_segment(uint16_t segment_id); - int8_t find_hour(uint8_t hour); - int8_t find_minute(uint8_t minute); - // std::vector * find_hour(uint8_t hour); - // std::vector * find_minute(uint8_t minute); - std::vector> *minutes; - std::vector> *hours; - std::vector *static_segments; - std::vector *hour_offsets; - - void start_idle_animation(); - void end_idle_animation(); - - // uint16_t **minutes; - // uint16_t **hours; - - bool valid_time{true}; - std::vector *segments; - esphome::time::RealTimeClock *time; - esphome::light::AddressableLight *light; - esphome::light::AddressableLightState *light_state; - esphome::addressable_light::AddressableLightDisplay *display; - light::AddressableRainbowLightEffect *rainbow; - light::AddressableTwinkleEffect *twinkle; - light::AddressableRandomTwinkleEffect *randomTwinkle; - + // std::vector> *minutes; + // std::vector> *hours; + // std::vector *hour_offsets; }; - - } // namespace wordclock } // namespace esphome diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5793ccb --- /dev/null +++ b/flake.lock @@ -0,0 +1,64 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1669833724, + "narHash": "sha256-/HEZNyGbnQecrgJnfE8d0WC5c1xuPSD2LUpB6YXlg4c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4d2b37a84fad1091b9de401eb450aae66f1a741e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "systems": "systems", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1680978846, + "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", + "owner": "nix-systems", + "repo": "x86_64-linux", + "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "x86_64-linux", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/image_test_3leds.yaml b/susannes_wordclock.yaml similarity index 61% rename from image_test_3leds.yaml rename to susannes_wordclock.yaml index f9d53b1..e47a2d5 100644 --- a/image_test_3leds.yaml +++ b/susannes_wordclock.yaml @@ -1,13 +1,5 @@ esphome: - name: "display-thing" - # on_boot: - # priority: 600 - # # ... - # then: - # - light.turn_on: - # id: neopixel_strip_1 - # brightness: 100% - # effect: random_twinkle + name: "wordclock" esp8266: board: d1_mini @@ -19,7 +11,6 @@ external_components: type: local path: components components: [ wordclock ] - # components: [ wordclock, addressable_light ] wifi: ssid: !secret wifi_ssid @@ -42,22 +33,12 @@ ota: password: "wordclock" logger: - # esp8266_store_log_strings_in_flash: false -# web_server: -# port: 80 time: - platform: sntp id: current_time timezone: !secret timezone - on_time_sync: - then: - - logger.log: "synced system clock" - # - light.turn_on: - # id: neopixel_strip_1 - # effect: "None" - # - light.turn_off: - # id: neopixel_strip_1 + light: - name: NeoPixel Strip 1 id: neopixel_strip_1 @@ -67,28 +48,15 @@ light: pin: GPIO3 num_leds: 198 restore_mode: ALWAYS_OFF - # default_transition_length: 0.7s - # color_correct: [100%, 100%, 100%] + # default_transition_length: 0.0s + # default_transition_length: 0.1s method: type: esp8266_dma effects: - # - random: - # - pulse: - # - strobe: - # - flicker: - - addressable_rainbow: - name: "rainbow" - # - addressable_color_wipe: - # - addressable_scan: - - addressable_twinkle: - name: "twinkle" - addressable_random_twinkle: name: "random_twinkle" - # - addressable_fireworks: - # - addressable_flicker: - # - wled: - + display: - platform: addressable_light @@ -104,20 +72,35 @@ display: int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16}; if (x > 9) return -1; if (y % 2 == 0) { - return (y * 18 + mapping_even[x]); // mapping_even[y]; + return (y * 18 + mapping_even[x]); } else { - return (y * 18 + mapping_odd[x]); // mapping_odd[y]; + return (y * 18 + mapping_odd[x]); } - # lambda: |- - # //ESP_LOGE("lambda", "display_ptr: %i", &it); - # //it.fill(Color(0xff00ff)); - # //it.line(0,0,0,0, Color(0x00FF00)); +# color: +# - id: col_on +# hex: "CC0000" +# - id: col_off +# hex: "005500" + +color: + - id: col_on + red: 90% + green: 50% + blue: 0% + - id: col_off + red: 20% + green: 20% + blue: 30% wordclock: time_id: current_time display_id: led_matrix_display addressable_light_id: neopixel_strip_1 + brightness: 100% + color_on: col_on + color_off: col_off + update_interval: 16ms static_segments: ["IT", "IS"] segments: - {name: "IT", line: {x1: 0, x2: 1, y1: 0}} @@ -182,44 +165,5 @@ wordclock: - {hour: 21, segments: "NINE"} - {hour: 22, segments: "TEN"} - {hour: 23, segments: "ELEVEN"} - # hours: - # - {hour: 0, segments: ["TWELVE", "AM"]} - # - {hour: 1, segments: ["ONE", "AM"]} - # - {hour: 2, segments: ["TWO", "AM"]} - # - {hour: 3, segments: ["THREE", "AM"]} - # - {hour: 4, segments: ["FOUR", "AM"]} - # - {hour: 5, segments: ["FIVE", "AM"]} - # - {hour: 6, segments: ["SIX", "AM"]} - # - {hour: 7, segments: ["SEVEN", "AM"]} - # - {hour: 8, segments: ["EIGHT", "AM"]} - # - {hour: 9, segments: ["NINE", "AM"]} - # - {hour: 10, segments: ["TEN", "AM"]} - # - {hour: 11, segments: ["ELEVEN", "AM"]} - # - {hour: 12, segments: ["TWELVE", "PM"]} - # - {hour: 13, segments: ["ONE", "PM"]} - # - {hour: 14, segments: ["TWO", "PM"]} - # - {hour: 15, segments: ["THREE", "PM"]} - # - {hour: 16, segments: ["FOUR", "PM"]} - # - {hour: 17, segments: ["FIVE", "PM"]} - # - {hour: 18, segments: ["SIX", "PM"]} - # - {hour: 19, segments: ["SEVEN", "PM"]} - # - {hour: 20, segments: ["EIGHT", "PM"]} - # - {hour: 21, segments: ["NINE", "PM"]} - # - {hour: 22, segments: ["TEN", "PM"]} - # - {hour: 23, segments: ["ELEVEN", "PM"]} - # minutes: - # - {minute: 0, segments: ["A"]} - # - {minute: 5, segments: ["B"]} - # - {minute: 10, segments: ["A"]} - # - {minute: 15, segments: ["B"]} - # - {minute: 20, segments: ["A"]} - # - {minute: 25, segments: ["B"]} - # - {minute: 30, segments: ["A"]} - # - {minute: 35, segments: ["B"]} - # - {minute: 40, segments: ["A"]} - # - {minute: 45, segments: ["B"]} - # - {minute: 50, segments: ["A"]} - # - {minute: 55, segments: ["B"]} - # hours: - # - {hour: 0, segments: ["C"]} + \ No newline at end of file From b712b90be1b0774b0843ec0d914da4ae8ff45448 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Fri, 12 May 2023 17:16:24 +0200 Subject: [PATCH 05/16] feat: add rainbow effect for segments --- components/wordclock/wordclock.cpp | 39 ++++++++++++++---- components/wordclock/wordclock.h | 65 +++++++++++++++++++++++++++--- susannes_wordclock.yaml | 14 ++----- 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp index 848b636..ee41e18 100644 --- a/components/wordclock/wordclock.cpp +++ b/components/wordclock/wordclock.cpp @@ -80,23 +80,31 @@ void Wordclock::update() { // Color on = this->on_color * this->on_transformer->apply().value_or(255); // Color off = this->off_color * this->off_transformer->apply().value_or(255); + this->current_position = 0; uint8_t transition_progress = this->on_transformer->apply().value_or(255); // uint8_t on_progress = this->on_transformer->apply().value_or(255); // uint8_t off_progress = this->off_transformer->apply().value_or(255); // ESP_LOGD("wordclock.cpp", "off progress [%d]", off_progress); - Color added_color = this->off_color.gradient(this->on_color, transition_progress); + // Color added_color = this->off_color.gradient(this->on_color, transition_progress); Color removed_color = this->on_color.gradient(this->off_color, transition_progress); // ESP_LOGD("wordclock.cpp", "transition progress [%d], added [0x%06x], removed [0x%06x]", transition_progress, added_color.raw_32, removed_color.raw_32); - for (uint16_t segment_idx : *this->added_segments) { - // ESP_LOGD("wordclock.cpp", "on [%d]", segment_idx); - this->draw_segment(segment_idx, added_color); - // this->draw_segment(segment_idx, on_progress); - } + for (uint16_t segment_idx : *this->removed_segments) { // ESP_LOGD("wordclock.cpp", "off [%d]", segment_idx); - this->draw_segment(segment_idx, removed_color); + // this->disable_segment(segment_idx, this->off_color, transition_progress); + this->disable_segment_effect(segment_idx, this->off_color, transition_progress); + // this->disable_segment(segment_idx, removed_color); // this->draw_segment(segment_idx, off_progress); } + for (uint16_t segment_idx : *this->added_segments) { + // ESP_LOGD("wordclock.cpp", "on [%d]", segment_idx); + // this->segment_effect_base(segment_idx, this->off_color, transition_progress); + this->enable_segment_effect(segment_idx, this->off_color, transition_progress); + // this->draw_segment(segment_idx, on_progress); + } + for (uint16_t segment_idx : *this->staying_segments) { + this->apply_segment_effect(segment_idx); + } } } @@ -106,6 +114,8 @@ void Wordclock::find_difference(std::vector *a_vec, std::vectoradded_segments->clear(); this->removed_segments->clear(); + this->staying_segments->clear(); + auto a = a_vec->begin(); auto b = b_vec->begin(); @@ -115,10 +125,22 @@ void Wordclock::find_difference(std::vector *a_vec, std::vectorend()) { this->removed_segments->push_back(*a); a++; } else if (*a > *b) { this->added_segments-> push_back(*b); b++; } else if (*a < *b) { this->removed_segments->push_back(*a); a++; } - else if (*a == *b) { a++; b++; } + else if (*a == *b) { this->staying_segments->push_back(*a); a++; b++; } } } +Color Wordclock::get_next_color(uint32_t position, const Color ¤t_color) { + uint32_t speed_ = 3; + uint16_t width_ = 100; + // uint16_t width_ = 50; + esphome::light::ESPHSVColor hsv; + hsv.value = 255; + hsv.saturation = 240; + hsv.hue = ((millis() * speed_ + (position * (0xFFFF / width_))) % 0xFFFF) >> 8; + // hsv.hue = hue >> 8; + return hsv.to_rgb(); +} + int8_t Wordclock::find_hour(uint8_t target_value) { std::vector *elements = this->hours; for (int i = 0; i < elements->size(); i++) { @@ -184,6 +206,7 @@ Wordclock::Wordclock( this->added_segments = new std::vector(); this->removed_segments = new std::vector(); + this->staying_segments = new std::vector(); this->on_transformer = new BrightnessTransitionTransformer(); // this->on_transformer->setup(0, this->brightness, 700); diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index 226efc0..6104dfb 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -164,9 +164,13 @@ protected: std::vector *added_segments; std::vector *removed_segments; + std::vector *staying_segments; + std::vector *current_segments; std::vector *previous_segments; + uint32_t current_position{0}; + Color get_next_color(uint32_t position, const Color ¤t_color); // Color // Color get_on_color() { // return this->on_color * this->brightness; @@ -185,14 +189,63 @@ protected: // void draw_segment(uint16_t segment_id) { // this->draw_segment(segment_id, this->brightness); // } - void draw_segment(uint16_t segment_id, uint8_t brightness) { - this->draw_segment(segment_id, this->on_color * brightness); - } - void draw_segment(uint16_t segment_id, Color color) { - SegmentCoords s = this->segments->at(segment_id); + // void draw_segment(uint16_t segment_id, uint8_t brightness) { + // this->draw_segment(segment_id, this->on_color * brightness); + // } + + // void draw_segment(uint16_t segment_id, Color color) { + // SegmentCoords s = this->segments->at(segment_id); + // // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32); + // this->display->line(s.x1, s.y1, s.x2, s.y2, color); + // } + + void segment_effect_base(uint16_t segment_id, bool to_effect, Color base_color, uint8_t transition_progress) { // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32); - this->display->line(s.x1, s.y1, s.x2, s.y2, color); + SegmentCoords s = this->segments->at(segment_id); + int x1 = s.x1; int y1 = s.y1; int x2 = s.x2; int y2 = s.y2; + Color color_to_draw; + Color effect_color; + + const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; + const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; + int32_t err = dx + dy; + while (true) { + effect_color = this->get_next_color(x1 + y1, Color(0)); + if (to_effect) { + color_to_draw = base_color.gradient(effect_color, transition_progress); + } else { + color_to_draw = effect_color.gradient(base_color, transition_progress); + // c = base_color.gradient(this->get_next_color(x1 + y1, Color(0)), transition_progress); + } + + this->display->draw_pixel_at(x1, y1, color_to_draw); + // this->display->draw_pixel_at(x1, y1, this->get_next_color(this->current_position++, Color(0))); + if (x1 == x2 && y1 == y2) break; + int32_t e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x1 += sx; + } + if (e2 <= dx) { + err += dx; + y1 += sy; + } + } } + + void enable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) { + segment_effect_base(segment_id, true, off_color, transition_progress); + } + void disable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) { + segment_effect_base(segment_id, false, off_color, transition_progress); + } + void apply_segment_effect(uint16_t segment_id) { + segment_effect_base(segment_id, true, Color(0), 255); + } + // void disable_segment(uint16_t segment_id, Color off_color, uint8_t transition_progress) { + // SegmentCoords s = this->segments->at(segment_id); + // this->display->line(s.x1, s.y1, s.x2, s.y2, color); + // } void start_idle_animation() { this->display->fill(this->off_color); this->display->set_enabled(false); diff --git a/susannes_wordclock.yaml b/susannes_wordclock.yaml index e47a2d5..d947563 100644 --- a/susannes_wordclock.yaml +++ b/susannes_wordclock.yaml @@ -48,8 +48,6 @@ light: pin: GPIO3 num_leds: 198 restore_mode: ALWAYS_OFF - # default_transition_length: 0.0s - # default_transition_length: 0.1s method: type: esp8266_dma @@ -77,21 +75,15 @@ display: return (y * 18 + mapping_odd[x]); } -# color: -# - id: col_on -# hex: "CC0000" -# - id: col_off -# hex: "005500" - color: - id: col_on red: 90% green: 50% blue: 0% - id: col_off - red: 20% - green: 20% - blue: 30% + red: 0% + green: 0% + blue: 0% wordclock: time_id: current_time From 70458d4ed92d178f705eb5f5be457d2771f23424 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Fri, 12 May 2023 17:18:15 +0200 Subject: [PATCH 06/16] chore: add gitignore rule for ignored folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 08f0921..bcce0d2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.pio /.vscode __pycache__ +/_ignored From 519b36bfedc06b47325c9f44ceaef5179297af70 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Fri, 12 May 2023 17:37:21 +0200 Subject: [PATCH 07/16] chore: cleanup and mild refactoring --- components/wordclock/__init__.py | 52 +---------------- components/wordclock/wordclock.cpp | 74 ++++--------------------- components/wordclock/wordclock.h | 89 +++++------------------------- 3 files changed, 26 insertions(+), 189 deletions(-) diff --git a/components/wordclock/__init__.py b/components/wordclock/__init__.py index 242f2b6..449526b 100644 --- a/components/wordclock/__init__.py +++ b/components/wordclock/__init__.py @@ -97,17 +97,12 @@ WORDCLOCK_SCHEMA = cv.Schema( cv.Required(CONF_SEGMENTS): cv.ensure_list(WORDCLOCK_SEGMENT_SCHEMA), cv.Required(CONF_MINUTES): cv.ensure_list(WORDCLOCK_MINUTE_SCHEMA), cv.Required(CONF_HOURS): cv.ensure_list(WORDCLOCK_HOUR_SCHEMA), - # cv.Optional( - # CONF_FADE_LENGTH, default="700ms" - # ): cv., cv.Optional( CONF_FADE_LENGTH, default="700ms" ): cv.positive_time_period_milliseconds, cv.Optional( CONF_UPDATE_INTERVAL, default="16ms" ): cv.positive_time_period_milliseconds, - # cv.GenerateID(DATA_VECTOR_SEGMENTS_HOUR): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), - # cv.GenerateID(DATA_VECTOR_SEGMENTS_MINUTE): cv.declare_id(cg.std_vector.template(cg.std_vector.template(cg.uint16))), cv.GenerateID(DATA_VECTOR_SEGMENTS): cv.declare_id(cg.std_vector.template(cg.uint16)), } ) @@ -127,16 +122,8 @@ async def to_code(config): wrapped_color_off = await cg.get_variable(config[CONF_COLOR_OFF]) cg.add(var.set_off_color(wrapped_color_off)) - # segments_vector_ptr = cg.new_Pvariable(config[DATA_VECTOR_SEGMENTS]) -# void Wordclock::setup_transitions(uint32_t milliseconds) { -# this->on_transformer->setup(0, this->brightness, milliseconds); -# this->off_transformer->setup(this->brightness, 0, milliseconds); -# } cg.add(var.setup_transitions(config[CONF_FADE_LENGTH])) cg.add(var.set_brightness(int(config[CONF_BRIGHTNESS] * 255))) - - - SEGMENT_MAP = dict() for idx, segment in enumerate(config[CONF_SEGMENTS]): @@ -151,7 +138,6 @@ async def to_code(config): exp = cg.StructInitializer(SegmentCoords, ("x1", x1), ("y1", y1), ("x2", x2), ("y2", y2),) cg.add(var.add_segment(exp)) - if CONF_WORDCLOCK_STATIC_SEGMENTS in config: for segment_name in config[CONF_WORDCLOCK_STATIC_SEGMENTS]: cg.add(var.add_static(SEGMENT_MAP[segment_name])) @@ -159,45 +145,9 @@ async def to_code(config): for idx, hour in enumerate(config[CONF_HOURS]): segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] cg.add(var.add_hour(hour[CONF_HOUR], cg.std_vector.template(cg.uint16).new(segment_ids))) - # segments_vector_ptr = segments_vector_ptr.new(segment_ids) - # foo = await cg.get_variable(config[DATA_VECTOR_SEGMENTS]) - # foo = cg.std_vector.template(cg.uint16).new(segment_ids) - - # del foo for idx, minute in enumerate(config[CONF_MINUTES]): segment_ids = [SEGMENT_MAP[a] for a in minute[CONF_SEGMENTS]] cg.add(var.add_minute(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET], cg.std_vector.template(cg.uint16).new(segment_ids))) - # segments_vector_ptr = segments_vector_ptr.new(segment_ids) - # segments_vector_ptr = cg.new_Pvariable(config[DATA_VECTOR_SEGMENTS], segment_ids) - # foo = await cg.get_variable(config[DATA_VECTOR_SEGMENTS]) - # foo = cg.std_vector.template(cg.uint16).new(segment_ids) - # segments_vector = cg.new_Pvariable(core.ID(None, type=cg.std_vector.template(cg.uint16)), segment_ids) - # del foo + await cg.register_component(var, config) - - # hours = [] - # for idx, hour in enumerate(config[CONF_HOURS]): - # segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] - # exp = cg.std_vector.template(cg.uint16)(segment_ids) - # hours.append(exp) - # hours_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_HOUR], cg.std_vector.template(cg.std_vector.template(cg.uint16))(hours)) - - # minutes = [] - # for idx, hour in enumerate(config[CONF_MINUTES]): - # segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]] - # exp = cg.std_vector.template(cg.uint16)(segment_ids) - # minutes.append(exp) - # minutes_array = cg.new_variable(config[DATA_VECTOR_SEGMENTS_MINUTE], cg.std_vector.template(cg.std_vector.template(cg.uint16))(minutes)) - - # for idx, hour in enumerate(config[CONF_HOURS]): - # exp = cg.std_vector.template(cg.uint16)(segment_ids) - # cg.add(var.add_hour(hour[CONF_HOUR], cpp.UnaryOpExpression("&", hours_array[idx]))) - - # for idx, minute in enumerate(config[CONF_MINUTES]): - # exp = cg.std_vector.template(cg.uint16)(segment_ids) - # cg.add(var.add_minute(minute[CONF_MINUTE], cpp.UnaryOpExpression("&", minutes_array[idx]))) - - # for idx, minute in enumerate(config[CONF_MINUTES]): - # cg.add(var.add_hour_offset(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET])) - diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp index ee41e18..8ec0262 100644 --- a/components/wordclock/wordclock.cpp +++ b/components/wordclock/wordclock.cpp @@ -8,7 +8,6 @@ void Wordclock::setup() { if (!this->valid_time) { this->start_idle_animation(); } - // this->valid_time = true; } void Wordclock::update() { @@ -45,62 +44,42 @@ void Wordclock::update() { if (dirty) { this->previous_segments->clear(); + for (uint16_t segment_idx : *this->current_segments) { this->previous_segments->push_back(segment_idx); } - // std::sort(this->previous_segments->begin(), this->previous_segments->end()); - - // std::sort(s.begin(), s.end(), [](int a, int b) - // { - // return a > b; - // }); this->current_segments->clear(); + for (uint16_t segment_idx : *this->static_segments) { this->current_segments->push_back(segment_idx); - // this->draw_segment(segment_idx); } for (uint16_t segment_idx : *(this->minutes->at(minute).segments)) { this->current_segments->push_back(segment_idx); - // this->draw_segment(segment_idx); } for (uint16_t segment_idx : *(this->hours->at(hour).segments)) { this->current_segments->push_back(segment_idx); - // this->draw_segment(segment_idx); } + std::sort(this->current_segments->begin(), this->current_segments->end()); this->find_difference(this->previous_segments, this->current_segments); - // ESP_LOGD("wordclock.cpp", "reset"); + this->on_transformer->reset(); this->off_transformer->reset(); } - // Color on = this->on_color * this->on_transformer->apply().value_or(255); - // Color off = this->off_color * this->off_transformer->apply().value_or(255); this->current_position = 0; uint8_t transition_progress = this->on_transformer->apply().value_or(255); - // uint8_t on_progress = this->on_transformer->apply().value_or(255); - // uint8_t off_progress = this->off_transformer->apply().value_or(255); - // ESP_LOGD("wordclock.cpp", "off progress [%d]", off_progress); - // Color added_color = this->off_color.gradient(this->on_color, transition_progress); Color removed_color = this->on_color.gradient(this->off_color, transition_progress); - // ESP_LOGD("wordclock.cpp", "transition progress [%d], added [0x%06x], removed [0x%06x]", transition_progress, added_color.raw_32, removed_color.raw_32); for (uint16_t segment_idx : *this->removed_segments) { - // ESP_LOGD("wordclock.cpp", "off [%d]", segment_idx); - // this->disable_segment(segment_idx, this->off_color, transition_progress); this->disable_segment_effect(segment_idx, this->off_color, transition_progress); - // this->disable_segment(segment_idx, removed_color); - // this->draw_segment(segment_idx, off_progress); } for (uint16_t segment_idx : *this->added_segments) { - // ESP_LOGD("wordclock.cpp", "on [%d]", segment_idx); - // this->segment_effect_base(segment_idx, this->off_color, transition_progress); this->enable_segment_effect(segment_idx, this->off_color, transition_progress); - // this->draw_segment(segment_idx, on_progress); } for (uint16_t segment_idx : *this->staying_segments) { this->apply_segment_effect(segment_idx); @@ -109,14 +88,10 @@ void Wordclock::update() { } void Wordclock::find_difference(std::vector *a_vec, std::vector *b_vec) { - // for (uint16_t segment_idx : a) { - // this->current_segments->push_back(segment_idx); - // } this->added_segments->clear(); this->removed_segments->clear(); this->staying_segments->clear(); - auto a = a_vec->begin(); auto b = b_vec->begin(); @@ -129,18 +104,20 @@ void Wordclock::find_difference(std::vector *a_vec, std::vector> 8; - // hsv.hue = hue >> 8; + hsv.hue = ((millis() * speed + (position * (0xFFFF / width))) % 0xFFFF) >> 8; return hsv.to_rgb(); } +Color Wordclock::get_next_color(uint32_t position, const Color ¤t_color) { + uint32_t speed_ = 3; + uint16_t width_ = 100; // Original width_ = 50 + return get_next_color_base_(position, speed_, width_, current_color); +} + int8_t Wordclock::find_hour(uint8_t target_value) { std::vector *elements = this->hours; for (int i = 0; i < elements->size(); i++) { @@ -151,15 +128,6 @@ int8_t Wordclock::find_hour(uint8_t target_value) { } } return elements->size() - 1; - // uint16_t last_defined_index = -1; - // for (int i = 0; i < elements->size(); i++) { - // if (elements->at(i).size()) { - // last_defined_index = i; - // } - // if (i >= target_value) break; - // } - // return last_defined_index; - } int8_t Wordclock::find_minute(uint8_t target_value) { @@ -179,11 +147,6 @@ void Wordclock::setup_transitions(uint32_t milliseconds) { this->off_transformer->setup(this->brightness, 0, milliseconds); } - - -// Wordclock::Wordclock() : PollingComponent(1000) { -// } - Wordclock::Wordclock( esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, @@ -209,20 +172,7 @@ Wordclock::Wordclock( this->staying_segments = new std::vector(); this->on_transformer = new BrightnessTransitionTransformer(); - // this->on_transformer->setup(0, this->brightness, 700); this->off_transformer = new BrightnessTransitionTransformer(); - // this->off_transformer->setup(this->brightness, 0, 700); - -// wordclock_wordclock->add_segment(wordclock::SegmentCoords{ -// .x1 = 0, -// .x2 = 4, -// .y1 = 8, -// .y2 = 8, -// }); - - // this->hour_offsets = new std::vector(60, 0); - // this->minutes = new std::vector>(60, std::vector()); - // this->hours = new std::vector>(24, std::vector()); } } // namespace wordclock } // namespace esphome diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index 6104dfb..f4d1e19 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -22,9 +22,9 @@ struct SegmentCoords { uint16_t y2; }; -class BrightnessTransformer { +class BrightnessTransitionTransformer { public: - virtual ~BrightnessTransformer() = default; + virtual ~BrightnessTransitionTransformer() = default; void setup(const uint8_t start_values, const uint8_t target_values, uint32_t length) { this->start_time_ = millis(); @@ -33,29 +33,22 @@ public: this->target_values_ = target_values; this->start(); } - - /// Indicates whether this transformation is finished. virtual bool is_finished() { return this->get_progress_() >= 1.0f; } - - /// This will be called before the transition is started. virtual void start() {} - - /// This will be called while the transformer is active to apply the transition to the light. Can either write to the - /// light directly, or return LightColorValues that will be applied. - virtual optional apply() = 0; - - /// This will be called after transition is finished. + // virtual optional apply() = 0; + virtual optional apply() { + float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_()); + return esphome::lerp(v, this->start_values_, this->target_values_); + } virtual void stop() {} - void reset() { this->start_time_ = millis(); } - const uint8_t &get_start_values() const { return this->start_values_; } const uint8_t &get_target_values() const { return this->target_values_; } protected: - /// The progress of this transition, on a scale of 0 to 1. + // The progress of this transition, on a scale of 0 to 1. float get_progress_() { uint32_t now = esphome::millis(); if (now < this->start_time_) @@ -65,29 +58,16 @@ protected: return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); } + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } + uint32_t start_time_; uint32_t length_; uint8_t start_values_; uint8_t target_values_; }; -class BrightnessTransitionTransformer : public BrightnessTransformer { -public: - optional apply() override { - float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_()); - return esphome::lerp(v, this->start_values_, this->target_values_); - } -protected: - // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like - // transition from 0 to 1 on x = [0, 1] - static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } -}; - - class Wordclock : public esphome::PollingComponent { - public: - // Wordclock(); Wordclock( esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, @@ -96,9 +76,8 @@ public: void setup(); void update(); - // void setup_color(uint8_t brightness, Color on_color, Color off_color); void set_brightness(uint8_t brightness) { -this->brightness = brightness; + this->brightness = brightness; } void set_on_color(Color on_color) { this->on_color = on_color; @@ -131,17 +110,6 @@ this->brightness = brightness; }); } - // void add_hour_segment(uint8_t index, uint16_t segment_id) { - // this->hours->at(index).segments->push_back(segment_id); - // } - // void add_minute_segment(uint8_t index, uint16_t segment_id) { - // this->minutes->at(index).segments->push_back(segment_id); - // } - // void add_hour(uint8_t index, std::vector *segments); - // void add_minute(uint8_t index, std::vector *segments); - // void add_hour_offset(uint8_t index, int8_t offset) { - // this->minutes->at(index).hour_offset = offset; - // } protected: // ESPHome Components esphome::time::RealTimeClock *time; @@ -170,14 +138,9 @@ protected: std::vector *previous_segments; uint32_t current_position{0}; + Color get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color ¤t_color); Color get_next_color(uint32_t position, const Color ¤t_color); - // Color - // Color get_on_color() { - // return this->on_color * this->brightness; - // } - // Color get_off_color() { - // return this->off_color * this->brightness; - // } + Color on_color {Color(0xFFFFFF)}; Color off_color {Color(0x000000)}; uint8_t brightness{255}; @@ -186,21 +149,8 @@ protected: int8_t find_hour(uint8_t target_value); int8_t find_minute(uint8_t target_value); void find_difference(std::vector *a_vec, std::vector *b_vec); - // void draw_segment(uint16_t segment_id) { - // this->draw_segment(segment_id, this->brightness); - // } - // void draw_segment(uint16_t segment_id, uint8_t brightness) { - // this->draw_segment(segment_id, this->on_color * brightness); - // } - - // void draw_segment(uint16_t segment_id, Color color) { - // SegmentCoords s = this->segments->at(segment_id); - // // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32); - // this->display->line(s.x1, s.y1, s.x2, s.y2, color); - // } void segment_effect_base(uint16_t segment_id, bool to_effect, Color base_color, uint8_t transition_progress) { - // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32); SegmentCoords s = this->segments->at(segment_id); int x1 = s.x1; int y1 = s.y1; int x2 = s.x2; int y2 = s.y2; Color color_to_draw; @@ -215,11 +165,8 @@ protected: color_to_draw = base_color.gradient(effect_color, transition_progress); } else { color_to_draw = effect_color.gradient(base_color, transition_progress); - // c = base_color.gradient(this->get_next_color(x1 + y1, Color(0)), transition_progress); } - this->display->draw_pixel_at(x1, y1, color_to_draw); - // this->display->draw_pixel_at(x1, y1, this->get_next_color(this->current_position++, Color(0))); if (x1 == x2 && y1 == y2) break; int32_t e2 = 2 * err; if (e2 >= dy) { @@ -242,10 +189,6 @@ protected: void apply_segment_effect(uint16_t segment_id) { segment_effect_base(segment_id, true, Color(0), 255); } - // void disable_segment(uint16_t segment_id, Color off_color, uint8_t transition_progress) { - // SegmentCoords s = this->segments->at(segment_id); - // this->display->line(s.x1, s.y1, s.x2, s.y2, color); - // } void start_idle_animation() { this->display->fill(this->off_color); this->display->set_enabled(false); @@ -259,13 +202,7 @@ protected: this->previous_segments->clear(); this->current_segments->clear(); } - - - // std::vector> *minutes; - // std::vector> *hours; - // std::vector *hour_offsets; }; - } // namespace wordclock } // namespace esphome From 3e31b32853737241c7004803a44ba7b8b07a3420 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 13:52:13 +0200 Subject: [PATCH 08/16] chore(flake): switch to nixpkgs unstable --- flake.lock | 8 ++++---- flake.nix | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 5793ccb..48ccbe4 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1669833724, - "narHash": "sha256-/HEZNyGbnQecrgJnfE8d0WC5c1xuPSD2LUpB6YXlg4c=", + "lastModified": 1683777345, + "narHash": "sha256-V2p/A4RpEGqEZussOnHYMU6XglxBJGCODdzoyvcwig8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4d2b37a84fad1091b9de401eb450aae66f1a741e", + "rev": "635a306fc8ede2e34cb3dd0d6d0a5d49362150ed", "type": "github" }, "original": { "owner": "NixOS", - "ref": "22.11", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index d696ee7..208d1ce 100644 --- a/flake.nix +++ b/flake.nix @@ -2,8 +2,8 @@ description = "EspHome Word Clock"; inputs = { systems.url = "github:nix-systems/x86_64-linux"; - # nixpkgs.url = "github:NixOS/nixpkgs/unstable"; - nixpkgs.url = "github:NixOS/nixpkgs/22.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + # nixpkgs.url = "github:NixOS/nixpkgs/22.11"; utils.url = "github:numtide/flake-utils"; utils.inputs.systems.follows = "systems"; }; From 4e9041aec5580213eeb8744066e4b1fcca0096cf Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 13:53:48 +0200 Subject: [PATCH 09/16] feat(esp8266): random changes to now abandonned esp8266 platform. --- susannes_wordclock.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/susannes_wordclock.yaml b/susannes_wordclock.yaml index d947563..4680d96 100644 --- a/susannes_wordclock.yaml +++ b/susannes_wordclock.yaml @@ -5,6 +5,11 @@ esp8266: board: d1_mini framework: version: recommended +# esp32: +# board: ttgo-t7-v13-mini32 +# framework: +# type: arduino + external_components: - source: @@ -34,6 +39,23 @@ ota: logger: +select: + - platform: template + name: "Template select" + restore_value: true + set_action: + - lambda: |- + return id(wordclock_1)->set_effect_active(false); + optimistic: true + options: + - effect + - color + initial_option: effect + +web_server: + port: 80 + version: 1 + time: - platform: sntp id: current_time @@ -46,10 +68,12 @@ light: type: GRB variant: WS2812 pin: GPIO3 + # pin: GPIO22 num_leds: 198 restore_mode: ALWAYS_OFF method: type: esp8266_dma + # type: esp32_i2s effects: - addressable_random_twinkle: @@ -86,6 +110,7 @@ color: blue: 0% wordclock: + id: wordclock_1 time_id: current_time display_id: led_matrix_display addressable_light_id: neopixel_strip_1 From 8f90d6b1cacf32c7351d1dc4b35709d0685d7ab0 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 13:54:17 +0200 Subject: [PATCH 10/16] feat(case): initial commit of the case --- case/d1_mini_esp32-Body.stl | Bin 0 -> 30684 bytes case/d1_mini_esp32.FCStd | Bin 0 -> 120058 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 case/d1_mini_esp32-Body.stl create mode 100644 case/d1_mini_esp32.FCStd diff --git a/case/d1_mini_esp32-Body.stl b/case/d1_mini_esp32-Body.stl new file mode 100644 index 0000000000000000000000000000000000000000..64adf673c60831602a29637f8a6d607848fafb90 GIT binary patch literal 30684 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≶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≦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 literal 0 HcmV?d00001 From 03a012650f9b693a4ce97e78cd23de94fc887f41 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 13:59:29 +0200 Subject: [PATCH 11/16] chore: minor refactoring --- components/wordclock/wordclock.cpp | 1 + components/wordclock/wordclock.h | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp index 8ec0262..6418759 100644 --- a/components/wordclock/wordclock.cpp +++ b/components/wordclock/wordclock.cpp @@ -1,3 +1,4 @@ + #include "wordclock.h" namespace esphome { diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index f4d1e19..563770e 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -1,5 +1,7 @@ #pragma once + #include "esphome.h" +// #include "esphome/components/sensor/sensor.h" namespace esphome { namespace wordclock { @@ -110,6 +112,10 @@ public: }); } + Color on_color {Color(0xFFFFFF)}; + Color off_color {Color(0x000000)}; + uint8_t brightness{255}; + protected: // ESPHome Components esphome::time::RealTimeClock *time; @@ -141,9 +147,7 @@ protected: Color get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color ¤t_color); Color get_next_color(uint32_t position, const Color ¤t_color); - Color on_color {Color(0xFFFFFF)}; - Color off_color {Color(0x000000)}; - uint8_t brightness{255}; + // Utils int8_t find_hour(uint8_t target_value); From e4bdcf71dc16e82ddb93cc35fdd261ed181aea89 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 14:01:29 +0200 Subject: [PATCH 12/16] feat: add variable to toggle the rainbow effect on or off --- components/wordclock/wordclock.cpp | 5 +++++ components/wordclock/wordclock.h | 3 +++ 2 files changed, 8 insertions(+) diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp index 6418759..68a23d8 100644 --- a/components/wordclock/wordclock.cpp +++ b/components/wordclock/wordclock.cpp @@ -114,10 +114,15 @@ Color Wordclock::get_next_color_base_(uint32_t position, uint32_t speed, uint16_ } Color Wordclock::get_next_color(uint32_t position, const Color ¤t_color) { + if (this->effect_active) { uint32_t speed_ = 3; uint16_t width_ = 100; // Original width_ = 50 return get_next_color_base_(position, speed_, width_, current_color); } + else { + return Color(this->on_color); + } +} int8_t Wordclock::find_hour(uint8_t target_value) { std::vector *elements = this->hours; diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index 563770e..54af2ef 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -81,6 +81,8 @@ public: void set_brightness(uint8_t brightness) { this->brightness = brightness; } + bool get_effect_active() { return this->effect_active; } + void set_effect_active(bool x) { this->effect_active = x; } void set_on_color(Color on_color) { this->on_color = on_color; } @@ -144,6 +146,7 @@ protected: std::vector *previous_segments; uint32_t current_position{0}; + bool effect_active{true}; Color get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color ¤t_color); Color get_next_color(uint32_t position, const Color ¤t_color); From 4962f344e35b300fdf01a8f20d45a757071c401e Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 14:02:39 +0200 Subject: [PATCH 13/16] fix: brightness attribute now correctly controls the brightness, even with active effect. --- components/wordclock/wordclock.cpp | 12 ++++++------ components/wordclock/wordclock.h | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/wordclock/wordclock.cpp b/components/wordclock/wordclock.cpp index 68a23d8..57b932c 100644 --- a/components/wordclock/wordclock.cpp +++ b/components/wordclock/wordclock.cpp @@ -115,10 +115,10 @@ Color Wordclock::get_next_color_base_(uint32_t position, uint32_t speed, uint16_ Color Wordclock::get_next_color(uint32_t position, const Color ¤t_color) { if (this->effect_active) { - uint32_t speed_ = 3; - uint16_t width_ = 100; // Original width_ = 50 - return get_next_color_base_(position, speed_, width_, current_color); -} + uint32_t speed_ = 3; + uint16_t width_ = 100; // Original width_ = 50 + return get_next_color_base_(position, speed_, width_, current_color); + } else { return Color(this->on_color); } @@ -149,8 +149,8 @@ int8_t Wordclock::find_minute(uint8_t target_value) { } void Wordclock::setup_transitions(uint32_t milliseconds) { - this->on_transformer->setup(0, this->brightness, milliseconds); - this->off_transformer->setup(this->brightness, 0, milliseconds); + this->on_transformer->setup(0, 255, milliseconds); + this->off_transformer->setup(255, 0, milliseconds); } Wordclock::Wordclock( diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index 54af2ef..4973d6e 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -167,7 +167,7 @@ protected: const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; int32_t err = dx + dy; while (true) { - effect_color = this->get_next_color(x1 + y1, Color(0)); + effect_color = this->get_next_color(x1 + y1, Color(0)) * this->brightness; if (to_effect) { color_to_draw = base_color.gradient(effect_color, transition_progress); } else { From 587a968451083ad8581316a885020511f3d1e073 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 14:33:07 +0200 Subject: [PATCH 14/16] feat(esp32): move to esp32 platform and add configuration inputs. --- susannes_wordclock_esp32.yaml | 298 ++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 susannes_wordclock_esp32.yaml diff --git a/susannes_wordclock_esp32.yaml b/susannes_wordclock_esp32.yaml new file mode 100644 index 0000000..11f029f --- /dev/null +++ b/susannes_wordclock_esp32.yaml @@ -0,0 +1,298 @@ +esphome: + name: "wordclock" + +esp32: + # board: ttgo-t7-v13-mini32 + board: wemos_d1_mini32 + framework: + type: arduino + +external_components: + - source: + type: local + path: components + components: [ wordclock ] + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "WordClock Configuration" + password: !secret ap_password + manual_ip: + static_ip: !secret manualip_static_ip + gateway: !secret manualip_gateway + subnet: !secret manualip_subnet + dns1: 1.1.1.1 + dns2: 1.0.0.1 + +captive_portal: + +# api: + +ota: + password: "wordclock" + +logger: + +switch: + - platform: template + name: "5 Rainbow ON/OFF" + # lambda: |- + # return id(wordclock_1)->get_effect_active(); + restore_state: true + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + turn_on_action: + - lambda: |- + id(wordclock_1)->set_effect_active(true); + turn_off_action: + - lambda: |- + id(wordclock_1)->set_effect_active(false); + +# on_...: +# - globals.set: +# id: my_global_var +# value: '10' + + +# output: +# - platform: template +# id: dummy_out +# type: float +# write_action: +# - lambda: |- +# return id(wordclock_1)->set_effect_active(true); + # Example configuration entry +globals: + - id: my_global_int + type: int + restore_value: no + initial_value: '0' + # Example for global string variable + - id: my_global_string + type: std::string + restore_value: no # Strings cannot be saved/restored + initial_value: '"Global value is"' + +number: + - platform: template + name: "1 Red" + optimistic: true + restore_value: true + initial_value: 255 + # lambda: |- + # return id(wordclock_1)->on_color.red; + on_value: + - lambda: |- + id(wordclock_1)->on_color.red = x; + set_action: + - lambda: |- + id(wordclock_1)->on_color.red = x; + min_value: 0 + max_value: 255 + step: 1 + - platform: template + name: "2 Green" + optimistic: true + restore_value: true + initial_value: 255 + # lambda: |- + # return id(wordclock_1)->on_color.green; + on_value: + - lambda: |- + id(wordclock_1)->on_color.green = x; + set_action: + - lambda: |- + id(wordclock_1)->on_color.green = x; + min_value: 0 + max_value: 255 + step: 1 + - platform: template + name: "3 Blue" + optimistic: true + restore_value: true + initial_value: 255 + # lambda: |- + # return id(wordclock_1)->on_color.blue; + on_value: + - lambda: |- + id(wordclock_1)->on_color.blue = x; + set_action: + - lambda: |- + id(wordclock_1)->on_color.blue = x; + min_value: 0 + max_value: 255 + step: 1 + - platform: template + name: "4 Brightness" + optimistic: true + restore_value: true + initial_value: 255 + # lambda: |- + # return id(wordclock_1)->brightness; + on_value: + - lambda: |- + id(wordclock_1)->brightness = x; + set_action: + - lambda: |- + id(wordclock_1)->brightness = x; + min_value: 0 + max_value: 255 + step: 1 +# select: +# - platform: template +# name: "Rainbow ON/OFF" +# restore_value: true +# set_action: +# - lambda: |- +# return id(wordclock_1)->set_color_value(x); +# optimistic: true +# options: +# - effect +# - color +# initial_option: effect +# - platform: template +# name: "Template select" +# restore_value: true +# set_action: +# - lambda: |- +# return id(wordclock_1)->set_color_value(x); +# optimistic: true +# options: +# - effect +# - color +# initial_option: effect + + +web_server: + port: 80 + version: 2 + local: true + +time: + - platform: sntp + id: current_time + timezone: !secret timezone + + +light: + - name: NeoPixel Strip 1 + id: neopixel_strip_1 + platform: neopixelbus + type: GRB + variant: WS2812 + pin: GPIO22 + num_leds: 198 + restore_mode: ALWAYS_OFF + internal: true + method: + type: esp32_i2s + effects: + - addressable_random_twinkle: + name: "random_twinkle" + +display: + - platform: addressable_light + id: led_matrix_display + addressable_light_id: neopixel_strip_1 + width: 18 + height: 11 + rotation: 270° + update_interval: 16ms + auto_clear_enabled: false + pixel_mapper: |- + int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16}; + int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16}; + if (x > 9) return -1; + if (y % 2 == 0) { + return (y * 18 + mapping_even[x]); + } else { + return (y * 18 + mapping_odd[x]); + } + +color: + - id: col_on + red: 90% + green: 50% + blue: 0% + - id: col_off + red: 0% + green: 0% + blue: 0% + +wordclock: + id: wordclock_1 + time_id: current_time + display_id: led_matrix_display + addressable_light_id: neopixel_strip_1 + brightness: 100% + color_on: col_on + color_off: col_off + update_interval: 16ms + static_segments: ["IT", "IS"] + segments: + - {name: "IT", line: {x1: 0, x2: 1, y1: 0}} + - {name: "IS", line: {x1: 3, x2: 4, y1: 0}} + - {name: "AM", line: {x1: 7, x2: 8, y1: 0}} + - {name: "PM", line: {x1: 9, x2: 10, y1: 0}} + - {name: "QUARTER", line: {x1: 2, x2: 8, y1: 1}} + - {name: "TWENTY", line: {x1: 0, x2: 5, y1: 2}} + - {name: "FIVE_MINUTES", line: {x1: 6, x2: 9, y1: 2}} + - {name: "HALF", line: {x1: 0, x2: 3, y1: 3}} + - {name: "TEN_MINUTES", line: {x1: 5, x2: 7, y1: 3}} + - {name: "TO", line: {x1: 9, x2: 10, y1: 3}} + - {name: "PAST", line: {x1: 0, x2: 3, y1: 4}} + - {name: "NINE", line: {x1: 7, x2: 10, y1: 4}} + - {name: "ONE", line: {x1: 0, x2: 2, y1: 5}} + - {name: "SIX", line: {x1: 3, x2: 5, y1: 5}} + - {name: "THREE", line: {x1: 6, x2: 10, y1: 5}} + - {name: "FOUR", line: {x1: 0, x2: 3, y1: 6}} + - {name: "FIVE", line: {x1: 4, x2: 7, y1: 6}} + - {name: "TWO", line: {x1: 8, x2: 10, y1: 6}} + - {name: "EIGHT", line: {x1: 0, x2: 4, y1: 7}} + - {name: "ELEVEN", line: {x1: 5, x2: 10, y1: 7}} + - {name: "SEVEN", line: {x1: 0, x2: 4, y1: 8}} + - {name: "TWELVE", line: {x1: 5, x2: 10, y1: 8}} + - {name: "TEN", line: {x1: 0, x2: 2, y1: 9}} + - {name: "OCLOCK", line: {x1: 5, x2: 10, y1: 9}} + minutes: + - {minute: 0, hour_offset: 0, segments: ["OCLOCK"]} + - {minute: 5, hour_offset: 0, segments: ["FIVE_MINUTES", "PAST"]} + - {minute: 10, hour_offset: 0, segments: ["TEN_MINUTES", "PAST"]} + - {minute: 15, hour_offset: 0, segments: ["QUARTER", "PAST"]} + - {minute: 20, hour_offset: 0, segments: ["TWENTY", "PAST"]} + - {minute: 25, hour_offset: 0, segments: ["TWENTY", "FIVE_MINUTES", "PAST"]} + - {minute: 30, hour_offset: 0, segments: ["HALF", "PAST"]} + - {minute: 35, hour_offset: 1, segments: ["TWENTY", "FIVE_MINUTES", "TO"]} + - {minute: 40, hour_offset: 1, segments: ["TWENTY", "TO"]} + - {minute: 45, hour_offset: 1, segments: ["QUARTER", "TO"]} + - {minute: 50, hour_offset: 1, segments: ["TEN_MINUTES", "TO"]} + - {minute: 55, hour_offset: 1, segments: ["FIVE_MINUTES", "TO"]} + hours: + - {hour: 0, segments: "TWELVE"} + - {hour: 1, segments: "ONE"} + - {hour: 2, segments: "TWO"} + - {hour: 3, segments: "THREE"} + - {hour: 4, segments: "FOUR"} + - {hour: 5, segments: "FIVE"} + - {hour: 6, segments: "SIX"} + - {hour: 7, segments: "SEVEN"} + - {hour: 8, segments: "EIGHT"} + - {hour: 9, segments: "NINE"} + - {hour: 10, segments: "TEN"} + - {hour: 11, segments: "ELEVEN"} + - {hour: 12, segments: "TWELVE"} + - {hour: 13, segments: "ONE"} + - {hour: 14, segments: "TWO"} + - {hour: 15, segments: "THREE"} + - {hour: 16, segments: "FOUR"} + - {hour: 17, segments: "FIVE"} + - {hour: 18, segments: "SIX"} + - {hour: 19, segments: "SEVEN"} + - {hour: 20, segments: "EIGHT"} + - {hour: 21, segments: "NINE"} + - {hour: 22, segments: "TEN"} + - {hour: 23, segments: "ELEVEN"} + + \ No newline at end of file From 47f31301ddb28e96ae1866306eaa80b042f5cbe8 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 14:34:50 +0200 Subject: [PATCH 15/16] chore: update gitignore file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index bcce0d2..53dc580 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ /.vscode __pycache__ /_ignored +esp32_test.code-workspace +case/d1_mini_esp32.FCStd1 From c54ff63f89eb94b45c6ec7efb59b2dcc05249959 Mon Sep 17 00:00:00 2001 From: Philip Stark Date: Sat, 10 Jun 2023 18:50:46 +0200 Subject: [PATCH 16/16] feat(effect): add attributes, getters, and setters for effect-speed and -width --- components/wordclock/wordclock.h | 10 +++++++++- devlog.md | 9 +++++++++ susannes_wordclock_esp32.yaml | 29 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 devlog.md diff --git a/components/wordclock/wordclock.h b/components/wordclock/wordclock.h index 4973d6e..0530e4d 100644 --- a/components/wordclock/wordclock.h +++ b/components/wordclock/wordclock.h @@ -83,6 +83,13 @@ public: } bool get_effect_active() { return this->effect_active; } void set_effect_active(bool x) { this->effect_active = x; } + + bool get_effect_width() { return this->effect_width; } + void set_effect_width(bool x) { this->effect_width = x; } + + bool get_effect_speed() { return this->effect_speed; } + void set_effect_speed(bool x) { this->effect_speed = x; } + void set_on_color(Color on_color) { this->on_color = on_color; } @@ -147,11 +154,12 @@ protected: uint32_t current_position{0}; bool effect_active{true}; + uint32_t effect_speed{12}; + uint16_t effect_width{50}; Color get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color ¤t_color); Color get_next_color(uint32_t position, const Color ¤t_color); - // Utils int8_t find_hour(uint8_t target_value); int8_t find_minute(uint8_t target_value); diff --git a/devlog.md b/devlog.md new file mode 100644 index 0000000..2a687de --- /dev/null +++ b/devlog.md @@ -0,0 +1,9 @@ +# Wordclock Development Log + + +## Feature "make speed and width of rainbow effect user-configurable" [2023-06-10 17:13] + +[17:16] Step - add attributes, getters, and setters for effect-speed and -width. + - [x] add attr/funcs to header file. + - [x] add inputs to yaml. + - \ No newline at end of file diff --git a/susannes_wordclock_esp32.yaml b/susannes_wordclock_esp32.yaml index 11f029f..38987ce 100644 --- a/susannes_wordclock_esp32.yaml +++ b/susannes_wordclock_esp32.yaml @@ -140,6 +140,35 @@ number: min_value: 0 max_value: 255 step: 1 + - platform: template + name: "6 Rainbow Speed" + optimistic: true + restore_value: true + initial_value: 12 + on_value: + - lambda: |- + id(wordclock_1)->set_effect_speed(x); + set_action: + - lambda: |- + id(wordclock_1)->set_effect_speed(x); + min_value: 1 + max_value: 4096 + step: 1 + - platform: template + name: "7 Rainbow Width" + optimistic: true + restore_value: true + initial_value: 50 + on_value: + - lambda: |- + id(wordclock_1)->set_effect_width(x); + set_action: + - lambda: |- + id(wordclock_1)->set_effect_width(x); + min_value: 1 + max_value: 65535 + step: 1 + # select: # - platform: template # name: "Rainbow ON/OFF"