aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2025-12-07 15:37:04 -0700
committerJosh Rahm <joshuarahm@gmail.com>2025-12-07 15:37:04 -0700
commitc9efee6f3ffcf19e139d72d946aa4d837bbcbb9e (patch)
treedc1c5c4c78e9758140029118a7f7c969703572b5
parentb2b344ebec8f7b55506deb0c2138a1e2b9ffeb46 (diff)
downloadesp32-ws2812b-c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e.tar.gz
esp32-ws2812b-c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e.tar.bz2
esp32-ws2812b-c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e.zip
Better timing.
No longer hammering the SPI bus, but rather being more controlled, and interpolation is handled better.
-rw-r--r--include/param.h1
-rw-r--r--include/state_params.i7
-rw-r--r--main/main.c49
-rw-r--r--main/param/int32_t.c43
-rw-r--r--main/station.c3
-rw-r--r--main/ws2812b_writer.c209
6 files changed, 210 insertions, 102 deletions
diff --git a/include/param.h b/include/param.h
index b66b1b1..5259e0a 100644
--- a/include/param.h
+++ b/include/param.h
@@ -25,3 +25,4 @@ DECL_PARAMETER_INSTANCE(bool);
DECL_PARAMETER_INSTANCE(uint32_t);
DECL_PARAMETER_INSTANCE(color_int_t);
DECL_PARAMETER_INSTANCE(int);
+DECL_PARAMETER_INSTANCE(int32_t);
diff --git a/include/state_params.i b/include/state_params.i
index 875328d..e768768 100644
--- a/include/state_params.i
+++ b/include/state_params.i
@@ -1,8 +1,10 @@
+// version 1
+
// List of parameters.
// type name display name, default value
-STATE_PARAM(uint32_t, time, "Current Time", 0)
-STATE_PARAM(int, timetick, "Speed", 100)
+STATE_PARAM(int32_t, time, "Current Time", 0)
+STATE_PARAM(int, speed, "Speed", 1000)
STATE_PARAM(uint8_t, brightness, "Brightness", 50)
STATE_PARAM(uint8_t, n_snow, "Snow Effect", 10)
STATE_PARAM(uint8_t, n_red, "Desaturation", 32)
@@ -14,3 +16,4 @@ STATE_PARAM(bool, use_static_color, "Use Static Color", false)
STATE_PARAM(color_int_t, static_color, "Static Color", 0xffddbb)
STATE_PARAM(bool, twinkle, "Twinkle", true)
STATE_PARAM(uint8_t, twink_amt, "Twinkle Amount", 0)
+STATE_PARAM(uint8_t, base_brightness, "Base Brightness", 0)
diff --git a/main/main.c b/main/main.c
index c57c414..3248b93 100644
--- a/main/main.c
+++ b/main/main.c
@@ -48,28 +48,6 @@
static ws_params_t ws_params;
-/* Ticks the clock at a constant rate independent of the time it takes to write
- * to the led strip. */
-portTASK_FUNCTION(time_keeper, params_)
-{
- ws_params_t* params = (ws_params_t*)params_;
-
- TickType_t last_wake;
- const TickType_t freq = 1;
-
- // Initialise the xLastWakeTime variable with the current time.
- last_wake = xTaskGetTickCount();
-
- for (;;) {
- // Wait for the next cycle.
- vTaskDelayUntil(&last_wake, freq);
- if (xSemaphoreTake(params->state_lock, portMAX_DELAY) == pdTRUE) {
- params->state.time += params->state.timetick;
- xSemaphoreGive(params->state_lock);
- }
- }
-}
-
void app_main(void)
{
esp_err_t error;
@@ -116,30 +94,23 @@ void app_main(void)
wifi_init_station("Wort", "JoshIsBau5");
ESP_LOGI("main", "Complete!");
- if (xSemaphoreTake(ws_params.state_lock, portMAX_DELAY) == pdTRUE) {
- esp_err_t load_err = ws_state_load(&ws_params.state);
- if (load_err == ESP_OK) {
- ESP_LOGI("main", "Restored state from NVS");
- } else {
- ESP_LOGI("main", "No saved state (%d); using defaults", load_err);
- }
- xSemaphoreGive(ws_params.state_lock);
- }
+ // if (xSemaphoreTake(ws_params.state_lock, portMAX_DELAY) == pdTRUE) {
+ // esp_err_t load_err = ws_state_load(&ws_params.state);
+ // if (load_err == ESP_OK) {
+ // ESP_LOGI("main", "Restored state from NVS");
+ // } else {
+ // ESP_LOGI("main", "No saved state (%d); using defaults", load_err);
+ // }
+ // xSemaphoreGive(ws_params.state_lock);
+ // }
- xTaskCreate(
- time_keeper,
- "time_keeper",
- /* stack_depth = */ 1024,
- &ws_params,
- /* priority = */ 1,
- NULL);
/* Pin the LED writer to core 1 to avoid preemption by Wi-Fi/IDF tasks. */
xTaskCreatePinnedToCore(
ws2812b_write_task,
"ws2812b_writer",
/* stack_depth = */ 4096,
&ws_params,
- /* priority = */ 1,
+ /* priority = */ 4,
NULL,
/* core_id = */ 1);
xTaskCreate(
diff --git a/main/param/int32_t.c b/main/param/int32_t.c
new file mode 100644
index 0000000..ebf9d4e
--- /dev/null
+++ b/main/param/int32_t.c
@@ -0,0 +1,43 @@
+#include "param.h"
+#include "http_server.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void handle_option__int32_t(
+ const char* attr, sockbuf_t* sockbuf, int32_t* ptr)
+{
+ int tmp = (int)(*ptr);
+ handle_option__int(attr, sockbuf, &tmp);
+ *ptr = (int32_t)tmp;
+}
+
+size_t serialize_to_json__int32_t(
+ char** out, size_t len, const char* attr, const char* dn, int32_t value)
+{
+ size_t newlen = snprintf(
+ *out,
+ len,
+ "\"%s\":{\"type\":\"int\",\"value\":\"%ld\",\"disp\":\"%s\"}",
+ attr,
+ (long)value,
+ dn);
+ *out += newlen;
+ return len - newlen;
+}
+
+#define TAG "param_int32"
+void http_handle__int32_t(httpd_req_t* req, int32_t* val)
+{
+ int tmp = (int)(*val);
+ http_handle__int(req, &tmp);
+ *val = (int32_t)tmp;
+}
+
+void print__int32_t(sockbuf_t* sockbuf, const char* attr, int32_t val)
+{
+ char buf[128];
+ snprintf(buf, sizeof(buf), "%s: %ld :: int32_t\n", attr, (long)val);
+ sockbuf_write(sockbuf, buf);
+}
diff --git a/main/station.c b/main/station.c
index d4c2d00..6f66a79 100644
--- a/main/station.c
+++ b/main/station.c
@@ -68,6 +68,9 @@ void wifi_init_station(const char* ssid, const char* psk)
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
+ /* Keep Wi-Fi awake to reduce latency/jank impacting LED timing. */
+ esp_wifi_set_ps(WIFI_PS_NONE);
+
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
diff --git a/main/ws2812b_writer.c b/main/ws2812b_writer.c
index 50d89b5..90c7405 100644
--- a/main/ws2812b_writer.c
+++ b/main/ws2812b_writer.c
@@ -1,8 +1,10 @@
#include "ws2812b_writer.h"
+#include <math.h>
#include <stdlib.h>
#include <string.h>
+#include "esp_log.h"
#include "pattern/twinkle.h"
const int NUMBER_PARAMS =
@@ -12,6 +14,8 @@ const int NUMBER_PARAMS =
;
#define SIZE 300
+/* 60 FPS -> ~16.67ms per frame. Keep time scaling aligned. */
+#define FRAME_TIME_DELTA 167
/* Returns round(sin(2πn / 256) * 127.5 + 127.5). */
uint8_t byte_sin(uint8_t n);
@@ -27,6 +31,13 @@ static inline uint8_t byte_scale(uint8_t n, uint8_t sc)
/* returns (in / 256)^n * 256. */
uint8_t amp(uint8_t in, uint8_t n);
+/* Per-channel calibration to balance the LEDs (boost red, soften green/blue).
+ */
+#define CAL_R_NUM 140 /* 1.40x */
+#define CAL_G_NUM 90 /* 0.90x */
+#define CAL_B_NUM 90 /* 0.90x */
+#define CAL_DEN 100
+
void reset_parameters(ws_params_t* params)
{
if (params->state_lock) {
@@ -42,80 +53,149 @@ void reset_parameters(ws_params_t* params)
}
}
-void calculate_colors(const ws_state_t* state, ws2812b_buffer_t* buffer)
+struct rgba {
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+ uint8_t a;
+};
+
+uint32_t asi(struct rgba rgb)
+{
+ return (rgb.r << 16) | (rgb.g << 8) | rgb.b;
+}
+struct rgba get_rgba(int x, int64_t time, const ws_state_t* state)
{
uint32_t red = 0, green = 0, blue = 0;
- int time;
+ uint8_t brightness = state->brightness;
+ if (!state->use_static_color) {
+ red = byte_scale(
+ byte_sin((uint8_t)(time / 1000 + x * 4)), 255 - state->n_red) +
+ state->n_red;
+ green = 255 - red;
+
+ if (state->cool) {
+ uint32_t tmp = green;
+ green = blue;
+ blue = tmp;
+ }
+
+ /* Add a little white flair that comes around every once in a while. */
+
+ uint32_t whitesum = 0;
+ if (state->n_snow) {
+ uint32_t white[] = {
+ /* Parallax "snow" */
+ time / 179 + x * 8,
+ time / 193 + x * 12,
+ time / 211 + x * 16,
+ time / 233 + x * 8,
+ // (state->time + 128) / 233 + x * 8,
+ };
+
+ for (int i = 0; i < sizeof(white) / sizeof(uint32_t); ++i) {
+ if ((white[i] / 256) % state->n_snow == 0) {
+ white[i] = amp(byte_sin(white[i]), 20);
+ } else {
+ white[i] = 0;
+ }
+ whitesum += white[i];
+ }
+ }
+
+ red = min(red + whitesum, 255);
+ green = min(green + whitesum, 255);
+ blue = min(blue + whitesum, 255);
+ } else {
+ red = (state->static_color >> 16) & 0xff;
+ green = (state->static_color >> 8) & 0xff;
+ blue = state->static_color & 0xff;
+ }
+
+ if (state->invert) {
+ red = 255 - red;
+ green = 255 - green;
+ blue = 255 - blue;
+ }
+
+ /* Channel calibration to balance perceived brightness. */
+ red = min((red * CAL_R_NUM) / CAL_DEN, 255);
+ green = min((green * CAL_G_NUM) / CAL_DEN, 255);
+ blue = min((blue * CAL_B_NUM) / CAL_DEN, 255);
+
+ uint8_t twink = 0xff;
+ if (state->twinkle) {
+ twink = twinkle(time / 300, x, state->twink_amt);
+ }
+
+ red = byte_scale(
+ red, max(byte_scale(brightness, twink), state->base_brightness));
+ green = byte_scale(
+ green, max(byte_scale(brightness, twink), state->base_brightness));
+ blue = byte_scale(
+ blue, max(byte_scale(brightness, twink), state->base_brightness));
+
+ return (struct rgba){red, green, blue, 0xff};
+}
+
+/* return a new color which is an interoplation between c1 and c2 by the
+ * provided amount. The semantic amount is amt / 255.0. */
+struct rgba interpolate(struct rgba c1, struct rgba c2, uint8_t amt)
+{
+#define scale(a, b, s) ((a * (255 - s) + b * s) / 255)
+
+ return (struct rgba){scale(c1.r, c2.r, amt),
+ scale(c1.g, c2.g, amt),
+ scale(c1.b, c2.b, amt),
+ 0xff};
+#undef scale
+}
+
+void calculate_colors(const ws_state_t* state, ws2812b_buffer_t* buffer)
+{
for (int i = 0; i < SIZE; ++i) {
int x = i * 255 / state->x_scale;
- time = state->time;
if (!state->power) {
ws2812b_buffer_set_rgb(buffer, i, 0, 0, 0);
continue;
}
+ struct rgba c;
- uint8_t brightness = state->brightness;
- if (!state->use_static_color) {
- red = byte_scale(byte_sin(time / 1000 + x * 4), 255 - state->n_red) +
- state->n_red;
- green = 255 - red;
-
- if (state->cool) {
- uint32_t tmp = green;
- green = blue;
- blue = tmp;
+ int64_t real_time = state->time * FRAME_TIME_DELTA;
+ if (state->speed % 1000 == 0) {
+ // speed = 1000 is the "canonical" time speed. If speed is divisible by
+ // 1000, then no interpolation is required.
+ c = get_rgba(x, (real_time * state->speed) / 1000, state);
+ } else {
+ int64_t adj_time = real_time * state->speed;
+ int64_t bot = adj_time / 1000;
+ int64_t top =
+ adj_time / 1000 + FRAME_TIME_DELTA * (adj_time >= 0 ? 1 : -1);
+ int64_t mod = adj_time % 1000;
+ if (mod < 0) {
+ mod += 1000;
}
-
- /* Add a little white flair that comes around every once in a while. */
-
- uint32_t whitesum = 0;
- if (state->n_snow) {
- uint32_t white[] = {
- /* Parallax "snow" */
- time / 179 + x * 8,
- time / 193 + x * 12,
- time / 211 + x * 16,
- time / 233 + x * 8,
- // (state->time + 128) / 233 + x * 8,
- };
-
- for (int i = 0; i < sizeof(white) / sizeof(uint32_t); ++i) {
- if ((white[i] / 256) % state->n_snow == 0) {
- white[i] = amp(byte_sin(white[i]), 20);
- } else {
- white[i] = 0;
- }
- whitesum += white[i];
- }
+ uint8_t interp = (uint8_t)((mod * 255) / 1000);
+
+ struct rgba c1 = get_rgba(x, bot, state);
+ struct rgba c2 = get_rgba(x, top, state);
+
+ if (i == 5 && asi(c1) > 0) {
+ ESP_LOGI(
+ "main",
+ "%06x %06x %d = %06x",
+ asi(c1),
+ asi(c2),
+ interp,
+ asi(interpolate(c1, c2, interp)));
}
- red = min(red + whitesum, 255);
- green = min(green + whitesum, 255);
- blue = min(blue + whitesum, 255);
- } else {
- red = (state->static_color >> 16) & 0xff;
- green = (state->static_color >> 8) & 0xff;
- blue = state->static_color & 0xff;
- }
-
- if (state->invert) {
- red = 255 - red;
- green = 255 - green;
- blue = 255 - blue;
+ c = interpolate(c1, c2, interp);
}
- uint8_t twink = 0xff;
- if (state->twinkle) {
- twink = twinkle(time / 300, x, state->twink_amt);
- }
-
- red = byte_scale(red, byte_scale(brightness, twink));
- green = byte_scale(green, byte_scale(brightness, twink));
- blue = byte_scale(blue, byte_scale(brightness, twink));
-
- ws2812b_buffer_set_rgb(buffer, i, red, green, blue);
+ ws2812b_buffer_set_rgb(buffer, i, c.r, c.g, c.b);
}
}
@@ -124,12 +204,14 @@ portTASK_FUNCTION(ws2812b_write_task, params)
ws_params_t* ws_params = (ws_params_t*)params;
ws2812b_buffer_t* buffer = ws2812b_new_buffer(SIZE);
+ TickType_t last_wake = xTaskGetTickCount();
+ const TickType_t period = pdMS_TO_TICKS(17); /* ~60 FPS */
for (;;) {
// Copy the parameters to avoid a change occuring during the calculation.
ws_state_t state_snapshot;
if (xSemaphoreTake(ws_params->state_lock, portMAX_DELAY) != pdTRUE) {
- vTaskDelay(1);
+ vTaskDelayUntil(&last_wake, period);
continue;
}
state_snapshot = ws_params->state;
@@ -139,10 +221,15 @@ portTASK_FUNCTION(ws2812b_write_task, params)
if (ws2812b_write(ws_params->drv, buffer) != ESP_OK) {
/* If we fail to write, back off briefly instead of hammering the SPI
* bus, which can otherwise cause visible flicker. */
- vTaskDelay(1);
+ vTaskDelayUntil(&last_wake, period);
continue;
}
- vTaskDelay(1);
+ /* Advance time once per frame. */
+ if (xSemaphoreTake(ws_params->state_lock, portMAX_DELAY) == pdTRUE) {
+ ws_params->state.time += 1;
+ xSemaphoreGive(ws_params->state_lock);
+ }
+ vTaskDelayUntil(&last_wake, period);
}
}