273 lines
11 KiB
C++
273 lines
11 KiB
C++
#pragma once
|
|
|
|
#include "esphome/core/component.h"
|
|
#include "esphome/core/hal.h"
|
|
#include "esphome/components/light/light_output.h"
|
|
#include "esphome/components/time/real_time_clock.h"
|
|
|
|
#include <driver/gptimer.h>
|
|
|
|
namespace esphome {
|
|
namespace aip33628 {
|
|
|
|
// Two AiP33628 drivers behind the HU-058D panel, one per two-wire bus.
|
|
// Mapping, scan timing and the current budget are documented in
|
|
// docs/display-map.md.
|
|
|
|
static const uint8_t DRAM_ATTR COM_SEQ[4] = {0x30, 0x0C, 0x03, 0xC0};
|
|
|
|
// Each COM pair holds the bus for 600us, so a full four pair cycle is 2400us
|
|
// and the panel refreshes at 416.7Hz, within a hertz of what the stock
|
|
// firmware ran.
|
|
//
|
|
// That 600us is subdivided into binary weighted sub-frames, 40, 80, 160 and
|
|
// 320us. A channel wanting duty level L is lit in the sub-frames whose weight
|
|
// bits are set in L, so four sub-frames buy sixteen levels rather than the
|
|
// five that four equal ones would. The shortest sub-frame is 40us against a
|
|
// 6.4us frame send, so there is room to spare.
|
|
static const uint8_t COLOR_BITS = 4;
|
|
static const uint8_t COLOR_LEVELS = 1 << COLOR_BITS; // 0 to 15 inclusive
|
|
static const uint32_t UNIT_US = 40;
|
|
static const uint8_t MAX_STEPS = 4 * COLOR_BITS;
|
|
|
|
// Current step to mA, from the datasheet. Tenths of a mA.
|
|
static const uint16_t IS_MA[16] = {25, 51, 76, 101, 126, 152, 177, 202,
|
|
227, 253, 278, 303, 328, 354, 379, 404};
|
|
|
|
// Current step to the nibble that goes on the wire. The frame carries IS[0]
|
|
// at bit 27 and IS[3] at bit 24, so the field is bit reversed against the
|
|
// rest of the frame. Steps 0 and 15 are palindromes, which is why getting
|
|
// this wrong looks correct at both ends of the brightness range and scrambles
|
|
// the order everywhere in between. The table is its own inverse.
|
|
static const uint8_t DRAM_ATTR IS_WIRE[16] = {0x0, 0x8, 0x4, 0xC, 0x2, 0xA, 0x6, 0xE,
|
|
0x1, 0x9, 0x5, 0xD, 0x3, 0xB, 0x7, 0xF};
|
|
|
|
enum Channel : uint8_t { CH_BLUE = 0, CH_GREEN = 1, CH_RED = 2 };
|
|
|
|
// Where each LED physically sits, from docs/led-layout.md. id is the number
|
|
// used by the board layout map and by set_position_color, 1 to 33, and 0
|
|
// marks a position the panel can address but nothing is wired to. nx runs 0
|
|
// at the left edge of the first digit to 255 at the right edge of the last,
|
|
// ny 0 at the top of a digit to 255 at the bottom.
|
|
struct PosGeom {
|
|
uint8_t id;
|
|
uint8_t nx;
|
|
uint8_t ny;
|
|
};
|
|
extern const PosGeom GEOM[4][9];
|
|
|
|
// Running effects. Each owns the panel while it is selected, and selecting
|
|
// NONE hands it back to whatever the colors were before.
|
|
enum class Effect : uint8_t { NONE = 0, CYCLE = 1, FLASH = 2 };
|
|
|
|
// How far apart an effect spreads its phase across the panel. PANEL moves
|
|
// everything together, DIGIT gives each digit its own phase, LED gives every
|
|
// position its own. The direction the spread runs is the effect angle.
|
|
enum class Spread : uint8_t { PANEL = 0, DIGIT = 1, LED = 2 };
|
|
|
|
// What the panel is showing. Everything except TIME is temporary and expires
|
|
// on its own, so no caller can leave the clock stuck not being a clock.
|
|
enum class Mode : uint8_t { TIME, SECONDS, NUMBER, LAMP };
|
|
|
|
// One step of the scan schedule: latch this pattern on both drivers, then
|
|
// hold it for this long. Adjacent sub-frames with identical data collapse
|
|
// into a single longer step, so a saturated color costs one send per COM
|
|
// pair, exactly what the two level scan cost before.
|
|
struct ScanStep {
|
|
uint8_t units; // dwell, in UNIT_US ticks, 1 to 15
|
|
uint8_t cs;
|
|
uint16_t ss[2];
|
|
};
|
|
|
|
// One complete scan pattern. render_() fills the back buffer and then flips
|
|
// front_, so the scan callback can never read a half written pattern.
|
|
struct ScanBuf {
|
|
ScanStep step[MAX_STEPS];
|
|
uint8_t n;
|
|
uint8_t is;
|
|
};
|
|
|
|
class Aip33628Panel : public Component {
|
|
public:
|
|
void set_pins(InternalGPIOPin *clk, InternalGPIOPin *data, InternalGPIOPin *clk2,
|
|
InternalGPIOPin *data2) {
|
|
clk_ = clk;
|
|
data_ = data;
|
|
clk2_ = clk2;
|
|
data2_ = data2;
|
|
}
|
|
void set_time(time::RealTimeClock *rtc) { time_ = rtc; }
|
|
void set_max_current(uint8_t is) { max_current_ = is; }
|
|
// Both of these are live. Home Assistant drives them through template
|
|
// switches, so each has to force a redraw rather than wait for the next
|
|
// rollover.
|
|
void set_twelve_hour(bool v) {
|
|
twelve_hour_ = v;
|
|
dirty_ = true;
|
|
}
|
|
void set_blink_colon(bool v) {
|
|
blink_colon_ = v;
|
|
dirty_ = true;
|
|
}
|
|
// Offline drops the upper colon dot, so a glance at the panel says whether
|
|
// the time is still being kept honest. Driven from the wifi triggers in
|
|
// clock.yaml rather than by including the wifi component here.
|
|
void set_online(bool v) {
|
|
online_ = v;
|
|
dirty_ = true;
|
|
}
|
|
|
|
void setup() override;
|
|
void loop() override;
|
|
void dump_config() override;
|
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
|
|
|
// Called by the light platform. Color components and brightness are 0 to 1.
|
|
void set_light(bool on, float r, float g, float b, float brightness);
|
|
|
|
// Temporary displays, driven from the api actions in clock.yaml. Each takes
|
|
// a lifetime in milliseconds and falls back to the time when it runs out.
|
|
void show_seconds(int ms);
|
|
void show_number(int value, const std::string &unit, int ms);
|
|
// Every populated position, white, at a fixed brightness. Ignores the
|
|
// color tiers, any running effect, and the light being off.
|
|
void lamp_test(int ms);
|
|
|
|
// Per digit color. Blocks are 0 to 3, left to right, and each carries its
|
|
// own annunciator: block 0 the AM mark, block 1 the colon, block 2 the date
|
|
// dash. Components run 0 to 1 and are used as given rather than normalized,
|
|
// so a digit can be dimmer than its neighbors as well as a different hue,
|
|
// which is what a per digit fade needs. A digit with no color of its own
|
|
// follows the master light.
|
|
void set_digit_color(int digit, float r, float g, float b);
|
|
void clear_digit_colors();
|
|
|
|
// One LED, by the id in docs/led-layout.md. Overrides the digit color,
|
|
// which in turn overrides the master light.
|
|
void set_position_color(int id, float r, float g, float b);
|
|
// A linear ramp between two colors across the panel. The angle is in
|
|
// degrees, 0 running left to right and 90 top to bottom, and the ramp is
|
|
// always stretched to cover the whole panel whatever the angle.
|
|
void set_gradient(float r0, float g0, float b0, float r1, float g1, float b1,
|
|
float angle_deg);
|
|
|
|
// Effects. Everything here is live and takes hold on the next update.
|
|
void set_effect(int mode);
|
|
void set_effect_speed(float seconds);
|
|
void set_effect_spread(int mode);
|
|
void set_effect_angle(float deg);
|
|
void set_effect_hue_span(float deg);
|
|
void set_flash_fade(float seconds);
|
|
|
|
protected:
|
|
void render_();
|
|
void apply_colors_();
|
|
void update_effect_(uint32_t now_ms);
|
|
void recompute_axis_();
|
|
void clear_positions_();
|
|
void write_digit_(uint8_t block, char c);
|
|
void write_pos_(uint8_t block, uint8_t seg, bool on);
|
|
void draw_number_(int value, char unit);
|
|
static bool scan_tick_(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata,
|
|
void *arg);
|
|
void send_pair_(uint16_t ss1, uint16_t ss2, uint8_t cs, uint8_t is);
|
|
|
|
InternalGPIOPin *clk_{nullptr};
|
|
InternalGPIOPin *data_{nullptr};
|
|
InternalGPIOPin *clk2_{nullptr};
|
|
InternalGPIOPin *data2_{nullptr};
|
|
time::RealTimeClock *time_{nullptr};
|
|
|
|
// Port bit masks for the four pins. A frame goes out as direct register
|
|
// stores with both buses clocked together, which takes 6.4us against 28.0us
|
|
// for two passes through ISRInternalGPIOPin. All four pins have to live
|
|
// below GPIO32 for this, which __init__.py enforces at config time.
|
|
uint32_t clk_mask_{0}, data_mask_{0}, clk2_mask_{0}, data2_mask_{0};
|
|
|
|
|
|
uint8_t max_current_{15};
|
|
bool twelve_hour_{true};
|
|
bool blink_colon_{true};
|
|
bool online_{false};
|
|
|
|
// Which positions are lit, before color is applied.
|
|
bool on_[4][9]{}; // [block][segment], segments A..G then annunciator, annunciator 2
|
|
bool enabled_{false};
|
|
// [block][segment][channel], 0 to COLOR_LEVELS - 1, index by Channel.
|
|
uint8_t level_[4][9][3]{};
|
|
uint8_t requested_current_{15};
|
|
|
|
// Color before it is quantized, in three tiers. A position with pos_set_
|
|
// wins, then a block with digit_set_, then the master light's base_rgb_.
|
|
float base_rgb_[3]{1.0f, 1.0f, 1.0f};
|
|
float digit_rgb_[4][3]{};
|
|
bool digit_set_[4]{};
|
|
float pos_rgb_[4][9][3]{};
|
|
bool pos_set_[4][9]{};
|
|
|
|
// Scales every color on its way to a duty level. The flash effect drives
|
|
// it and nothing else touches it, so it stays at 1 the rest of the time.
|
|
float envelope_{1.0f};
|
|
|
|
Effect effect_{Effect::NONE};
|
|
Spread spread_{Spread::PANEL};
|
|
float effect_speed_{10.0f}; // seconds for one full cycle
|
|
float effect_angle_{0.0f};
|
|
// How much of the color wheel the panel covers end to end. A full turn on
|
|
// four digits packs the whole spectrum into a hand span and reads as
|
|
// noise, so the useful settings are narrow, a slice rather than the lot.
|
|
float hue_span_{90.0f};
|
|
// Seconds each flash transition takes. Zero snaps.
|
|
float flash_fade_{0.0f};
|
|
uint32_t effect_t0_{0};
|
|
uint32_t effect_at_{0};
|
|
// Where each position sits along the effect axis, 0 to 1, recomputed only
|
|
// when the angle changes rather than every frame.
|
|
float axis_pos_[4][9]{};
|
|
float axis_blk_[4]{};
|
|
|
|
ScanBuf buf_[2]{};
|
|
volatile uint8_t front_{0};
|
|
volatile uint8_t step_{0};
|
|
volatile uint8_t wait_{0}; // ticks left before the next step is latched
|
|
|
|
Mode mode_{Mode::TIME};
|
|
uint32_t mode_until_{0};
|
|
uint8_t lamp_current_{0};
|
|
int number_{0};
|
|
char unit_{'\0'};
|
|
|
|
int last_key_{-1}; // whatever the current mode reduces its content to
|
|
bool last_colon_{false};
|
|
bool dirty_{true};
|
|
};
|
|
|
|
class Aip33628Light : public light::LightOutput {
|
|
public:
|
|
void set_panel(Aip33628Panel *panel) { panel_ = panel; }
|
|
|
|
light::LightTraits get_traits() override {
|
|
auto traits = light::LightTraits();
|
|
traits.set_supported_color_modes({light::ColorMode::RGB});
|
|
return traits;
|
|
}
|
|
|
|
void write_state(light::LightState *state) override {
|
|
auto v = state->current_values;
|
|
// Color ratio comes from the raw components, which ESPHome has already
|
|
// normalized so the largest is 1. Magnitude comes through the helper
|
|
// rather than v.get_brightness(), because only the helper carries the
|
|
// transition state, without which a fade never moves. Both arrive linear,
|
|
// since gamma lives in the component.
|
|
float bright;
|
|
state->current_values_as_brightness(&bright);
|
|
panel_->set_light(v.is_on(), v.get_red(), v.get_green(), v.get_blue(), bright);
|
|
}
|
|
|
|
protected:
|
|
Aip33628Panel *panel_{nullptr};
|
|
};
|
|
|
|
} // namespace aip33628
|
|
} // namespace esphome
|