20260326
This commit is contained in:
5
esphome/custom_component/nixie_display/CMakeLists.txt
Normal file
5
esphome/custom_component/nixie_display/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "."
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES "spi"
|
||||
)
|
||||
91
esphome/custom_component/nixie_display/EXAMPLE_USAGE.md
Normal file
91
esphome/custom_component/nixie_display/EXAMPLE_USAGE.md
Normal file
@@ -0,0 +1,91 @@
|
||||
EXAMPLE_USAGE.md
|
||||
|
||||
# ESPHome Nixie Tube Display Component
|
||||
|
||||
This is a custom ESPHome component for controlling a 6-digit nixie tube display with SPI interface.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the `esphome_component` folder to your ESPHome custom components directory:
|
||||
```
|
||||
~/.esphome/custom_components/nixie_display/
|
||||
```
|
||||
|
||||
2. The component should have:
|
||||
- `__init__.py` - Component configuration
|
||||
- `nixie_display.h` - Header file
|
||||
- `nixie_display.cpp` - Implementation
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your ESPHome YAML:
|
||||
|
||||
```yaml
|
||||
spi:
|
||||
id: nixie_spi
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
|
||||
display:
|
||||
- platform: nixie_display
|
||||
id: nixie
|
||||
spi_id: nixie_spi
|
||||
anode0_pin: GPIO5
|
||||
anode1_pin: GPIO13
|
||||
anode2_pin: GPIO17
|
||||
le_pin: GPIO22
|
||||
|
||||
# Lambda to update display with time
|
||||
lambda: |-
|
||||
it.display_text("000000"); // Display as string of 6 digits
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: time_display
|
||||
entity_id: sensor.time
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **6 Nixie Tubes**: Organized as 3 anode sets of 2 tubes each
|
||||
- **SPI Control**: Fast serial interface for cathode control
|
||||
- **Anti-poisoning**: Implements digit cycling to prevent cathode poisoning
|
||||
- **Text Display**: Can display any 6-character string of digits
|
||||
- **60 Hz Refresh Rate**: Smooth flicker-free display
|
||||
|
||||
## Pin Configuration
|
||||
|
||||
- `anode0_pin`: Controls first tube pair (digits 0-1)
|
||||
- `anode1_pin`: Controls second tube pair (digits 2-3)
|
||||
- `anode2_pin`: Controls third tube pair (digits 4-5)
|
||||
- `le_pin`: Latch Enable pin for SPI data locking
|
||||
- SPI pins: CLK, MOSI, MISO (configured via SPI component)
|
||||
|
||||
## Display Format
|
||||
|
||||
The display expects a 6-character string of digits (0-9):
|
||||
- Position 0-1: First anode set
|
||||
- Position 2-3: Second anode set
|
||||
- Position 4-5: Third anode set
|
||||
|
||||
## Usage Example
|
||||
|
||||
```yaml
|
||||
lambda: |-
|
||||
// Display current time
|
||||
auto time_obj = id(homeassistant_time).now();
|
||||
if (time_obj.is_valid()) {
|
||||
char buf[7];
|
||||
snprintf(buf, sizeof(buf), "%02d%02d%02d",
|
||||
time_obj.hour, time_obj.minute, time_obj.second);
|
||||
it.display_text(buf);
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Poisoning Feature
|
||||
|
||||
Automatically enabled during transitions to cycle through digit values and prevent cathode poisoning that occurs with static displays. The algorithm:
|
||||
1. Cycles all digits for 10 iterations
|
||||
2. Then incrementally changes digits to target values
|
||||
3. Total cycle takes ~20 iterations at the refresh rate
|
||||
2
esphome/custom_component/nixie_display/__init__.py
Normal file
2
esphome/custom_component/nixie_display/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@yourgithubname"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
63
esphome/custom_component/nixie_display/display.py
Normal file
63
esphome/custom_component/nixie_display/display.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import display, spi, time
|
||||
from esphome.const import CONF_ID, CONF_SPI_ID
|
||||
|
||||
DEPENDENCIES = ["spi", "time"]
|
||||
|
||||
nixie_display_ns = cg.esphome_ns.namespace("nixie_display")
|
||||
NixieDisplay = nixie_display_ns.class_(
|
||||
"NixieDisplay", display.DisplayBuffer, spi.SPIDevice
|
||||
)
|
||||
|
||||
CONF_ANODE0_PIN = "anode0_pin"
|
||||
CONF_ANODE1_PIN = "anode1_pin"
|
||||
CONF_ANODE2_PIN = "anode2_pin"
|
||||
CONF_LE_PIN = "le_pin"
|
||||
CONF_REFRESH_INTERVAL_US = "refresh_interval_us"
|
||||
CONF_TIME_ID = "time_id"
|
||||
CONF_DEBUG_LOGGING = "debug_logging"
|
||||
CONF_ANTI_POISONING = "anti_poisoning"
|
||||
CONF_LAMBDA_HOLD_MS = "lambda_hold_ms"
|
||||
CONF_AUTO_TIME = "auto_time"
|
||||
|
||||
CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(NixieDisplay),
|
||||
cv.Required(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
|
||||
cv.Required(CONF_ANODE0_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_ANODE1_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_ANODE2_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_LE_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_REFRESH_INTERVAL_US, default=1000): cv.int_range(
|
||||
min=200, max=20000
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_DEBUG_LOGGING, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ANTI_POISONING, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LAMBDA_HOLD_MS, default=1500): cv.int_range(min=0, max=60000),
|
||||
cv.Optional(CONF_AUTO_TIME, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
anode0 = await cg.gpio_pin_expression(config[CONF_ANODE0_PIN])
|
||||
anode1 = await cg.gpio_pin_expression(config[CONF_ANODE1_PIN])
|
||||
anode2 = await cg.gpio_pin_expression(config[CONF_ANODE2_PIN])
|
||||
le_pin = await cg.gpio_pin_expression(config[CONF_LE_PIN])
|
||||
|
||||
cg.add(var.set_anode_pins(anode0, anode1, anode2, le_pin))
|
||||
cg.add(var.set_refresh_interval_us(config[CONF_REFRESH_INTERVAL_US]))
|
||||
cg.add(var.set_debug_logging(config[CONF_DEBUG_LOGGING]))
|
||||
cg.add(var.set_anti_poisoning(config[CONF_ANTI_POISONING]))
|
||||
cg.add(var.set_lambda_hold_ms(config[CONF_LAMBDA_HOLD_MS]))
|
||||
cg.add(var.set_auto_time(config[CONF_AUTO_TIME]))
|
||||
if CONF_TIME_ID in config:
|
||||
time_var = await cg.get_variable(config[CONF_TIME_ID])
|
||||
cg.add(var.set_time_source(time_var))
|
||||
111
esphome/custom_component/nixie_display/example.yaml
Normal file
111
esphome/custom_component/nixie_display/example.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
esphome:
|
||||
name: nixie-clock
|
||||
platform: esp32
|
||||
board: esp32-c3-devkitm-1
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
ssid: "Nixie-Clock"
|
||||
password: "12345678"
|
||||
|
||||
captive_portal:
|
||||
|
||||
logger:
|
||||
level: INFO
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
ota:
|
||||
password: !secret ota_password
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
spi:
|
||||
id: nixie_spi
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
frequency: 1MHz
|
||||
|
||||
display:
|
||||
- platform: nixie_display
|
||||
id: nixie_clock
|
||||
spi_id: nixie_spi
|
||||
anode0_pin: GPIO5
|
||||
anode1_pin: GPIO13
|
||||
anode2_pin: GPIO17
|
||||
le_pin: GPIO22
|
||||
|
||||
# Update display with current time HH:MM:SS
|
||||
lambda: |-
|
||||
auto time_obj = id(sntp_time).now();
|
||||
if (time_obj.is_valid()) {
|
||||
char time_str[7];
|
||||
snprintf(time_str, sizeof(time_str), "%02d%02d%02d",
|
||||
time_obj.hour,
|
||||
time_obj.minute,
|
||||
time_obj.second);
|
||||
it.printf(time_str);
|
||||
} else {
|
||||
it.printf("000000");
|
||||
}
|
||||
return;
|
||||
|
||||
# Buttons from defines.h
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO2
|
||||
mode: INPUT_PULLUP
|
||||
name: "Up Button"
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Up button pressed"
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO4
|
||||
mode: INPUT_PULLUP
|
||||
name: "Down Button"
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Down button pressed"
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO16
|
||||
mode: INPUT_PULLUP
|
||||
name: "Mode Button"
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Mode button pressed"
|
||||
|
||||
# Underglow LED (RGB) for tube illumination from defines.h
|
||||
light:
|
||||
- platform: rgb
|
||||
name: "Tube Underglow"
|
||||
red: underglow_led_r
|
||||
green: underglow_led_g
|
||||
blue: underglow_led_b
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: GPIO25
|
||||
id: underglow_led_r
|
||||
- platform: gpio
|
||||
pin: GPIO26
|
||||
id: underglow_led_g
|
||||
- platform: gpio
|
||||
pin: GPIO27
|
||||
id: underglow_led_b
|
||||
323
esphome/custom_component/nixie_display/nixie_display.cpp
Normal file
323
esphome/custom_component/nixie_display/nixie_display.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "nixie_display.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome {
|
||||
namespace nixie_display {
|
||||
|
||||
static const char *const TAG = "nixie_display";
|
||||
|
||||
void NixieDisplay::setup() {
|
||||
ESP_LOGI(TAG, "Setting up Nixie Display (6 tubes)");
|
||||
|
||||
// Setup GPIO pins
|
||||
this->anode0_pin_->setup();
|
||||
this->anode1_pin_->setup();
|
||||
this->anode2_pin_->setup();
|
||||
this->le_pin_->setup();
|
||||
|
||||
// Initialize display
|
||||
this->displayed_string_ = "120000";
|
||||
this->target_string_ = this->displayed_string_;
|
||||
this->current_mode_ = MODE_TIME;
|
||||
this->target_mode_ = MODE_TIME;
|
||||
this->current_anode_ = 0;
|
||||
this->last_update_us_ = micros();
|
||||
this->last_external_text_ms_ = millis();
|
||||
this->anti_poison_active_ = false;
|
||||
this->anti_poison_counter_ = 0;
|
||||
|
||||
// Initialize SPI through parent class
|
||||
this->spi_setup();
|
||||
|
||||
// Request high-frequency main loop scheduling to keep multiplexing stable.
|
||||
this->high_freq_.start();
|
||||
}
|
||||
|
||||
void NixieDisplay::printf(const char *format, ...) {
|
||||
char buffer[32];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
va_end(args);
|
||||
this->set_target_string_(buffer, MODE_UNKNOWN);
|
||||
}
|
||||
|
||||
void NixieDisplay::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Nixie Display:");
|
||||
LOG_PIN(" Anode 0 Pin: ", this->anode0_pin_);
|
||||
LOG_PIN(" Anode 1 Pin: ", this->anode1_pin_);
|
||||
LOG_PIN(" Anode 2 Pin: ", this->anode2_pin_);
|
||||
LOG_PIN(" LE Pin: ", this->le_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Refresh interval: %u us", this->refresh_interval_us_);
|
||||
ESP_LOGCONFIG(TAG, " Time source configured: %s", this->time_source_ != nullptr ? "YES" : "NO");
|
||||
ESP_LOGCONFIG(TAG, " Debug logging: %s", this->debug_logging_ ? "YES" : "NO");
|
||||
ESP_LOGCONFIG(TAG, " Anti poisoning: %s", this->anti_poisoning_ ? "YES" : "NO");
|
||||
ESP_LOGCONFIG(TAG, " Auto time: %s", this->auto_time_ ? "YES" : "NO");
|
||||
ESP_LOGCONFIG(TAG, " Date mode: %s", this->date_mode_ ? "ON" : "OFF");
|
||||
ESP_LOGCONFIG(TAG, " Auto rotate: %s", this->auto_rotate_ ? "ON" : "OFF");
|
||||
ESP_LOGCONFIG(TAG, " Anti poison on mode change: %s", this->anti_poison_on_mode_change_ ? "YES" : "NO");
|
||||
ESP_LOGCONFIG(TAG, " Lambda hold: %u ms", this->lambda_hold_ms_);
|
||||
}
|
||||
|
||||
void NixieDisplay::update() {
|
||||
// Keep default display integration behavior available.
|
||||
this->do_update_();
|
||||
}
|
||||
|
||||
void NixieDisplay::loop() {
|
||||
const uint32_t now_ms = millis();
|
||||
|
||||
// Render desired content at a fixed logic cadence.
|
||||
if ((now_ms - this->last_logic_update_ms_) >= 200) {
|
||||
this->last_logic_update_ms_ = now_ms;
|
||||
|
||||
bool effective_date_mode = this->date_mode_;
|
||||
if (this->auto_rotate_) {
|
||||
const time_t rotate_epoch = ::time(nullptr);
|
||||
if (rotate_epoch > 1700000000) {
|
||||
struct tm rotate_tm;
|
||||
localtime_r(&rotate_epoch, &rotate_tm);
|
||||
// Show date for 5 seconds every 30 seconds when auto-rotate is enabled.
|
||||
effective_date_mode = (rotate_tm.tm_sec % 30) < 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Run YAML lambda/pages only when a writer exists.
|
||||
if (this->writer_.has_value()) {
|
||||
this->do_update_();
|
||||
}
|
||||
|
||||
// Let lambda-provided content temporarily override auto-rendered content.
|
||||
const bool lambda_override_active =
|
||||
(this->lambda_hold_ms_ > 0) && ((now_ms - this->last_external_text_ms_) < this->lambda_hold_ms_);
|
||||
|
||||
if (this->auto_time_ && !lambda_override_active) {
|
||||
bool wrote_time = false;
|
||||
if (this->time_source_ != nullptr) {
|
||||
auto now = this->time_source_->now();
|
||||
if (now.is_valid()) {
|
||||
const bool mode_changed = (effective_date_mode != this->last_effective_date_mode_);
|
||||
const bool second_changed = (now.second != this->last_render_second_);
|
||||
if (mode_changed || second_changed) {
|
||||
char buf[7];
|
||||
if (effective_date_mode) {
|
||||
snprintf(buf, sizeof(buf), "%02d%02d%02d", now.day_of_month, now.month, now.year % 100);
|
||||
this->set_target_string_(buf, MODE_DATE);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%02d%02d%02d", now.hour, now.minute, now.second);
|
||||
this->set_target_string_(buf, MODE_TIME);
|
||||
}
|
||||
this->last_render_second_ = now.second;
|
||||
this->last_effective_date_mode_ = effective_date_mode;
|
||||
}
|
||||
wrote_time = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Always allow system-time fallback even if time_id wiring failed.
|
||||
if (!wrote_time) {
|
||||
const time_t epoch = ::time(nullptr);
|
||||
if (epoch > 1700000000) {
|
||||
struct tm tm_now;
|
||||
localtime_r(&epoch, &tm_now);
|
||||
const bool mode_changed = (effective_date_mode != this->last_effective_date_mode_);
|
||||
const bool second_changed = (tm_now.tm_sec != this->last_render_second_);
|
||||
if (mode_changed || second_changed) {
|
||||
char buf[7];
|
||||
if (effective_date_mode) {
|
||||
snprintf(buf, sizeof(buf), "%02d%02d%02d", tm_now.tm_mday, tm_now.tm_mon + 1,
|
||||
(tm_now.tm_year + 1900) % 100);
|
||||
this->set_target_string_(buf, MODE_DATE);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%02d%02d%02d", tm_now.tm_hour, tm_now.tm_min,
|
||||
tm_now.tm_sec);
|
||||
this->set_target_string_(buf, MODE_TIME);
|
||||
}
|
||||
this->last_render_second_ = tm_now.tm_sec;
|
||||
this->last_effective_date_mode_ = effective_date_mode;
|
||||
}
|
||||
} else {
|
||||
this->set_target_string_(effective_date_mode ? "010100" : "120000",
|
||||
effective_date_mode ? MODE_DATE : MODE_TIME);
|
||||
this->last_render_second_ = -1;
|
||||
this->last_effective_date_mode_ = effective_date_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->anti_poisoning_ && this->anti_poison_pending_ && this->displayed_string_ != this->target_string_) {
|
||||
this->displayed_string_ = this->anti_poison_transition_(this->displayed_string_, this->target_string_);
|
||||
// anti_poison_transition_ marks anti_poison_active_ false after one full cycle.
|
||||
// Clear pending here so we don't immediately restart another cycle.
|
||||
if (!this->anti_poison_active_) {
|
||||
this->anti_poison_pending_ = false;
|
||||
this->current_mode_ = this->target_mode_;
|
||||
}
|
||||
} else {
|
||||
this->displayed_string_ = this->target_string_;
|
||||
this->anti_poison_active_ = false;
|
||||
this->anti_poison_pending_ = false;
|
||||
this->current_mode_ = this->target_mode_;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->debug_logging_ && (now_ms - this->last_debug_log_ms_) >= 2000) {
|
||||
this->last_debug_log_ms_ = now_ms;
|
||||
const time_t epoch = ::time(nullptr);
|
||||
auto rtc_now = this->time_source_ != nullptr ? this->time_source_->now() : esphome::ESPTime();
|
||||
const bool epoch_valid = epoch > 1700000000;
|
||||
ESP_LOGW(TAG,
|
||||
"debug status: rtc_configured=%s rtc_valid=%s epoch_valid=%s date_mode=%s auto_rotate=%s epoch=%ld shown=%s",
|
||||
this->time_source_ != nullptr ? "yes" : "no",
|
||||
rtc_now.is_valid() ? "yes" : "no",
|
||||
epoch_valid ? "yes" : "no",
|
||||
this->date_mode_ ? "on" : "off",
|
||||
this->auto_rotate_ ? "on" : "off",
|
||||
(long) epoch,
|
||||
this->displayed_string_.c_str());
|
||||
}
|
||||
|
||||
// Multiplex scanning must run continuously, independent of poll interval.
|
||||
const uint32_t now = micros();
|
||||
if ((uint32_t) (now - this->last_update_us_) >= this->refresh_interval_us_) {
|
||||
this->last_update_us_ = now;
|
||||
this->update_tube_display_(this->displayed_string_);
|
||||
}
|
||||
}
|
||||
|
||||
void NixieDisplay::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// Nixie tubes don't support pixel-based drawing
|
||||
// This is a text display component
|
||||
}
|
||||
|
||||
void NixieDisplay::set_anode_(uint8_t anode_index) {
|
||||
this->anode0_pin_->digital_write(false);
|
||||
this->anode1_pin_->digital_write(false);
|
||||
this->anode2_pin_->digital_write(false);
|
||||
|
||||
if (anode_index == 0)
|
||||
this->anode0_pin_->digital_write(true);
|
||||
else if (anode_index == 1)
|
||||
this->anode1_pin_->digital_write(true);
|
||||
else if (anode_index == 2)
|
||||
this->anode2_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
uint8_t NixieDisplay::char_to_digit_(char c) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
return c - '0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void NixieDisplay::set_target_string_(const std::string &value, DisplayMode mode) {
|
||||
std::string filtered;
|
||||
filtered.reserve(NUM_TUBES);
|
||||
|
||||
for (char c : value) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
filtered.push_back(c);
|
||||
if (filtered.size() == NUM_TUBES)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (filtered.size() < NUM_TUBES)
|
||||
filtered.append(NUM_TUBES - filtered.size(), '0');
|
||||
|
||||
this->target_mode_ = mode;
|
||||
if (this->anti_poison_on_mode_change_ &&
|
||||
this->current_mode_ != MODE_UNKNOWN &&
|
||||
this->target_mode_ != MODE_UNKNOWN &&
|
||||
this->current_mode_ != this->target_mode_) {
|
||||
this->anti_poison_pending_ = true;
|
||||
}
|
||||
|
||||
this->target_string_ = filtered;
|
||||
}
|
||||
|
||||
void NixieDisplay::update_tube_display_(const std::string &display_str) {
|
||||
// Ensure string is exactly 6 characters
|
||||
if (display_str.length() != NUM_TUBES) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tube pair for current anode set
|
||||
int cur_tube = this->current_anode_ * 2;
|
||||
|
||||
uint8_t digit1 = this->char_to_digit_(display_str[cur_tube]);
|
||||
uint8_t digit2 = this->char_to_digit_(display_str[cur_tube + 1]);
|
||||
|
||||
uint32_t var32 = this->digit_bitmap_[digit1];
|
||||
uint32_t tmp_var = this->digit_bitmap_[digit2];
|
||||
var32 |= (tmp_var << 10);
|
||||
|
||||
// SPI transfer (3 bytes)
|
||||
this->set_anode_(255); // blank all anodes while shifting out new cathode data
|
||||
this->le_pin_->digital_write(false); // Transparent mode
|
||||
|
||||
const uint8_t tx[3] = {(uint8_t) (var32 >> 16),
|
||||
(uint8_t) (var32 >> 8),
|
||||
(uint8_t) var32};
|
||||
this->enable();
|
||||
this->write_array(tx, sizeof(tx));
|
||||
this->disable();
|
||||
|
||||
this->le_pin_->digital_write(true); // Latch data
|
||||
|
||||
// Set active anode
|
||||
this->set_anode_(this->current_anode_);
|
||||
|
||||
// Cycle to next anode set
|
||||
this->current_anode_++;
|
||||
if (this->current_anode_ >= 3) {
|
||||
this->current_anode_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::string NixieDisplay::anti_poison_transition_(const std::string &from_str,
|
||||
const std::string &to_str) {
|
||||
static uint8_t current_digits[6];
|
||||
static uint8_t target_digits[6];
|
||||
static uint8_t iteration_counter = 0;
|
||||
|
||||
if (!this->anti_poison_active_) {
|
||||
this->anti_poison_active_ = true;
|
||||
for (int i = 0; i < NUM_TUBES; i++) {
|
||||
current_digits[i] = this->char_to_digit_(from_str[i]);
|
||||
target_digits[i] = this->char_to_digit_(to_str[i]);
|
||||
}
|
||||
iteration_counter = 0;
|
||||
}
|
||||
|
||||
// Increment all digits to prevent cathode poisoning
|
||||
for (int i = 0; i < NUM_TUBES; i++) {
|
||||
if (iteration_counter < 5) {
|
||||
current_digits[i]++;
|
||||
} else if (current_digits[i] != target_digits[i]) {
|
||||
current_digits[i]++;
|
||||
}
|
||||
if (current_digits[i] >= 10) {
|
||||
current_digits[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
iteration_counter++;
|
||||
if (iteration_counter >= 10) {
|
||||
iteration_counter = 0;
|
||||
this->anti_poison_active_ = false;
|
||||
}
|
||||
|
||||
std::string result;
|
||||
result.reserve(NUM_TUBES);
|
||||
for (int i = 0; i < NUM_TUBES; i++) {
|
||||
result += (char)('0' + current_digits[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace nixie_display
|
||||
} // namespace esphome
|
||||
150
esphome/custom_component/nixie_display/nixie_display.h
Normal file
150
esphome/custom_component/nixie_display/nixie_display.h
Normal file
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include <cstdarg>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace nixie_display {
|
||||
|
||||
class NixieDisplay : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST,
|
||||
spi::CLOCK_POLARITY_HIGH,
|
||||
spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_200KHZ> {
|
||||
public:
|
||||
enum DisplayMode : uint8_t {
|
||||
MODE_UNKNOWN = 0,
|
||||
MODE_TIME = 1,
|
||||
MODE_DATE = 2,
|
||||
};
|
||||
|
||||
void set_anode_pins(InternalGPIOPin *anode0, InternalGPIOPin *anode1,
|
||||
InternalGPIOPin *anode2, InternalGPIOPin *le_pin) {
|
||||
this->anode0_pin_ = anode0;
|
||||
this->anode1_pin_ = anode1;
|
||||
this->anode2_pin_ = anode2;
|
||||
this->le_pin_ = le_pin;
|
||||
}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
display::DisplayType get_display_type() override {
|
||||
return display::DISPLAY_TYPE_BINARY;
|
||||
}
|
||||
void printf(const char *format, ...);
|
||||
void display_text(const char *text) {
|
||||
this->last_external_text_ms_ = millis();
|
||||
this->set_target_string_(text, MODE_UNKNOWN);
|
||||
}
|
||||
void display_time_text(const char *text) {
|
||||
this->last_external_text_ms_ = millis();
|
||||
this->set_target_string_(text, MODE_TIME);
|
||||
}
|
||||
void display_date_text(const char *text) {
|
||||
this->last_external_text_ms_ = millis();
|
||||
this->set_target_string_(text, MODE_DATE);
|
||||
}
|
||||
void set_time_source(time::RealTimeClock *rtc) { this->time_source_ = rtc; }
|
||||
void set_debug_logging(bool enabled) { this->debug_logging_ = enabled; }
|
||||
void set_anti_poisoning(bool enabled) { this->anti_poisoning_ = enabled; }
|
||||
void set_lambda_hold_ms(uint32_t hold_ms) { this->lambda_hold_ms_ = hold_ms; }
|
||||
void set_auto_time(bool enabled) { this->auto_time_ = enabled; }
|
||||
void set_date_mode(bool enabled) {
|
||||
this->date_mode_ = enabled;
|
||||
if (enabled) {
|
||||
this->auto_rotate_ = false;
|
||||
}
|
||||
}
|
||||
bool is_date_mode() const { return this->date_mode_; }
|
||||
void set_auto_rotate(bool enabled) {
|
||||
this->auto_rotate_ = enabled;
|
||||
if (enabled) {
|
||||
this->date_mode_ = false;
|
||||
}
|
||||
}
|
||||
bool is_auto_rotate() const { return this->auto_rotate_; }
|
||||
void set_refresh_interval_us(uint32_t interval_us) {
|
||||
this->refresh_interval_us_ = interval_us;
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
int get_height_internal() override { return 1; }
|
||||
int get_width_internal() override { return NUM_TUBES; }
|
||||
|
||||
private:
|
||||
InternalGPIOPin *anode0_pin_{nullptr};
|
||||
InternalGPIOPin *anode1_pin_{nullptr};
|
||||
InternalGPIOPin *anode2_pin_{nullptr};
|
||||
InternalGPIOPin *le_pin_{nullptr};
|
||||
|
||||
// Bitmap for digits 0-9, 10 bits per digit (cathode pins)
|
||||
const uint16_t digit_bitmap_[10] = {
|
||||
1022, // 0
|
||||
1021, // 1
|
||||
1019, // 2
|
||||
1015, // 3
|
||||
1007, // 4
|
||||
991, // 5
|
||||
959, // 6
|
||||
895, // 7
|
||||
767, // 8
|
||||
511 // 9
|
||||
};
|
||||
|
||||
static constexpr int NUM_TUBES = 6;
|
||||
static constexpr uint32_t DEFAULT_REFRESH_INTERVAL_US = 1000;
|
||||
|
||||
uint32_t last_update_us_;
|
||||
uint32_t last_logic_update_ms_{0};
|
||||
uint32_t refresh_interval_us_{DEFAULT_REFRESH_INTERVAL_US};
|
||||
uint32_t lambda_hold_ms_{1500};
|
||||
uint8_t current_anode_;
|
||||
std::string displayed_string_;
|
||||
std::string target_string_;
|
||||
DisplayMode current_mode_{MODE_UNKNOWN};
|
||||
DisplayMode target_mode_{MODE_UNKNOWN};
|
||||
bool anti_poison_active_;
|
||||
unsigned long anti_poison_start_;
|
||||
uint8_t anti_poison_counter_;
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
time::RealTimeClock *time_source_{nullptr};
|
||||
bool debug_logging_{false};
|
||||
bool anti_poisoning_{true};
|
||||
bool auto_time_{true};
|
||||
bool date_mode_{false};
|
||||
bool auto_rotate_{false};
|
||||
bool anti_poison_on_mode_change_{true};
|
||||
bool anti_poison_pending_{false};
|
||||
uint32_t last_debug_log_ms_{0};
|
||||
uint32_t last_external_text_ms_{0};
|
||||
int last_render_second_{-1};
|
||||
bool last_effective_date_mode_{false};
|
||||
|
||||
// Anti-poisoning function: cycles through digits to prevent cathode buildup
|
||||
std::string anti_poison_transition_(const std::string &from_str,
|
||||
const std::string &to_str);
|
||||
|
||||
// Update the SPI display for current tube pair and anode
|
||||
void update_tube_display_(const std::string &display_str);
|
||||
|
||||
// Set active anode
|
||||
void set_anode_(uint8_t anode_index);
|
||||
|
||||
// Convert character to digit (0-9)
|
||||
uint8_t char_to_digit_(char c);
|
||||
|
||||
void set_target_string_(const std::string &value, DisplayMode mode);
|
||||
};
|
||||
|
||||
} // namespace nixie_display
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user