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

+34 -19
View File
@@ -1,22 +1,27 @@
# CLAUDE.md
ESPHome external component for a Samsung 20L203 2×20 serial VFD (RS-232
input, 9600 8N1, write-only). It is consumed from git by an ESPHome server through
`external_components: source: github://...`, so the layout must stay
input, write-only, factory default 9600 8N1). An ESPHome server consumes it
from git (`https://mangavalk.com/mangavalk/esphome-samsung-vfd`, a self-hosted
server, so `type: git` with a URL, not `github://`). The layout must stay
`components/samsung_vfd/` at the repo root.
## Layout
- `components/samsung_vfd/__init__.py`: config schema, codegen and the action
registrations (`print`, `printf`, `clear`, `home`).
- `components/samsung_vfd/samsung_vfd.h`: the class, the control-byte constants
and the templated action classes.
- `components/samsung_vfd/__init__.py`: the config schema (size, brightness,
code table, international charset, user characters), codegen and every
`samsung_vfd.*` action registration.
- `components/samsung_vfd/samsung_vfd.h`: the protocol constants, enums, the
class and the templated action classes.
- `components/samsung_vfd/samsung_vfd.cpp`: the shadow buffer, word wrap,
UTF-8 → single-byte sanitising and `flush_()`.
UTF-8 → display byte encoding, settings re-apply and the raw commands.
- `components/samsung_vfd/charmaps.h`: **generated** by
`python tools/gen_charmaps.py`; don't edit it by hand.
- `docs/20L203DA12-datasheet.pdf`: the command set (section 5.4), jumpers,
wiring and the character tables. Check it before adding protocol features.
wiring and the CG-ROM tables (section 6). Check it before changing the
protocol.
- `examples/samsung-vfd.yaml`: the full reference device config. It pulls the
component from GitHub; for local work, switch to the commented
component from git; for local work, switch to the commented
`type: local, path: ../components` source.
## Design rules
@@ -25,17 +30,26 @@ input, 9600 8N1, write-only). It is consumed from git by an ESPHome server throu
Every change goes through `flush_()`. It rebuilds the payload, returns early
if the payload equals `last_payload_`, and otherwise sends `0B` (home) plus
the payload. Don't clear before redrawing: that causes flicker.
- Anything that changes the display out of band (clear, init) must reset
`last_payload_` so the next flush retransmits.
- Anything that changes the screen out of band (clear, init, raw writes, CAN,
user-character changes, the self-test) must call `invalidate_()` so the next
flush retransmits.
- Settings (brightness, blink/on, annunciators, code table, charset, user
characters, mode) are stored in members and re-sent by `apply_settings_()`
after `ESC @` (`reinit()`) and after the self-test. A new setting must be
added there too.
- Text sent to the display is one byte per character; always pass it through
`sanitize_()`.
- Keep the protocol constants in `samsung_vfd.h`. The display boots in
overwrite mode: the cursor wraps from the end of line 1 to line 2 and from
the end of line 2 back home, and nothing scrolls. A full-screen redraw is
therefore `0B` plus `rows × cols` characters.
`encode()`, which uses the current code table, then strips accents, then
falls back to `?`.
- The display boots in overwrite mode: the cursor wraps from the end of line 1
to line 2 and from the end of line 2 back home, and nothing scrolls. The
buffered text API depends on this; the scroll modes are only for raw writes.
- The RXD input is RS-232. The reference build uses a MAX3232 (no inversion
in config); a directly wired ESP32 pin needs `inverted: true` on the UART TX
pin.
pin. Baud rate and parity come from the board's jumpers, so they are not
validated by the component.
- User characters: up to 4, codes 0x20–0x7E. Each is 5 column bytes, bit 6 =
top row. `pattern_to_columns()` must turn the datasheet's `$` example into
`12 2A 7F 2A 24`.
## Checking changes
@@ -49,8 +63,9 @@ esphome config samsung-vfd.yaml # schema check
esphome compile samsung-vfd.yaml # full C++ build
```
`secrets.yaml` is gitignored. Copy `examples/secrets.yaml.example` to
`examples/secrets.yaml` to compile.
On Windows, run the compile from PowerShell or cmd, not Git Bash: the ESP-IDF
installer refuses MSYS shells. `secrets.yaml` is gitignored. Copy
`examples/secrets.yaml.example` to `examples/secrets.yaml` to compile.
Follow ESPHome C++ style: `this->` on members, a trailing `_` on protected
members, and the `esphome::samsung_vfd` namespace.
+188 -57
View File
@@ -1,12 +1,16 @@
# Samsung serial VFD — ESPHome component
An ESPHome external component for the Samsung 20L203 2-line × 20-character VFD
(vacuum fluorescent display) pole display, driven over a single serial wire at
9600 baud.
(vacuum fluorescent display) pole display, driven over a single serial wire.
It keeps a copy of the screen in RAM and only sends data when something
changes. Updates overwrite the screen in place instead of clearing it first,
so the clock doesn't flicker.
- Keeps a copy of the screen in RAM and only sends data when something
changes. Updates overwrite the screen in place instead of clearing it first,
so a clock doesn't flicker.
- Supports every command in the datasheet: brightness, blinking, screen
on/off, the triangle markers, cursor control, scroll modes, user-defined
characters, code tables and the self-test.
- Converts UTF-8 text to the display's built-in font, so `é`, `ß`, `°`, `½`,
`█`, Greek and Cyrillic come out as real glyphs.
<p>
<img src="docs/front.jpg" alt="Front of the display" height="300">
@@ -14,10 +18,11 @@ so the clock doesn't flicker.
</p>
```
components/samsung_vfd/ the component (Python schema + C++)
examples/samsung-vfd.yaml full example: clock + two Home Assistant text entities
components/samsung_vfd/ the component (Python schema + C++)
examples/samsung-vfd.yaml full example device config
examples/secrets.yaml.example
docs/ photos and the 20L203DA12 datasheet
docs/ photos and the 20L203DA12 datasheet
tools/gen_charmaps.py regenerates components/samsung_vfd/charmaps.h
```
## Using it from your ESPHome server
@@ -27,16 +32,19 @@ compiles.
```yaml
external_components:
- source: github://REPO_OWNER/REPO_NAME@main
- source:
type: git
url: https://mangavalk.com/mangavalk/esphome-samsung-vfd
ref: main
components: [samsung_vfd]
refresh: 1d
uart:
id: uart_vfd
tx_pin: GPIO17
baud_rate: 9600
baud_rate: 9600 # match the display's jumpers (factory default: 9600, no parity)
time: # needed by the component (used by samsung_vfd.printf)
time: # required by the component (used by samsung_vfd.printf)
- platform: sntp
id: sntp_time
@@ -102,18 +110,41 @@ first thing to check: off with a MAX3232, on without one.
### Jumpers
The 2×8 pin header on the board selects the following. All jumpers open is the
factory default.
The 2×8 pin header on the board selects the following. Jumpers are read only
at power-on. All open is the factory default.
| Jumpers | Setting | Default (open) |
|---------|---------|----------------|
| 1, 2 | baud rate: 9600 / 4800 / 2400 / 19200 | 9600 |
| 3, 4 | parity: none / odd / even | none |
| 5 | self-test at power-on | off |
| 6, 7, 8 | font for codes 0x80–0xFF | PC437 |
| Jumper 1, 2 | Baud rate |
|---|---|
| open, open | **9600** |
| open, short | 4800 |
| short, open | 2400 |
| short, short | 19200 |
This component expects 9600 baud and no parity, which ESPHome's UART uses by
default.
| Jumper 3, 4 | Parity |
|---|---|
| open, any | **none** |
| short, open | odd |
| short, short | even |
| Jumper 5 | |
|---|---|
| open | **normal** |
| short | self-test at power-on (about 20 s) |
| Jumper 6, 7, 8 | Font for 0x80–0xFF |
|---|---|
| open, open, open | **PC437** |
| short, open, open | PC850 |
| open, short, open | PC866 |
| short, short, open | PC737 |
| open, open, short | PC775 |
| short, open, short | CP10 for DP86-20 |
| open, short, short | PC864 |
| short, short, short | PC852 |
Set `baud_rate` and `parity` (`NONE`, `ODD` or `EVEN`) under `uart:` to match
jumpers 1–4. The component sends the font from its `code_table` setting at
start-up, so jumpers 6–8 don't matter.
## Configuration
@@ -121,83 +152,183 @@ default.
samsung_vfd:
id: vfd
uart_id: uart_vfd
cols: 20 # default 20 (1–80)
rows: 2 # default 2 (1–8)
send_newlines: false # default false; leave off for this display
cols: 20 # default 20 (1–80)
rows: 2 # default 2 (1–8)
brightness: 100% # 40/60/80/100 %; other values are rounded
code_table: PC437 # font for 0x80–0xFF, see "Characters"
international_charset: USA # see "Characters"
send_newlines: false # leave off for this display
user_characters: # up to 4 custom glyphs
- code: "~" # the ASCII character it replaces
pattern: # 7 rows × 5 pixels, top row first
- ".#.#." # '#', 'X', '1' or '*' = on
- "#####" # '.', '-', '0', '_' or ' ' = off
- "#####"
- "#####"
- ".###."
- "..#.."
- "....."
```
The UART must be 9600 baud with a `tx_pin`. The component requires a `time:`
component in the config.
The UART needs a `tx_pin`. The component requires a `time:` component in the
config.
The component remembers brightness, blink, on/off, triangle markers, code
table, character set and user characters. It sends them again after
`samsung_vfd.reinit` or a self-test.
## Actions
`id:` is optional in every action when there is only one display.
### Text
| Action | Options |
|--------|---------|
| `samsung_vfd.print` | `text` (templatable). Optional `line:` (0-based) replaces one line. Without `line:`, the text is word-wrapped over the whole screen; `\n` forces a new line. Optional `centered: true`. |
| `samsung_vfd.printf` | Shows the current time formatted with strftime, e.g. `format: "%H:%M:%S"`. Requires `time_id:`. Takes the same `line:` and `centered:` options. Does nothing until the clock has synced. |
| `samsung_vfd.clear` | Clears the screen. |
| `samsung_vfd.home` | Moves the cursor home. |
```yaml
- samsung_vfd.print:
id: vfd
line: 0
text: !lambda 'return "Temp " + to_string((int) id(temp).state) + " C";'
text: !lambda 'return "Temp " + to_string((int) id(temp).state) + "°C";'
- samsung_vfd.printf:
id: vfd
time_id: sntp_time
line: 1
centered: true
format: "%a %d %b %H:%M"
```
From a lambda you can call `id(vfd).print(...)`, `print_line(line, text, centered)`,
`clear()`, `home()` and `line_text(line)`.
### Display settings
Text is converted to one byte per character. Accented Latin-1 letters lose
their accent (é → e), other non-ASCII characters become `?`, and control
characters become spaces.
| Action | Options |
|--------|---------|
| `samsung_vfd.set_brightness` | `brightness:` (templatable percentage). Short form: `samsung_vfd.set_brightness: 60%` |
| `samsung_vfd.set_blink` | `interval:` (templatable time): on for that long, off for that long, in 13 ms steps up to 3.3 s. `0ms` stops blinking. |
| `samsung_vfd.turn_off` / `turn_on` | Blanks the screen and brings it back without losing its contents. |
| `samsung_vfd.set_annunciator` | `column:` 1–20, or 0 for all; `state:` true/false (both templatable). Switches the triangle under that column. |
| `samsung_vfd.reinit` | Resets the display, re-applies all settings and redraws the screen. Use it when the display was power-cycled without the ESP. |
| `samsung_vfd.self_test` | Runs the built-in self-test (about 20 s), then restores settings and the screen. |
### Raw control
The text actions above assume overwrite mode and always redraw from the
top-left. The commands below give direct access to the cursor. After raw
writes, the next `print` redraws the whole screen.
| Action | Options |
|--------|---------|
| `samsung_vfd.home` | Moves the cursor to the top-left. |
| `samsung_vfd.set_cursor` | `column:` and `line:` (0-based, templatable). |
| `samsung_vfd.write` | Writes at the cursor. Either `text:` (templatable, converted like `print`) or `data:`, which sends bytes unchanged, e.g. `[0x1F, 0x58, 0x02]` or `"abc"`. |
| `samsung_vfd.set_display_mode` | `OVERWRITE` (default), `VERTICAL_SCROLL` or `HORIZONTAL_SCROLL`. The scroll modes are for raw `write`s, e.g. a ticker; switch back to `OVERWRITE` before using `print` again. |
### Lambda API
Every datasheet command (section 5.4) has a method on `id(vfd)`:
| # | Command | Bytes | Method |
|---|---------|-------|--------|
| 1 | BS | `08` | `cursor_left()` |
| 2 | HT | `09` | `cursor_right()` |
| 3 | LF | `0A` | `cursor_down()` |
| 4 | US LF | `1F 0A` | `cursor_up()` |
| 5 | HOM | `0B` | `home()` |
| 6 | CR | `0D` | `cursor_line_start()` |
| 7 | US CR | `1F 0D` | `cursor_line_end()` |
| 8 | US B | `1F 42` | `cursor_bottom()` |
| 9 | US $ n m | `1F 24 n m` | `set_cursor(column, line)` (0-based) |
| 10 | CLR | `0C` | `clear()` |
| 11 | CAN | `18` | `clear_cursor_line()` |
| 12 | ESC @ | `1B 40` | `reinit()` |
| 13 | ESC % n | `1B 25 n` | sent automatically by `define_char()` |
| 14 | ESC & … | `1B 26 01 n n 05 p1…p5` | `define_char(code, {p1, p2, p3, p4, p5})` |
| 15 | ESC ? n | `1B 3F n` | `cancel_char(code)` |
| 16 | ESC = n | `1B 3D n` | `select_peripheral(n)` (1 = pass-through only, 2 = display, 3 = both) |
| 17 | ESC R n | `1B 52 n` | `set_international_charset(samsung_vfd::CHARSET_GERMANY)` |
| 18 | ESC t n | `1B 74 n` | `set_code_table(samsung_vfd::CODE_TABLE_PC850)` |
| 19–21 | US MD1/2/3 | `1F 01`/`02`/`03` | `set_display_mode(samsung_vfd::MODE_OVERWRITE)` |
| 22 | US E n | `1F 45 n` | `set_blink(ms)`, `turn_off()`, `turn_on()` |
| 23 | US X n | `1F 58 n` | `set_brightness(0.0–1.0)` |
| 24 | US # n m | `1F 23 n m` | `set_annunciator(column, state)` |
| 25 | US @ | `1F 40` | `self_test()` |
The buffered text API is `print(text, centered)`, `print_line(line, text, centered)`
and `line_text(line)`. For anything else, use `write_text(text)`,
`write_raw(bytes)` and `encode(text)`, which returns the display bytes.
## Characters
Text is UTF-8, and each character becomes one byte:
1. ASCII (0x20–0x7E) passes through unchanged. Control characters become
spaces.
2. Other characters are looked up in the selected `code_table`, the display's
font for bytes 0x80–0xFF. With PC437, for example, `é` → 0x82, `°` → 0xF8,
`½` → 0xAB and `█` → 0xDB. The pages at the end of the datasheet show
every table.
3. Accented letters missing from the table lose their accent (`ő` → `o`).
Anything else becomes `?`.
| `code_table` | Covers |
|---|---|
| `PC437` | US / Western European, box drawing, some Greek and maths |
| `PC850` | Western European (more accented capitals) |
| `PC852` | Central European (Polish, Czech, Hungarian …) |
| `PC866` | Cyrillic |
| `PC737` | Greek |
| `PC775` | Baltic |
| `PC864` | Arabic |
| `CP10` | DP86-20 compatible; no conversion table, accents are stripped |
The conversion tables in
[charmaps.h](components/samsung_vfd/charmaps.h) are generated from Python's
standard code page codecs by [tools/gen_charmaps.py](tools/gen_charmaps.py).
`international_charset` (`USA`, `FRANCE`, `GERMANY`, `UK`, `DENMARK1`, `SWEDEN`,
`ITALY`, `SPAIN1`, `JAPAN`, `NORWAY`, `DENMARK2`) replaces 12 ASCII characters
such as `#`, `[`, `\`, `]` and `{` with national letters. For example, with
`GERMANY`, `[` shows as `Ä`. The text conversion doesn't account for this, so
keep `USA` unless you want those swaps.
User characters replace an ASCII character: with the example config, every
`~` in your text shows the heart.
## What the example config does
- On boot: clears the screen, shows `Hello World!` for 3 s, then restores line 1.
- On boot: clears the screen, shows `Hello World! ♥` for 3 s, then restores
line 1.
- Bottom line: a live clock, `%a %d %b %H:%M:%S`, updated every second (SNTP,
Europe/Amsterdam).
- Two Home Assistant `text` entities:
- **VFD line 1** (max 20 chars) is shown on the top line and kept across
reboots.
- **VFD status** (max 40 chars) shows a full-screen, word-wrapped, centred
banner for 10 s. The clock pauses meanwhile, then line 1 and the clock come
back.
- Home Assistant entities:
- **VFD line 1** (text, max 20 chars) is shown on the top line and kept
across reboots.
- **VFD status** (text, max 40 chars) shows a full-screen, word-wrapped,
centred banner for 10 s. The clock pauses meanwhile, then line 1 and the
clock come back.
- **VFD brightness** (select: 40/60/80/100 %).
- **VFD display** (switch) blanks the screen and brings it back.
- Wi-Fi with a captive-portal fallback AP, OTA updates and an encrypted native
API.
## Protocol
These are the commands the component uses. The datasheet, section 5.4, lists
the rest, including cursor positioning, brightness, blinking and the
annunciator triangles.
| Bytes | Effect |
|-------|--------|
| `1B 40` (`ESC @`) | initialise: reset settings, clear, cursor home |
| `0C` | clear screen, cursor home |
| `0B` | cursor home (top-left) |
| `20`–`FF` | print a character and move the cursor right |
## How updates are sent
At power-on the display is in overwrite mode. After the last column of the top
line the cursor continues on the bottom line, and after the last column of the
bottom line it goes back to the top-left. Nothing scrolls. Each update
therefore sends `0B` (home) followed by both lines, each padded to exactly 20
characters.
characters. Nothing is sent when the content didn't change.
## Troubleshooting
- **Garbage or nothing on the display:** toggle `inverted:` on the TX pin (see
[Signal levels](#signal-levels)). Check that the baud jumpers 1 and 2 are
open (9600) and that parity jumpers 3 and 4 are open (none).
[Signal levels](#signal-levels)). Check that `baud_rate` and `parity` match
jumpers 1–4.
- **The display runs its self-test for about 20 s after power-on:** jumper 5 is
shorted.
- **Line 2 text lands on line 1:** make sure `send_newlines` is off. With
20-column lines the cursor has already wrapped to line 2, and an extra line
feed sends it back to line 1.
- **The display was power-cycled and lost its settings or text:** call
`samsung_vfd.reinit`, for example from a Home Assistant button.
+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
+49 -4
View File
@@ -21,7 +21,7 @@ esphome:
id: vfd
- samsung_vfd.print:
id: vfd
text: "Hello World!"
text: "Hello World! ~" # "~" is the heart defined below
centered: true
- delay: 3s
# Bring back the last text Home Assistant set for the top line.
@@ -31,7 +31,10 @@ esphome:
text: !lambda "return id(vfd_line1).state;"
external_components:
- source: github://REPO_OWNER/REPO_NAME@main
- source:
type: git
url: https://mangavalk.com/mangavalk/esphome-samsung-vfd
ref: main
components: [samsung_vfd]
refresh: 1d
# While developing the component, use the checkout instead:
@@ -78,12 +81,14 @@ time:
- 0.pool.ntp.org
- 1.pool.ntp.org
# Must match the display's jumpers; all open (factory default) = 9600, no parity.
uart:
id: uart_vfd
tx_pin:
number: GPIO17
inverted: false # true when wired directly, without a MAX3232
baud_rate: 9600
parity: NONE
# The display is output-only for us; no rx_pin needed.
samsung_vfd:
@@ -91,8 +96,20 @@ samsung_vfd:
uart_id: uart_vfd
cols: 20
rows: 2
# Lines are padded to 20 chars and the display wraps to line 2 by itself.
send_newlines: false
brightness: 100%
code_table: PC437 # glyphs for é, ü, °, ½, box drawing ... (match jumpers 6-8)
international_charset: USA
# Up to 4 custom glyphs, replacing a printable ASCII character you don't use.
user_characters:
- code: "~"
pattern:
- ".#.#."
- "#####"
- "#####"
- "#####"
- ".###."
- "..#.."
- "....."
# Bottom line shows the SNTP clock every second, except while a status banner is up.
interval:
@@ -171,3 +188,31 @@ text:
- script.execute:
id: show_status
msg: !lambda "return x;"
# Brightness (4 levels) and power from Home Assistant.
select:
- platform: template
name: "VFD brightness"
icon: mdi:brightness-6
optimistic: true
restore_value: true
options: ["40%", "60%", "80%", "100%"]
initial_option: "100%"
on_value:
then:
- samsung_vfd.set_brightness:
id: vfd
brightness: !lambda "return (i + 2) * 0.2f;"
switch:
- platform: template
name: "VFD display"
icon: mdi:monitor
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
turn_on_action:
- samsung_vfd.turn_on:
id: vfd
turn_off_action:
- samsung_vfd.turn_off:
id: vfd
+78
View File
@@ -0,0 +1,78 @@
"""Generate components/samsung_vfd/charmaps.h from Python's code page codecs.
Each table lists the Unicode code point shown for bytes 0x80..0xFF in one of
the display's CG-ROM fonts (datasheet section 6), or 0 where there is none.
CP10 (for DP86-20) has no standard codec, so it gets no table.
Run from the repo root: python tools/gen_charmaps.py
"""
from pathlib import Path
# Same order as the ESC t n argument (datasheet 5.4.18).
TABLES = [
("PC437", "cp437"),
("PC850", "cp850"),
("PC866", "cp866"),
("PC737", "cp737"),
("PC775", "cp775"),
("CP10", None),
("PC864", "cp864"),
("PC852", "cp852"),
]
OUT = Path(__file__).resolve().parent.parent / "components" / "samsung_vfd" / "charmaps.h"
def high_half(codec):
points = []
for b in range(0x80, 0x100):
try:
ch = bytes([b]).decode(codec)
except UnicodeDecodeError:
ch = ""
points.append(ord(ch) if len(ch) == 1 else 0)
return points
def main():
lines = [
"// Generated by tools/gen_charmaps.py -- do not edit.",
"#pragma once",
"",
"#include <cstdint>",
"",
"namespace esphome {",
"namespace samsung_vfd {",
"",
]
names = []
for name, codec in TABLES:
if codec is None:
names.append("nullptr")
continue
ident = f"CHARMAP_{name}"
names.append(ident)
pts = high_half(codec)
lines.append(f"// {name} ({codec}), bytes 0x80..0xFF")
lines.append(f"static const uint16_t {ident}[128] = {{")
for i in range(0, 128, 8):
row = ", ".join(f"0x{p:04X}" for p in pts[i : i + 8])
lines.append(f" {row}, // 0x{0x80 + i:02X}")
lines.append("};")
lines.append("")
lines.append("// Indexed by CodeTable (the ESC t argument); nullptr = no mapping known.")
lines.append(f"static const uint16_t *const CHARMAPS[{len(TABLES)}] = {{")
for (name, _), ident in zip(TABLES, names):
lines.append(f" {ident}, // {name}")
lines.append("};")
lines.append("")
lines.append("} // namespace samsung_vfd")
lines.append("} // namespace esphome")
lines.append("")
OUT.write_text("\n".join(lines), encoding="utf-8", newline="\n")
print(f"wrote {OUT}")
if __name__ == "__main__":
main()