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:
1 parent
3fcd15ad86
commit
95686a3608
8 files changed
+1317
-143
No files matched your search
@@ -1,22 +1,27 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
ESPHome external component for a Samsung 20L203 2×20 serial VFD (RS-232
|
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
|
input, write-only, factory default 9600 8N1). An ESPHome server consumes it
|
||||||
`external_components: source: github://...`, so the layout must stay
|
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.
|
`components/samsung_vfd/` at the repo root.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `components/samsung_vfd/__init__.py`: config schema, codegen and the action
|
- `components/samsung_vfd/__init__.py`: the config schema (size, brightness,
|
||||||
registrations (`print`, `printf`, `clear`, `home`).
|
code table, international charset, user characters), codegen and every
|
||||||
- `components/samsung_vfd/samsung_vfd.h`: the class, the control-byte constants
|
`samsung_vfd.*` action registration.
|
||||||
and the templated action classes.
|
- `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,
|
- `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,
|
- `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
|
- `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.
|
`type: local, path: ../components` source.
|
||||||
|
|
||||||
## Design rules
|
## 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
|
Every change goes through `flush_()`. It rebuilds the payload, returns early
|
||||||
if the payload equals `last_payload_`, and otherwise sends `0B` (home) plus
|
if the payload equals `last_payload_`, and otherwise sends `0B` (home) plus
|
||||||
the payload. Don't clear before redrawing: that causes flicker.
|
the payload. Don't clear before redrawing: that causes flicker.
|
||||||
- Anything that changes the display out of band (clear, init) must reset
|
- Anything that changes the screen out of band (clear, init, raw writes, CAN,
|
||||||
`last_payload_` so the next flush retransmits.
|
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
|
- Text sent to the display is one byte per character; always pass it through
|
||||||
`sanitize_()`.
|
`encode()`, which uses the current code table, then strips accents, then
|
||||||
- Keep the protocol constants in `samsung_vfd.h`. The display boots in
|
falls back to `?`.
|
||||||
overwrite mode: the cursor wraps from the end of line 1 to line 2 and from
|
- The display boots in overwrite mode: the cursor wraps from the end of line 1
|
||||||
the end of line 2 back home, and nothing scrolls. A full-screen redraw is
|
to line 2 and from the end of line 2 back home, and nothing scrolls. The
|
||||||
therefore `0B` plus `rows × cols` characters.
|
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
|
- 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
|
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
|
## Checking changes
|
||||||
|
|
||||||
@@ -49,8 +63,9 @@ esphome config samsung-vfd.yaml # schema check
|
|||||||
esphome compile samsung-vfd.yaml # full C++ build
|
esphome compile samsung-vfd.yaml # full C++ build
|
||||||
```
|
```
|
||||||
|
|
||||||
`secrets.yaml` is gitignored. Copy `examples/secrets.yaml.example` to
|
On Windows, run the compile from PowerShell or cmd, not Git Bash: the ESP-IDF
|
||||||
`examples/secrets.yaml` to compile.
|
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
|
Follow ESPHome C++ style: `this->` on members, a trailing `_` on protected
|
||||||
members, and the `esphome::samsung_vfd` namespace.
|
members, and the `esphome::samsung_vfd` namespace.
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
# Samsung serial VFD — ESPHome component
|
# Samsung serial VFD — ESPHome component
|
||||||
|
|
||||||
An ESPHome external component for the Samsung 20L203 2-line × 20-character VFD
|
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
|
(vacuum fluorescent display) pole display, driven over a single serial wire.
|
||||||
9600 baud.
|
|
||||||
|
|
||||||
It keeps a copy of the screen in RAM and only sends data when something
|
- 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,
|
changes. Updates overwrite the screen in place instead of clearing it first,
|
||||||
so the clock doesn't flicker.
|
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>
|
<p>
|
||||||
<img src="docs/front.jpg" alt="Front of the display" height="300">
|
<img src="docs/front.jpg" alt="Front of the display" height="300">
|
||||||
@@ -15,9 +19,10 @@ so the clock doesn't flicker.
|
|||||||
|
|
||||||
```
|
```
|
||||||
components/samsung_vfd/ the component (Python schema + C++)
|
components/samsung_vfd/ the component (Python schema + C++)
|
||||||
examples/samsung-vfd.yaml full example: clock + two Home Assistant text entities
|
examples/samsung-vfd.yaml full example device config
|
||||||
examples/secrets.yaml.example
|
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
|
## Using it from your ESPHome server
|
||||||
@@ -27,16 +32,19 @@ compiles.
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
external_components:
|
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]
|
components: [samsung_vfd]
|
||||||
refresh: 1d
|
refresh: 1d
|
||||||
|
|
||||||
uart:
|
uart:
|
||||||
id: uart_vfd
|
id: uart_vfd
|
||||||
tx_pin: GPIO17
|
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
|
- platform: sntp
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
|
|
||||||
@@ -102,18 +110,41 @@ first thing to check: off with a MAX3232, on without one.
|
|||||||
|
|
||||||
### Jumpers
|
### Jumpers
|
||||||
|
|
||||||
The 2×8 pin header on the board selects the following. All jumpers open is the
|
The 2×8 pin header on the board selects the following. Jumpers are read only
|
||||||
factory default.
|
at power-on. All open is the factory default.
|
||||||
|
|
||||||
| Jumpers | Setting | Default (open) |
|
| Jumper 1, 2 | Baud rate |
|
||||||
|---------|---------|----------------|
|
|---|---|
|
||||||
| 1, 2 | baud rate: 9600 / 4800 / 2400 / 19200 | 9600 |
|
| open, open | **9600** |
|
||||||
| 3, 4 | parity: none / odd / even | none |
|
| open, short | 4800 |
|
||||||
| 5 | self-test at power-on | off |
|
| short, open | 2400 |
|
||||||
| 6, 7, 8 | font for codes 0x80–0xFF | PC437 |
|
| short, short | 19200 |
|
||||||
|
|
||||||
This component expects 9600 baud and no parity, which ESPHome's UART uses by
|
| Jumper 3, 4 | Parity |
|
||||||
default.
|
|---|---|
|
||||||
|
| 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
|
## Configuration
|
||||||
|
|
||||||
@@ -123,81 +154,181 @@ samsung_vfd:
|
|||||||
uart_id: uart_vfd
|
uart_id: uart_vfd
|
||||||
cols: 20 # default 20 (1–80)
|
cols: 20 # default 20 (1–80)
|
||||||
rows: 2 # default 2 (1–8)
|
rows: 2 # default 2 (1–8)
|
||||||
send_newlines: false # default false; leave off for this display
|
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:`
|
The UART needs a `tx_pin`. The component requires a `time:` component in the
|
||||||
component in the config.
|
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
|
## Actions
|
||||||
|
|
||||||
|
`id:` is optional in every action when there is only one display.
|
||||||
|
|
||||||
|
### Text
|
||||||
|
|
||||||
| Action | Options |
|
| 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.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.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.clear` | Clears the screen. |
|
||||||
| `samsung_vfd.home` | Moves the cursor home. |
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- samsung_vfd.print:
|
- samsung_vfd.print:
|
||||||
id: vfd
|
|
||||||
line: 0
|
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:
|
- samsung_vfd.printf:
|
||||||
id: vfd
|
|
||||||
time_id: sntp_time
|
time_id: sntp_time
|
||||||
line: 1
|
line: 1
|
||||||
centered: true
|
centered: true
|
||||||
format: "%a %d %b %H:%M"
|
format: "%a %d %b %H:%M"
|
||||||
```
|
```
|
||||||
|
|
||||||
From a lambda you can call `id(vfd).print(...)`, `print_line(line, text, centered)`,
|
### Display settings
|
||||||
`clear()`, `home()` and `line_text(line)`.
|
|
||||||
|
|
||||||
Text is converted to one byte per character. Accented Latin-1 letters lose
|
| Action | Options |
|
||||||
their accent (é → e), other non-ASCII characters become `?`, and control
|
|--------|---------|
|
||||||
characters become spaces.
|
| `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
|
## 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,
|
- Bottom line: a live clock, `%a %d %b %H:%M:%S`, updated every second (SNTP,
|
||||||
Europe/Amsterdam).
|
Europe/Amsterdam).
|
||||||
- Two Home Assistant `text` entities:
|
- Home Assistant entities:
|
||||||
- **VFD line 1** (max 20 chars) is shown on the top line and kept across
|
- **VFD line 1** (text, max 20 chars) is shown on the top line and kept
|
||||||
reboots.
|
across reboots.
|
||||||
- **VFD status** (max 40 chars) shows a full-screen, word-wrapped, centred
|
- **VFD status** (text, max 40 chars) shows a full-screen, word-wrapped,
|
||||||
banner for 10 s. The clock pauses meanwhile, then line 1 and the clock come
|
centred banner for 10 s. The clock pauses meanwhile, then line 1 and the
|
||||||
back.
|
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
|
- Wi-Fi with a captive-portal fallback AP, OTA updates and an encrypted native
|
||||||
API.
|
API.
|
||||||
|
|
||||||
## Protocol
|
## How updates are sent
|
||||||
|
|
||||||
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 |
|
|
||||||
|
|
||||||
At power-on the display is in overwrite mode. After the last column of the top
|
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
|
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
|
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
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
- **Garbage or nothing on the display:** toggle `inverted:` on the TX pin (see
|
- **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
|
[Signal levels](#signal-levels)). Check that `baud_rate` and `parity` match
|
||||||
open (9600) and that parity jumpers 3 and 4 are open (none).
|
jumpers 1–4.
|
||||||
- **The display runs its self-test for about 20 s after power-on:** jumper 5 is
|
- **The display runs its self-test for about 20 s after power-on:** jumper 5 is
|
||||||
shorted.
|
shorted.
|
||||||
- **Line 2 text lands on line 1:** make sure `send_newlines` is off. With
|
- **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
|
20-column lines the cursor has already wrapped to line 2, and an extra line
|
||||||
feed sends it back to line 1.
|
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.
|
||||||
@@ -2,25 +2,154 @@ import esphome.codegen as cg
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.components import time as time_, uart
|
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"]
|
DEPENDENCIES = ["uart", "time"]
|
||||||
|
|
||||||
CONF_COLS = "cols"
|
CONF_COLS = "cols"
|
||||||
|
CONF_COLUMN = "column"
|
||||||
CONF_ROWS = "rows"
|
CONF_ROWS = "rows"
|
||||||
CONF_LINE = "line"
|
CONF_LINE = "line"
|
||||||
CONF_FORMAT = "format"
|
CONF_FORMAT = "format"
|
||||||
CONF_CENTERED = "centered"
|
CONF_CENTERED = "centered"
|
||||||
CONF_SEND_NEWLINES = "send_newlines"
|
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")
|
samsung_vfd_ns = cg.esphome_ns.namespace("samsung_vfd")
|
||||||
|
|
||||||
SamsungVfd = samsung_vfd_ns.class_("SamsungVfd", cg.Component, uart.UARTDevice)
|
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)
|
PrintAction = samsung_vfd_ns.class_("PrintAction", automation.Action)
|
||||||
PrintfAction = samsung_vfd_ns.class_("PrintfAction", automation.Action)
|
PrintfAction = samsung_vfd_ns.class_("PrintfAction", automation.Action)
|
||||||
ClearAction = samsung_vfd_ns.class_("ClearAction", automation.Action)
|
ClearAction = samsung_vfd_ns.class_("ClearAction", automation.Action)
|
||||||
HomeAction = samsung_vfd_ns.class_("HomeAction", 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 = (
|
CONFIG_SCHEMA = (
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
@@ -31,15 +160,21 @@ CONFIG_SCHEMA = (
|
|||||||
# Off by default: lines are padded to exactly `cols` characters and the
|
# Off by default: lines are padded to exactly `cols` characters and the
|
||||||
# display wraps to the next line by itself.
|
# display wraps to the next line by itself.
|
||||||
cv.Optional(CONF_SEND_NEWLINES, default=False): cv.boolean,
|
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(cv.COMPONENT_SCHEMA)
|
||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
# Baud rate and parity must match the display's jumpers, so they are left to
|
||||||
"samsung_vfd", baud_rate=9600, require_tx=True
|
# the uart: block.
|
||||||
)
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("samsung_vfd", require_tx=True)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
@@ -48,6 +183,35 @@ async def to_code(config):
|
|||||||
await uart.register_uart_device(var, config)
|
await uart.register_uart_device(var, config)
|
||||||
cg.add(var.set_size(config[CONF_COLS], config[CONF_ROWS]))
|
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_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(
|
@automation.register_action(
|
||||||
@@ -55,7 +219,7 @@ async def to_code(config):
|
|||||||
PrintAction,
|
PrintAction,
|
||||||
cv.Schema(
|
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.Required(CONF_TEXT): cv.templatable(cv.string),
|
||||||
cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)),
|
cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)),
|
||||||
cv.Optional(CONF_CENTERED): cv.templatable(cv.boolean),
|
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,
|
PrintfAction,
|
||||||
cv.Schema(
|
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_TIME_ID): cv.use_id(time_.RealTimeClock),
|
||||||
cv.Required(CONF_FORMAT): cv.templatable(cv.string),
|
cv.Required(CONF_FORMAT): cv.templatable(cv.string),
|
||||||
cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)),
|
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(
|
@automation.register_action(
|
||||||
"samsung_vfd.clear",
|
"samsung_vfd.set_brightness",
|
||||||
ClearAction,
|
SetBrightnessAction,
|
||||||
cv.Schema({cv.Required(CONF_ID): cv.use_id(SamsungVfd)}),
|
cv.maybe_simple_value(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(SamsungVfd),
|
||||||
|
cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage),
|
||||||
|
},
|
||||||
|
key=CONF_BRIGHTNESS,
|
||||||
|
),
|
||||||
synchronous=True,
|
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)
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
await cg.register_parented(var, config[CONF_ID])
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
cg.add(var.set_brightness(await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_)))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
"samsung_vfd.home",
|
"samsung_vfd.set_blink",
|
||||||
HomeAction,
|
SetBlinkAction,
|
||||||
cv.Schema({cv.Required(CONF_ID): cv.use_id(SamsungVfd)}),
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(SamsungVfd),
|
||||||
|
cv.Required(CONF_INTERVAL): cv.templatable(cv.positive_time_period_milliseconds),
|
||||||
|
}
|
||||||
|
),
|
||||||
synchronous=True,
|
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)
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
await cg.register_parented(var, config[CONF_ID])
|
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
|
return var
|
||||||
@@ -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
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
#include "samsung_vfd.h"
|
#include "samsung_vfd.h"
|
||||||
|
#include "charmaps.h"
|
||||||
|
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace samsung_vfd {
|
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"
|
static const char LATIN1_FOLD[] = "AAAAAAACEEEEIIIIDNOOOOOxOUUUUYPs"
|
||||||
"aaaaaaaceeeeiiiidnooooo/ouuuuypy";
|
"aaaaaaaceeeeiiiidnooooo/ouuuuypy";
|
||||||
|
|
||||||
|
static const char *const CODE_TABLE_NAMES[] = {"PC437", "PC850", "PC866", "PC737",
|
||||||
|
"PC775", "CP10", "PC864", "PC852"};
|
||||||
|
|
||||||
void SamsungVfd::setup() {
|
void SamsungVfd::setup() {
|
||||||
this->resize_lines_();
|
this->resize_lines_();
|
||||||
this->init_display_();
|
this->init_display_();
|
||||||
@@ -20,57 +26,168 @@ void SamsungVfd::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, "Samsung VFD:");
|
ESP_LOGCONFIG(TAG, "Samsung VFD:");
|
||||||
ESP_LOGCONFIG(TAG, " Size: %u cols x %u rows", this->cols_, this->rows_);
|
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, " 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_() {
|
// ---------- low-level output ----------
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SamsungVfd::write_str_(const std::string &s) {
|
void SamsungVfd::write_str_(const std::string &s) {
|
||||||
if (!s.empty())
|
if (!s.empty())
|
||||||
this->write_array(reinterpret_cast<const uint8_t *>(s.data()), s.size());
|
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;
|
std::string out;
|
||||||
out.reserve(text.size());
|
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]);
|
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' : ' ');
|
out.push_back(keep_newlines ? '\n' : ' ');
|
||||||
} else if (c < 0x20 || c == 0x7F) {
|
} else if (cp < 0x20 || cp == 0x7F) {
|
||||||
out.push_back(' ');
|
out.push_back(' ');
|
||||||
} else if (c < 0x80) {
|
} else if (cp < 0x80) {
|
||||||
out.push_back(static_cast<char>(c));
|
out.push_back(static_cast<char>(cp));
|
||||||
} else if (c >= 0xC0) {
|
} else {
|
||||||
// Start of a multi-byte UTF-8 sequence: decode 2-byte Latin-1 letters,
|
out.push_back(this->encode_codepoint_(cp));
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
// Stray continuation bytes (0x80..0xBF) are dropped.
|
|
||||||
}
|
}
|
||||||
return out;
|
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 SamsungVfd::fit_(const std::string &text, bool centered) const {
|
||||||
std::string line = text.substr(0, this->cols_);
|
std::string line = text.substr(0, this->cols_);
|
||||||
if (centered && line.size() < 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) {
|
void SamsungVfd::print(const std::string &raw, bool centered) {
|
||||||
this->resize_lines_();
|
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
|
// 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.
|
// 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_)
|
if (this->lines_.size() < this->rows_)
|
||||||
this->resize_lines_();
|
this->resize_lines_();
|
||||||
|
|
||||||
this->lines_[line] = this->fit_(sanitize_(text, false), centered);
|
this->lines_[line] = this->fit_(this->encode(text, false), centered);
|
||||||
this->flush_();
|
this->flush_();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamsungVfd::clear() {
|
void SamsungVfd::clear() {
|
||||||
this->resize_lines_();
|
this->resize_lines_();
|
||||||
this->last_payload_.clear();
|
this->invalidate_();
|
||||||
this->write_byte(CMD_CLEAR);
|
this->write_byte(CMD_CLEAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamsungVfd::home() { this->write_byte(CMD_HOME); }
|
|
||||||
|
|
||||||
void SamsungVfd::flush_() {
|
void SamsungVfd::flush_() {
|
||||||
std::string payload;
|
std::string payload;
|
||||||
for (uint8_t r = 0; r < this->rows_; r++) {
|
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
|
// No clear: every line is padded to the full width, so going home and
|
||||||
// overwriting replaces all characters without the blank-screen flicker.
|
// 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_byte(CMD_HOME);
|
||||||
this->write_str_(payload);
|
this->write_str_(payload);
|
||||||
this->last_payload_ = 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 samsung_vfd
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "esphome/components/time/real_time_clock.h"
|
#include "esphome/components/time/real_time_clock.h"
|
||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -14,11 +15,78 @@ namespace samsung_vfd {
|
|||||||
|
|
||||||
static const char *const TAG = "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_ESC = 0x1B;
|
||||||
static const uint8_t CMD_INIT = 0x40; // ESC @ -> reset / init display
|
static const uint8_t CMD_US = 0x1F;
|
||||||
static const uint8_t CMD_CLEAR = 0x0C; // FF -> clear screen (cursor to home)
|
|
||||||
static const uint8_t CMD_HOME = 0x0B; // VT -> cursor home
|
// 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 {
|
class SamsungVfd : public Component, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
@@ -27,44 +95,116 @@ class SamsungVfd : public Component, public uart::UARTDevice {
|
|||||||
// Before the default (DATA) priority, so restored entity values can be shown.
|
// Before the default (DATA) priority, so restored entity values can be shown.
|
||||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
// ---------- configuration (before setup) ----------
|
||||||
void set_size(uint8_t cols, uint8_t rows) {
|
void set_size(uint8_t cols, uint8_t rows) {
|
||||||
this->cols_ = cols == 0 ? 20 : cols;
|
this->cols_ = cols == 0 ? 20 : cols;
|
||||||
this->rows_ = rows == 0 ? 2 : rows;
|
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 cols() const { return this->cols_; }
|
||||||
uint8_t rows() const { return rows_; }
|
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.
|
/// Replace the whole screen with `text`, wrapping it over the available lines.
|
||||||
void print(const std::string &text, bool centered = false);
|
void print(const std::string &text, bool centered = false);
|
||||||
/// Replace a single line (0-based). Text is truncated/padded to the column count.
|
/// 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);
|
void print_line(uint8_t line, const std::string &text, bool centered = false);
|
||||||
|
/// Clear the screen (and the buffer).
|
||||||
void clear();
|
void clear();
|
||||||
void home();
|
|
||||||
|
|
||||||
std::string line_text(uint8_t line) const {
|
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:
|
protected:
|
||||||
void init_display_();
|
void init_display_();
|
||||||
|
void apply_settings_();
|
||||||
|
void send_user_chars_();
|
||||||
|
void send_annunciators_();
|
||||||
void resize_lines_();
|
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;
|
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.
|
/// Rebuild the wire payload from the shadow buffer and send it only if it changed.
|
||||||
void flush_();
|
void flush_();
|
||||||
void write_str_(const std::string &s);
|
void write_str_(const std::string &s);
|
||||||
|
void send_esc_(uint8_t cmd);
|
||||||
/// Map UTF-8 to one byte per character: accented Latin-1 letters lose their
|
void send_esc_(uint8_t cmd, uint8_t arg);
|
||||||
/// accent, control characters become spaces, anything else becomes '?'.
|
void send_us_(uint8_t cmd);
|
||||||
static std::string sanitize_(const std::string &text, bool keep_newlines);
|
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 cols_{20};
|
||||||
uint8_t rows_{2};
|
uint8_t rows_{2};
|
||||||
bool send_newlines_{false};
|
bool send_newlines_{false};
|
||||||
std::vector<std::string> lines_;
|
std::vector<std::string> lines_;
|
||||||
std::string last_payload_;
|
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 ----------
|
// ---------- actions ----------
|
||||||
@@ -120,5 +260,81 @@ template<typename... Ts> class HomeAction : public Action<Ts...>, public Parente
|
|||||||
void play(const Ts &...x) override { this->parent_->home(); }
|
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 samsung_vfd
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
@@ -21,7 +21,7 @@ esphome:
|
|||||||
id: vfd
|
id: vfd
|
||||||
- samsung_vfd.print:
|
- samsung_vfd.print:
|
||||||
id: vfd
|
id: vfd
|
||||||
text: "Hello World!"
|
text: "Hello World! ~" # "~" is the heart defined below
|
||||||
centered: true
|
centered: true
|
||||||
- delay: 3s
|
- delay: 3s
|
||||||
# Bring back the last text Home Assistant set for the top line.
|
# Bring back the last text Home Assistant set for the top line.
|
||||||
@@ -31,7 +31,10 @@ esphome:
|
|||||||
text: !lambda "return id(vfd_line1).state;"
|
text: !lambda "return id(vfd_line1).state;"
|
||||||
|
|
||||||
external_components:
|
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]
|
components: [samsung_vfd]
|
||||||
refresh: 1d
|
refresh: 1d
|
||||||
# While developing the component, use the checkout instead:
|
# While developing the component, use the checkout instead:
|
||||||
@@ -78,12 +81,14 @@ time:
|
|||||||
- 0.pool.ntp.org
|
- 0.pool.ntp.org
|
||||||
- 1.pool.ntp.org
|
- 1.pool.ntp.org
|
||||||
|
|
||||||
|
# Must match the display's jumpers; all open (factory default) = 9600, no parity.
|
||||||
uart:
|
uart:
|
||||||
id: uart_vfd
|
id: uart_vfd
|
||||||
tx_pin:
|
tx_pin:
|
||||||
number: GPIO17
|
number: GPIO17
|
||||||
inverted: false # true when wired directly, without a MAX3232
|
inverted: false # true when wired directly, without a MAX3232
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
|
parity: NONE
|
||||||
# The display is output-only for us; no rx_pin needed.
|
# The display is output-only for us; no rx_pin needed.
|
||||||
|
|
||||||
samsung_vfd:
|
samsung_vfd:
|
||||||
@@ -91,8 +96,20 @@ samsung_vfd:
|
|||||||
uart_id: uart_vfd
|
uart_id: uart_vfd
|
||||||
cols: 20
|
cols: 20
|
||||||
rows: 2
|
rows: 2
|
||||||
# Lines are padded to 20 chars and the display wraps to line 2 by itself.
|
brightness: 100%
|
||||||
send_newlines: false
|
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.
|
# Bottom line shows the SNTP clock every second, except while a status banner is up.
|
||||||
interval:
|
interval:
|
||||||
@@ -171,3 +188,31 @@ text:
|
|||||||
- script.execute:
|
- script.execute:
|
||||||
id: show_status
|
id: show_status
|
||||||
msg: !lambda "return x;"
|
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
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user