#include "samsung_vfd.h" #include "esphome/core/log.h" #include namespace esphome { namespace samsung_vfd { // Unaccented stand-ins for U+00C0..U+00FF (À..ÿ), one per code point. static const char LATIN1_FOLD[] = "AAAAAAACEEEEIIIIDNOOOOOxOUUUUYPs" "aaaaaaaceeeeiiiidnooooo/ouuuuypy"; 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_)); } void SamsungVfd::resize_lines_() { this->lines_.assign(this->rows_, std::string(this->cols_, ' ')); } void SamsungVfd::init_display_() { // ESC @ (reset), then clear. this->write_byte(CMD_ESC); this->write_byte(CMD_INIT); this->write_byte(CMD_CLEAR); // Force the next flush to retransmit whatever is in the shadow buffer. this->last_payload_.clear(); } void SamsungVfd::write_str_(const std::string &s) { if (!s.empty()) this->write_array(reinterpret_cast(s.data()), s.size()); } std::string SamsungVfd::sanitize_(const std::string &text, bool keep_newlines) { std::string out; out.reserve(text.size()); for (size_t i = 0; i < text.size(); i++) { auto c = static_cast(text[i]); if (c == '\n') { out.push_back(keep_newlines ? '\n' : ' '); } else if (c < 0x20 || c == 0x7F) { out.push_back(' '); } else if (c < 0x80) { out.push_back(static_cast(c)); } else if (c >= 0xC0) { // Start of a multi-byte UTF-8 sequence: decode 2-byte Latin-1 letters, // replace everything else with a single '?'. size_t len = c >= 0xF0 ? 4 : c >= 0xE0 ? 3 : 2; char repl = '?'; if (len == 2 && i + 1 < text.size()) { uint32_t cp = ((c & 0x1F) << 6) | (static_cast(text[i + 1]) & 0x3F); if (cp >= 0xC0 && cp <= 0xFF) repl = LATIN1_FOLD[cp - 0xC0]; else if (cp == 0xA0) repl = ' '; } out.push_back(repl); i += len - 1; } // Stray continuation bytes (0x80..0xBF) are dropped. } return out; } 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 = sanitize_(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_(sanitize_(text, false), centered); this->flush_(); } void SamsungVfd::clear() { this->resize_lines_(); this->last_payload_.clear(); this->write_byte(CMD_CLEAR); } void SamsungVfd::home() { this->write_byte(CMD_HOME); } 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. this->write_byte(CMD_HOME); this->write_str_(payload); this->last_payload_ = payload; } } // namespace samsung_vfd } // namespace esphome