From 9630c6c2b4fd51e63cafff785e8163e04f31fcc1 Mon Sep 17 00:00:00 2001 From: zeyus Date: Sun, 23 Feb 2025 20:24:49 +0100 Subject: [PATCH] initial commit --- README.md | 17 +++++++- fluid.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++++ fluid.py | 92 ++++++++++++++++++++++++++++++++++++++ sketch.ino | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 fluid.cpp create mode 100644 fluid.py create mode 100644 sketch.ino diff --git a/README.md b/README.md index 3413b1c..c6eade3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ -# FLIP-ESP32-I2C-OLED -more details to come, a project for making a FLIP fluid simulation with accelerometer + gyro with a cheap OLED and an ESP32 board +I wanted to make an ESP32 based 128x64 I2C OLED FLIP simulator, +inspired by: +https://mitxela.com/projects/fluid-pendant + +but with way cheaper and easier components. + +It seemse like there's not a huge body of work on this, but I did find this project: +https://wokwi.com/projects/420908950128021505 + +(no user information) + + +anyway...documentation will go on my site: https://zeyus.com/ + +I'm going to use the following components: diff --git a/fluid.cpp b/fluid.cpp new file mode 100644 index 0000000..ee6a579 --- /dev/null +++ b/fluid.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +using namespace std; + +// Simulation constants +const int SIM_WIDTH = 64; +const int SIM_HEIGHT = 32; +const int NUM_PARTICLES = 100; + +struct Particle { + double x, y; + double vx, vy; +}; + +vector particles(NUM_PARTICLES); + +void initializeParticles() { + for (auto& p : particles) { + p.x = rand() % SIM_WIDTH; + p.y = rand() % (SIM_HEIGHT / 2); + p.vx = (rand() % 200 - 100) / 100.0 * 0.7; + p.vy = (rand() % 100 - 50) / 100.0; + } +} + +void updatePhysics() { + const double gravity = 0.15; + const double damping = 0.82; + const double border = 2.0; + + for (auto& p : particles) { + p.vy += gravity; + p.x += p.vx; + p.y += p.vy; + + // Horizontal boundary collisions + if (p.x < border || p.x >= SIM_WIDTH - border) { + p.vx = -p.vx * damping; + p.x = max(border, min(p.x, SIM_WIDTH - border - 0.1)); + } + + // Vertical boundary collisions + if (p.y < border || p.y >= SIM_HEIGHT - border) { + p.vy = -p.vy * damping; + p.y = max(border, min(p.y, SIM_HEIGHT - border - 0.1)); + } + } +} + +void drawFrame() { + vector > grid(SIM_HEIGHT, vector(SIM_WIDTH, ' ')); + + // Plot particles + for (const auto& p : particles) { + int x = static_cast(p.x); + int y = static_cast(p.y); + x = max(0, min(SIM_WIDTH - 1, x)); + y = max(0, min(SIM_HEIGHT - 1, y)); + grid[y][x] = 'o'; + } + + // Clear screen and reset cursor + cout << "\033[H"; + + // Draw grid + for (const auto& row : grid) { + for (char c : row) { + cout << c; + } + cout << '\n'; + } + cout << flush; +} + +int main() { + srand(time(nullptr)); + initializeParticles(); + + // Hide cursor + cout << "\033[?25l"; + + auto last_frame = chrono::steady_clock::now(); + const chrono::milliseconds frame_delay(33); + + try { + while (true) { + auto now = chrono::steady_clock::now(); + if (now - last_frame >= frame_delay) { + updatePhysics(); + drawFrame(); + last_frame = now; + } + } + } catch (...) { + // Show cursor before exiting + cout << "\033[?25h"; + } + + // Restore cursor + cout << "\033[?25h"; + return 0; +} diff --git a/fluid.py b/fluid.py new file mode 100644 index 0000000..575de2c --- /dev/null +++ b/fluid.py @@ -0,0 +1,92 @@ +import time +import random +import sys + +# Simulation constants +SIM_WIDTH = 128 +SIM_HEIGHT = 64 +NUM_PARTICLES = 200 +GRAVITY = 0.5 +DAMPING = 0.82 +BORDER = 2.0 +FRAME_DELAY = 0.033 # ~30 FPS + +class Particle: + def __init__(self): + self.x = random.uniform(BORDER, SIM_WIDTH - BORDER) + self.y = random.uniform(BORDER, SIM_HEIGHT/2 - BORDER) + self.vx = random.uniform(-1, 1) * 0.7 + self.vy = random.uniform(-0.5, 0.5) + +def initialize_particles(num_particles): + return [Particle() for _ in range(num_particles)] + +def update_physics(particles): + for p in particles: + # Apply gravity + p.vy += GRAVITY + + # Update position + p.x += p.vx + p.y += p.vy + + # Horizontal boundary collisions + if p.x < BORDER or p.x >= SIM_WIDTH - BORDER: + p.vx *= -DAMPING + p.x = max(BORDER, min(p.x, SIM_WIDTH - BORDER - 0.1)) + + # Vertical boundary collisions + if p.y < BORDER or p.y >= SIM_HEIGHT - BORDER: + p.vy *= -DAMPING + p.y = max(BORDER, min(p.y, SIM_HEIGHT - BORDER - 0.1)) + +def draw_frame(particles): + # Initialize empty grid + grid = [[' ' for _ in range(SIM_WIDTH)] for _ in range(SIM_HEIGHT)] + + # Plot particles + for p in particles: + x = int(p.x) + y = int(p.y) + if 0 <= x < SIM_WIDTH and 0 <= y < SIM_HEIGHT: + grid[y][x] = 'o' + + # Build frame buffer + buffer = [] + for row in grid: + buffer.append(''.join(row)) + + # Clear screen and move cursor to top-left + sys.stdout.write('\033[H\033[J') + sys.stdout.write('\n'.join(buffer)) + sys.stdout.flush() + +def main(): + particles = initialize_particles(NUM_PARTICLES) + + # Hide cursor + sys.stdout.write('\033[?25l') + sys.stdout.flush() + + try: + while True: + start_time = time.monotonic() + + update_physics(particles) + draw_frame(particles) + + # Frame rate control + elapsed = time.monotonic() - start_time + sleep_time = FRAME_DELAY - elapsed + if sleep_time > 0: + time.sleep(sleep_time) + + except KeyboardInterrupt: + pass + finally: + # Show cursor before exiting + sys.stdout.write('\033[?25h') + sys.stdout.flush() + +if __name__ == "__main__": + main() diff --git a/sketch.ino b/sketch.ino new file mode 100644 index 0000000..79bcd8d --- /dev/null +++ b/sketch.ino @@ -0,0 +1,126 @@ +#include +#include + +// Config +#define PANEL_RES_X 64 +#define PANEL_RES_Y 32 +#define NUM_PANELS 2 +#define SIM_WIDTH (PANEL_RES_X * NUM_PANELS) +#define SIM_HEIGHT PANEL_RES_Y +#define NUM_PARTICLES 100 +#define FIXED_SHIFT 8 + +typedef int16_t fixed_t; +#define TO_FIXED(x) ((fixed_t)((x) * (1 << FIXED_SHIFT))) +#define TO_FLOAT(x) ((float)(x) / (1 << FIXED_SHIFT)) + +struct Particle { + fixed_t x, y; + fixed_t vx, vy; +}; + +Particle particles[NUM_PARTICLES]; +MatrixPanel_I2S_DMA *dma_display; + +/* +// Wokwi-compatible pin configuration +HUB75_I2S_CFG::i2s_pins _pins = { + .r1 = 25, .g1 = 26, .b1 = 27, + .r2 = 14, .g2 = 12, .b2 = 13, + .a = 23, .b = 19, .c = 5, .d = 17, + .e = -1, // Required for 64x64 panels + .lat = 4, .oe = 15, .clk = 16 +}; +*/ + +// ESP32-S3-WROOM-1 HUB75 Pin Mapping +HUB75_I2S_CFG::i2s_pins _pins = { + .r1 = 1, .g1 = 2, .b1 = 3, + .r2 = 4, .g2 = 5, .b2 = 6, + .a = 7, .b = 8, .c = 9, + .d = 10, .e = -1, // 'e' only needed for 64x64 panels + .lat = 11, .oe = 12, .clk = 13 +}; + +// Color palette +const uint16_t COLORS[] = { + 0x001F, 0x03FF, 0x07FF, 0x7FE0, 0x7F80, 0xFFE0, 0xFD20, 0xF800 +}; +const int NUM_COLORS = sizeof(COLORS)/sizeof(COLORS[0]); + +void setup() { + + HUB75_I2S_CFG mxconfig( + PANEL_RES_X, + PANEL_RES_Y, + NUM_PANELS, + _pins, + HUB75_I2S_CFG::FM6126A, + false, + HUB75_I2S_CFG::HZ_10M, + true, + HUB75_I2S_CFG::SHIFTREG, + false, + 0 + ); + + mxconfig.double_buff = true; + dma_display = new MatrixPanel_I2S_DMA(mxconfig); + dma_display->begin(); + dma_display->setBrightness(255); + + // Initialize particles + for (int i = 0; i < NUM_PARTICLES; i++) { + particles[i].x = TO_FIXED(random(SIM_WIDTH)); + particles[i].y = TO_FIXED(random(SIM_HEIGHT/2)); + particles[i].vx = TO_FIXED((random(-100, 100)/100.0) * 0.7); + particles[i].vy = TO_FIXED((random(-50, 50)/100.0)); + } +} + +void updatePhysics() { + const fixed_t gravity = TO_FIXED(0.15); + const fixed_t damping = TO_FIXED(0.82); + const fixed_t border = TO_FIXED(2.0); + + for (int i = 0; i < NUM_PARTICLES; i++) { + particles[i].vy += gravity; + particles[i].x += particles[i].vx; + particles[i].y += particles[i].vy; + + // Boundary collisions + if (particles[i].x < border || particles[i].x >= TO_FIXED(SIM_WIDTH) - border) { + particles[i].vx = -particles[i].vx * damping; + particles[i].x = constrain(particles[i].x, border, TO_FIXED(SIM_WIDTH) - border); + } + if (particles[i].y < border || particles[i].y >= TO_FIXED(SIM_HEIGHT) - border) { + particles[i].vy = -particles[i].vy * damping; + particles[i].y = constrain(particles[i].y, border, TO_FIXED(SIM_HEIGHT) - border); + } + } +} + +void drawParticles() { + dma_display->fillScreen(0); + + for (int i = 0; i < NUM_PARTICLES; i++) { + int x = constrain(TO_FLOAT(particles[i].x), 0, SIM_WIDTH-1); + int y = constrain(TO_FLOAT(particles[i].y), 0, SIM_HEIGHT-1); + + // Simplified color selection + uint16_t color = COLORS[(abs(particles[i].vx) + abs(particles[i].vy)) % NUM_COLORS]; + + dma_display->drawPixel(x, y, color); + } +} + +void loop() { + static uint32_t last_frame = 0; + const uint32_t frame_time = 33; // ~30 FPS + + if (millis() - last_frame >= frame_time) { + updatePhysics(); + drawParticles(); + last_frame = millis(); + } +}