[new] restructure (new display class), impl. demo mode action

This commit is contained in:
endym 2024-05-10 12:04:04 +02:00
parent b9ee2b2deb
commit ef9f16a9dc
6 changed files with 963 additions and 494 deletions

View file

@ -0,0 +1,72 @@
#pragma once
#include "display.h"
#include "esphome/core/automation.h"
#include "max6921.h"
namespace esphome {
namespace max6921 {
class Display;
template<typename... Ts> class SetBrightnessAction : public Action<Ts...>, public Parented<MAX6921Component> {
public:
TEMPLATABLE_VALUE(float, brightness)
void play(Ts... x) override { this->parent_->set_brightness(this->brightness_.value(x...)); }
};
#if 0
template<typename... Ts> class SetDemoModeAction : public Action<Ts...>, public Parented<MAX6921Component> {
public:
TEMPLATABLE_VALUE(DemoMode, mode)
TEMPLATABLE_VALUE(uint8_t, cycle_num)
void play(Ts... x) override {
this->parent_->set_demo_mode(this->mode_.value(x...), this->cycle_num_.optional_value(x...));
}
};
#endif
template<typename... Ts> class SetDemoModeAction : public Action<Ts...> {
public:
explicit SetDemoModeAction(MAX6921Component *max9621) : max9621_(max9621) {}
TEMPLATABLE_VALUE(std::string, mode)
TEMPLATABLE_VALUE(uint8_t, cycle_num)
// overlay to cover string inputs
// void set_mode(const std::string mode);
// void set_mode(const std::string mode) {
// if (str_equals_case_insensitive(mode, "off")) {
// this->set_mode(DEMO_MODE_OFF);
// } else if (str_equals_case_insensitive(mode, "scroll_font")) {
// this->set_mode(DEMO_MODE_SCROLL_FONT);
// } else {
// ESP_LOGW(TAG, "Invalid demo mode %s", mode.c_str());
// }
// }
void play(Ts... x) override {
auto cycle_num = this->cycle_num_.value(x...);
this->max9621_->display_->set_demo_mode(this->mode_.value(x...), cycle_num);
}
protected:
// TemplatableValue<const std::string, Ts...> mode{};
MAX6921Component *max9621_;
// DemoMode mode_;
};
template<typename... Ts> class SetTextAction : public Action<Ts...>, public Parented<MAX6921Component> {
public:
TEMPLATABLE_VALUE(std::string, text)
void play(Ts... x) override { this->parent_->set_text(this->text_.value(x...)); }
};
} // namespace max9621
} // namespace esphome

View file

@ -0,0 +1,640 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "display.h"
#include "max6921.h"
namespace esphome {
namespace max6921 {
static const char *const TAG = "max6921.display";
// segments of 7-segment character
static const uint8_t SEG_A = (1<<0);
static const uint8_t SEG_B = (1<<1);
static const uint8_t SEG_C = (1<<2);
static const uint8_t SEG_D = (1<<3);
static const uint8_t SEG_E = (1<<4);
static const uint8_t SEG_F = (1<<5);
static const uint8_t SEG_G = (1<<6);
static const uint8_t SEG_DP = (1<<7);
static const uint8_t SEG_UNSUPPORTED_CHAR = 0;
// ASCII table from 0x20..0x7E
const uint8_t ASCII_TO_SEG[FONT_SIZE] PROGMEM = {
0, // ' ', (0x20)
SEG_UNSUPPORTED_CHAR, // '!', (0x21)
SEG_B|SEG_F, // '"', (0x22)
SEG_UNSUPPORTED_CHAR, // '#', (0x23)
SEG_UNSUPPORTED_CHAR, // '$', (0x24)
SEG_UNSUPPORTED_CHAR, // '%', (0x25)
SEG_UNSUPPORTED_CHAR, // '&', (0x26)
SEG_F, // ''', (0x27)
SEG_A|SEG_D|SEG_E|SEG_F, // '(', (0x28)
SEG_A|SEG_B|SEG_C|SEG_D, // ')', (0x29)
SEG_UNSUPPORTED_CHAR, // '*', (0x2A)
SEG_UNSUPPORTED_CHAR, // '+', (0x2B)
SEG_DP, // ',', (0x2C)
SEG_G, // '-', (0x2D)
SEG_DP, // '.', (0x2E)
SEG_UNSUPPORTED_CHAR, // '/', (0x2F)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // '0', (0x30)
SEG_B|SEG_C, // '1', (0x31)
SEG_A|SEG_B|SEG_D|SEG_E|SEG_G, // '2', (0x32)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_G, // '3', (0x33)
SEG_B|SEG_C|SEG_F|SEG_G, // '4', (0x34)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // '5', (0x35)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // '6', (0x36)
SEG_A|SEG_B|SEG_C, // '7', (0x37)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // '8', (0x38)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_F|SEG_G, // '9', (0x39)
SEG_UNSUPPORTED_CHAR, // ':', (0x3A)
SEG_UNSUPPORTED_CHAR, // ';', (0x3B)
SEG_UNSUPPORTED_CHAR, // '<', (0x3C)
SEG_D|SEG_G, // '=', (0x3D)
SEG_UNSUPPORTED_CHAR, // '>', (0x3E)
SEG_A|SEG_B|SEG_E|SEG_G, // '?', (0x3F)
SEG_A|SEG_B|SEG_D|SEG_E|SEG_F|SEG_G, // '@', (0x40)
SEG_A|SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'A', (0x41)
SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // 'B', (0x42)
SEG_A|SEG_D|SEG_E|SEG_F, // 'C', (0x43)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_G, // 'D', (0x44)
SEG_A|SEG_D|SEG_E|SEG_F|SEG_G, // 'E', (0x45)
SEG_A|SEG_E|SEG_F|SEG_G, // 'F', (0x46)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F, // 'G', (0x47)
SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'H', (0x48)
SEG_B|SEG_C, // 'I', (0x49)
SEG_B|SEG_C|SEG_D|SEG_E, // 'J', (0x4A)
SEG_UNSUPPORTED_CHAR, // 'K', (0x4B)
SEG_D|SEG_E|SEG_F, // 'L', (0x4C)
SEG_UNSUPPORTED_CHAR, // 'M', (0x4D)
SEG_C|SEG_E|SEG_G, // 'N', (0x4E)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // 'O', (0x4F)
SEG_A|SEG_B|SEG_E|SEG_F|SEG_G, // 'P', (0x50)
SEG_UNSUPPORTED_CHAR, // 'Q', (0x51)
SEG_E|SEG_G, // 'R', (0x52)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // 'S', (0x53)
SEG_UNSUPPORTED_CHAR, // 'T', (0x54)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // 'U', (0x55)
SEG_UNSUPPORTED_CHAR, // 'V', (0x56)
SEG_UNSUPPORTED_CHAR, // 'W', (0x57)
SEG_UNSUPPORTED_CHAR, // 'X', (0x58)
SEG_B|SEG_E|SEG_F|SEG_G, // 'Y', (0x59)
SEG_UNSUPPORTED_CHAR, // 'Z', (0x5A)
SEG_A|SEG_D|SEG_E|SEG_F, // '[', (0x5B)
SEG_UNSUPPORTED_CHAR, // '\', (0x5C)
SEG_A|SEG_B|SEG_C|SEG_D, // ']', (0x5D)
SEG_UNSUPPORTED_CHAR, // '^', (0x5E)
SEG_D, // '_', (0x5F)
SEG_F, // '`', (0x60)
SEG_A|SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'a', (0x61)
SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // 'b', (0x62)
SEG_D|SEG_E|SEG_G, // 'c', (0x63)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_G, // 'd', (0x64)
SEG_A|SEG_D|SEG_E|SEG_F|SEG_G, // 'e', (0x65)
SEG_A|SEG_E|SEG_F|SEG_G, // 'f', (0x66)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F, // 'g', (0x67)
SEG_C|SEG_E|SEG_F|SEG_G, // 'h', (0x68)
SEG_C, // 'i', (0x69)
SEG_B|SEG_C|SEG_D|SEG_E, // 'j', (0x6A)
SEG_UNSUPPORTED_CHAR, // 'k', (0x6B)
SEG_D|SEG_E|SEG_F, // 'l', (0x6C)
SEG_UNSUPPORTED_CHAR, // 'm', (0x6D)
SEG_C|SEG_E|SEG_G, // 'n', (0x6E)
SEG_C|SEG_D|SEG_E|SEG_G, // 'o', (0x6F)
SEG_A|SEG_B|SEG_E|SEG_F|SEG_G, // 'p', (0x70)
SEG_UNSUPPORTED_CHAR, // 'q', (0x71)
SEG_E|SEG_G, // 'r', (0x72)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // 's', (0x73)
SEG_UNSUPPORTED_CHAR, // 't', (0x74)
SEG_C|SEG_D|SEG_E, // 'u', (0x75)
SEG_UNSUPPORTED_CHAR, // 'v', (0x76)
SEG_UNSUPPORTED_CHAR, // 'w', (0x77)
SEG_UNSUPPORTED_CHAR, // 'x', (0x78)
SEG_B|SEG_E|SEG_F|SEG_G, // 'y', (0x79)
SEG_UNSUPPORTED_CHAR, // 'z', (0x7A)
SEG_B|SEG_C|SEG_G, // '{', (0x7B)
SEG_UNSUPPORTED_CHAR, // '|', (0x7C)
SEG_E|SEG_F|SEG_G, // '}', (0x7D)
SEG_UNSUPPORTED_CHAR, // '~', (0x7E)
};
void Display::setup(std::vector<uint8_t>& seg_to_out_map, std::vector<uint8_t>& pos_to_out_map) {
this->seg_to_out_map_ = seg_to_out_map;
this->pos_to_out_map_ = pos_to_out_map;
this->num_digits_ = pos_to_out_map.size();
ESP_LOGCONFIG(TAG, "Display digits: %u", this->num_digits_);
// setup font...
init_font__();
// display output buffer...
this->out_buf_size_ = this->num_digits_ * 3;
this->out_buf_ = new uint8_t[this->out_buf_size_]; // NOLINT
memset(this->out_buf_, 0, this->out_buf_size_);
// find smallest segment DOUT number...
this->seg_out_smallest_ = 19;
for (uint8_t i=0; i<this->seg_to_out_map_.size(); i++) {
if (this->seg_to_out_map_[i] < this->seg_out_smallest_)
this->seg_out_smallest_ = this->seg_to_out_map_[i];
}
ESP_LOGCONFIG(TAG, "Display smallest DOUT number: %u", this->seg_out_smallest_);
// calculate refresh period for 60Hz
this->refresh_period_us_ = 1000000 / 60 / this->num_digits_;
ESP_LOGCONFIG(TAG, "Set display refresh period: %" PRIu32 "us for %u digits @ 60Hz",
this->refresh_period_us_, this->num_digits_);
/* Setup display refresh.
* Using a timer is not an option, because the WiFi component uses timer as
* well, which leads to unstable refresh cycles (flickering). Therefore a
* thread on 2nd MCU core is used.
*/
xTaskCreatePinnedToCore(&Display::display_refresh_task_,
"display_refresh_task", // name
2048, // stack size
this, // pass component pointer as task parameter pv
1, // priority (one above IDLE task)
nullptr, // handle
1 // core
);
ESP_LOGCONFIG(TAG, "Display mode: %u", this->mode);
ESP_LOGCONFIG(TAG, "Display scroll mode: %u", this->scroll_mode);
}
void HOT Display::display_refresh_task_(void *pv) {
Display *display = (Display*)pv;
static uint count = display->num_digits_;
static uint current_pos = 1;
if (display->refresh_period_us_ == 0) {
ESP_LOGE(TAG, "Invalid display refresh period -> using default 2ms");
display->refresh_period_us_ = 2000;
}
while (true) {
// one-time verbose output for all positions after any content change...
if (display->disp_text_.content_changed) {
count = 0;
display->disp_text_.content_changed = false;
}
if (count < display->num_digits_) {
count++;
ESP_LOGVV(TAG, "%s(): SPI transfer for position %u: 0x%02x%02x%02x",
__func__, current_pos,
display->out_buf_[(current_pos-1)*3],
display->out_buf_[(current_pos-1)*3+1],
display->out_buf_[(current_pos-1)*3+2]);
}
// write MAX9621 data of current display position...
display->max6921_->write_data(&display->out_buf_[(current_pos-1)*3], 3);
// next display position...
if (++current_pos > display->num_digits_)
current_pos = 1;
delayMicroseconds(display->refresh_period_us_);
}
}
void Display::dump_config() {
char seg_name[3];
// display segment to DOUTx mapping...
for (uint i=0; i<this->seg_to_out_map_.size(); i++) {
if (i < 7) {
seg_name[0] = 'a' + i;
seg_name[1] = 0;
} else
strncpy(seg_name, "dp", sizeof(seg_name));
ESP_LOGCONFIG(TAG, " Display segment %2s: OUT%u", seg_name, this->seg_to_out_map_[i]);
}
// display position to DOUTx mapping...
for (uint i=0; i<this->seg_to_out_map_.size(); i++) {
ESP_LOGCONFIG(TAG, " Display position %2u: OUT%u", i, this->pos_to_out_map_[i]);
}
ESP_LOGCONFIG(TAG, " Brightness: %.1f", get_brightness());
}
/**
* @brief Checks if the given character activates the point segment only.
*
* @param c character to check
*
* @return true, if character activates the point segment only, otherwise false
*/
bool Display::isPointSegOnly(char c) {
return ((c == ',') || (c == '.'));
}
void Display::init_font__(void) {
uint8_t seg_data;
this->ascii_out_data_ = new uint8_t[ARRAY_ELEM_COUNT(ASCII_TO_SEG)]; // NOLINT
for (size_t ascii_idx=0; ascii_idx<ARRAY_ELEM_COUNT(ASCII_TO_SEG); ascii_idx++) {
this->ascii_out_data_[ascii_idx] = 0;
seg_data = progmem_read_byte(&ASCII_TO_SEG[ascii_idx]);
if (seg_data & SEG_A)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[0] % 8));
if (seg_data & SEG_B)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[1] % 8));
if (seg_data & SEG_C)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[2] % 8));
if (seg_data & SEG_D)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[3] % 8));
if (seg_data & SEG_E)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[4] % 8));
if (seg_data & SEG_F)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[5] % 8));
if (seg_data & SEG_G)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[6] % 8));
if (seg_data & SEG_DP)
this->ascii_out_data_[ascii_idx] |= (1 << (this->seg_to_out_map_[7] % 8));
}
}
/**
* @brief Clears the whole display buffer or only the given position.
*
* @param pos display position 0..n (optional, default=whole display)
*/
void Display::clear(int pos) {
if (pos < 0)
memset(this->out_buf_, 0, this->out_buf_size_); // clear whole display buffer
else if (pos < this->num_digits_)
memset(&this->out_buf_[pos*3], 0, 3); // clear display buffer at given position
else
ESP_LOGW(TAG, "Invalid display position %i (max=%u)", pos, this->num_digits_ - 1);
}
/**
* @brief Updates the display.
*/
void Display::update(void) {
// handle display brightness...
if (this->brightness_cfg_changed_) {
uint32_t inverted_duty = this->brightness_max_duty_ - \
this->brightness_max_duty_ * \
this->brightness_cfg_value_; // calc duty for low-active BLANK pin
ESP_LOGD(TAG, "Change display brightness to %.1f (off-time duty=%u/%u)",
brightness_cfg_value_, inverted_duty, this->brightness_max_duty_);
ledcWrite(this->brightness_pwm_channel_, inverted_duty);
this->brightness_cfg_changed_ = false;
}
// handle display scroll modes...
switch (this->scroll_mode) {
case DISP_SCROLL_MODE_LEFT:
update_out_buf_(this->disp_text_);
scroll_left_(this->disp_text_);
break;
case DISP_SCROLL_MODE_OFF:
default:
this->mode = DISP_MODE_PRINT;
break;
}
}
void Display::set_demo_mode(demo_mode_t mode, uint8_t cycle_num) {
uint text_idx, font_idx;
ESP_LOGD(TAG, "demo_mode=%i, cycle_num=%u", mode, cycle_num);
// this->display.demo_mode_cycle_num = cycle_num;
// this->display.demo_mode = mode;
switch (mode) {
case DEMO_MODE_SCROLL_FONT:
// generate scroll text based on font...
for (text_idx=0,font_idx=0; font_idx<ARRAY_ELEM_COUNT(ASCII_TO_SEG); font_idx++) {
if (this->ascii_out_data_[font_idx] > 0) // displayable character?
this->disp_text_.text[text_idx++] = ' ' + font_idx; // add character to string
if (text_idx >= sizeof(this->disp_text_.text) - 1) { // max. text buffer lenght reached?
ESP_LOGD(TAG, "Font too large for internal text buffer");
break;
}
}
this->disp_text_.text[text_idx] = 0;
ESP_LOGV(TAG, "%s(): text: %s", __func__, this->disp_text_.text);
set_scroll_mode(&this->disp_text_, DISP_SCROLL_MODE_LEFT, cycle_num);
break;
default:
break;
}
set_mode(DISP_MODE_OTHER);
clear();
}
void Display::set_demo_mode(const std::string& mode, uint8_t cycle_num) {
if (str_equals_case_insensitive(mode, "off")) {
this->set_demo_mode(DEMO_MODE_OFF, cycle_num);
} else if (str_equals_case_insensitive(mode, "scroll_font")) {
this->set_demo_mode(DEMO_MODE_SCROLL_FONT, cycle_num);
} else {
ESP_LOGW(TAG, "Invalid demo mode: %s", mode.c_str());
}
}
/**
* @brief Shows the given text on the display at given position.
*
* @param start_pos display position 0..n
* @param str text to display
*
* @return number of characters displayed
*/
int Display::set_text(uint8_t start_pos, const char *str) {
ESP_LOGVV(TAG, "%s(): str=%s, prev=%s", __func__, str, this->disp_text_.text);
if (strncmp(str, this->disp_text_.text, sizeof(this->disp_text_.text)) == 0) // text not changed?
// if (strncmp(str, &this->disp_text_.text[this->disp_text_.visible_idx],
// this->disp_text_.visible_len) == 0) // text not changed?
return strlen(str); // yes -> exit function
ESP_LOGV(TAG, "%s(): text changed: str=%s, prev=%s", __func__, str, this->disp_text_.text);
// store new text...
this->disp_text_.set(start_pos, this->num_digits_ - 1, str);
// update visible text...
this->disp_text_.visible_idx = 0;
switch (this->scroll_mode) {
case DISP_SCROLL_MODE_LEFT:
this->disp_text_.visible_len = 1;
break;
case DISP_SCROLL_MODE_OFF:
default:
this->disp_text_.visible_len = std::min(strlen(this->disp_text_.text), this->num_digits_-start_pos);
break;
}
return update_out_buf_(this->disp_text_);
}
/**
* @brief Updates the display buffer containing the MAX6921 OUT data according
* to current visible text.
*
* @param text display text object
* @return number of visible characters
*/
int Display::update_out_buf_(DisplayText& disp_text) {
uint visible_idx_offset = 0;
for (uint pos=0; pos<this->num_digits_; pos++) {
char pos_char;
uint32_t out_data;
bool bGetNextChar, bClearPos = true;
do {
// determine character for current display position...
if ((pos < disp_text.start_pos) || // empty position before text or
((disp_text.start_pos == 0) && (pos >= disp_text.visible_len))) // empty positions after text?
pos_char = ' ';
else
pos_char = disp_text.text[disp_text.visible_idx + visible_idx_offset++];
// special handling for point segment...
bGetNextChar = false;
if (isPointSegOnly(pos_char)) { // is point segment only?
if (disp_text.visible_idx+visible_idx_offset-1 > 0) { // not the 1st text character?
if (isPointSegOnly(disp_text.text[disp_text.visible_idx + visible_idx_offset - 2])) { // previous text character wasn't a point?
if (pos == 0) { // 1st (most left) display position?
bGetNextChar = true; // yes -> ignore point, get next character
} else {
--pos; // no -> add point to previous display position
bClearPos = false;
}
}
}
}
} while (bGetNextChar);
if (bClearPos)
clear(pos);
// create segment data...
if ((pos_char >= ' ') &&
((pos_char - ' ') < ARRAY_ELEM_COUNT(ASCII_TO_SEG))) { // supported char?
out_data = this->ascii_out_data_[pos_char - ' ']; // yes ->
} else {
ESP_LOGW(TAG, "Encountered unsupported character '%c (0x%02x)'!", pos_char, pos_char);
out_data = SEG_UNSUPPORTED_CHAR;
}
ESP_LOGVV(TAG, "%s(): segment data: 0x%06x", __func__, out_data);
#if 0
// At the moment an unsupport character is equal to blank (' ').
// To distinguish an unsupported character from blank we would need to
// increase font data type from uint8_t to uint16_t!
if (out_data == SEG_UNSUPPORTED_CHAR) {
ESP_LOGW(TAG, "Encountered character '%c (0x%02x)' with no display representation!", *vi_text, *vi_text);
}
#endif
// shift data to the smallest segment OUT position...
out_data <<= (this->seg_out_smallest_);
ESP_LOGVV(TAG, "%s(): segment data shifted to first segment bit (OUT%u): 0x%06x",
__func__, this->seg_out_smallest_, out_data);
// add position data...
out_data |= (1 << this->pos_to_out_map_[pos]);
ESP_LOGVV(TAG, "%s(): OUT data with position: 0x%06x", __func__, out_data);
// write to appropriate position of display buffer...
this->out_buf_[pos*3+0] |= (uint8_t)((out_data >> 16) & 0xFF);
this->out_buf_[pos*3+1] |= (uint8_t)((out_data >> 8) & 0xFF);
this->out_buf_[pos*3+2] |= (uint8_t)(out_data & 0xFF);
ESP_LOGVV(TAG, "%s(): display buffer of position %u: 0x%02x%02x%02x",
__func__, pos+1, this->out_buf_[pos*3+0], this->out_buf_[pos*3+1],
this->out_buf_[pos*3+2]);
ESP_LOGV(TAG, "%s(): pos=%u, char='%c' (0x%02x), vi-idx=%u, vi-idx-off=%u, vi-len=%u",
__func__, pos, pos_char, pos_char, disp_text.visible_idx, visible_idx_offset, disp_text.visible_len);
}
disp_text.content_changed = true;
return this->num_digits_ - disp_text.start_pos;
}
/**
* @brief Constructor.
*/
DisplayScrollMode::DisplayScrollMode() {
this->scroll_mode = DISP_SCROLL_MODE_OFF;
this->disp_text_ = NULL;
}
/**
* @brief Inits the text object according to scroll mode.
*/
void DisplayScrollMode::init_scroll_mode_(void) {
switch (this->scroll_mode) {
case DISP_SCROLL_MODE_LEFT:
this->disp_text_->start_pos = this->disp_text_->max_pos; // start at right side
this->disp_text_->visible_idx = 0;
this->disp_text_->visible_len = 1;
break;
}
}
/**
* @brief Sets the scroll mode.
*
* @param disp_text display text object
* @param mode scroll mode
* @param cycle_num number of scroll cycles (optional, default=endless)
*/
void DisplayScrollMode::set_scroll_mode(DisplayText *disp_text, display_scroll_mode_t mode, uint8_t cycle_num)
{
if (!disp_text) {
ESP_LOGE(TAG, "Invalid display text object");
return;
}
if (mode >= DISP_SCROLL_MODE_LAST_ENUM) {
ESP_LOGE(TAG, "Invalid display scroll mode: %i", mode);
return;
}
this->disp_text_ = disp_text;
this->scroll_mode = mode;
this->cycle_num = cycle_num;
init_scroll_mode_();
}
/**
* @brief Updates the mode "scroll left".
*
* @param text display text object
*/
void DisplayScrollMode::scroll_left_(DisplayText& disp_text) {
ESP_LOGV(TAG, "%s(): ENTRY: start-idx=%u, text-idx=%u, text-len=%u", __func__,
disp_text.start_pos, disp_text.visible_idx, disp_text.visible_len);
// update visible text...
if (disp_text.start_pos > 0) { // no start at left side of display (scroll in from right side)?
--disp_text.start_pos; // decrement display start position
++disp_text.visible_len; // increment visible text length
} else {
++disp_text.visible_idx; // increment visible start index
if ((disp_text.visible_idx + disp_text.visible_len) >= strlen(disp_text.text)) // visible part reached at end of text?
--disp_text.visible_len; // decrement visible text length (scroll out to left side)
}
// update scroll mode...
if (disp_text.visible_len == 0) {
init_scroll_mode_();
if (this->cycle_num > 0) {
if (--this->cycle_num == 0) {
this->scroll_mode = DISP_SCROLL_MODE_OFF;
return;
}
}
}
ESP_LOGV(TAG, "%s(): EXIT: start-idx=%u, text-idx=%u, text-len=%u", __func__,
disp_text.start_pos, disp_text.visible_idx, disp_text.visible_len);
}
/**
* @brief Constructor.
*/
DisplayText::DisplayText() {
this->text[0] = 0;
this->visible_idx = 0;
this->visible_len = 0;
this->content_changed = false;
this->start_pos = 0;
this->max_pos = 0;
}
/**
* @brief Stores the given text.
*
* @param start_pos display start position (0..n)
* @param max_pos display max. position
* @param str text to store
*
* @return number of stored characters
*/
int DisplayText::set(uint start_pos, uint max_pos, const char *str) {
// check start position...
if (start_pos >= max_pos) {
ESP_LOGW(TAG, "Invalid start position: %u");
this->start_pos = 0;
}
else
this->start_pos = start_pos;
this->max_pos = max_pos;
strncpy(this->text, str, sizeof(this->text) - 1);
this->text[sizeof(this->text)-1] = 0;
return strlen(this->text);
}
/**
* @brief Configures the PWM for display brightness control.
*
* @param pwm_pin_no PWM pin number
* @param channel PWM channel
* @param resolution PWM resolution
* @param freq PWM frequency
*
* @return frequency supported by hardware (0 = no support)
*/
uint32_t DisplayBrightness::config_brightness_pwm(uint8_t pwm_pin_no, uint8_t channel,
uint8_t resolution, uint32_t freq) {
uint32_t freq_supported;
if ((freq_supported = ledcSetup(channel, freq, resolution)) != 0) {
ledcAttachPin(pwm_pin_no, channel);
this->brightness_pwm_channel_ = channel;
this->brightness_max_duty_ = pow(2,resolution); // max. duty value for given resolution
ESP_LOGD(TAG, "Prepare brightness PWM: pin=%u, channel=%u, resolution=%ubit, freq=%uHz",
pwm_pin_no, channel, resolution, freq_supported);
} else {
ESP_LOGD(TAG, "Failed to configure brightness PWM");
}
return freq_supported;
}
/**
* @brief Sets the display brightness.
*
* @param percent brightness in percent (0.0-1.0)
*/
void DisplayBrightness::set_brightness(float percent) {
if ((percent >= 0.0) && (percent <= 1.0)) {
this->brightness_cfg_value_ = percent;
this->brightness_cfg_changed_ = true;
} else
ESP_LOGW(TAG, "Invalid brightness value: %f", percent);
}
/**
* @brief Constructor.
*/
DisplayMode::DisplayMode() {
this->mode = DISP_MODE_PRINT;
}
void DisplayMode::set_mode(display_mode_t display_mode) {
if (display_mode >= DISP_MODE_LAST_ENUM) {
ESP_LOGE(TAG, "Invalid display mode: %i", display_mode);
return;
}
this->mode = display_mode;
ESP_LOGD(TAG, "Set display mode: %i", this->mode);
}
} // namespace max6921
} // namespace esphome

View file

@ -0,0 +1,121 @@
#pragma once
#include <esp32-hal-gpio.h>
#include <string>
#include <vector>
#include "esphome/core/time.h"
namespace esphome {
namespace max6921 {
class MAX6921Component;
#define FONT_SIZE 95
#define DISPLAY_TEXT_LEN FONT_SIZE // at least font size for demo mode "scroll font"
enum display_mode_t {
DISP_MODE_PRINT, // input by it-functions
DISP_MODE_OTHER, // input by actions
DISP_MODE_LAST_ENUM
};
enum display_scroll_mode_t {
DISP_SCROLL_MODE_OFF, // show text at given position, cut if too long
DISP_SCROLL_MODE_LEFT, // scroll left, start with 1st char at right position
DISP_SCROLL_MODE_LAST_ENUM
};
enum demo_mode_t {
DEMO_MODE_OFF,
DEMO_MODE_SCROLL_FONT,
};
class DisplayBrightness
{
public:
uint32_t config_brightness_pwm(uint8_t pwm_pin_no, uint8_t channel,
uint8_t resolution, uint32_t freq_wanted);
float get_brightness(void) { return this->brightness_cfg_value_; }
void set_brightness(float percent);
protected:
float brightness_cfg_value_; // brightness in percent (0.0-1.0)
bool brightness_cfg_changed_;
uint32_t brightness_max_duty_;
uint8_t brightness_pwm_channel_;
};
class DisplayText
{
public:
bool content_changed;
uint max_pos; // max. display position
uint start_pos; // current display start position (0..n)
char text[DISPLAY_TEXT_LEN + 1]; // current text to display (may be larger then display)
uint visible_idx; // current index of start of visible part
uint visible_len; // current length of visible text
DisplayText();
int set(uint start_pos, uint max_pos, const char *str);
};
class DisplayMode
{
public:
display_mode_t mode; // display mode
DisplayMode();
void set_mode(display_mode_t display_mode);
protected:
};
class DisplayScrollMode
{
public:
display_scroll_mode_t scroll_mode; // scroll mode
uint8_t cycle_num;
DisplayScrollMode();
void set_scroll_mode(DisplayText *disp_text, display_scroll_mode_t scroll_mode, uint8_t cycle_num=0);
protected:
DisplayText *disp_text_;
void init_scroll_mode_(void);
void scroll_left_(DisplayText& disp_text);
};
class Display : public DisplayBrightness,
public DisplayMode,
public DisplayScrollMode
{
public:
Display(MAX6921Component *max6921) { max6921_ = max6921; }
void clear(int pos=-1);
void dump_config();
bool isPointSegOnly(char c);
void setup(std::vector<uint8_t>& seg_to_out_map, std::vector<uint8_t>& pos_to_out_map);
void set_demo_mode(demo_mode_t mode, uint8_t cycle_num);
void set_demo_mode(const std::string& mode, uint8_t cycle_num);
int set_text(uint8_t start_pos, const char *str);
void update(void);
protected:
MAX6921Component *max6921_;
std::vector<uint8_t> seg_to_out_map_; // mapping of display segments to MAX6921 OUT pins
std::vector<uint8_t> pos_to_out_map_; // mapping of display positions to MAX6921 OUT pins
uint num_digits_; // number of display positions
uint8_t *ascii_out_data_;
uint8_t *out_buf_; // current MAX9621 data (3 bytes for every display position)
size_t out_buf_size_;
uint seg_out_smallest_;
uint32_t refresh_period_us_;
DisplayText disp_text_;
static void display_refresh_task_(void *pv);
int update_out_buf_(DisplayText& disp_text);
private:
void init_font__(void);
};
} // namespace max6921
} // namespace esphome

View file

@ -2,12 +2,17 @@ from esphome import pins, automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display, spi
from esphome.const import CONF_ID, CONF_BRIGHTNESS, CONF_LAMBDA
from esphome.const import (
CONF_ID,
CONF_BRIGHTNESS,
CONF_LAMBDA,
CONF_MODE,
# CONF_POSITION,
)
DEPENDENCIES = ["spi", "esp32"]
CODEOWNERS = ["@endym"]
CONF_DEMO_MODE = "demo_mode"
CONF_LOAD_PIN = "load_pin"
CONF_BLANK_PIN = "blank_pin"
CONF_OUT_PIN_MAPPING = "out_pin_mapping"
@ -34,6 +39,9 @@ CONF_POS_9_PIN = "pos_9_pin"
CONF_POS_10_PIN = "pos_10_pin"
CONF_POS_11_PIN = "pos_11_pin"
CONF_POS_12_PIN = "pos_12_pin"
# CONF_DEMO_MODE = "demo_mode"
CONF_CYCLE_NUM = "cycle_num"
CONF_TEXT = "text"
max6921_ns = cg.esphome_ns.namespace("max6921")
@ -42,6 +50,8 @@ MAX6921Component = max6921_ns.class_(
)
MAX6921ComponentRef = MAX6921Component.operator("ref")
SetBrightnessAction = max6921_ns.class_("SetBrightnessAction", automation.Action)
SetDemoModeAction = max6921_ns.class_("SetDemoModeAction", automation.Action)
SetTextAction = max6921_ns.class_("SetTextAction", automation.Action)
# optional "demo_mode" configuration
@ -116,9 +126,9 @@ CONFIG_SCHEMA = (
cv.Required(CONF_BLANK_PIN): pins.internal_gpio_output_pin_schema,
cv.Required(CONF_OUT_PIN_MAPPING): OUT_PIN_MAPPING_SCHEMA,
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.templatable(cv.percentage),
cv.Optional(CONF_DEMO_MODE, default=CONF_DEMO_MODE_OFF): cv.enum(
DEMO_MODES
),
# cv.Optional(CONF_DEMO_MODE, default=CONF_DEMO_MODE_OFF): cv.enum(
# DEMO_MODES
# ),
}
)
.extend(cv.polling_component_schema("500ms"))
@ -154,7 +164,7 @@ async def to_code(config):
)
)
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
cg.add(var.set_demo_mode(config[CONF_DEMO_MODE]))
# cg.add(var.set_demo_mode(config[CONF_DEMO_MODE]))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
@ -192,3 +202,66 @@ async def max6921_set_brightness_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float)
cg.add(var.set_brightness(template_))
return var
"""
def validate_action_set_text(value):
print(f"validate_action_set_text: {value}")
return value
ACTION_SET_TEXT_SCHEMA = cv.All(
automation.maybe_simple_id(
ACTION_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_TEXT): cv.templatable(cv.string),
cv.Optional(CONF_POSITION): cv.templatable(cv.int_range(min=0, max=13))
}
)
)
),
validate_action_set_text,
)
@automation.register_action(
"max6921.set_text", SetTextAction, ACTION_SET_TEXT_SCHEMA
)
async def max6921_set_text_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_TEXT], args, cg.std_string)
cg.add(var.set_text(template_))
return var
"""
ACTION_SET_DEMO_MODE_SCHEMA = cv.All(
automation.maybe_simple_id(
ACTION_SCHEMA.extend(
cv.Schema(
{
# cv.Required(CONF_MODE): cv.templatable(cv.enum(DEMO_MODES, lower=True)),
cv.Required(CONF_MODE): cv.templatable(cv.string),
cv.Optional(CONF_CYCLE_NUM, default=0): cv.templatable(cv.uint8_t),
}
)
)
),
)
@automation.register_action(
"max6921.set_demo_mode", SetDemoModeAction, ACTION_SET_DEMO_MODE_SCHEMA
)
async def max6921_set_demo_mode_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
# template_ = await cg.templatable(config[CONF_MODE], args, DemoMode)
template_ = await cg.templatable(config[CONF_MODE], args, cg.std_string)
cg.add(var.set_mode(template_))
if CONF_CYCLE_NUM in config:
template_ = await cg.templatable(config[CONF_CYCLE_NUM], args, cg.uint8)
cg.add(var.set_cycle_num(template_))
return var

View file

@ -1,408 +1,51 @@
// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/MAX6921-MAX6931.pdf
#include "max6921.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
#include <cinttypes>
#include "display.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "max6921.h"
namespace esphome {
namespace max6921 {
#define ARRAY_ELEM_COUNT(array) (sizeof(array)/sizeof(array[0]))
static const char *const TAG = "max6921";
// max. display intensity (brightness)...
static const uint MAX_DISPLAY_INTENSITY = 16;
// segments of 7-segment character
static const uint8_t SEG_A = (1<<0);
static const uint8_t SEG_B = (1<<1);
static const uint8_t SEG_C = (1<<2);
static const uint8_t SEG_D = (1<<3);
static const uint8_t SEG_E = (1<<4);
static const uint8_t SEG_F = (1<<5);
static const uint8_t SEG_G = (1<<6);
static const uint8_t SEG_DP = (1<<7);
static const uint8_t SEG_UNSUPPORTED_CHAR = 0;
// ASCII table from 0x20..0x7E
const uint8_t ASCII_TO_SEG[95] PROGMEM = {
0, // ' ', (0x20)
SEG_B|SEG_C|SEG_DP, // '!', (0x21)
SEG_B|SEG_F, // '"', (0x22)
SEG_UNSUPPORTED_CHAR, // '#', (0x23)
SEG_UNSUPPORTED_CHAR, // '$', (0x24)
SEG_UNSUPPORTED_CHAR, // '%', (0x25)
SEG_UNSUPPORTED_CHAR, // '&', (0x26)
SEG_F, // ''', (0x27)
SEG_A|SEG_D|SEG_E|SEG_F, // '(', (0x28)
SEG_A|SEG_B|SEG_C|SEG_D, // ')', (0x29)
SEG_UNSUPPORTED_CHAR, // '*', (0x2A)
SEG_UNSUPPORTED_CHAR, // '+', (0x2B)
SEG_DP, // ',', (0x2C)
SEG_G, // '-', (0x2D)
SEG_DP, // '.', (0x2E)
SEG_UNSUPPORTED_CHAR, // '/', (0x2F)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // '0', (0x30)
SEG_B|SEG_C, // '1', (0x31)
SEG_A|SEG_B|SEG_D|SEG_E|SEG_G, // '2', (0x32)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_G, // '3', (0x33)
SEG_B|SEG_C|SEG_F|SEG_G, // '4', (0x34)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // '5', (0x35)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // '6', (0x36)
SEG_A|SEG_B|SEG_C, // '7', (0x37)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // '8', (0x38)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_F|SEG_G, // '9', (0x39)
SEG_UNSUPPORTED_CHAR, // ':', (0x3A)
SEG_UNSUPPORTED_CHAR, // ';', (0x3B)
SEG_UNSUPPORTED_CHAR, // '<', (0x3C)
SEG_D|SEG_G, // '=', (0x3D)
SEG_UNSUPPORTED_CHAR, // '>', (0x3E)
SEG_A|SEG_B|SEG_E|SEG_G, // '?', (0x3F)
SEG_A|SEG_B|SEG_D|SEG_E|SEG_F|SEG_G, // '@', (0x40)
SEG_A|SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'A', (0x41)
SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // 'B', (0x42)
SEG_A|SEG_D|SEG_E|SEG_F, // 'C', (0x43)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_G, // 'D', (0x44)
SEG_A|SEG_D|SEG_E|SEG_F|SEG_G, // 'E', (0x45)
SEG_A|SEG_E|SEG_F|SEG_G, // 'F', (0x46)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F, // 'G', (0x47)
SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'H', (0x48)
SEG_B|SEG_C, // 'I', (0x49)
SEG_B|SEG_C|SEG_D|SEG_E, // 'J', (0x4A)
SEG_UNSUPPORTED_CHAR, // 'K', (0x4B)
SEG_D|SEG_E|SEG_F, // 'L', (0x4C)
SEG_UNSUPPORTED_CHAR, // 'M', (0x4D)
SEG_C|SEG_E|SEG_G, // 'N', (0x4E)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // 'O', (0x4F)
SEG_A|SEG_B|SEG_E|SEG_F|SEG_G, // 'P', (0x50)
SEG_UNSUPPORTED_CHAR, // 'Q', (0x51)
SEG_E|SEG_G, // 'R', (0x52)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // 'S', (0x53)
SEG_UNSUPPORTED_CHAR, // 'T', (0x54)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // 'U', (0x55)
SEG_UNSUPPORTED_CHAR, // 'V', (0x56)
SEG_UNSUPPORTED_CHAR, // 'W', (0x57)
SEG_UNSUPPORTED_CHAR, // 'X', (0x58)
SEG_B|SEG_E|SEG_F|SEG_G, // 'Y', (0x59)
SEG_UNSUPPORTED_CHAR, // 'Z', (0x5A)
SEG_A|SEG_D|SEG_E|SEG_F, // '[', (0x5B)
SEG_UNSUPPORTED_CHAR, // '\', (0x5C)
SEG_A|SEG_B|SEG_C|SEG_D, // ']', (0x5D)
SEG_UNSUPPORTED_CHAR, // '^', (0x5E)
SEG_D, // '_', (0x5F)
SEG_F, // '`', (0x60)
SEG_A|SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'a', (0x61)
SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // 'b', (0x62)
SEG_D|SEG_E|SEG_G, // 'c', (0x63)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_G, // 'd', (0x64)
SEG_A|SEG_D|SEG_E|SEG_F|SEG_G, // 'e', (0x65)
SEG_A|SEG_E|SEG_F|SEG_G, // 'f', (0x66)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F, // 'g', (0x67)
SEG_C|SEG_E|SEG_F|SEG_G, // 'h', (0x68)
SEG_C, // 'i', (0x69)
SEG_B|SEG_C|SEG_D|SEG_E, // 'j', (0x6A)
SEG_UNSUPPORTED_CHAR, // 'k', (0x6B)
SEG_D|SEG_E|SEG_F, // 'l', (0x6C)
SEG_UNSUPPORTED_CHAR, // 'm', (0x6D)
SEG_C|SEG_E|SEG_G, // 'n', (0x6E)
SEG_C|SEG_D|SEG_E|SEG_G, // 'o', (0x6F)
SEG_A|SEG_B|SEG_E|SEG_F|SEG_G, // 'p', (0x70)
SEG_UNSUPPORTED_CHAR, // 'q', (0x71)
SEG_E|SEG_G, // 'r', (0x72)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // 's', (0x73)
SEG_UNSUPPORTED_CHAR, // 't', (0x74)
SEG_C|SEG_D|SEG_E, // 'u', (0x75)
SEG_UNSUPPORTED_CHAR, // 'v', (0x76)
SEG_UNSUPPORTED_CHAR, // 'w', (0x77)
SEG_UNSUPPORTED_CHAR, // 'x', (0x78)
SEG_B|SEG_E|SEG_F|SEG_G, // 'y', (0x79)
SEG_UNSUPPORTED_CHAR, // 'z', (0x7A)
SEG_B|SEG_C|SEG_G, // '{', (0x7B)
SEG_UNSUPPORTED_CHAR, // '|', (0x7C)
SEG_E|SEG_F|SEG_G, // '}', (0x7D)
SEG_UNSUPPORTED_CHAR, // '~', (0x7E)
};
// MAX6921Component *global_max6921; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void MAX6921Component::init_display_(void) {
uint32_t freq;
const uint32_t PWM_FREQ_WANTED = 5000;
const uint8_t PWM_RESOLUTION = 8;
// setup PWM for blank pin (intensity)...
this->display_.brightness.pwm_channel = 0;
freq = ledcSetup(this->display_.brightness.pwm_channel, PWM_FREQ_WANTED, PWM_RESOLUTION);
if (freq != 0) {
ledcAttachPin(this->display_.brightness.pwm_pin->get_pin(), this->display_.brightness.pwm_channel);
this->display_.brightness.max_duty = pow(2,PWM_RESOLUTION); // max. duty value for given resolution
this->display_.brightness.duty_quotient = this->display_.brightness.max_duty / MAX_DISPLAY_INTENSITY; // pre-calc fixed duty quotient (256 / 16)
ESP_LOGD(TAG, "Prepare intensity PWM: pin=%u, channel=%u, freq=%uHz, resolution=%ubit, duty quotient=%u",
this->display_.brightness.pwm_pin->get_pin(),
this->display_.brightness.pwm_channel, freq, PWM_RESOLUTION,
this->display_.brightness.duty_quotient);
} else {
ESP_LOGE(TAG, "Failed to configure PWM -> set to max. intensity");
pinMode(this->display_.brightness.pwm_pin->get_pin(), OUTPUT);
this->disable_blank(); // enable display (max. intensity)
}
// display output buffer...
this->display_.out_buf_size_ = this->display_.num_digits * 3;
this->display_.out_buf_ = new uint8_t[this->display_.out_buf_size_]; // NOLINT
memset(this->display_.out_buf_, 0, this->display_.out_buf_size_);
// display text buffer...
this->display_.current_text_buf_size = this->display_.num_digits * 2 + 1; // twice number of digits, because of possible points + string terminator
this->display_.current_text = new char[this->display_.current_text_buf_size]; // NOLINT
this->display_.current_text[0] = 0;
this->display_.text_changed = false;
// display position...
this->display_.current_pos = 1;
// find smallest segment DOUT number...
this->display_.seg_out_smallest = 19;
for (uint8_t i=0; i<this->display_.seg_to_out_map.size(); i++) {
if (this->display_.seg_to_out_map[i] < this->display_.seg_out_smallest)
this->display_.seg_out_smallest = this->display_.seg_to_out_map[i];
}
// calculate refresh period for 60Hz
this->display_.refresh_period_us = 1000000 / 60 / this->display_.num_digits;
ESP_LOGD(TAG, "Set display refresh period: %" PRIu32 "us for %u digits @ 60Hz",
this->display_.refresh_period_us, this->display_.num_digits);
}
void MAX6921Component::init_font_(void) {
uint8_t seg_data;
this->ascii_out_data_ = new uint8_t[ARRAY_ELEM_COUNT(ASCII_TO_SEG)]; // NOLINT
for (size_t ascii_idx=0; ascii_idx<ARRAY_ELEM_COUNT(ASCII_TO_SEG); ascii_idx++) {
this->ascii_out_data_[ascii_idx] = 0;
seg_data = progmem_read_byte(&ASCII_TO_SEG[ascii_idx]);
if (seg_data & SEG_A)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[0] % 8));
if (seg_data & SEG_B)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[1] % 8));
if (seg_data & SEG_C)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[2] % 8));
if (seg_data & SEG_D)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[3] % 8));
if (seg_data & SEG_E)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[4] % 8));
if (seg_data & SEG_F)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[5] % 8));
if (seg_data & SEG_G)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[6] % 8));
if (seg_data & SEG_DP)
this->ascii_out_data_[ascii_idx] |= (1 << (this->display_.seg_to_out_map[7] % 8));
}
}
/**
* @brief Clears the whole display buffer or only the given position.
*
* @param pos display position 0..n (optional, default=whole display)
*/
void MAX6921Component::clear_display(int pos) {
if (pos < 0)
memset(this->display_.out_buf_, 0, this->display_.out_buf_size_); // clear whole display buffer
else if (pos < this->display_.num_digits)
memset(&this->display_.out_buf_[pos*3], 0, 3); // clear display buffer at given position
else
ESP_LOGW(TAG, "Invalid display position %i (max=%u)", pos, this->display_.num_digits - 1);
}
/**
* @brief Shows the given text on the display at given position.
*
* @param start_pos display position 0..n
* @param str text to display
*
* @return number of characters displayed
*/
int MAX6921Component::set_display(uint8_t start_pos, const char *str) {
uint8_t pos = start_pos;
for (; *str != '\0'; str++) {
uint32_t out_data;
if (pos >= this->display_.num_digits) {
ESP_LOGE(TAG, "MAX6921 string too long or invalid position for the display!");
break;
}
// create segment data...
ESP_LOGVV(TAG, "%s(): pos: %u, char: '%c' (0x%02x)", __func__, pos+1, *str, *str);
if ((*str >= ' ') &&
((*str - ' ') < ARRAY_ELEM_COUNT(ASCII_TO_SEG))) { // supported char?
out_data = this->ascii_out_data_[*str - ' ']; // yes ->
} else {
ESP_LOGW(TAG, "Encountered unsupported character '%c (0x%02x)'!", *str, *str);
out_data = SEG_UNSUPPORTED_CHAR;
}
ESP_LOGVV(TAG, "%s(): segment data: 0x%06x", __func__, out_data);
#if 0
// At the moment an unsupport character is equal to blank (' ').
// To distinguish an unsupported character from blank we would need to
// increase font data type from uint8_t to uint16_t!
if (out_data == SEG_UNSUPPORTED_CHAR) {
ESP_LOGW(TAG, "Encountered character '%c (0x%02x)' with no display representation!", *str, *str);
}
#endif
if (((*str == ',') || (*str == '.')) && (pos > start_pos)) // is point/comma?
pos--; // yes -> modify display buffer of previous position
else
this->clear_display(pos); // no -> clear display buffer of current position (for later OR operation)
// shift data to the smallest segment OUT position...
out_data <<= (this->display_.seg_out_smallest);
ESP_LOGVV(TAG, "%s(): segment data shifted to first segment bit (OUT%u): 0x%06x",
__func__, this->display_.seg_out_smallest, out_data);
// add position data...
out_data |= (1 << this->display_.pos_to_out_map[pos]);
ESP_LOGVV(TAG, "%s(): OUT data with position: 0x%06x", __func__, out_data);
// write to appropriate position of display buffer...
this->display_.out_buf_[pos*3+0] |= (uint8_t)((out_data >> 16) & 0xFF);
this->display_.out_buf_[pos*3+1] |= (uint8_t)((out_data >> 8) & 0xFF);
this->display_.out_buf_[pos*3+2] |= (uint8_t)(out_data & 0xFF);
ESP_LOGVV(TAG, "%s(): display buffer of position %u: 0x%02x%02x%02x",
__func__, pos+1, this->display_.out_buf_[pos*3+0],
this->display_.out_buf_[pos*3+1], this->display_.out_buf_[pos*3+2]);
pos++;
}
this->display_.text_changed = true;
return pos - start_pos;
}
void MAX6921Component::update_display_(void) {
// handle display intensity...
if (this->display_.brightness.config_changed) {
// calc duty for low-active BLANK pin...
uint32_t inverted_duty = this->display_.brightness.max_duty - \
this->display_.brightness.max_duty * \
this->display_.brightness.config_value;
ESP_LOGD(TAG, "Change display brightness to %.1f (off-time duty=%u/%u)",
this->display_.brightness.config_value, inverted_duty,
this->display_.brightness.max_duty);
ledcWrite(this->display_.brightness.pwm_channel, inverted_duty);
this->display_.brightness.config_changed = false;
}
// handle demo modes...
switch (this->get_demo_mode()) {
case DEMO_MODE_SCROLL_FONT:
this->update_demo_mode_scroll_font_();
break;
default:
break;
}
}
void MAX6921Component::update_demo_mode_scroll_font_(void) {
static char *text = new char[this->display_.num_digits + 2 + 1]; // comma and point do not use an extra display position
static uint8_t start_pos = this->display_.num_digits - 1; // start at right side
static uint start_font_idx = 0;
uint font_idx, text_idx = 0, char_with_point_num = 0;
// build text...
font_idx = start_font_idx;
ESP_LOGV(TAG, "%s(): ENTRY: start-pos=%u, start-font-idx=%u",
__func__, start_pos, start_font_idx);
do {
if (text_idx < start_pos) { // before start position?
text[text_idx++] = ' '; // add blank to string
} else {
if (this->ascii_out_data_[font_idx] > 0) { // displayable character?
text[text_idx] = ' ' + font_idx; // add character to string
if ((text[text_idx] == '.') || (text[text_idx] == ',')) { // point-only character?
if (text_idx > 0) { // yes -> point after a character?
++text_idx;
++char_with_point_num;
}
} else
++text_idx;
}
font_idx = (font_idx + 1) % ARRAY_ELEM_COUNT(ASCII_TO_SEG); // next font character
}
ESP_LOGV(TAG, "%s(): LOOP: pos=%u, font-idx=%u, char='%c'",
__func__, (text_idx>0)?text_idx-1:0, font_idx, (text_idx>0)?text[text_idx-1]:' ');
} while ((text_idx - char_with_point_num) < this->display_.num_digits);
text[text_idx] = 0;
// determine next start font index...
if (start_pos == 0) {
start_font_idx = text[1] - ' ';
}
// update display start position...
if (start_pos > 0)
--start_pos;
ESP_LOGV(TAG, "%s(): EXIT: start-pos=%u, start-font-idx=%u, text={%s}",
__func__, start_pos, start_font_idx, text);
this->set_display(0, text);
}
float MAX6921Component::get_setup_priority() const { return setup_priority::HARDWARE; }
void MAX6921Component::setup() {
const uint32_t PWM_FREQ_WANTED = 5000;
const uint8_t PWM_RESOLUTION = 8;
ESP_LOGCONFIG(TAG, "Setting up MAX6921...");
// global_max6921 = this;
this->spi_setup();
this->load_pin_->setup();
this->load_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->disable_load(); // disable output latch
this->init_display_();
this->init_font_();
this->disable_load_(); // disable output latch
this->display_ = new Display(this);
this->display_->setup(this->seg_to_out_map__, this->pos_to_out_map__);
/* Setup display refresh.
* Using a timer is not an option, because the WiFi component uses timer as
* well, which leads to unstable refresh cycles (flickering). Therefore a
* thread on 2nd MCU core is used.
*/
xTaskCreatePinnedToCore(&MAX6921Component::display_refresh_task,
"display_refresh_task", // name
2048, // stack size
this, // pass component pointer as task parameter pv
1, // priority (one above IDLE task)
nullptr, // handle
1 // core
);
// setup display brightness (PWM for BLANK pin)...
if (this->display_->config_brightness_pwm(this->blank_pin_->get_pin(), 0,
PWM_RESOLUTION, PWM_FREQ_WANTED) == 0) {
ESP_LOGE(TAG, "Failed to configure PWM -> set to max. brightness");
pinMode(this->blank_pin_->get_pin(), OUTPUT);
this->disable_blank_(); // enable display (max. brightness)
}
this->setup_finished = true;
}
void MAX6921Component::dump_config() {
char seg_name[3];
ESP_LOGCONFIG(TAG, "MAX6921:");
LOG_PIN(" LOAD Pin: ", this->load_pin_);
ESP_LOGCONFIG(TAG, " BLANK Pin: GPIO%u", this->display_.brightness.pwm_pin->get_pin());
// display segment to DOUTx mapping...
for (uint i=0; i<this->display_.seg_to_out_map.size(); i++) {
if (i < 7) {
seg_name[0] = 'a' + i;
seg_name[1] = 0;
} else
strncpy(seg_name, "dp", sizeof(seg_name));
ESP_LOGCONFIG(TAG, " Display segment %2s: OUT%u", seg_name, this->display_.seg_to_out_map[i]);
}
// display position to DOUTx mapping...
for (uint i=0; i<this->display_.seg_to_out_map.size(); i++) {
ESP_LOGCONFIG(TAG, " Display position %2u: OUT%u", i, this->display_.pos_to_out_map[i]);
}
// ESP_LOGCONFIG(TAG, " Number of digits: %u", this->display_.num_digits);
ESP_LOGCONFIG(TAG, " Brightness: %.1f", this->display_.brightness.config_value);
ESP_LOGCONFIG(TAG, " Demo mode: %u", this->display_.demo_mode);
ESP_LOGCONFIG(TAG, " BLANK Pin: GPIO%u", this->blank_pin_->get_pin());
this->display_->dump_config();
}
void MAX6921Component::set_brightness(float brightness) {
@ -410,13 +53,16 @@ void MAX6921Component::set_brightness(float brightness) {
ESP_LOGD(TAG, "Set brightness: setup not finished -> discard brightness value");
return;
}
if ((brightness == 0.0) || (brightness != this->display_.brightness.config_value)) {
this->display_.brightness.config_value = brightness;
ESP_LOGD(TAG, "Set brightness: %.1f", this->display_.brightness.config_value);
this->display_.brightness.config_changed = true;
if ((brightness == 0.0) || (brightness != this->display_->get_brightness())) {
this->display_->set_brightness(brightness);
ESP_LOGD(TAG, "Set brightness: %.1f", this->display_->get_brightness());
}
}
void MAX6921Component::set_text(const std::string& text) {
}
/**
* @brief Clocks data into MAX6921 via SPI (MSB first).
* Data must contain 3 bytes with following format:
@ -429,72 +75,32 @@ void HOT MAX6921Component::write_data(uint8_t *ptr, size_t length) {
static bool first_call_logged = false;
assert(length == 3);
this->disable_load(); // set LOAD to low
this->disable_load_(); // set LOAD to low
memcpy(data, ptr, sizeof(data)); // make copy of data, because transfer buffer will be overwritten with SPI answer
if (!first_call_logged)
ESP_LOGVV(TAG, "SPI(%u): 0x%02x%02x%02x", length, data[0], data[1], data[2]);
first_call_logged = true;
this->transfer_array(data, sizeof(data));
this->enable_load(); // set LOAD to high to update output latch
this->enable_load_(); // set LOAD to high to update output latch
}
void MAX6921Component::update() {
this->update_display_();
this->display_->update();
if (this->writer_.has_value())
(*this->writer_)(*this);
}
void HOT MAX6921Component::display_refresh_task(void *pv) {
MAX6921Component *max6921_comp = (MAX6921Component*)pv;
static uint count = max6921_comp->display_.num_digits;
if (max6921_comp->display_.refresh_period_us == 0) {
ESP_LOGE(TAG, "Invalid display refresh period -> using default 2ms");
max6921_comp->display_.refresh_period_us = 2000;
}
while (true) {
// one-time debug output after any text change for all digits...
if (max6921_comp->display_.text_changed) {
count = 0;
max6921_comp->display_.text_changed = false;
}
if (count < max6921_comp->display_.num_digits) {
count++;
ESP_LOGVV(TAG, "%s(): SPI transfer for position %u: 0x%02x%02x%02x", __func__,
max6921_comp->display_.current_pos,
max6921_comp->display_.out_buf_[(max6921_comp->display_.current_pos-1)*3],
max6921_comp->display_.out_buf_[(max6921_comp->display_.current_pos-1)*3+1],
max6921_comp->display_.out_buf_[(max6921_comp->display_.current_pos-1)*3+2]);
}
// write MAX9621 data of current position...
max6921_comp->write_data(&max6921_comp->display_.out_buf_[(max6921_comp->display_.current_pos-1)*3], 3);
// next display position...
if (++max6921_comp->display_.current_pos > max6921_comp->display_.num_digits)
max6921_comp->display_.current_pos = 1;
delayMicroseconds(max6921_comp->display_.refresh_period_us);
}
}
/*
* Evaluates lambda function
* start_pos: 0..n = left..right display position
* str : display text
* vi_text : display text
*/
uint8_t MAX6921Component::print(uint8_t start_pos, const char *str) {
if ((this->get_demo_mode() != DEMO_MODE_OFF) || // demo mode enabled or
(strcmp(str, this->display_.current_text) == 0)) // display text not changed?
if (this->display_->mode != DISP_MODE_PRINT) // not in "it.print" mode?
return strlen(str); // yes -> abort
ESP_LOGV(TAG, "%s(): text changed: str=%s, prev=%s", __func__, str, this->display_.current_text);
strncpy(this->display_.current_text, str, this->display_.current_text_buf_size-1);
this->display_.current_text[this->display_.current_text_buf_size-1] = 0;
return this->set_display(start_pos, str);
return this->display_->set_text(start_pos, str);
}
uint8_t MAX6921Component::print(const char *str) { return this->print(0, str); }

View file

@ -1,68 +1,38 @@
#pragma once
#include <esp32-hal-gpio.h>
#include <string>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/time.h"
#include "esphome/components/spi/spi.h"
#include <esp32-hal-gpio.h>
namespace esphome {
namespace max6921 {
#define ARRAY_ELEM_COUNT(array) (sizeof(array)/sizeof(array[0]))
class MAX6921Component;
class Display;
using max6921_writer_t = std::function<void(MAX6921Component &)>;
enum demo_mode_t {
DEMO_MODE_OFF,
DEMO_MODE_SCROLL_FONT,
};
typedef struct {
InternalGPIOPin *pwm_pin;
float config_value; // brightness in percent (0.0-1.0)
bool config_changed;
uint32_t max_duty;
uint32_t duty_quotient;
uint8_t pwm_channel;
}display_brightness_t;
typedef struct {
std::vector<uint8_t> seg_to_out_map;
std::vector<uint8_t> pos_to_out_map;
uint num_digits;
display_brightness_t brightness;
uint8_t *out_buf_;
size_t out_buf_size_;
uint seg_out_smallest;
char *current_text;
uint current_text_buf_size;
uint current_pos;
bool text_changed;
uint32_t refresh_period_us;
demo_mode_t demo_mode;
}display_t;
class MAX6921Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_4MHZ> {
public:
Display *display_;
void dump_config() override;
float get_setup_priority() const override;
uint8_t print(uint8_t pos, const char *str);
uint8_t print(const char *str);
void set_blank_pin(InternalGPIOPin *pin) { this->display_.brightness.pwm_pin = pin; }
void set_blank_pin(InternalGPIOPin *pin) { blank_pin_ = pin; }
void set_brightness(float brightness);
void set_demo_mode(demo_mode_t mode) { this->display_.demo_mode = mode; }
void set_load_pin(GPIOPin *load) { this->load_pin_ = load; }
void set_num_digits(uint8_t num_digits) { this->display_.num_digits = num_digits; }
void set_seg_to_out_pin_map(const std::vector<uint8_t> &pin_map) { this->display_.seg_to_out_map = pin_map; }
void set_pos_to_out_pin_map(const std::vector<uint8_t> &pin_map) {
this->display_.pos_to_out_map = pin_map;
this->display_.num_digits = pin_map.size();
}
void set_seg_to_out_pin_map(const std::vector<uint8_t> &pin_map) { this->seg_to_out_map__ = pin_map; }
void set_pos_to_out_pin_map(const std::vector<uint8_t> &pin_map) { this->pos_to_out_map__ = pin_map; }
void set_text(const std::string& text);
void set_writer(max6921_writer_t &&writer);
void setup() override;
uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0)));
@ -72,31 +42,18 @@ class MAX6921Component : public PollingComponent,
protected:
GPIOPin *load_pin_{};
InternalGPIOPin *blank_pin_;
bool setup_finished{false};
display_t display_;
uint8_t *ascii_out_data_;
void clear_display(int pos=-1);
void disable_blank() { digitalWrite(this->display_.brightness.pwm_pin->get_pin(), LOW); } // display on
void IRAM_ATTR HOT disable_load() { this->load_pin_->digital_write(false); }
static void display_refresh_task(void *pv);
void enable_blank() { digitalWrite(this->display_.brightness.pwm_pin->get_pin(), HIGH); } // display off
void IRAM_ATTR HOT enable_load() { this->load_pin_->digital_write(true); }
demo_mode_t get_demo_mode(void) { return this->display_.demo_mode; }
int set_display(uint8_t pos, const char *str);
void disable_blank_() { digitalWrite(this->blank_pin_->get_pin(), LOW); } // display on
void IRAM_ATTR HOT disable_load_() { this->load_pin_->digital_write(false); }
void enable_blank_() { digitalWrite(this->blank_pin_->get_pin(), HIGH); } // display off
void IRAM_ATTR HOT enable_load_() { this->load_pin_->digital_write(true); }
void update_demo_mode_scroll_font_(void);
optional<max6921_writer_t> writer_{};
private:
void init_display_(void);
void init_font_(void);
void update_display_();
};
template<typename... Ts> class SetBrightnessAction : public Action<Ts...>, public Parented<MAX6921Component> {
public:
TEMPLATABLE_VALUE(float, brightness)
void play(Ts... x) override { this->parent_->set_brightness(this->brightness_.value(x...)); }
std::vector<uint8_t> seg_to_out_map__; // mapping of display segments to MAX6921 OUT pins
std::vector<uint8_t> pos_to_out_map__; // mapping of display positions to MAX6921 OUT pins
};