324 lines
11 KiB
C++
324 lines
11 KiB
C++
#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
|