Support every datasheet command and map text to the CG-ROM fonts
- Brightness, blink, screen on/off, annunciators, cursor control, display modes, user-defined characters, code table, international charset, peripheral select, self-test, raw writes and reinit - UTF-8 text is encoded with the selected code table (tables generated from Python codecs by tools/gen_charmaps.py), falling back to accent-stripping - Settings survive reinit and the self-test - Example gains brightness select and display switch, and uses the git source on mangavalk.com Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3fcd15ad86
commit
95686a3608
8 files changed
+1322
-148
No files matched your search
@@ -1,16 +1,22 @@
|
||||
#include "samsung_vfd.h"
|
||||
#include "charmaps.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome {
|
||||
namespace samsung_vfd {
|
||||
|
||||
// Unaccented stand-ins for U+00C0..U+00FF (À..ÿ), one per code point.
|
||||
// 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_();
|
||||
@@ -20,57 +26,168 @@ 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());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
// ---------- low-level output ----------
|
||||
|
||||
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) {
|
||||
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<char>(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());
|
||||
for (size_t i = 0; i < text.size(); i++) {
|
||||
size_t i = 0;
|
||||
while (i < text.size()) {
|
||||
auto c = static_cast<uint8_t>(text[i]);
|
||||
if (c == '\n') {
|
||||
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<uint8_t>(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 (c < 0x20 || c == 0x7F) {
|
||||
} else if (cp < 0x20 || cp == 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;
|
||||
} else if (cp < 0x80) {
|
||||
out.push_back(static_cast<char>(cp));
|
||||
} else {
|
||||
out.push_back(this->encode_codepoint_(cp));
|
||||
}
|
||||
// Stray continuation bytes (0x80..0xBF) are dropped.
|
||||
}
|
||||
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_)
|
||||
@@ -81,7 +198,7 @@ std::string SamsungVfd::fit_(const std::string &text, bool centered) const {
|
||||
|
||||
void SamsungVfd::print(const std::string &raw, bool centered) {
|
||||
this->resize_lines_();
|
||||
const std::string text = sanitize_(raw, true);
|
||||
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.
|
||||
@@ -124,18 +241,16 @@ void SamsungVfd::print_line(uint8_t line, const std::string &text, bool centered
|
||||
if (this->lines_.size() < this->rows_)
|
||||
this->resize_lines_();
|
||||
|
||||
this->lines_[line] = this->fit_(sanitize_(text, false), centered);
|
||||
this->lines_[line] = this->fit_(this->encode(text, false), centered);
|
||||
this->flush_();
|
||||
}
|
||||
|
||||
void SamsungVfd::clear() {
|
||||
this->resize_lines_();
|
||||
this->last_payload_.clear();
|
||||
this->invalidate_();
|
||||
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++) {
|
||||
@@ -149,10 +264,161 @@ void SamsungVfd::flush_() {
|
||||
|
||||
// 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<int>(std::lround((b - 0.2f) / 0.2f));
|
||||
return static_cast<uint8_t>(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<uint8_t>(std::max<uint32_t>(1, std::min<uint32_t>(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<uint8_t>(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<uint8_t, 5> &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<uint8_t>(column + 1), static_cast<uint8_t>(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<uint8_t> &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
|
||||
Reference in new issue
Block a user