- 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>
341 lines
13 KiB
C++
341 lines
13 KiB
C++
#pragma once
|
|
|
|
#include "esphome/core/component.h"
|
|
#include "esphome/core/automation.h"
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/components/time/real_time_clock.h"
|
|
#include "esphome/components/uart/uart.h"
|
|
|
|
#include <array>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace esphome {
|
|
namespace samsung_vfd {
|
|
|
|
static const char *const TAG = "samsung_vfd";
|
|
|
|
// Control codes, see section 5.4 of docs/20L203DA12-datasheet.pdf.
|
|
static const uint8_t CMD_BS = 0x08; // cursor left
|
|
static const uint8_t CMD_HT = 0x09; // cursor right
|
|
static const uint8_t CMD_LF = 0x0A; // cursor down
|
|
static const uint8_t CMD_HOME = 0x0B; // cursor to top-left
|
|
static const uint8_t CMD_CLEAR = 0x0C; // clear screen, cursor home
|
|
static const uint8_t CMD_CR = 0x0D; // cursor to start of line
|
|
static const uint8_t CMD_CAN = 0x18; // clear cursor line
|
|
static const uint8_t CMD_ESC = 0x1B;
|
|
static const uint8_t CMD_US = 0x1F;
|
|
|
|
// Second byte after ESC.
|
|
static const uint8_t ESC_INIT = 0x40; // ESC @ initialise
|
|
static const uint8_t ESC_USER_FONT_SET = 0x25; // ESC % n select/cancel user font set
|
|
static const uint8_t ESC_DEFINE_CHAR = 0x26; // ESC & ... define user characters
|
|
static const uint8_t ESC_CANCEL_CHAR = 0x3F; // ESC ? n cancel a user character
|
|
static const uint8_t ESC_PERIPHERAL = 0x3D; // ESC = n select peripheral device
|
|
static const uint8_t ESC_INTL_CHARSET = 0x52; // ESC R n international character set
|
|
static const uint8_t ESC_CODE_TABLE = 0x74; // ESC t n CG-ROM table for 0x80..0xFF
|
|
|
|
// Second byte after US.
|
|
static const uint8_t US_MODE_OVERWRITE = 0x01; // US MD1
|
|
static const uint8_t US_MODE_VSCROLL = 0x02; // US MD2
|
|
static const uint8_t US_MODE_HSCROLL = 0x03; // US MD3
|
|
static const uint8_t US_CURSOR_UP = 0x0A; // US LF
|
|
static const uint8_t US_LINE_END = 0x0D; // US CR
|
|
static const uint8_t US_ANNUNCIATOR = 0x23; // US # n m
|
|
static const uint8_t US_SET_CURSOR = 0x24; // US $ n m
|
|
static const uint8_t US_SELF_TEST = 0x40; // US @
|
|
static const uint8_t US_BOTTOM = 0x42; // US B
|
|
static const uint8_t US_BLINK = 0x45; // US E n
|
|
static const uint8_t US_BRIGHTNESS = 0x58; // US X n
|
|
|
|
static const uint8_t BLINK_OFF = 0x00;
|
|
static const uint8_t BLINK_BLANK = 0xFF; // US E FF: screen off, contents kept
|
|
static const uint8_t MAX_USER_CHARS = 4; // per datasheet 5.4.14
|
|
|
|
enum CodeTable : uint8_t {
|
|
CODE_TABLE_PC437 = 0,
|
|
CODE_TABLE_PC850 = 1,
|
|
CODE_TABLE_PC866 = 2,
|
|
CODE_TABLE_PC737 = 3,
|
|
CODE_TABLE_PC775 = 4,
|
|
CODE_TABLE_CP10 = 5,
|
|
CODE_TABLE_PC864 = 6,
|
|
CODE_TABLE_PC852 = 7,
|
|
};
|
|
|
|
enum InternationalCharset : uint8_t {
|
|
CHARSET_USA = 0,
|
|
CHARSET_FRANCE = 1,
|
|
CHARSET_GERMANY = 2,
|
|
CHARSET_UK = 3,
|
|
CHARSET_DENMARK1 = 4,
|
|
CHARSET_SWEDEN = 5,
|
|
CHARSET_ITALY = 6,
|
|
CHARSET_SPAIN1 = 7,
|
|
CHARSET_JAPAN = 8,
|
|
CHARSET_NORWAY = 9,
|
|
CHARSET_DENMARK2 = 10,
|
|
};
|
|
|
|
enum DisplayMode : uint8_t {
|
|
MODE_OVERWRITE = US_MODE_OVERWRITE,
|
|
MODE_VERTICAL_SCROLL = US_MODE_VSCROLL,
|
|
MODE_HORIZONTAL_SCROLL = US_MODE_HSCROLL,
|
|
};
|
|
|
|
struct UserChar {
|
|
uint8_t code;
|
|
std::array<uint8_t, 5> columns; // one byte per column, bit 6 = top row, bit 0 = bottom row
|
|
};
|
|
|
|
class SamsungVfd : public Component, public uart::UARTDevice {
|
|
public:
|
|
void setup() override;
|
|
void dump_config() override;
|
|
// Before the default (DATA) priority, so restored entity values can be shown.
|
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
|
|
|
// ---------- configuration (before setup) ----------
|
|
void set_size(uint8_t cols, uint8_t rows) {
|
|
this->cols_ = cols == 0 ? 20 : cols;
|
|
this->rows_ = rows == 0 ? 2 : rows;
|
|
}
|
|
void set_send_newlines(bool v) { this->send_newlines_ = v; }
|
|
void set_initial_brightness(float b) { this->brightness_level_ = brightness_to_level_(b); }
|
|
void set_initial_code_table(CodeTable t) { this->code_table_ = t; }
|
|
void set_initial_charset(InternationalCharset c) { this->charset_ = c; }
|
|
void add_user_char(uint8_t code, uint8_t c0, uint8_t c1, uint8_t c2, uint8_t c3, uint8_t c4) {
|
|
this->user_chars_.push_back(UserChar{code, {c0, c1, c2, c3, c4}});
|
|
}
|
|
|
|
uint8_t cols() const { return this->cols_; }
|
|
uint8_t rows() const { return this->rows_; }
|
|
|
|
// ---------- buffered text (the normal way to use the display) ----------
|
|
/// Replace the whole screen with `text`, wrapping it over the available lines.
|
|
void print(const std::string &text, bool centered = false);
|
|
/// Replace a single line (0-based). Text is truncated/padded to the column count.
|
|
void print_line(uint8_t line, const std::string &text, bool centered = false);
|
|
/// Clear the screen (and the buffer).
|
|
void clear();
|
|
std::string line_text(uint8_t line) const {
|
|
return line < this->lines_.size() ? this->lines_[line] : std::string();
|
|
}
|
|
|
|
// ---------- display settings (kept and re-applied after reinit()) ----------
|
|
/// 0.0..1.0, rounded to the display's four levels (40/60/80/100 %).
|
|
void set_brightness(float brightness);
|
|
/// Blink the whole screen: `ms` on, `ms` off (13 ms steps). 0 = no blinking.
|
|
void set_blink(uint32_t ms);
|
|
/// Blank the screen without losing its contents, and bring it back.
|
|
void turn_off();
|
|
void turn_on();
|
|
bool is_on() const { return this->on_; }
|
|
/// Triangle under column `column` (1-based); 0 = all of them.
|
|
void set_annunciator(uint8_t column, bool state);
|
|
void set_code_table(CodeTable table);
|
|
void set_international_charset(InternationalCharset charset);
|
|
/// Define (or redefine) a user character at `code` (0x20..0x7E). Up to 4.
|
|
void define_char(uint8_t code, const std::array<uint8_t, 5> &columns);
|
|
/// Remove a user character; `code` shows its normal glyph again.
|
|
void cancel_char(uint8_t code);
|
|
/// Reset the display (ESC @), re-apply all settings above and redraw.
|
|
/// Use this when the display was power-cycled separately from the ESP.
|
|
void reinit();
|
|
|
|
// ---------- raw cursor and mode commands ----------
|
|
// The buffered text functions assume overwrite mode and redraw from the
|
|
// top-left. Writing text after these commands makes the next print redraw
|
|
// the whole screen.
|
|
void home();
|
|
void cursor_left();
|
|
void cursor_right();
|
|
void cursor_down();
|
|
void cursor_up();
|
|
void cursor_line_start();
|
|
void cursor_line_end();
|
|
void cursor_bottom();
|
|
/// 0-based column and line.
|
|
void set_cursor(uint8_t column, uint8_t line);
|
|
void clear_cursor_line();
|
|
void set_display_mode(DisplayMode mode);
|
|
/// 1 = this display off (pass-through only), 2 = this display (default), 3 = both.
|
|
void select_peripheral(uint8_t n);
|
|
/// Runs for about 20 s; the display ignores input meanwhile.
|
|
void self_test();
|
|
/// Write UTF-8 text at the cursor, converted like print() does.
|
|
void write_text(const std::string &text);
|
|
/// Send bytes unchanged.
|
|
void write_raw(const std::vector<uint8_t> &data);
|
|
void write_raw(const uint8_t *data, size_t len);
|
|
|
|
/// Convert UTF-8 to display bytes for the current code table.
|
|
std::string encode(const std::string &text, bool keep_newlines = false) const;
|
|
|
|
protected:
|
|
void init_display_();
|
|
void apply_settings_();
|
|
void send_user_chars_();
|
|
void send_annunciators_();
|
|
void resize_lines_();
|
|
/// Fit `text` (already encoded) into one line: truncate or pad, optionally centred.
|
|
std::string fit_(const std::string &text, bool centered) const;
|
|
/// Rebuild the wire payload from the shadow buffer and send it only if it changed.
|
|
void flush_();
|
|
void write_str_(const std::string &s);
|
|
void send_esc_(uint8_t cmd);
|
|
void send_esc_(uint8_t cmd, uint8_t arg);
|
|
void send_us_(uint8_t cmd);
|
|
void send_us_(uint8_t cmd, uint8_t arg);
|
|
void invalidate_() { this->last_payload_.clear(); }
|
|
char encode_codepoint_(uint32_t cp) const;
|
|
static uint8_t brightness_to_level_(float b);
|
|
|
|
uint8_t cols_{20};
|
|
uint8_t rows_{2};
|
|
bool send_newlines_{false};
|
|
std::vector<std::string> lines_;
|
|
std::string last_payload_;
|
|
|
|
uint8_t brightness_level_{4};
|
|
uint8_t blink_{BLINK_OFF};
|
|
bool on_{true};
|
|
uint32_t annunciators_{0}; // bit n = column n (1..20)
|
|
CodeTable code_table_{CODE_TABLE_PC437};
|
|
InternationalCharset charset_{CHARSET_USA};
|
|
DisplayMode mode_{MODE_OVERWRITE};
|
|
std::vector<UserChar> user_chars_;
|
|
};
|
|
|
|
// ---------- actions ----------
|
|
|
|
template<typename... Ts> class PrintAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(std::string, text)
|
|
TEMPLATABLE_VALUE(int, line)
|
|
TEMPLATABLE_VALUE(bool, centered)
|
|
|
|
void play(const Ts &...x) override {
|
|
std::string text = this->text_.value(x...);
|
|
bool centered = this->centered_.has_value() && this->centered_.value(x...);
|
|
if (this->line_.has_value())
|
|
this->parent_->print_line(static_cast<uint8_t>(this->line_.value(x...)), text, centered);
|
|
else
|
|
this->parent_->print(text, centered);
|
|
}
|
|
};
|
|
|
|
/// Like print, but formats the current time with strftime (e.g. "%H:%M:%S").
|
|
template<typename... Ts> class PrintfAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(std::string, format)
|
|
TEMPLATABLE_VALUE(int, line)
|
|
TEMPLATABLE_VALUE(bool, centered)
|
|
|
|
void set_time(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
|
|
|
|
void play(const Ts &...x) override {
|
|
auto now = this->rtc_->now();
|
|
if (!now.is_valid())
|
|
return; // clock not synchronised yet, leave the screen as it is
|
|
std::string text = now.strftime(this->format_.value(x...));
|
|
bool centered = this->centered_.has_value() && this->centered_.value(x...);
|
|
if (this->line_.has_value())
|
|
this->parent_->print_line(static_cast<uint8_t>(this->line_.value(x...)), text, centered);
|
|
else
|
|
this->parent_->print(text, centered);
|
|
}
|
|
|
|
protected:
|
|
time::RealTimeClock *rtc_{nullptr};
|
|
};
|
|
|
|
template<typename... Ts> class ClearAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void play(const Ts &...x) override { this->parent_->clear(); }
|
|
};
|
|
|
|
template<typename... Ts> class HomeAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void play(const Ts &...x) override { this->parent_->home(); }
|
|
};
|
|
|
|
template<typename... Ts> class SetBrightnessAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(float, brightness)
|
|
void play(const Ts &...x) override { this->parent_->set_brightness(this->brightness_.value(x...)); }
|
|
};
|
|
|
|
template<typename... Ts> class SetBlinkAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(uint32_t, interval)
|
|
void play(const Ts &...x) override { this->parent_->set_blink(this->interval_.value(x...)); }
|
|
};
|
|
|
|
template<typename... Ts> class TurnOnAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void play(const Ts &...x) override { this->parent_->turn_on(); }
|
|
};
|
|
|
|
template<typename... Ts> class TurnOffAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void play(const Ts &...x) override { this->parent_->turn_off(); }
|
|
};
|
|
|
|
template<typename... Ts> class SetAnnunciatorAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(int, column)
|
|
TEMPLATABLE_VALUE(bool, state)
|
|
void play(const Ts &...x) override {
|
|
this->parent_->set_annunciator(static_cast<uint8_t>(this->column_.value(x...)), this->state_.value(x...));
|
|
}
|
|
};
|
|
|
|
template<typename... Ts> class SetCursorAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(int, column)
|
|
TEMPLATABLE_VALUE(int, line)
|
|
void play(const Ts &...x) override {
|
|
this->parent_->set_cursor(static_cast<uint8_t>(this->column_.value(x...)),
|
|
static_cast<uint8_t>(this->line_.value(x...)));
|
|
}
|
|
};
|
|
|
|
template<typename... Ts> class SetDisplayModeAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void set_mode(DisplayMode mode) { this->mode_ = mode; }
|
|
void play(const Ts &...x) override { this->parent_->set_display_mode(this->mode_); }
|
|
|
|
protected:
|
|
DisplayMode mode_{MODE_OVERWRITE};
|
|
};
|
|
|
|
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
TEMPLATABLE_VALUE(std::string, text)
|
|
void set_data(const std::vector<uint8_t> &data) { this->data_ = data; }
|
|
|
|
void play(const Ts &...x) override {
|
|
if (this->text_.has_value())
|
|
this->parent_->write_text(this->text_.value(x...));
|
|
else
|
|
this->parent_->write_raw(this->data_);
|
|
}
|
|
|
|
protected:
|
|
std::vector<uint8_t> data_;
|
|
};
|
|
|
|
template<typename... Ts> class ReinitAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void play(const Ts &...x) override { this->parent_->reinit(); }
|
|
};
|
|
|
|
template<typename... Ts> class SelfTestAction : public Action<Ts...>, public Parented<SamsungVfd> {
|
|
public:
|
|
void play(const Ts &...x) override { this->parent_->self_test(); }
|
|
};
|
|
|
|
} // namespace samsung_vfd
|
|
} // namespace esphome
|