ESPHome external component for Samsung 2x20 serial VFD
Component (components/samsung_vfd), example device config, README and CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
bd9389e04a
9 files changed
+796
No files matched your search
@@ -0,0 +1,158 @@
|
||||
#include "samsung_vfd.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<const uint8_t *>(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<uint8_t>(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<char>(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<uint8_t>(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<size_t>(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
|
||||
Reference in new issue
Block a user