Files

424 lines
13 KiB
C++
Raw Permalink Normal View History

#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. 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<const uint8_t *>(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<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());
size_t i = 0;
while (i < text.size()) {
auto c = static_cast<uint8_t>(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<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 (cp < 0x20 || cp == 0x7F) {
out.push_back(' ');
} else if (cp < 0x80) {
out.push_back(static_cast<char>(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<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_(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<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