diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 9652d01efa..bc2e558f1b 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -8,12 +8,19 @@ from esphome.const import ( CONF_MODEL, CONF_RESET_PIN, CONF_BRIGHTNESS, + CONF_CONTRAST, + CONF_INVERT, ) ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base") SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer) SSD1306Model = ssd1306_base_ns.enum("SSD1306Model") +CONF_FLIP_X = "flip_x" +CONF_FLIP_Y = "flip_y" +CONF_OFFSET_X = "offset_x" +CONF_OFFSET_Y = "offset_y" + MODELS = { "SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32, "SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64, @@ -23,21 +30,44 @@ MODELS = { "SH1106_128X64": SSD1306Model.SH1106_MODEL_128_64, "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, + "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, + "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_") + +def _validate(value): + model = value[CONF_MODEL] + if model not in ("SSD1305_128X32", "SSD1305_128X64"): + # Contrast is default value (1.0) while brightness is not + # Indicates user is using old `brightness` option + if value[CONF_BRIGHTNESS] != 1.0 and value[CONF_CONTRAST] == 1.0: + raise cv.Invalid( + "SSD1306/SH1106 no longer accepts brightness option, " + 'please use "contrast" instead.' + ) + + return value + + SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( { cv.Required(CONF_MODEL): SSD1306_MODEL, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_CONTRAST, default=1.0): cv.percentage, cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, + cv.Optional(CONF_FLIP_X, default=True): cv.boolean, + cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, + cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=15), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=15), + cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) -async def setup_ssd1036(var, config): +async def setup_ssd1306(var, config): await cg.register_component(var, config) await display.register_display(var, config) @@ -47,8 +77,20 @@ async def setup_ssd1036(var, config): cg.add(var.set_reset_pin(reset)) if CONF_BRIGHTNESS in config: cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_CONTRAST in config: + cg.add(var.init_contrast(config[CONF_CONTRAST])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_FLIP_X in config: + cg.add(var.init_flip_x(config[CONF_FLIP_X])) + if CONF_FLIP_Y in config: + cg.add(var.init_flip_y(config[CONF_FLIP_X])) + if CONF_OFFSET_X in config: + cg.add(var.init_offset_x(config[CONF_OFFSET_X])) + if CONF_OFFSET_Y in config: + cg.add(var.init_offset_y(config[CONF_OFFSET_Y])) + if CONF_INVERT in config: + cg.add(var.init_invert(config[CONF_INVERT])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index d321933e8f..b1a2538ebd 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -8,12 +8,13 @@ namespace ssd1306_base { static const char *const TAG = "ssd1306"; static const uint8_t SSD1306_MAX_CONTRAST = 255; +static const uint8_t SSD1305_MAX_BRIGHTNESS = 255; static const uint8_t SSD1306_COMMAND_DISPLAY_OFF = 0xAE; static const uint8_t SSD1306_COMMAND_DISPLAY_ON = 0xAF; static const uint8_t SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV = 0xD5; static const uint8_t SSD1306_COMMAND_SET_MULTIPLEX = 0xA8; -static const uint8_t SSD1306_COMMAND_SET_DISPLAY_OFFSET = 0xD3; +static const uint8_t SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y = 0xD3; static const uint8_t SSD1306_COMMAND_SET_START_LINE = 0x40; static const uint8_t SSD1306_COMMAND_CHARGE_PUMP = 0x8D; static const uint8_t SSD1306_COMMAND_MEMORY_MODE = 0x20; @@ -28,33 +29,60 @@ static const uint8_t SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME = 0xA4; static const uint8_t SSD1306_COMMAND_DEACTIVATE_SCROLL = 0x2E; static const uint8_t SSD1306_COMMAND_COLUMN_ADDRESS = 0x21; static const uint8_t SSD1306_COMMAND_PAGE_ADDRESS = 0x22; +static const uint8_t SSD1306_COMMAND_NORMAL_DISPLAY = 0xA6; +static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7; -static const uint8_t SSD1306_NORMAL_DISPLAY = 0xA6; +static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82; +static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); + // Turn off display during initialization (0xAE) this->command(SSD1306_COMMAND_DISPLAY_OFF); - this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); - this->command(0x80); // suggested ratio + // Set oscillator frequency to 4'b1000 with no clock division (0xD5) + this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); + // Oscillator frequency <= 4'b1000, no clock division + this->command(0x80); + + // Enable low power display mode for SSD1305 (0xD8) + if (this->is_ssd1305_()) { + this->command(SSD1305_COMMAND_SET_AREA_COLOR); + this->command(0x05); + } + + // Set mux ratio to [Y pixels - 1] (0xA8) this->command(SSD1306_COMMAND_SET_MULTIPLEX); this->command(this->get_height_internal() - 1); - this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET); - this->command(0x00); // no offset - this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); // start at line 0 - this->command(SSD1306_COMMAND_CHARGE_PUMP); - if (this->external_vcc_) - this->command(0x10); - else - this->command(0x14); + // Set Y offset (0xD3) + this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); + this->command(0x00 + this->offset_y_); + // Set start line at line 0 (0x40) + this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + // SSD1305 does not have charge pump + if (!this->is_ssd1305_()) { + // Enable charge pump (0x8D) + this->command(SSD1306_COMMAND_CHARGE_PUMP); + if (this->external_vcc_) + this->command(0x10); + else + this->command(0x14); + } + + // Set addressing mode to horizontal (0x20) this->command(SSD1306_COMMAND_MEMORY_MODE); this->command(0x00); - this->command(SSD1306_COMMAND_SEGRE_MAP | 0x01); - this->command(SSD1306_COMMAND_COM_SCAN_DEC); + // X flip mode (0xA0, 0xA1) + this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_); + + // Y flip mode (0xC0, 0xC8) + this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3)); + + // Set pin configuration (0xDA) this->command(SSD1306_COMMAND_SET_COM_PINS); switch (this->model_) { case SSD1306_MODEL_128_32: @@ -67,25 +95,37 @@ void SSD1306::setup() { case SH1106_MODEL_128_64: case SSD1306_MODEL_64_48: case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: this->command(0x12); break; } + // Pre-charge period (0xD9) this->command(SSD1306_COMMAND_SET_PRE_CHARGE); if (this->external_vcc_) this->command(0x22); else this->command(0xF1); + // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); this->command(0x00); + // Display output follow RAM (0xA4) this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); - this->command(SSD1306_NORMAL_DISPLAY); + // Inverse display mode (0xA6, 0xA7) + this->command(SSD1306_COMMAND_NORMAL_DISPLAY | this->invert_); + + // Disable scrolling mode (0x2E) this->command(SSD1306_COMMAND_DEACTIVATE_SCROLL); - set_brightness(this->brightness_); + // Contrast and brighrness + // SSD1306 does not have brightness setting + set_contrast(this->contrast_); + if (this->is_ssd1305_()) + set_brightness(this->brightness_); this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on this->display(); // ...write buffer, which actually clears the display's memory @@ -101,12 +141,12 @@ void SSD1306::display() { this->command(SSD1306_COMMAND_COLUMN_ADDRESS); switch (this->model_) { case SSD1306_MODEL_64_48: - this->command(0x20); - this->command(0x20 + this->get_width_internal() - 1); + this->command(0x20 + this->offset_x_); + this->command(0x20 + this->offset_x_ + this->get_width_internal() - 1); break; default: - this->command(0); // Page start address, 0 - this->command(this->get_width_internal() - 1); + this->command(0 + this->offset_x_); // Page start address, 0 + this->command(this->get_width_internal() + this->offset_x_ - 1); break; } @@ -122,16 +162,28 @@ bool SSD1306::is_sh1106_() const { return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 || this->model_ == SH1106_MODEL_128_64; } +bool SSD1306::is_ssd1305_() const { + return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; +} void SSD1306::update() { this->do_update_(); this->display(); } +void SSD1306::set_contrast(float contrast) { + // validation + this->contrast_ = clamp(contrast, 0.0F, 1.0F); + // now write the new contrast level to the display (0x81) + this->command(SSD1306_COMMAND_SET_CONTRAST); + this->command(int(SSD1306_MAX_CONTRAST * (this->contrast_))); +} void SSD1306::set_brightness(float brightness) { // validation + if (!this->is_ssd1305_()) + return; this->brightness_ = clamp(brightness, 0.0F, 1.0F); - // now write the new brightness level to the display - this->command(SSD1306_COMMAND_SET_CONTRAST); - this->command(int(SSD1306_MAX_CONTRAST * (this->brightness_))); + // now write the new brightness level to the display (0x82) + this->command(SSD1305_COMMAND_SET_BRIGHTNESS); + this->command(int(SSD1305_MAX_BRIGHTNESS * (this->brightness_))); } bool SSD1306::is_on() { return this->is_on_; } void SSD1306::turn_on() { @@ -146,9 +198,11 @@ int SSD1306::get_height_internal() { switch (this->model_) { case SSD1306_MODEL_128_32: case SH1106_MODEL_128_32: + case SSD1305_MODEL_128_32: return 32; case SSD1306_MODEL_128_64: case SH1106_MODEL_128_64: + case SSD1305_MODEL_128_64: return 64; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: @@ -166,6 +220,8 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_128_32: case SSD1306_MODEL_128_64: case SH1106_MODEL_128_64: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: return 128; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: @@ -227,6 +283,10 @@ const char *SSD1306::model_str_() { return "SH1106 96x16"; case SH1106_MODEL_64_48: return "SH1106 64x48"; + case SSD1305_MODEL_128_32: + return "SSD1305 128x32"; + case SSD1305_MODEL_128_64: + return "SSD1305 128x32"; default: return "Unknown"; } diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 2c54af7a67..54cb10d153 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -16,6 +16,8 @@ enum SSD1306Model { SH1106_MODEL_128_64, SH1106_MODEL_96_16, SH1106_MODEL_64_48, + SSD1305_MODEL_128_32, + SSD1305_MODEL_128_64, }; class SSD1306 : public PollingComponent, public display::DisplayBuffer { @@ -29,8 +31,15 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1306Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + void init_contrast(float contrast) { this->contrast_ = contrast; } + void set_contrast(float contrast); void init_brightness(float brightness) { this->brightness_ = brightness; } void set_brightness(float brightness); + void init_flip_x(boolean flip_x) { this->flip_x_ = flip_x; } + void init_flip_y(boolean flip_y) { this->flip_y_ = flip_y; } + void init_offset_x(uint8_t offset_x) { this->offset_x_ = offset_x; } + void init_offset_y(uint8_t offset_y) { this->offset_y_ = offset_y; } + void init_invert(boolean invert) { this->invert_ = invert; } bool is_on(); void turn_on(); void turn_off(); @@ -43,6 +52,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_reset_(); bool is_sh1106_() const; + bool is_ssd1305_() const; void draw_absolute_pixel_internal(int x, int y, Color color) override; @@ -55,7 +65,13 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; bool is_on_{false}; + float contrast_{1.0}; float brightness_{1.0}; + bool flip_x_{true}; + bool flip_y_{true}; + uint8_t offset_x_{0}; + uint8_t offset_y_{0}; + bool invert_{false}; }; } // namespace ssd1306_base diff --git a/esphome/components/ssd1306_i2c/display.py b/esphome/components/ssd1306_i2c/display.py index 4b51a90431..c51ab5f93e 100644 --- a/esphome/components/ssd1306_i2c/display.py +++ b/esphome/components/ssd1306_i2c/display.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import ssd1306_base, i2c +from esphome.components.ssd1306_base import _validate from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES AUTO_LOAD = ["ssd1306_base"] @@ -18,10 +19,11 @@ CONFIG_SCHEMA = cv.All( .extend(cv.COMPONENT_SCHEMA) .extend(i2c.i2c_device_schema(0x3C)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await ssd1306_base.setup_ssd1036(var, config) + await ssd1306_base.setup_ssd1306(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 45fda4870e..fddea25fc8 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -25,6 +25,11 @@ void I2CSSD1306::dump_config() { ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + ESP_LOGCONFIG(TAG, " Flip X: %s", YESNO(this->flip_x_)); + ESP_LOGCONFIG(TAG, " Flip Y: %s", YESNO(this->flip_y_)); + ESP_LOGCONFIG(TAG, " Offset X: %d", this->offset_x_); + ESP_LOGCONFIG(TAG, " Offset Y: %d", this->offset_y_); + ESP_LOGCONFIG(TAG, " Inverted Color: %s", YESNO(this->invert_)); LOG_UPDATE_INTERVAL(this); if (this->error_code_ == COMMUNICATION_FAILED) { diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index f7dd1553ba..0af1168bde 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import spi, ssd1306_base +from esphome.components.ssd1306_base import _validate from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES AUTO_LOAD = ["ssd1306_base"] @@ -20,12 +21,13 @@ CONFIG_SCHEMA = cv.All( .extend(cv.COMPONENT_SCHEMA) .extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await ssd1306_base.setup_ssd1036(var, config) + await ssd1306_base.setup_ssd1306(var, config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 5ef25b8139..33d474a8ee 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -22,6 +22,11 @@ void SPISSD1306::dump_config() { LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + ESP_LOGCONFIG(TAG, " Flip X: %s", YESNO(this->flip_x_)); + ESP_LOGCONFIG(TAG, " Flip Y: %s", YESNO(this->flip_y_)); + ESP_LOGCONFIG(TAG, " Offset X: %d", this->offset_x_); + ESP_LOGCONFIG(TAG, " Offset Y: %d", this->offset_y_); + ESP_LOGCONFIG(TAG, " Inverted Color: %s", YESNO(this->invert_)); LOG_UPDATE_INTERVAL(this); } void SPISSD1306::command(uint8_t value) { diff --git a/tests/test1.yaml b/tests/test1.yaml index 5bec56c80f..6109e9f5c2 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2072,7 +2072,7 @@ display: reset_pin: GPIO23 address: 0x3C id: display1 - brightness: 60% + contrast: 60% pages: - id: page1 lambda: |-