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

+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.