diff --git a/CLAUDE.md b/CLAUDE.md
index b32a866..d25c68d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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.
diff --git a/README.md b/README.md
index 7ff029e..6e3a604 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -14,10 +18,11 @@ so the clock doesn't flicker.
```
-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.
diff --git a/components/samsung_vfd/__init__.py b/components/samsung_vfd/__init__.py
index 5304ae7..119823c 100644
--- a/components/samsung_vfd/__init__.py
+++ b/components/samsung_vfd/__init__.py
@@ -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
diff --git a/components/samsung_vfd/charmaps.h b/components/samsung_vfd/charmaps.h
new file mode 100644
index 0000000..c97d0e1
--- /dev/null
+++ b/components/samsung_vfd/charmaps.h
@@ -0,0 +1,162 @@
+// Generated by tools/gen_charmaps.py -- do not edit.
+#pragma once
+
+#include
+
+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
diff --git a/components/samsung_vfd/samsung_vfd.cpp b/components/samsung_vfd/samsung_vfd.cpp
index a7b4558..ad88328 100644
--- a/components/samsung_vfd/samsung_vfd.cpp
+++ b/components/samsung_vfd/samsung_vfd.cpp
@@ -1,16 +1,22 @@
#include "samsung_vfd.h"
+#include "charmaps.h"
#include "esphome/core/log.h"
#include
+#include
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(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(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(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(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(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(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(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(std::lround((b - 0.2f) / 0.2f));
+ return static_cast(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(std::max(1, std::min(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(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 &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(column + 1), static_cast(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 &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
diff --git a/components/samsung_vfd/samsung_vfd.h b/components/samsung_vfd/samsung_vfd.h
index ee72776..628f600 100644
--- a/components/samsung_vfd/samsung_vfd.h
+++ b/components/samsung_vfd/samsung_vfd.h
@@ -6,6 +6,7 @@
#include "esphome/components/time/real_time_clock.h"
#include "esphome/components/uart/uart.h"
+#include
#include
#include
@@ -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 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 &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 &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 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 user_chars_;
};
// ---------- actions ----------
@@ -120,5 +260,81 @@ template class HomeAction : public Action, public Parente
void play(const Ts &...x) override { this->parent_->home(); }
};
+template class SetBrightnessAction : public Action, public Parented {
+ public:
+ TEMPLATABLE_VALUE(float, brightness)
+ void play(const Ts &...x) override { this->parent_->set_brightness(this->brightness_.value(x...)); }
+};
+
+template class SetBlinkAction : public Action, public Parented {
+ public:
+ TEMPLATABLE_VALUE(uint32_t, interval)
+ void play(const Ts &...x) override { this->parent_->set_blink(this->interval_.value(x...)); }
+};
+
+template class TurnOnAction : public Action, public Parented {
+ public:
+ void play(const Ts &...x) override { this->parent_->turn_on(); }
+};
+
+template class TurnOffAction : public Action, public Parented {
+ public:
+ void play(const Ts &...x) override { this->parent_->turn_off(); }
+};
+
+template class SetAnnunciatorAction : public Action, public Parented {
+ public:
+ TEMPLATABLE_VALUE(int, column)
+ TEMPLATABLE_VALUE(bool, state)
+ void play(const Ts &...x) override {
+ this->parent_->set_annunciator(static_cast(this->column_.value(x...)), this->state_.value(x...));
+ }
+};
+
+template class SetCursorAction : public Action, public Parented {
+ public:
+ TEMPLATABLE_VALUE(int, column)
+ TEMPLATABLE_VALUE(int, line)
+ void play(const Ts &...x) override {
+ this->parent_->set_cursor(static_cast(this->column_.value(x...)),
+ static_cast(this->line_.value(x...)));
+ }
+};
+
+template class SetDisplayModeAction : public Action, public Parented {
+ 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 class WriteAction : public Action, public Parented {
+ public:
+ TEMPLATABLE_VALUE(std::string, text)
+ void set_data(const std::vector &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 data_;
+};
+
+template class ReinitAction : public Action, public Parented {
+ public:
+ void play(const Ts &...x) override { this->parent_->reinit(); }
+};
+
+template class SelfTestAction : public Action, public Parented {
+ public:
+ void play(const Ts &...x) override { this->parent_->self_test(); }
+};
+
} // namespace samsung_vfd
} // namespace esphome
diff --git a/examples/samsung-vfd.yaml b/examples/samsung-vfd.yaml
index 39d96d2..5680b98 100644
--- a/examples/samsung-vfd.yaml
+++ b/examples/samsung-vfd.yaml
@@ -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
diff --git a/tools/gen_charmaps.py b/tools/gen_charmaps.py
new file mode 100644
index 0000000..c33962d
--- /dev/null
+++ b/tools/gen_charmaps.py
@@ -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 ",
+ "",
+ "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()