#include "nixie_display.h" #include "esphome/core/log.h" #include #include #include 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