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:
mangavalkandClaude Opus 5.5 committed 2026-10-31 17:55:36 +01:00
1 parent 3fcd15ad86
commit 95686a3608
8 files changed
+1322 -148

No files matched your search

+275 -14
View File
@@ -2,25 +2,154 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import time as time_, uart
from esphome.const import CONF_ID, CONF_TEXT, CONF_TIME_ID
from esphome.const import (
CONF_BRIGHTNESS,
CONF_DATA,
CONF_ID,
CONF_INTERVAL,
CONF_MODE,
CONF_STATE,
CONF_TEXT,
CONF_TIME_ID,
)
DEPENDENCIES = ["uart", "time"]
CONF_COLS = "cols"
CONF_COLUMN = "column"
CONF_ROWS = "rows"
CONF_LINE = "line"
CONF_FORMAT = "format"
CONF_CENTERED = "centered"
CONF_SEND_NEWLINES = "send_newlines"
CONF_CODE_TABLE = "code_table"
CONF_INTERNATIONAL_CHARSET = "international_charset"
CONF_USER_CHARACTERS = "user_characters"
CONF_CODE = "code"
CONF_PATTERN = "pattern"
samsung_vfd_ns = cg.esphome_ns.namespace("samsung_vfd")
SamsungVfd = samsung_vfd_ns.class_("SamsungVfd", cg.Component, uart.UARTDevice)
CodeTable = samsung_vfd_ns.enum("CodeTable")
CODE_TABLES = {
"PC437": CodeTable.CODE_TABLE_PC437,
"PC850": CodeTable.CODE_TABLE_PC850,
"PC866": CodeTable.CODE_TABLE_PC866,
"PC737": CodeTable.CODE_TABLE_PC737,
"PC775": CodeTable.CODE_TABLE_PC775,
"CP10": CodeTable.CODE_TABLE_CP10,
"PC864": CodeTable.CODE_TABLE_PC864,
"PC852": CodeTable.CODE_TABLE_PC852,
}
InternationalCharset = samsung_vfd_ns.enum("InternationalCharset")
CHARSETS = {
"USA": InternationalCharset.CHARSET_USA,
"FRANCE": InternationalCharset.CHARSET_FRANCE,
"GERMANY": InternationalCharset.CHARSET_GERMANY,
"UK": InternationalCharset.CHARSET_UK,
"DENMARK1": InternationalCharset.CHARSET_DENMARK1,
"SWEDEN": InternationalCharset.CHARSET_SWEDEN,
"ITALY": InternationalCharset.CHARSET_ITALY,
"SPAIN1": InternationalCharset.CHARSET_SPAIN1,
"JAPAN": InternationalCharset.CHARSET_JAPAN,
"NORWAY": InternationalCharset.CHARSET_NORWAY,
"DENMARK2": InternationalCharset.CHARSET_DENMARK2,
}
DisplayMode = samsung_vfd_ns.enum("DisplayMode")
DISPLAY_MODES = {
"OVERWRITE": DisplayMode.MODE_OVERWRITE,
"VERTICAL_SCROLL": DisplayMode.MODE_VERTICAL_SCROLL,
"HORIZONTAL_SCROLL": DisplayMode.MODE_HORIZONTAL_SCROLL,
}
PrintAction = samsung_vfd_ns.class_("PrintAction", automation.Action)
PrintfAction = samsung_vfd_ns.class_("PrintfAction", automation.Action)
ClearAction = samsung_vfd_ns.class_("ClearAction", automation.Action)
HomeAction = samsung_vfd_ns.class_("HomeAction", automation.Action)
SetBrightnessAction = samsung_vfd_ns.class_("SetBrightnessAction", automation.Action)
SetBlinkAction = samsung_vfd_ns.class_("SetBlinkAction", automation.Action)
TurnOnAction = samsung_vfd_ns.class_("TurnOnAction", automation.Action)
TurnOffAction = samsung_vfd_ns.class_("TurnOffAction", automation.Action)
SetAnnunciatorAction = samsung_vfd_ns.class_("SetAnnunciatorAction", automation.Action)
SetCursorAction = samsung_vfd_ns.class_("SetCursorAction", automation.Action)
SetDisplayModeAction = samsung_vfd_ns.class_("SetDisplayModeAction", automation.Action)
WriteAction = samsung_vfd_ns.class_("WriteAction", automation.Action)
ReinitAction = samsung_vfd_ns.class_("ReinitAction", automation.Action)
SelfTestAction = samsung_vfd_ns.class_("SelfTestAction", automation.Action)
PIXEL_ON = "#X1*"
PIXEL_OFF = ".-0 _"
def user_char_code(value):
if isinstance(value, str):
if len(value) != 1:
raise cv.Invalid("code must be a single character or a number")
value = ord(value)
value = cv.int_(value)
if not 0x20 <= value <= 0x7E:
raise cv.Invalid("code must be between 0x20 and 0x7E (a printable ASCII character)")
return value
def user_char_pattern(value):
"""7 strings of 5 pixels, top row first, e.g. '.###.'."""
value = cv.ensure_list(cv.string_strict)(value)
if len(value) != 7:
raise cv.Invalid("pattern must have exactly 7 rows")
for row in value:
if len(row) != 5:
raise cv.Invalid(f"pattern row '{row}' must be exactly 5 pixels wide")
for ch in row:
if ch not in PIXEL_ON + PIXEL_OFF:
raise cv.Invalid(
f"pattern row '{row}': use one of '{PIXEL_ON}' for on and '{PIXEL_OFF}' for off"
)
return value
def pattern_to_columns(pattern):
# One byte per column; bit 6 = top row, bit 0 = bottom row (datasheet 5.4.14).
cols = []
for c in range(5):
byte = 0
for r, row in enumerate(pattern):
if row[c] in PIXEL_ON:
byte |= 1 << (6 - r)
cols.append(byte)
return cols
def unique_codes(value):
codes = [uc[CONF_CODE] for uc in value]
for code in codes:
if codes.count(code) > 1:
raise cv.Invalid(f"user character code 0x{code:02X} defined twice")
return value
def validate_write(config):
if (CONF_TEXT in config) == (CONF_DATA in config):
raise cv.Invalid("specify exactly one of 'text' or 'data'")
return config
def raw_data(value):
if isinstance(value, str):
return list(value.encode("latin-1"))
return cv.ensure_list(cv.hex_uint8_t)(value)
USER_CHAR_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): user_char_code,
cv.Required(CONF_PATTERN): user_char_pattern,
}
)
CONFIG_SCHEMA = (
cv.Schema(
@@ -31,15 +160,21 @@ CONFIG_SCHEMA = (
# Off by default: lines are padded to exactly `cols` characters and the
# display wraps to the next line by itself.
cv.Optional(CONF_SEND_NEWLINES, default=False): cv.boolean,
cv.Optional(CONF_BRIGHTNESS, default="100%"): cv.percentage,
cv.Optional(CONF_CODE_TABLE, default="PC437"): cv.enum(CODE_TABLES, upper=True),
cv.Optional(CONF_INTERNATIONAL_CHARSET, default="USA"): cv.enum(CHARSETS, upper=True),
cv.Optional(CONF_USER_CHARACTERS, default=[]): cv.All(
cv.ensure_list(USER_CHAR_SCHEMA), cv.Length(max=4), unique_codes
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"samsung_vfd", baud_rate=9600, require_tx=True
)
# Baud rate and parity must match the display's jumpers, so they are left to
# the uart: block.
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("samsung_vfd", require_tx=True)
async def to_code(config):
@@ -48,6 +183,35 @@ async def to_code(config):
await uart.register_uart_device(var, config)
cg.add(var.set_size(config[CONF_COLS], config[CONF_ROWS]))
cg.add(var.set_send_newlines(config[CONF_SEND_NEWLINES]))
cg.add(var.set_initial_brightness(config[CONF_BRIGHTNESS]))
cg.add(var.set_initial_code_table(config[CONF_CODE_TABLE]))
cg.add(var.set_initial_charset(config[CONF_INTERNATIONAL_CHARSET]))
for uc in config[CONF_USER_CHARACTERS]:
cg.add(var.add_user_char(uc[CONF_CODE], *pattern_to_columns(uc[CONF_PATTERN])))
# ---------- actions ----------
SIMPLE_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(SamsungVfd)})
async def simple_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
for _name, _cls in (
("clear", ClearAction),
("home", HomeAction),
("turn_on", TurnOnAction),
("turn_off", TurnOffAction),
("reinit", ReinitAction),
("self_test", SelfTestAction),
):
automation.register_action(
f"samsung_vfd.{_name}", _cls, SIMPLE_ACTION_SCHEMA, synchronous=True
)(simple_action_to_code)
@automation.register_action(
@@ -55,7 +219,7 @@ async def to_code(config):
PrintAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(SamsungVfd),
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_TEXT): cv.templatable(cv.string),
cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)),
cv.Optional(CONF_CENTERED): cv.templatable(cv.boolean),
@@ -79,7 +243,7 @@ async def samsung_vfd_print_to_code(config, action_id, template_arg, args):
PrintfAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(SamsungVfd),
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_TIME_ID): cv.use_id(time_.RealTimeClock),
cv.Required(CONF_FORMAT): cv.templatable(cv.string),
cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)),
@@ -101,24 +265,121 @@ async def samsung_vfd_printf_to_code(config, action_id, template_arg, args):
@automation.register_action(
"samsung_vfd.clear",
ClearAction,
cv.Schema({cv.Required(CONF_ID): cv.use_id(SamsungVfd)}),
"samsung_vfd.set_brightness",
SetBrightnessAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage),
},
key=CONF_BRIGHTNESS,
),
synchronous=True,
)
async def samsung_vfd_clear_to_code(config, action_id, template_arg, args):
async def samsung_vfd_set_brightness_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_brightness(await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_)))
return var
@automation.register_action(
"samsung_vfd.home",
HomeAction,
cv.Schema({cv.Required(CONF_ID): cv.use_id(SamsungVfd)}),
"samsung_vfd.set_blink",
SetBlinkAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_INTERVAL): cv.templatable(cv.positive_time_period_milliseconds),
}
),
synchronous=True,
)
async def samsung_vfd_home_to_code(config, action_id, template_arg, args):
async def samsung_vfd_set_blink_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_interval(await cg.templatable(config[CONF_INTERVAL], args, cg.uint32)))
return var
@automation.register_action(
"samsung_vfd.set_annunciator",
SetAnnunciatorAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_COLUMN): cv.templatable(cv.int_range(min=0, max=20)),
cv.Required(CONF_STATE): cv.templatable(cv.boolean),
}
),
synchronous=True,
)
async def samsung_vfd_set_annunciator_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_column(await cg.templatable(config[CONF_COLUMN], args, cg.int_)))
cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_)))
return var
@automation.register_action(
"samsung_vfd.set_cursor",
SetCursorAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_COLUMN): cv.templatable(cv.int_range(min=0, max=79)),
cv.Required(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)),
}
),
synchronous=True,
)
async def samsung_vfd_set_cursor_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_column(await cg.templatable(config[CONF_COLUMN], args, cg.int_)))
cg.add(var.set_line(await cg.templatable(config[CONF_LINE], args, cg.int_)))
return var
@automation.register_action(
"samsung_vfd.set_display_mode",
SetDisplayModeAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Required(CONF_MODE): cv.enum(DISPLAY_MODES, upper=True),
},
key=CONF_MODE,
),
synchronous=True,
)
async def samsung_vfd_set_display_mode_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_mode(config[CONF_MODE]))
return var
@automation.register_action(
"samsung_vfd.write",
WriteAction,
cv.All(
cv.Schema(
{
cv.GenerateID(): cv.use_id(SamsungVfd),
cv.Optional(CONF_TEXT): cv.templatable(cv.string),
cv.Optional(CONF_DATA): raw_data,
}
),
validate_write,
),
synchronous=True,
)
async def samsung_vfd_write_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
if CONF_TEXT in config:
cg.add(var.set_text(await cg.templatable(config[CONF_TEXT], args, cg.std_string)))
else:
cg.add(var.set_data(config[CONF_DATA]))
return var
+162
View File
@@ -0,0 +1,162 @@
// Generated by tools/gen_charmaps.py -- do not edit.
#pragma once
#include <cstdint>
namespace esphome {
namespace samsung_vfd {
// PC437 (cp437), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC437[128] = {
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, // 0x80
0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, // 0x88
0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, // 0x90
0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, // 0x98
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, // 0xA0
0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, // 0xA8
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, // 0xB0
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, // 0xB8
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, // 0xC0
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, // 0xC8
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, // 0xD0
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, // 0xD8
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, // 0xE0
0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, // 0xE8
0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, // 0xF0
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, // 0xF8
};
// PC850 (cp850), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC850[128] = {
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, // 0x80
0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, // 0x88
0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, // 0x90
0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x0192, // 0x98
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, // 0xA0
0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, // 0xA8
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x00C0, // 0xB0
0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, // 0xB8
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3, // 0xC0
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, // 0xC8
0x00F0, 0x00D0, 0x00CA, 0x00CB, 0x00C8, 0x0131, 0x00CD, 0x00CE, // 0xD0
0x00CF, 0x2518, 0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, // 0xD8
0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0x00FE, // 0xE0
0x00DE, 0x00DA, 0x00DB, 0x00D9, 0x00FD, 0x00DD, 0x00AF, 0x00B4, // 0xE8
0x00AD, 0x00B1, 0x2017, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, // 0xF0
0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, // 0xF8
};
// PC866 (cp866), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC866[128] = {
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, // 0x80
0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, // 0x88
0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, // 0x90
0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, // 0x98
0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, // 0xA0
0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, // 0xA8
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, // 0xB0
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, // 0xB8
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, // 0xC0
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, // 0xC8
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, // 0xD0
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, // 0xD8
0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, // 0xE0
0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, // 0xE8
0x0401, 0x0451, 0x0404, 0x0454, 0x0407, 0x0457, 0x040E, 0x045E, // 0xF0
0x00B0, 0x2219, 0x00B7, 0x221A, 0x2116, 0x00A4, 0x25A0, 0x00A0, // 0xF8
};
// PC737 (cp737), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC737[128] = {
0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, // 0x80
0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, // 0x88
0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, // 0x90
0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, // 0x98
0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, // 0xA0
0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, // 0xA8
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, // 0xB0
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, // 0xB8
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, // 0xC0
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, // 0xC8
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, // 0xD0
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, // 0xD8
0x03C9, 0x03AC, 0x03AD, 0x03AE, 0x03CA, 0x03AF, 0x03CC, 0x03CD, // 0xE0
0x03CB, 0x03CE, 0x0386, 0x0388, 0x0389, 0x038A, 0x038C, 0x038E, // 0xE8
0x038F, 0x00B1, 0x2265, 0x2264, 0x03AA, 0x03AB, 0x00F7, 0x2248, // 0xF0
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, // 0xF8
};
// PC775 (cp775), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC775[128] = {
0x0106, 0x00FC, 0x00E9, 0x0101, 0x00E4, 0x0123, 0x00E5, 0x0107, // 0x80
0x0142, 0x0113, 0x0156, 0x0157, 0x012B, 0x0179, 0x00C4, 0x00C5, // 0x88
0x00C9, 0x00E6, 0x00C6, 0x014D, 0x00F6, 0x0122, 0x00A2, 0x015A, // 0x90
0x015B, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x00A4, // 0x98
0x0100, 0x012A, 0x00F3, 0x017B, 0x017C, 0x017A, 0x201D, 0x00A6, // 0xA0
0x00A9, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x0141, 0x00AB, 0x00BB, // 0xA8
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x0104, 0x010C, 0x0118, // 0xB0
0x0116, 0x2563, 0x2551, 0x2557, 0x255D, 0x012E, 0x0160, 0x2510, // 0xB8
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0172, 0x016A, // 0xC0
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x017D, // 0xC8
0x0105, 0x010D, 0x0119, 0x0117, 0x012F, 0x0161, 0x0173, 0x016B, // 0xD0
0x017E, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, // 0xD8
0x00D3, 0x00DF, 0x014C, 0x0143, 0x00F5, 0x00D5, 0x00B5, 0x0144, // 0xE0
0x0136, 0x0137, 0x013B, 0x013C, 0x0146, 0x0112, 0x0145, 0x2019, // 0xE8
0x00AD, 0x00B1, 0x201C, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x201E, // 0xF0
0x00B0, 0x2219, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, // 0xF8
};
// PC864 (cp864), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC864[128] = {
0x00B0, 0x00B7, 0x2219, 0x221A, 0x2592, 0x2500, 0x2502, 0x253C, // 0x80
0x2524, 0x252C, 0x251C, 0x2534, 0x2510, 0x250C, 0x2514, 0x2518, // 0x88
0x03B2, 0x221E, 0x03C6, 0x00B1, 0x00BD, 0x00BC, 0x2248, 0x00AB, // 0x90
0x00BB, 0xFEF7, 0xFEF8, 0x0000, 0x0000, 0xFEFB, 0xFEFC, 0x0000, // 0x98
0x00A0, 0x00AD, 0xFE82, 0x00A3, 0x00A4, 0xFE84, 0x0000, 0x0000, // 0xA0
0xFE8E, 0xFE8F, 0xFE95, 0xFE99, 0x060C, 0xFE9D, 0xFEA1, 0xFEA5, // 0xA8
0x0660, 0x0661, 0x0662, 0x0663, 0x0664, 0x0665, 0x0666, 0x0667, // 0xB0
0x0668, 0x0669, 0xFED1, 0x061B, 0xFEB1, 0xFEB5, 0xFEB9, 0x061F, // 0xB8
0x00A2, 0xFE80, 0xFE81, 0xFE83, 0xFE85, 0xFECA, 0xFE8B, 0xFE8D, // 0xC0
0xFE91, 0xFE93, 0xFE97, 0xFE9B, 0xFE9F, 0xFEA3, 0xFEA7, 0xFEA9, // 0xC8
0xFEAB, 0xFEAD, 0xFEAF, 0xFEB3, 0xFEB7, 0xFEBB, 0xFEBF, 0xFEC1, // 0xD0
0xFEC5, 0xFECB, 0xFECF, 0x00A6, 0x00AC, 0x00F7, 0x00D7, 0xFEC9, // 0xD8
0x0640, 0xFED3, 0xFED7, 0xFEDB, 0xFEDF, 0xFEE3, 0xFEE7, 0xFEEB, // 0xE0
0xFEED, 0xFEEF, 0xFEF3, 0xFEBD, 0xFECC, 0xFECE, 0xFECD, 0xFEE1, // 0xE8
0xFE7D, 0x0651, 0xFEE5, 0xFEE9, 0xFEEC, 0xFEF0, 0xFEF2, 0xFED0, // 0xF0
0xFED5, 0xFEF5, 0xFEF6, 0xFEDD, 0xFED9, 0xFEF1, 0x25A0, 0x0000, // 0xF8
};
// PC852 (cp852), bytes 0x80..0xFF
static const uint16_t CHARMAP_PC852[128] = {
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x016F, 0x0107, 0x00E7, // 0x80
0x0142, 0x00EB, 0x0150, 0x0151, 0x00EE, 0x0179, 0x00C4, 0x0106, // 0x88
0x00C9, 0x0139, 0x013A, 0x00F4, 0x00F6, 0x013D, 0x013E, 0x015A, // 0x90
0x015B, 0x00D6, 0x00DC, 0x0164, 0x0165, 0x0141, 0x00D7, 0x010D, // 0x98
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x0104, 0x0105, 0x017D, 0x017E, // 0xA0
0x0118, 0x0119, 0x00AC, 0x017A, 0x010C, 0x015F, 0x00AB, 0x00BB, // 0xA8
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x011A, // 0xB0
0x015E, 0x2563, 0x2551, 0x2557, 0x255D, 0x017B, 0x017C, 0x2510, // 0xB8
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0102, 0x0103, // 0xC0
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, // 0xC8
0x0111, 0x0110, 0x010E, 0x00CB, 0x010F, 0x0147, 0x00CD, 0x00CE, // 0xD0
0x011B, 0x2518, 0x250C, 0x2588, 0x2584, 0x0162, 0x016E, 0x2580, // 0xD8
0x00D3, 0x00DF, 0x00D4, 0x0143, 0x0144, 0x0148, 0x0160, 0x0161, // 0xE0
0x0154, 0x00DA, 0x0155, 0x0170, 0x00FD, 0x00DD, 0x0163, 0x00B4, // 0xE8
0x00AD, 0x02DD, 0x02DB, 0x02C7, 0x02D8, 0x00A7, 0x00F7, 0x00B8, // 0xF0
0x00B0, 0x00A8, 0x02D9, 0x0171, 0x0158, 0x0159, 0x25A0, 0x00A0, // 0xF8
};
// Indexed by CodeTable (the ESC t argument); nullptr = no mapping known.
static const uint16_t *const CHARMAPS[8] = {
CHARMAP_PC437, // PC437
CHARMAP_PC850, // PC850
CHARMAP_PC866, // PC866
CHARMAP_PC737, // PC737
CHARMAP_PC775, // PC775
nullptr, // CP10
CHARMAP_PC864, // PC864
CHARMAP_PC852, // PC852
};
} // namespace samsung_vfd
} // namespace esphome
+305 -39
View File
@@ -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
+231 -15
View File
@@ -6,6 +6,7 @@
#include "esphome/components/time/real_time_clock.h"
#include "esphome/components/uart/uart.h"
#include <array>
#include <string>
#include <vector>
@@ -14,11 +15,78 @@ namespace samsung_vfd {
static const char *const TAG = "samsung_vfd";
// Control bytes, see section 5.4 of docs/20L203DA12-datasheet.pdf.
// 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_INIT = 0x40; // ESC @ -> reset / init display
static const uint8_t CMD_CLEAR = 0x0C; // FF -> clear screen (cursor to home)
static const uint8_t CMD_HOME = 0x0B; // VT -> cursor home
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:
@@ -27,44 +95,116 @@ class SamsungVfd : public Component, public uart::UARTDevice {
// 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) { send_newlines_ = v; }
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 cols_; }
uint8_t rows() const { return rows_; }
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();
void home();
std::string line_text(uint8_t line) const {
return line < lines_.size() ? lines_[line] : std::string();
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 single-byte) into one line: truncate or pad, optionally centred.
/// 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);
/// Map UTF-8 to one byte per character: accented Latin-1 letters lose their
/// accent, control characters become spaces, anything else becomes '?'.
static std::string sanitize_(const std::string &text, bool keep_newlines);
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 ----------
@@ -120,5 +260,81 @@ template<typename... Ts> class HomeAction : public Action<Ts...>, public Parente
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