commit 4173087c771feaeba9c12fa34a36d0537d132613 Author: Jan-Henrik Bruhn Date: Mon Apr 14 16:34:51 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90552b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pioenvs +.piolibdeps +.clang_complete +.gcc-flags.json +.pio + diff --git a/backplate.stl b/backplate.stl new file mode 100644 index 0000000..d905e03 Binary files /dev/null and b/backplate.stl differ diff --git a/stand.scad b/stand.scad new file mode 100644 index 0000000..59cff95 --- /dev/null +++ b/stand.scad @@ -0,0 +1,85 @@ +panel_width = 95; +panel_height = 200; + +thickness = 3; + +screw_right_right = 17.7; +screw_right_bottom = 8.8; + +screw_left_left = 13.8; +screw_left_bottom = 9; + +screw_top_right_right = 10.5; +screw_top_right_bottom = 108; + +screw_top_left_left = 10.5; +screw_top_left_bottom = 84; + +backside_hold_height = 50; + +bottom_margin_height = 30; + +total_height = backside_hold_height + bottom_margin_height; + +backside_standoff_height = 17; + +long_thingy_height = 116; +long_thingy_width = 18; +/* +difference() { + union() { + translate([0, -bottom_margin_height, 0]) cube([panel_width, total_height, thickness]); + + translate([25, 10, -backside_standoff_height])cylinder(h=backside_standoff_height, d=8, $fn=20); + translate([panel_width - 25, 10, -backside_standoff_height])cylinder(h=backside_standoff_height, d=8, $fn=20); + + translate([10, long_thingy_height, -backside_standoff_height])cylinder(h=backside_standoff_height, d=8, $fn=20); + translate([panel_width - 10, long_thingy_height, -backside_standoff_height])cylinder(h=backside_standoff_height, d=8, $fn=20); + + cube([long_thingy_width, long_thingy_height, thickness]); + translate([panel_width - long_thingy_width, 0, 0]) cube([long_thingy_width, long_thingy_height, thickness]); + + translate([0, long_thingy_height, 0]) cube([panel_width, 8, thickness]); + + translate([4, 12.7, -thickness+2])cube([panel_width/8, 2, 1]); + translate([4, 18.3, -thickness+2])cube([panel_width/8, 2, 1]); + } + + // knobs + translate([panel_width - bottom_margin_height/2, -bottom_margin_height/2, -1]) cylinder(h = 20, d = 7.2, $fn=20); + translate([panel_width - bottom_margin_height/2 - 25, -bottom_margin_height/2, -1]) cylinder(h = 20, d = 7.2, $fn=20); + + + // screws + translate([panel_width - screw_right_right, screw_right_bottom, -1]) cylinder(h = 20, d = 3.2, $fn=20); + translate([screw_left_left, screw_left_bottom, -1]) cylinder(h = 20, d = 3.2, $fn=20); + + translate([panel_width - screw_top_right_right, screw_top_right_bottom, -1]) cylinder(h = 20, d = 3.2, $fn=20); + translate([screw_top_left_left, screw_top_left_bottom, -1]) cylinder(h = 20, d = 3.2, $fn=20); + + translate([10, long_thingy_height, -1-backside_standoff_height]) cylinder(h = 30, d = 3, $fn=20); + translate([panel_width - 10, long_thingy_height, -1-backside_standoff_height]) cylinder(h = 30, d = 3, $fn=20); + + translate([25, 10, -1-backside_standoff_height]) cylinder(h = 30, d = 3, $fn=20); + translate([panel_width - 25, 10, -1-backside_standoff_height]) cylinder(h = 30, d = 3, $fn=20); +}*/ + +color("blue") translate([0, 0, -backside_standoff_height-thickness]) difference() { + union() { + cube([panel_width, long_thingy_height+50, thickness]); + + translate([0, 15, 0]) cube([panel_width/4, 3, backside_standoff_height+thickness-.2]); + + + + } + translate([panel_width / 2, long_thingy_height+40, -1]) cylinder(h=20, d=8, $fn=20); + + translate([10.5, 20, 12]) rotate([90, 0, 0]) cylinder(h = 20, d = 10, $fn=20); + + translate([10, long_thingy_height, -1-backside_standoff_height]) cylinder(h = 30, d = 3.2, $fn=20); + translate([panel_width - 10, long_thingy_height, -1-backside_standoff_height]) cylinder(h = 30, d = 3.2, $fn=20); + + translate([25, 10, -1-backside_standoff_height]) cylinder(h = 30, d = 3.2, $fn=20); + translate([panel_width - 25, 10, -1-backside_standoff_height]) cylinder(h = 30, d = 3.2, $fn=20); +} \ No newline at end of file diff --git a/transigione/.gitignore b/transigione/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/transigione/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/transigione/.vscode/extensions.json b/transigione/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/transigione/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/transigione/include/README b/transigione/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/transigione/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/transigione/lib/README b/transigione/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/transigione/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/transigione/platformio.ini b/transigione/platformio.ini new file mode 100644 index 0000000..3485e00 --- /dev/null +++ b/transigione/platformio.ini @@ -0,0 +1,29 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +build_flags = + -DCORE_DEBUG_LEVEL=3 +lib_deps = + mrfaptastic/ESP32 HUB75 LED MATRIX PANEL DMA Display@^3.0.12 + adafruit/Adafruit GFX Library@^1.12.0 + madhephaestus/ESP32Encoder@^0.11.7 + arduinogetstarted/ezButton@^1.0.6 + johboh/HomeAssistantEntities@^8.0.6 + johboh/MQTTRemote@^5.0.2 + bblanchon/ArduinoJson@^7.4.1 + juerd/ESP-WiFiSettings@^3.9.2 + +upload_protocol = espota +upload_port = transigione-902e41.local diff --git a/transigione/src/main.cpp b/transigione/src/main.cpp new file mode 100644 index 0000000..989945d --- /dev/null +++ b/transigione/src/main.cpp @@ -0,0 +1,783 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Information about this device. +// All these keys will be added to a "device" key in the Home Assistant configuration for each entity. +// Only a flat layout structure is supported, no nesting. +// We call the setupJsonForThisDevice() from the ardunio setup() function to populate the Json document. +// IJsonDocument can be replaced with nlohmann-json::json or ArduinoJson::JsonDocument +IJsonDocument _json_this_device_doc; +void setupJsonForThisDevice(String name) { + _json_this_device_doc["identifiers"] = name; + _json_this_device_doc["name"] = "Transigione"; + _json_this_device_doc["sw_version"] = "1.0.0"; + _json_this_device_doc["model"] = "transgione_matrix"; + _json_this_device_doc["manufacturer"] = "jhbruhn"; +} + +MQTTRemote* _mqtt_remote; + +// Create the Home Assistant bridge. This is shared across all entities. +// We only have one per device/hardware. In our example, the name of our device is "livingroom". +// See constructor of HaBridge for more documentation. +HaBridge* ha_bridge; + +// Create the two lights with the "Human readable" strings. This what will show up in Home Assistant. +// As we have two entities of the same type (light) for the same device, we need to add a child object +// id to separate them. +HaEntityLight* _ha_entity_light; + +ESP32Encoder encoder1; +ezButton encoder1Button(21); +ESP32Encoder encoder2; +ezButton encoder2Button(22); + +MatrixPanel_I2S_DMA *dma_display = nullptr; + +uint16_t myBLACK = dma_display->color565(0, 0, 0); +uint16_t myWHITE = dma_display->color565(255, 255, 255); +uint16_t myRED = dma_display->color565(255, 0, 0); +uint16_t myGREEN = dma_display->color565(0, 255, 0); +uint16_t myBLUE = dma_display->color565(0, 0, 255); + + +#define XMAX 64 +#define YMAX 32 + +#define BRIGHTNESS 127 + +void displaySetup() { + HUB75_I2S_CFG mxconfig( + 64, // module width + 32, // module height + 1 // Chain length + ); + + // If you are using a 64x64 matrix you need to pass a value for the E pin + // The trinity connects GPIO 18 to E. + // This can be commented out for any smaller displays (but should work fine with it) + //mxconfig.gpio.e = 18; + + + // May or may not be needed depending on your matrix + // Example of what needing it looks like: + // https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/134#issuecomment-866367216 + mxconfig.clkphase = false; + + // Some matrix panels use different ICs for driving them and some of them have strange quirks. + // If the display is not working right, try this. + //mxconfig.driver = HUB75_I2S_CFG::FM6126A; + + dma_display = new MatrixPanel_I2S_DMA(mxconfig); + dma_display->begin(); +} + +void setup_ota() { + ArduinoOTA.setHostname(WiFiSettings.hostname.c_str()); + + ArduinoOTA.begin(); +} + +bool power = true; +uint8_t brightness = BRIGHTNESS; + +void setup() { + Serial.begin(115200); + Serial.setDebugOutput(true); + Serial.println("Moini"); + displaySetup(); + + Serial.println("Moin"); + + encoder1Button.setDebounceTime(50); + encoder2Button.setDebounceTime(50); + + ESP32Encoder::useInternalWeakPullResistors = puType::none; + + encoder1.attachSingleEdge(34, 35); + encoder2.attachSingleEdge(32, 33); + encoder1.setFilter(1023); + encoder2.setFilter(1023); + + SPIFFS.begin(true); // On first run, will format after failing to mount + WiFiSettings.hostname = "transigione-"; + setupJsonForThisDevice(WiFiSettings.hostname); + String mqtt_host = WiFiSettings.string( "mqtt_host", "default.example.org"); + int mqtt_port = WiFiSettings.integer("mqtt_port", 0, 65535, 1883); + String mqtt_username = WiFiSettings.string("mqtt_username", "username"); + String mqtt_password = WiFiSettings.string("mqtt_password", "password"); + + + + // Set callbacks to start OTA when the portal is active + WiFiSettings.onPortal = []() { + setup_ota(); + }; + WiFiSettings.onPortalWaitLoop = []() { + ArduinoOTA.handle(); + }; + + WiFiSettings.connect(); + + _mqtt_remote = new MQTTRemote(WiFiSettings.hostname.c_str(), mqtt_host.c_str(), 1883, mqtt_username.c_str(), mqtt_password.c_str()); + ha_bridge = new HaBridge(*_mqtt_remote, WiFiSettings.hostname.c_str(), _json_this_device_doc); + _ha_entity_light = new HaEntityLight(*ha_bridge, "Light", "light", {.with_brightness = true}); + + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "sketch"; + } else { // U_SPIFFS + type = "filesystem"; + } + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }) + .onEnd([]() { + Serial.println("\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + Serial.println("Auth Failed"); + } else if (error == OTA_BEGIN_ERROR) { + Serial.println("Begin Failed"); + } else if (error == OTA_CONNECT_ERROR) { + Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + Serial.println("End Failed"); + } + }); + + + // Can be set between 0 and 255 + // WARNING: The birghter it is, the more power it uses + // Could take up to 3A on full brightness + dma_display->setBrightness8(BRIGHTNESS); //0-255 + + + dma_display->clearScreen(); + + Serial.println("Ready"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + + _mqtt_remote->setOnConnectionChange([](bool connected) { + if (connected) { + Serial.println("We are in"); + // Publish Home Assistant Configuration for both lights once connected to MQTT. + _ha_entity_light->publishConfiguration(); + _ha_entity_light->publishIsOn(power); + _ha_entity_light->publishBrightness(brightness << 1); + + // Subscribe to new light "on" state pushed by Home Assistant. + _ha_entity_light->setOnOn([&](bool on) { + power = on; + _ha_entity_light->publishIsOn(power); + }); + _ha_entity_light->setOnBrightness( + [&](uint8_t b) { Serial.println("Got brightness " + String(b) + " for light"); brightness = b >> 1; + _ha_entity_light->publishBrightness(b); }); + } + }); + + setup_ota(); +} + +void transition1(); +void transition2(); +void transition3(); +void transition4(); +void transition5(); +void transition6(); + +uint8_t parameter = 0; + +void loop() { + ArduinoOTA.handle(); + _mqtt_remote->handle(); + + encoder1Button.loop(); + encoder2Button.loop(); + + if (encoder1Button.isPressed() || encoder2Button.isPressed()) { + power = !power; + _ha_entity_light->publishIsOn(power); + } + + dma_display->setBrightness8(power ? brightness : 0); + + uint8_t program = encoder2.getCount(); + parameter = encoder1.getCount(); + + switch (program % 6) { + case 0: + transition6(); + break; + case 1: + transition2(); + break; + case 2: + transition3(); + break; + case 3: + transition4(); + break; + case 4: + transition5(); + break; + case 5: + transition1(); + break; + } +} + +uint16_t interpolateRGB565(uint16_t color1, uint16_t color2, uint8_t t) { + // Extract the RGB components from color1 + uint8_t r1 = (color1 >> 11) & 0x1F; + uint8_t g1 = (color1 >> 5) & 0x3F; + uint8_t b1 = color1 & 0x1F; + + // Extract the RGB components from color2 + uint8_t r2 = (color2 >> 11) & 0x1F; + uint8_t g2 = (color2 >> 5) & 0x3F; + uint8_t b2 = color2 & 0x1F; + // Calculate the difference for each component + int16_t rdiff = r2 - r1; + int16_t gdiff = g2 - g1; + int16_t bdiff = b2 - b1; + + // Interpolate each component + // Using 32-bit integers for intermediate calculations to avoid overflow + uint8_t r = r1 + ((rdiff * t + 128) >> 8); + uint8_t g = g1 + ((gdiff * t + 128) >> 8); + uint8_t b = b1 + ((bdiff * t + 128) >> 8); + + + // Combine the components back into a 16-bit RGB565 color + return ((uint16_t)r << 11) | ((uint16_t)g << 5) | b; +} + + + +/** + * Interpolates between two RGB565 colors using HSV color space with fixed-point arithmetic. + * No floating point operations are used. Ensures smooth color transitions. + * + * @param color1 Starting color in RGB565 format (5 bits R, 6 bits G, 5 bits B) + * @param color2 Ending color in RGB565 format (5 bits R, 6 bits G, 5 bits B) + * @param t Interpolation factor from 0 to 255 (0 = color1, 255 = color2) + * @return Interpolated color in RGB565 format + */ +uint16_t interpolateHSV565(uint16_t color1, uint16_t color2, uint8_t t) { + // Extract RGB components from RGB565 colors + uint8_t r1 = (color1 >> 11) & 0x1F; + uint8_t g1 = (color1 >> 5) & 0x3F; + uint8_t b1 = color1 & 0x1F; + + uint8_t r2 = (color2 >> 11) & 0x1F; + uint8_t g2 = (color2 >> 5) & 0x3F; + uint8_t b2 = color2 & 0x1F; + + // Scale up to 8 bits for better precision + r1 = (r1 << 3) | (r1 >> 2); + g1 = (g1 << 2) | (g1 >> 4); + b1 = (b1 << 3) | (b1 >> 2); + + r2 = (r2 << 3) | (r2 >> 2); + g2 = (g2 << 2) | (g2 >> 4); + b2 = (b2 << 3) | (b2 >> 2); + + // Convert RGB to HSV for both colors (using fixed-point) + uint16_t h1, s1, v1; + uint16_t h2, s2, v2; + + // For color1 + uint8_t max1 = r1 > g1 ? (r1 > b1 ? r1 : b1) : (g1 > b1 ? g1 : b1); + uint8_t min1 = r1 < g1 ? (r1 < b1 ? r1 : b1) : (g1 < b1 ? g1 : b1); + uint16_t delta1 = max1 - min1; + + // Value is easy + v1 = max1; + + // Saturation (scaled to 0-255) + s1 = max1 == 0 ? 0 : ((uint32_t)delta1 * 255) / max1; + + // Hue (scaled to 0-1535 for fixed-point representation, 0-360 degrees * 4.26) + if (delta1 == 0) { + h1 = 0; // undefined, default to red + } else if (max1 == r1) { + int16_t diff = g1 - b1; + h1 = 0 + (((int32_t)diff * 256) / delta1); + if (h1 < 0) h1 += 1536; + } else if (max1 == g1) { + int16_t diff = b1 - r1; + h1 = 512 + (((int32_t)diff * 256) / delta1); + } else { // max1 == b1 + int16_t diff = r1 - g1; + h1 = 1024 + (((int32_t)diff * 256) / delta1); + } + + // For color2 (same calculations) + uint8_t max2 = r2 > g2 ? (r2 > b2 ? r2 : b2) : (g2 > b2 ? g2 : b2); + uint8_t min2 = r2 < g2 ? (r2 < b2 ? r2 : b2) : (g2 < b2 ? g2 : b2); + uint16_t delta2 = max2 - min2; + + v2 = max2; + s2 = max2 == 0 ? 0 : ((uint32_t)delta2 * 255) / max2; + + if (delta2 == 0) { + h2 = 0; + } else if (max2 == r2) { + int16_t diff = g2 - b2; + h2 = 0 + (((int32_t)diff * 256) / delta2); + if (h2 < 0) h2 += 1536; + } else if (max2 == g2) { + int16_t diff = b2 - r2; + h2 = 512 + (((int32_t)diff * 256) / delta2); + } else { // max2 == b2 + int16_t diff = r2 - g2; + h2 = 1024 + (((int32_t)diff * 256) / delta2); + } + + // Handle special case for grayscale colors (undefined hue) + if (s1 < 8) h1 = h2; + if (s2 < 8) h2 = h1; + + // Interpolate HSV components + uint16_t h_interp, s_interp, v_interp; + + // Hue needs special handling for the shortest path around the circle + int16_t h_diff = h2 - h1; + if (h_diff > 768) h_diff -= 1536; + else if (h_diff < -768) h_diff += 1536; + + // Use 32-bit arithmetic for the interpolation to avoid overflow issues + h_interp = h1 + (((int32_t)h_diff * t) >> 8); + if (h_interp >= 1536) h_interp -= 1536; + else if (h_interp < 0) h_interp += 1536; + + s_interp = s1 + (((int32_t)(s2 - s1) * t) >> 8); + v_interp = v1 + (((int32_t)(v2 - v1) * t) >> 8); + + // Convert interpolated HSV back to RGB using more precise fixed-point math + uint8_t r, g, b; + + uint8_t region = h_interp / 256; + uint16_t remainder = (h_interp % 256); + + // These calculations now use 32-bit fixed-point to avoid rounding errors + uint32_t p = ((uint32_t)v_interp * (255 - s_interp)) / 255; + uint32_t q = ((uint32_t)v_interp * (255 - ((s_interp * remainder) / 256))) / 255; + uint32_t t_val = ((uint32_t)v_interp * (255 - ((s_interp * (255 - remainder)) / 256))) / 255; + + switch (region) { + case 0: + r = v_interp; + g = t_val; + b = p; + break; + case 1: + r = q; + g = v_interp; + b = p; + break; + case 2: + r = p; + g = v_interp; + b = t_val; + break; + case 3: + r = p; + g = q; + b = v_interp; + break; + case 4: + r = t_val; + g = p; + b = v_interp; + break; + default: // case 5 + r = v_interp; + g = p; + b = q; + break; + } + + // Convert back to RGB565 format + r = (r >> 3); + g = (g >> 2); + b = (b >> 3); + + return (r << 11) | (g << 5) | b; +} + +uint16_t RGB888ToRGB565(uint32_t rgb888) { + // Extract 8-bit color components + uint8_t red = (rgb888 >> 16) & 0xFF; + uint8_t green = (rgb888 >> 8) & 0xFF; + uint8_t blue = rgb888 & 0xFF; + + // Convert 8-bit components to 5-6-5 bit format + uint16_t r = (red >> 3) & 0x1F; // 5 bits for red + uint16_t g = (green >> 2) & 0x3F; // 6 bits for green + uint16_t b = (blue >> 3) & 0x1F; // 5 bits for blue + + // Combine the components into a 16-bit value + return (r << 11) | (g << 5) | b; +} + +void transition1() { + + const int color1 = dma_display->color565(0xdf, 0xf5, 0x4f); + const int color2 = dma_display->color565(0x8d, 0x1f, 0xf2); + + static uint8_t state[YMAX]; + static unsigned long last_move_time = 0; + if (last_move_time == 0) { + for (int y = 0; y < YMAX; y++) { + state[y] = 127; + } + } + if (last_move_time + ((parameter + 50) % 255) < millis()) { + for (int y = 0; y < YMAX; y++) { + int move_length = rand() - RAND_MAX / 2; + int dir = state[y] < 64 || state[y] > 191 ? 4 : 1; + if (move_length > 0) { + dir = -dir; + } + state[y] += dir; + if (state[y] < 0) state[y] = 0; + if (state[y] > 255) state[y] = 255; + } + last_move_time = millis(); + } + + for (int x = 0; x < XMAX; x++) { + for(int y = 0; y < YMAX; y++) { + int t = x << 2; + + t += state[y] - 127; + + if (t <= 0) t = 1; + if (t >= 255) t = 254; + + int color = interpolateHSV565(color1, color2, t); + + dma_display->drawPixel(x, y, color); + } + } +} + +void transition2() { + const int color1 = dma_display->color565(0xdf, 0xf5, 0x4f); + const int color2 = dma_display->color565(0x8d, 0x1f, 0xf2); + const int color3 = dma_display->color565(0x6f, 0x55, 0xff); + const int color4 = dma_display->color565(0x1d, 0x9f, 0x23); + static unsigned long last_step = 0; + + static uint8_t t1 = 0; + static uint8_t t2 = 254; + static int8_t dir1 = 1; + static int8_t dir2 = 1; + + if (last_step + (255-parameter) < millis()) { + t1 += dir1; + t2 += dir2; + if (t1 == 255) { + t1 = 254; + dir1 = -1; + } + if (t1 == 0) { + t1 = 1; + dir1 = 1; + } + if (t2 == 255) { + t2 = 254; + dir2 = -1; + } + if (t2 == 0) { + t2 = 1; + dir2 = 1; + } + last_step = millis(); + + for (int x = 0; x < XMAX; x++) { + for(int y = 0; y < YMAX; y++) { + + int colorA = interpolateHSV565(color1, color2, t1); + int colorB = interpolateHSV565(color3, color4, t2); + + int color = interpolateRGB565(colorA, colorB, x << 2); + + dma_display->drawPixel(x, y, color); + } + } + } +} + +void transition3() { + //0x274001, 0x828a00, 0xf29f05, 0xf25c05, 0xd6568c, 0x4d8584, 0xa62f03, 0x400d01 + const int color1 = RGB888ToRGB565(0x274001); + const int color2 = RGB888ToRGB565(0x828a00); + const int color3 = RGB888ToRGB565(0xf29f05); + const int color4 = RGB888ToRGB565(0xf25c05); + const int color5 = RGB888ToRGB565(0xd6568c); + const int color6 = RGB888ToRGB565(0x4d8584); + const int color7 = RGB888ToRGB565(0xa62f03); + const int color8 = RGB888ToRGB565(0x400d01); + static unsigned long last_step = 0; + + static uint8_t t1 = 0; + static uint8_t t2 = 0; + static int8_t dir1 = 1; + static int8_t dir2 = 1; + + if (last_step + 100 < millis()) { + t1 += dir1; + t2 += dir2; + if (t1 == 255) { + t1 = 254; + dir1 = -1; + } + if (t1 == 0) { + t1 = 1; + dir1 = 1; + } + if (t2 == 255) { + t2 = 254; + dir2 = -1; + } + if (t2 == 0) { + t2 = 1; + dir2 = 1; + } + last_step = millis(); + + for (int x = 0; x < XMAX; x++) { + for(int y = 0; y < YMAX; y++) { + + int colorA = interpolateHSV565(color1, color5, t1); + int colorB = interpolateHSV565(color2, color6, t2); + int colorC = interpolateHSV565(color3, color7, t1); + int colorD = interpolateHSV565(color4, color8, t2); + + int colorX = interpolateRGB565(colorA, colorB, x << 2); + int colorY = interpolateRGB565(colorC, colorD, x << 2); + + int color = interpolateRGB565(colorX, colorY, y << 3); + + dma_display->drawPixel(x, y, color); + } + } + } +} + +void transition4() { + const uint8_t NUM_COLORS = 4; + const uint8_t NUM_PALETTES = 8; +// Color palettes (up to 4 colors per palette) +const uint16_t palettes[NUM_PALETTES][NUM_COLORS] = { + // 0: Soft purples and blues + { 0x780F, 0x401F, 0x001F, 0x4010 }, + // 1: Warm tones + { 0xF800, 0xFA60, 0xFFE0, 0xFD20 }, + // 2: Cool tones + { 0x001F, 0x07FF, 0x05BF, 0x041F }, + // 3: Fire + { 0xF800, 0xFC00, 0xFFE0, 0xFCA0 }, + // 4: Ocean + { 0x001F, 0x021F, 0x03FF, 0x05DF }, + // 5: Sunset + { 0xF800, 0xF81F, 0x780F, 0x001F }, + // 6: Neon + { 0xF81F, 0x07FF, 0xFFE0, 0x07E0 }, + // 7: Grayscale + { 0x0000, 0x528A, 0xAD55, 0xFFFF } +}; + +// Select the active palette here (index 0-7) +uint8_t currentPaletteIndex = parameter % 8; + const uint32_t TRANSITION_DURATION = 200000UL; + uint32_t t = millis(); + const uint16_t* currentPalette = palettes[currentPaletteIndex]; + + uint32_t totalCycle = NUM_COLORS * TRANSITION_DURATION; + uint32_t localTime = t % totalCycle; + + uint8_t index1 = (localTime / TRANSITION_DURATION) % NUM_COLORS; + uint8_t index2 = (index1 + 1) % NUM_COLORS; + + uint16_t timeInStep = localTime % TRANSITION_DURATION; + uint8_t blendT = (timeInStep * 255UL) / TRANSITION_DURATION; + + for (int x = 0; x < XMAX; x++) { + for (int y = 0; y < YMAX; y++) { + uint8_t offset = ((x + y) * 2 + (t >> 4)) & 0x3F; + uint8_t finalBlend = (blendT + offset) & 0xFF; + + uint16_t color = interpolateRGB565(currentPalette[index1], currentPalette[index2], finalBlend); + dma_display->drawPixel(x, y, color); + } + } +} + + +void transition5() { + static const uint8_t NUM_COLORS = 4; + static const uint8_t NUM_PALETTES = 8; + static const uint32_t COLOR_DURATION = 100000UL; // 100s per color blend + static const uint32_t PALETTE_BLEND_DURATION = 200000UL; // 200s per full palette blend + + // More vibrant and distinct palettes + static const uint16_t palettes[NUM_PALETTES][NUM_COLORS] = { + { 0xF81F, 0x07FF, 0xFFE0, 0x07E0 }, // Neon Glow + { 0xF800, 0xFC00, 0xFFE0, 0xFFFF }, // Lava + { 0x001F, 0x07FF, 0x05BF, 0x87FF }, // Ocean + { 0x07FF, 0x780F, 0xFD20, 0xFFFF }, // Retro + { 0x001F, 0xF81F, 0x780F, 0x07E0 }, // Cyberpunk + { 0x07E0, 0xF81F, 0x780F, 0x07FF }, // Aurora + { 0xF800, 0xFFE0, 0x07E0, 0x001F }, // Rainbow + { 0x7800, 0x780F, 0xFD20, 0xFBE0 } // Sunset + }; + + uint32_t t = millis(); + + // Time into palette blend cycle + uint32_t paletteTime = t % PALETTE_BLEND_DURATION; + uint8_t paletteIndex1 = (t / PALETTE_BLEND_DURATION) % NUM_PALETTES; + uint8_t paletteIndex2 = (paletteIndex1 + 1) % NUM_PALETTES; + + // Blend between two palettes + uint8_t paletteBlendT = (paletteTime * 255UL) / PALETTE_BLEND_DURATION; + + // Color transition timing + uint32_t localColorTime = t % (NUM_COLORS * COLOR_DURATION); + uint8_t colorIndex1 = (localColorTime / COLOR_DURATION) % NUM_COLORS; + uint8_t colorIndex2 = (colorIndex1 + 1) % NUM_COLORS; + + uint16_t colorStepTime = localColorTime % COLOR_DURATION; + uint8_t baseBlend = (colorStepTime * 255UL) / COLOR_DURATION; + + // Blend the two palettes into a temporary working palette + uint16_t blendedPalette[NUM_COLORS]; + for (uint8_t i = 0; i < NUM_COLORS; i++) { + blendedPalette[i] = interpolateRGB565( + palettes[paletteIndex1][i], + palettes[paletteIndex2][i], + paletteBlendT + ); + } + + // Center coordinates + int16_t centerX = XMAX / 2; + int16_t centerY = YMAX / 2; + + for (int x = 0; x < XMAX; x++) { + for (int y = 0; y < YMAX; y++) { + int16_t dx = x - centerX; + int16_t dy = y - centerY; + + uint16_t distance = (uint16_t)sqrt(dx * dx + dy * dy); + uint8_t wave = (distance * 8 + (t >> 5)) & 0xFF; + uint8_t finalBlend = (baseBlend + wave) & 0xFF; + + uint16_t color = interpolateRGB565( + blendedPalette[colorIndex1], + blendedPalette[colorIndex2], + finalBlend + ); + + dma_display->drawPixel(x, y, color); + } + } +} + + +void transition6() { + static const uint8_t NUM_COLORS = 4; + static const uint8_t NUM_SEGMENTS = NUM_COLORS; // for smooth wrap (4 segments: 0→1→2→3→0) + static const uint8_t NUM_PALETTES = 8; + + static const uint32_t COLOR_DURATION = 200000UL; // 200s scroll loop + static const uint32_t PALETTE_BLEND_DURATION = 800000UL; // 800s palette transition + + static const uint8_t palettes[NUM_PALETTES][NUM_COLORS][3] = { + { {255, 0, 0}, {255, 127, 0}, {255, 255, 0}, {0, 255, 0} }, // Rainbow + { {85, 207, 252}, {247, 168, 184}, {255, 255, 255}, {85, 207, 252} }, // Trans + { {214, 2, 111}, {155, 79, 150}, {0, 56, 168}, {214, 2, 111} }, // Bi + { {255, 33, 140}, {255, 181, 0}, {0, 173, 239}, {255, 33, 140} }, // Pan + { {0, 0, 0}, {164, 164, 164}, {255, 255, 255}, {128, 0, 128} }, // Asexual + { {244, 200, 0}, {255, 255, 255}, {165, 165, 165}, {155, 89, 182} }, // Non-binary + { {255, 117, 160}, {243, 60, 140}, {108, 30, 143}, {76, 109, 155} }, // Genderfluid + { {214, 46, 0}, {255, 90, 54}, {255, 154, 139}, {209, 98, 169} } // Lesbian + }; + + uint32_t t = millis(); + + // Palette transition + uint32_t paletteTime = t % PALETTE_BLEND_DURATION; + uint8_t paletteIndex1 = (t / PALETTE_BLEND_DURATION) % NUM_PALETTES; + uint8_t paletteIndex2 = (paletteIndex1 + 1) % NUM_PALETTES; + uint8_t blendT = (paletteTime * 255UL) / PALETTE_BLEND_DURATION; + + // Create blended palette with wrap + uint16_t blendedPalette[NUM_COLORS + 1]; // add one for wraparound + for (uint8_t i = 0; i < NUM_COLORS; i++) { + uint32_t rgb1 = (palettes[paletteIndex1][i][0] << 16) | + (palettes[paletteIndex1][i][1] << 8) | + palettes[paletteIndex1][i][2]; + uint32_t rgb2 = (palettes[paletteIndex2][i][0] << 16) | + (palettes[paletteIndex2][i][1] << 8) | + palettes[paletteIndex2][i][2]; + uint16_t c1 = RGB888ToRGB565(rgb1); + uint16_t c2 = RGB888ToRGB565(rgb2); + blendedPalette[i] = interpolateRGB565(c1, c2, blendT); + } + // Wrap the first color to the end + blendedPalette[NUM_COLORS] = blendedPalette[0]; + + // Gradient and scroll setup + uint16_t gradientWidth = XMAX; + uint16_t segmentWidth = gradientWidth / NUM_SEGMENTS; + uint16_t scrollOffset = (uint32_t)(t % COLOR_DURATION) * gradientWidth / COLOR_DURATION; + + for (int x = 0; x < XMAX; x++) { + uint16_t gradientX = (x + scrollOffset) % gradientWidth; + uint8_t segment = gradientX / segmentWidth; + if (segment >= NUM_SEGMENTS) segment = NUM_SEGMENTS - 1; + + uint8_t localX = gradientX - segment * segmentWidth; + uint8_t blendFactor = (localX * 255) / segmentWidth; + + uint16_t color = interpolateRGB565( + blendedPalette[segment], + blendedPalette[segment + 1], + blendFactor + ); + + for (int y = 0; y < YMAX; y++) { + dma_display->drawPixel(x, y, color); + } + } +} + diff --git a/transigione/test/README b/transigione/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/transigione/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/wallmount.stl b/wallmount.stl new file mode 100644 index 0000000..79e5dd3 Binary files /dev/null and b/wallmount.stl differ