From a4930f91ced80d26758fae30f3e01eee7abe4345 Mon Sep 17 00:00:00 2001 From: Josh Rahm Date: Sun, 7 Dec 2025 16:59:51 -0700 Subject: Add a couple different parameter types. fraction_t: semantically represents a value 0.0-1.0. Internally this is represented as a uint8 with 255 being 1 and 0 being 0. ratio_t: represents a fracitonal number that can be larger than 1.0. --- controller_webpage/index.html | 37 ++++++++++- include/param.h | 4 ++ include/state_params.i | 20 +++--- main/param/fraction.c | 139 ++++++++++++++++++++++++++++++++++++++++++ main/param/ratio.c | 139 ++++++++++++++++++++++++++++++++++++++++++ main/ws2812b_writer.c | 10 +-- 6 files changed, 334 insertions(+), 15 deletions(-) create mode 100644 main/param/fraction.c create mode 100644 main/param/ratio.c diff --git a/controller_webpage/index.html b/controller_webpage/index.html index 1a308eb..9754759 100644 --- a/controller_webpage/index.html +++ b/controller_webpage/index.html @@ -181,6 +181,8 @@ params: {}, }; + const fmt = (v) => Number.parseFloat(v).toFixed(2); + function setStatus(text) { document.getElementById("status").textContent = text || ""; } @@ -251,6 +253,37 @@ field.appendChild(colorInput); field.appendChild(textInput); + } else if (value.type === "fraction" || value.type === "ratio") { + const input = document.createElement("input"); + input.type = "number"; + input.step = "0.01"; + input.min = "0"; + if (value.type === "fraction") { + input.max = "1"; + } + input.id = attr; + input.value = fmt(value.value); + field.appendChild(input); + + const actions = document.createElement("div"); + actions.className = "actions"; + + const setBtn = document.createElement("button"); + setBtn.textContent = "Set"; + setBtn.onclick = () => sendRequest(attr, "set", fmt(input.value)); + actions.appendChild(setBtn); + + const incBtn = document.createElement("button"); + incBtn.textContent = "+"; + incBtn.onclick = () => sendRequest(attr, "add", input.step || "0.01"); + actions.appendChild(incBtn); + + const decBtn = document.createElement("button"); + decBtn.textContent = "−"; + decBtn.onclick = () => sendRequest(attr, "sub", input.step || "0.01"); + actions.appendChild(decBtn); + + field.appendChild(actions); } else { const input = document.createElement("input"); input.type = "number"; @@ -318,7 +351,9 @@ } else { const newVal = value.type === "color" ? (value.value.startsWith("#") ? value.value : `#${value.value}`) - : value.value; + : (value.type === "fraction" || value.type === "ratio") + ? fmt(value.value) + : value.value; el.value = newVal; if (value.type === "color" && el.nextElementSibling) { el.nextElementSibling.value = newVal; diff --git a/include/param.h b/include/param.h index 5259e0a..d0b5bf0 100644 --- a/include/param.h +++ b/include/param.h @@ -6,6 +6,8 @@ typedef struct httpd_req httpd_req_t; typedef uint32_t color_int_t; +typedef uint8_t fraction_t; +typedef uint16_t ratio_t; #define DECL_PARAMETER_INSTANCE(ty) \ void handle_option__##ty(const char* attr, sockbuf_t* sockbuf, ty* ptr); \ @@ -24,5 +26,7 @@ DECL_PARAMETER_INSTANCE(uint8_t); DECL_PARAMETER_INSTANCE(bool); DECL_PARAMETER_INSTANCE(uint32_t); DECL_PARAMETER_INSTANCE(color_int_t); +DECL_PARAMETER_INSTANCE(fraction_t); +DECL_PARAMETER_INSTANCE(ratio_t); DECL_PARAMETER_INSTANCE(int); DECL_PARAMETER_INSTANCE(int32_t); diff --git a/include/state_params.i b/include/state_params.i index e768768..854008d 100644 --- a/include/state_params.i +++ b/include/state_params.i @@ -4,16 +4,16 @@ // type name display name, default value STATE_PARAM(int32_t, time, "Current Time", 0) -STATE_PARAM(int, speed, "Speed", 1000) -STATE_PARAM(uint8_t, brightness, "Brightness", 50) +STATE_PARAM(ratio_t, speed, "Speed", 255) +STATE_PARAM(fraction_t, brightness, "Brightness", 50) STATE_PARAM(uint8_t, n_snow, "Snow Effect", 10) -STATE_PARAM(uint8_t, n_red, "Desaturation", 32) -STATE_PARAM(int, x_scale, "X Scale", 255) -STATE_PARAM(bool, power, "Power", true) -STATE_PARAM(bool, cool, "Cool", false) -STATE_PARAM(bool, invert, "Invert", false) +STATE_PARAM(fraction_t, n_red, "Desaturation", 32) +STATE_PARAM(ratio_t, x_scale, "X Scale", 255) +STATE_PARAM(bool, power, "Power", true) +STATE_PARAM(bool, cool, "Cool", false) +STATE_PARAM(bool, invert, "Invert", false) 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) +STATE_PARAM(bool, twinkle, "Twinkle", true) +STATE_PARAM(fraction_t, twink_amt, "Twinkle Amount", 0) +STATE_PARAM(fraction_t, base_brightness, "Base Brightness", 0) diff --git a/main/param/fraction.c b/main/param/fraction.c new file mode 100644 index 0000000..adfc769 --- /dev/null +++ b/main/param/fraction.c @@ -0,0 +1,139 @@ +#include "param.h" +#include "http_server.h" + +#include +#include +#include +#include + +static inline uint8_t clamp_u8(int v) +{ + if (v < 0) return 0; + if (v > 255) return 255; + return (uint8_t)v; +} + +static int parse_fraction(const char* s, fraction_t* out) +{ + if (!s) return -1; + char* end = NULL; + float f = strtof(s, &end); + if (end == s) { + return -1; + } + *out = clamp_u8((int)lroundf(f * 255.0f)); + return 0; +} + +void cmd_handle(fraction_t)(const char* attr, sockbuf_t* sockbuf, fraction_t* ptr) +{ + char op_[8]; + char buf_[32]; + + char* op = read_token(sockbuf, op_, sizeof(op_)); + char* value = read_token(sockbuf, buf_, sizeof(buf_)); + + fraction_t curval = *ptr; + fraction_t parsed = 0; + + if (!strcmp(op, "+=") || !strcmp(op, "-=") || !strcmp(op, "=")) { + if (parse_fraction(value, &parsed) != 0) { + sockbuf_write(sockbuf, "Invalid value\n"); + return; + } + if (!strcmp(op, "+=")) { + curval = clamp_u8((int)curval + (int)parsed); + } else if (!strcmp(op, "-=")) { + curval = clamp_u8((int)curval - (int)parsed); + } else { + curval = parsed; + } + } else { + sockbuf_write(sockbuf, "Unknown operation: '"); + sockbuf_write(sockbuf, op); + sockbuf_write(sockbuf, "'\n"); + return; + } + + *ptr = curval; + snprintf(buf_, sizeof(buf_), "%s = %.3f\n", attr, curval / 255.0f); + sockbuf_write(sockbuf, buf_); +} + +size_t serialize_to_json(fraction_t)( + char** out, size_t len, const char* attr, const char* dn, fraction_t value) +{ + float fval = value / 255.0f; + size_t newlen = snprintf( + *out, + len, + "\"%s\":{\"type\":\"fraction\",\"value\":\"%.4f\",\"disp\":\"%s\"}", + attr, + fval, + dn); + *out += newlen; + return len - newlen; +} + +#define TAG "param_fraction" +void http_handle(fraction_t)(httpd_req_t* req, fraction_t* val) +{ + char buf[128]; + int buf_len = httpd_req_get_url_query_len(req) + 1; + esp_err_t err; + + if (buf_len > 1) { + if (buf_len > sizeof(buf)) { + ESP_LOGI(TAG, "Invalid request. Query string too long."); + httpd_resp_set_status(req, HTTPD_400); + return; + } + + if ((err = httpd_req_get_url_query_str(req, buf, buf_len)) == ESP_OK) { + char param[32]; + fraction_t parsed; + if (httpd_query_key_value(buf, "add", param, sizeof(param)) == ESP_OK) { + if (parse_fraction(param, &parsed) != 0) { + httpd_resp_set_status(req, HTTPD_400); + return; + } + *val = clamp_u8((int)(*val) + (int)parsed); + } else if ( + httpd_query_key_value(buf, "set", param, sizeof(param)) == ESP_OK) { + if (parse_fraction(param, &parsed) != 0) { + httpd_resp_set_status(req, HTTPD_400); + return; + } + *val = parsed; + } else if ( + httpd_query_key_value(buf, "sub", param, sizeof(param)) == ESP_OK) { + if (parse_fraction(param, &parsed) != 0) { + httpd_resp_set_status(req, HTTPD_400); + return; + } + *val = clamp_u8((int)(*val) - (int)parsed); + } else { + ESP_LOGI(TAG, "No valid parameters."); + httpd_resp_set_status(req, HTTPD_400); + return; + } + } else { + ESP_LOGI(TAG, "Unable to get URL query string. [%d]", err); + httpd_resp_set_status(req, HTTPD_500); + return; + } + } else { + ESP_LOGI(TAG, "No query string provided?"); + httpd_resp_set_status(req, HTTPD_400); + return; + } + + httpd_resp_set_status(req, HTTPD_204); +} + +void print(fraction_t)(sockbuf_t* sockbuf, const char* attr, fraction_t val) +{ + char buf[128]; + snprintf(buf, sizeof(buf), "%s: %.3f :: fraction\n", attr, val / 255.0f); + sockbuf_write(sockbuf, buf); +} diff --git a/main/param/ratio.c b/main/param/ratio.c new file mode 100644 index 0000000..bd7e3f4 --- /dev/null +++ b/main/param/ratio.c @@ -0,0 +1,139 @@ +#include "param.h" +#include "http_server.h" + +#include +#include +#include +#include + +static inline uint16_t clamp_u16(int v) +{ + if (v < 0) return 0; + if (v > 65535) return 65535; + return (uint16_t)v; +} + +static int parse_ratio(const char* s, ratio_t* out) +{ + if (!s) return -1; + char* end = NULL; + float f = strtof(s, &end); + if (end == s) { + return -1; + } + *out = clamp_u16((int)lroundf(f * 255.0f)); + return 0; +} + +void cmd_handle(ratio_t)(const char* attr, sockbuf_t* sockbuf, ratio_t* ptr) +{ + char op_[8]; + char buf_[32]; + + char* op = read_token(sockbuf, op_, sizeof(op_)); + char* value = read_token(sockbuf, buf_, sizeof(buf_)); + + ratio_t curval = *ptr; + ratio_t parsed = 0; + + if (!strcmp(op, "+=") || !strcmp(op, "-=") || !strcmp(op, "=")) { + if (parse_ratio(value, &parsed) != 0) { + sockbuf_write(sockbuf, "Invalid value\n"); + return; + } + if (!strcmp(op, "+=")) { + curval = clamp_u16((int)curval + (int)parsed); + } else if (!strcmp(op, "-=")) { + curval = clamp_u16((int)curval - (int)parsed); + } else { + curval = parsed; + } + } else { + sockbuf_write(sockbuf, "Unknown operation: '"); + sockbuf_write(sockbuf, op); + sockbuf_write(sockbuf, "'\n"); + return; + } + + *ptr = curval; + snprintf(buf_, sizeof(buf_), "%s = %.3f\n", attr, curval / 255.0f); + sockbuf_write(sockbuf, buf_); +} + +size_t serialize_to_json(ratio_t)( + char** out, size_t len, const char* attr, const char* dn, ratio_t value) +{ + float fval = value / 255.0f; + size_t newlen = snprintf( + *out, + len, + "\"%s\":{\"type\":\"ratio\",\"value\":\"%.4f\",\"disp\":\"%s\"}", + attr, + fval, + dn); + *out += newlen; + return len - newlen; +} + +#define TAG "param_ratio" +void http_handle(ratio_t)(httpd_req_t* req, ratio_t* val) +{ + char buf[128]; + int buf_len = httpd_req_get_url_query_len(req) + 1; + esp_err_t err; + + if (buf_len > 1) { + if (buf_len > sizeof(buf)) { + ESP_LOGI(TAG, "Invalid request. Query string too long."); + httpd_resp_set_status(req, HTTPD_400); + return; + } + + if ((err = httpd_req_get_url_query_str(req, buf, buf_len)) == ESP_OK) { + char param[32]; + ratio_t parsed; + if (httpd_query_key_value(buf, "add", param, sizeof(param)) == ESP_OK) { + if (parse_ratio(param, &parsed) != 0) { + httpd_resp_set_status(req, HTTPD_400); + return; + } + *val = clamp_u16((int)(*val) + (int)parsed); + } else if ( + httpd_query_key_value(buf, "set", param, sizeof(param)) == ESP_OK) { + if (parse_ratio(param, &parsed) != 0) { + httpd_resp_set_status(req, HTTPD_400); + return; + } + *val = parsed; + } else if ( + httpd_query_key_value(buf, "sub", param, sizeof(param)) == ESP_OK) { + if (parse_ratio(param, &parsed) != 0) { + httpd_resp_set_status(req, HTTPD_400); + return; + } + *val = clamp_u16((int)(*val) - (int)parsed); + } else { + ESP_LOGI(TAG, "No valid parameters."); + httpd_resp_set_status(req, HTTPD_400); + return; + } + } else { + ESP_LOGI(TAG, "Unable to get URL query string. [%d]", err); + httpd_resp_set_status(req, HTTPD_500); + return; + } + } else { + ESP_LOGI(TAG, "No query string provided?"); + httpd_resp_set_status(req, HTTPD_400); + return; + } + + httpd_resp_set_status(req, HTTPD_204); +} + +void print(ratio_t)(sockbuf_t* sockbuf, const char* attr, ratio_t val) +{ + char buf[128]; + snprintf(buf, sizeof(buf), "%s: %.3f :: ratio\n", attr, val / 255.0f); + sockbuf_write(sockbuf, buf); +} diff --git a/main/ws2812b_writer.c b/main/ws2812b_writer.c index 90c7405..63caf30 100644 --- a/main/ws2812b_writer.c +++ b/main/ws2812b_writer.c @@ -155,7 +155,8 @@ struct rgba interpolate(struct rgba c1, struct rgba c2, uint8_t amt) 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; + int x_scale = state->x_scale ? state->x_scale : 1; + int x = i * 255 / x_scale; if (!state->power) { ws2812b_buffer_set_rgb(buffer, i, 0, 0, 0); @@ -164,12 +165,13 @@ void calculate_colors(const ws_state_t* state, ws2812b_buffer_t* buffer) struct rgba c; int64_t real_time = state->time * FRAME_TIME_DELTA; - if (state->speed % 1000 == 0) { + int32_t speed_scaled = ((int32_t)state->speed * 1000) / 255; + if (speed_scaled % 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); + c = get_rgba(x, (real_time * speed_scaled) / 1000, state); } else { - int64_t adj_time = real_time * state->speed; + int64_t adj_time = real_time * speed_scaled; int64_t bot = adj_time / 1000; int64_t top = adj_time / 1000 + FRAME_TIME_DELTA * (adj_time >= 0 ? 1 : -1); -- cgit