#include "samsung_vfd.h" #include "charmaps.h" #include "esphome/core/log.h" #include #include namespace esphome { namespace samsung_vfd { // Unaccented stand-ins for U+00C0..U+00FF (À..ÿ), one per code point. Used when // the selected code table has no glyph for a character. static const char LATIN1_FOLD[] = "AAAAAAACEEEEIIIIDNOOOOOxOUUUUYPs" "aaaaaaaceeeeiiiidnooooo/ouuuuypy"; static const char *const CODE_TABLE_NAMES[] = {"PC437", "PC850", "PC866", "PC737", "PC775", "CP10", "PC864", "PC852"}; void SamsungVfd::setup() { this->resize_lines_(); this->init_display_(); } void SamsungVfd::dump_config() { ESP_LOGCONFIG(TAG, "Samsung VFD:"); ESP_LOGCONFIG(TAG, " Size: %u cols x %u rows", this->cols_, this->rows_); ESP_LOGCONFIG(TAG, " Newline between lines: %s", YESNO(this->send_newlines_)); ESP_LOGCONFIG(TAG, " Brightness level: %u/4", this->brightness_level_); ESP_LOGCONFIG(TAG, " Code table: %s", CODE_TABLE_NAMES[this->code_table_ & 7]); ESP_LOGCONFIG(TAG, " International charset: %u", this->charset_); ESP_LOGCONFIG(TAG, " User characters: %u", (unsigned) this->user_chars_.size()); } // ---------- low-level output ---------- void SamsungVfd::write_str_(const std::string &s) { if (!s.empty()) this->write_array(reinterpret_cast(s.data()), s.size()); } void SamsungVfd::send_esc_(uint8_t cmd) { const uint8_t b[] = {CMD_ESC, cmd}; this->write_array(b, sizeof(b)); } void SamsungVfd::send_esc_(uint8_t cmd, uint8_t arg) { const uint8_t b[] = {CMD_ESC, cmd, arg}; this->write_array(b, sizeof(b)); } void SamsungVfd::send_us_(uint8_t cmd) { const uint8_t b[] = {CMD_US, cmd}; this->write_array(b, sizeof(b)); } void SamsungVfd::send_us_(uint8_t cmd, uint8_t arg) { const uint8_t b[] = {CMD_US, cmd, arg}; this->write_array(b, sizeof(b)); } // ---------- initialisation ---------- void SamsungVfd::init_display_() { // ESC @ resets every setting, clears the screen and homes the cursor. this->send_esc_(ESC_INIT); this->apply_settings_(); // Force the next flush to retransmit whatever is in the shadow buffer. this->invalidate_(); } void SamsungVfd::apply_settings_() { this->send_esc_(ESC_CODE_TABLE, this->code_table_); this->send_esc_(ESC_INTL_CHARSET, this->charset_); this->send_user_chars_(); this->send_us_(US_BRIGHTNESS, this->brightness_level_); this->send_us_(US_BLINK, this->on_ ? this->blink_ : BLINK_BLANK); this->send_annunciators_(); if (this->mode_ != MODE_OVERWRITE) this->send_us_(this->mode_); } void SamsungVfd::send_user_chars_() { if (this->user_chars_.empty()) return; for (const auto &uc : this->user_chars_) { // ESC & s n m a p1..p5: s = 1 byte per column, one character (n = m), 5 columns. const uint8_t b[] = {CMD_ESC, ESC_DEFINE_CHAR, 0x01, uc.code, uc.code, 0x05}; this->write_array(b, sizeof(b)); this->write_array(uc.columns.data(), uc.columns.size()); } this->send_esc_(ESC_USER_FONT_SET, 0x01); } void SamsungVfd::send_annunciators_() { const uint32_t all = 0x1FFFFE; // columns 1..20 if ((this->annunciators_ & all) == all) { const uint8_t b[] = {CMD_US, US_ANNUNCIATOR, 0x01, 0x00}; this->write_array(b, sizeof(b)); return; } for (uint8_t col = 1; col <= 20; col++) { if (this->annunciators_ & (1UL << col)) { const uint8_t b[] = {CMD_US, US_ANNUNCIATOR, 0x01, col}; this->write_array(b, sizeof(b)); } } } void SamsungVfd::reinit() { this->init_display_(); this->flush_(); } // ---------- text encoding ---------- char SamsungVfd::encode_codepoint_(uint32_t cp) const { if (this->code_table_ < sizeof(CHARMAPS) / sizeof(CHARMAPS[0])) { const uint16_t *map = CHARMAPS[this->code_table_]; if (map != nullptr) { for (uint8_t i = 0; i < 128; i++) { if (map[i] == cp) return static_cast(0x80 + i); } } } if (cp == 0xA0) return ' '; if (cp >= 0xC0 && cp <= 0xFF) return LATIN1_FOLD[cp - 0xC0]; return '?'; } std::string SamsungVfd::encode(const std::string &text, bool keep_newlines) const { std::string out; out.reserve(text.size()); size_t i = 0; while (i < text.size()) { auto c = static_cast(text[i]); uint32_t cp; size_t len; if (c < 0x80) { cp = c; len = 1; } else if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; len = 2; } else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; len = 3; } else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; len = 4; } else { i++; // stray continuation byte continue; } bool valid = i + len <= text.size(); for (size_t k = 1; valid && k < len; k++) { auto cc = static_cast(text[i + k]); if ((cc & 0xC0) != 0x80) valid = false; else cp = (cp << 6) | (cc & 0x3F); } if (!valid) { out.push_back('?'); i++; continue; } i += len; if (cp == '\n') { out.push_back(keep_newlines ? '\n' : ' '); } else if (cp < 0x20 || cp == 0x7F) { out.push_back(' '); } else if (cp < 0x80) { out.push_back(static_cast(cp)); } else { out.push_back(this->encode_codepoint_(cp)); } } return out; } // ---------- buffered text ---------- void SamsungVfd::resize_lines_() { this->lines_.assign(this->rows_, std::string(this->cols_, ' ')); } std::string SamsungVfd::fit_(const std::string &text, bool centered) const { std::string line = text.substr(0, this->cols_); if (centered && line.size() < this->cols_) line.insert(0, (this->cols_ - line.size()) / 2, ' '); line.resize(this->cols_, ' '); return line; } void SamsungVfd::print(const std::string &raw, bool centered) { this->resize_lines_(); const std::string text = this->encode(raw, true); // Lay the text out over the lines: split on explicit newlines, then wrap each // part into line-width chunks, breaking at the last space where possible. uint8_t row = 0; size_t i = 0; while (row < this->rows_) { size_t nl = text.find('\n', i); size_t end = (nl == std::string::npos) ? text.size() : nl; // A part always occupies at least one line, so "A\nB" puts B on line 2. do { size_t n = std::min(this->cols_, end - i); size_t next = i + n; if (next < end && text[next] != ' ') { size_t sp = text.rfind(' ', next); if (sp != std::string::npos && sp > i) { n = sp - i; next = sp; } } this->lines_[row++] = this->fit_(text.substr(i, n), centered); i = next; while (i < end && text[i] == ' ') i++; // a wrapped line never starts with a space } while (i < end && row < this->rows_); if (nl == std::string::npos) break; i = nl + 1; } this->flush_(); } void SamsungVfd::print_line(uint8_t line, const std::string &text, bool centered) { if (line >= this->rows_) { ESP_LOGW(TAG, "print_line: line %u out of range (has %u lines)", line, this->rows_); return; } if (this->lines_.size() < this->rows_) this->resize_lines_(); this->lines_[line] = this->fit_(this->encode(text, false), centered); this->flush_(); } void SamsungVfd::clear() { this->resize_lines_(); this->invalidate_(); this->write_byte(CMD_CLEAR); } void SamsungVfd::flush_() { std::string payload; for (uint8_t r = 0; r < this->rows_; r++) { if (r != 0 && this->send_newlines_) payload += "\r\n"; payload += this->lines_[r]; } if (payload == this->last_payload_) return; // nothing changed -> no re-send // No clear: every line is padded to the full width, so going home and // overwriting replaces all characters without the blank-screen flicker. // In overwrite mode the cursor wraps from the end of line 1 to line 2. this->write_byte(CMD_HOME); this->write_str_(payload); this->last_payload_ = payload; } // ---------- settings ---------- uint8_t SamsungVfd::brightness_to_level_(float b) { // Levels 1..4 = 40/60/80/100 %. int level = static_cast(std::lround((b - 0.2f) / 0.2f)); return static_cast(std::max(1, std::min(4, level))); } void SamsungVfd::set_brightness(float brightness) { this->brightness_level_ = brightness_to_level_(brightness); this->send_us_(US_BRIGHTNESS, this->brightness_level_); } void SamsungVfd::set_blink(uint32_t ms) { // US E n: n * 13 ms on, n * 13 ms off. 0xFF means "off", so stop at 0xFE. this->blink_ = ms == 0 ? BLINK_OFF : static_cast(std::max(1, std::min(0xFE, (ms + 6) / 13))); if (this->on_) this->send_us_(US_BLINK, this->blink_); } void SamsungVfd::turn_off() { this->on_ = false; this->send_us_(US_BLINK, BLINK_BLANK); } void SamsungVfd::turn_on() { this->on_ = true; this->send_us_(US_BLINK, this->blink_); } void SamsungVfd::set_annunciator(uint8_t column, bool state) { if (column > 20) { ESP_LOGW(TAG, "set_annunciator: column %u out of range (0 = all, 1..20)", column); return; } if (column == 0) { this->annunciators_ = state ? 0x1FFFFE : 0; } else if (state) { this->annunciators_ |= 1UL << column; } else { this->annunciators_ &= ~(1UL << column); } const uint8_t b[] = {CMD_US, US_ANNUNCIATOR, static_cast(state ? 1 : 0), column}; this->write_array(b, sizeof(b)); } void SamsungVfd::set_code_table(CodeTable table) { this->code_table_ = table; this->send_esc_(ESC_CODE_TABLE, table); } void SamsungVfd::set_international_charset(InternationalCharset charset) { this->charset_ = charset; this->send_esc_(ESC_INTL_CHARSET, charset); } void SamsungVfd::define_char(uint8_t code, const std::array &columns) { if (code < 0x20 || code > 0x7E) { ESP_LOGW(TAG, "define_char: code 0x%02X out of range (0x20..0x7E)", code); return; } auto it = std::find_if(this->user_chars_.begin(), this->user_chars_.end(), [code](const UserChar &uc) { return uc.code == code; }); if (it != this->user_chars_.end()) { it->columns = columns; } else if (this->user_chars_.size() >= MAX_USER_CHARS) { ESP_LOGW(TAG, "define_char: at most %u user characters", MAX_USER_CHARS); return; } else { this->user_chars_.push_back(UserChar{code, columns}); } const uint8_t b[] = {CMD_ESC, ESC_DEFINE_CHAR, 0x01, code, code, 0x05}; this->write_array(b, sizeof(b)); this->write_array(columns.data(), columns.size()); this->send_esc_(ESC_USER_FONT_SET, 0x01); this->invalidate_(); } void SamsungVfd::cancel_char(uint8_t code) { this->user_chars_.erase(std::remove_if(this->user_chars_.begin(), this->user_chars_.end(), [code](const UserChar &uc) { return uc.code == code; }), this->user_chars_.end()); this->send_esc_(ESC_CANCEL_CHAR, code); this->invalidate_(); } // ---------- raw cursor and mode commands ---------- void SamsungVfd::home() { this->write_byte(CMD_HOME); } void SamsungVfd::cursor_left() { this->write_byte(CMD_BS); } void SamsungVfd::cursor_right() { this->write_byte(CMD_HT); } void SamsungVfd::cursor_down() { this->write_byte(CMD_LF); } void SamsungVfd::cursor_up() { this->send_us_(US_CURSOR_UP); } void SamsungVfd::cursor_line_start() { this->write_byte(CMD_CR); } void SamsungVfd::cursor_line_end() { this->send_us_(US_LINE_END); } void SamsungVfd::cursor_bottom() { this->send_us_(US_BOTTOM); } void SamsungVfd::set_cursor(uint8_t column, uint8_t line) { if (column >= this->cols_ || line >= this->rows_) { ESP_LOGW(TAG, "set_cursor: %u,%u out of range", column, line); return; } const uint8_t b[] = {CMD_US, US_SET_CURSOR, static_cast(column + 1), static_cast(line + 1)}; this->write_array(b, sizeof(b)); } void SamsungVfd::clear_cursor_line() { this->write_byte(CMD_CAN); this->invalidate_(); } void SamsungVfd::set_display_mode(DisplayMode mode) { this->mode_ = mode; this->send_us_(mode); if (mode != MODE_OVERWRITE) ESP_LOGW(TAG, "Scroll mode set: print/print_line assume overwrite mode and may scroll the screen"); } void SamsungVfd::select_peripheral(uint8_t n) { if (n < 1 || n > 3) { ESP_LOGW(TAG, "select_peripheral: %u out of range (1..3)", n); return; } this->send_esc_(ESC_PERIPHERAL, n); } void SamsungVfd::self_test() { this->send_us_(US_SELF_TEST); this->invalidate_(); // The self-test resets every setting except user characters and ignores // input for about 20 s; restore settings and the screen afterwards. this->set_timeout("self_test", 21000, [this]() { this->apply_settings_(); this->flush_(); }); } void SamsungVfd::write_text(const std::string &text) { this->write_str_(this->encode(text, false)); this->invalidate_(); } void SamsungVfd::write_raw(const std::vector &data) { this->write_raw(data.data(), data.size()); } void SamsungVfd::write_raw(const uint8_t *data, size_t len) { if (len != 0) this->write_array(data, len); this->invalidate_(); } } // namespace samsung_vfd } // namespace esphome