initial commit
This commit is contained in:
parent
918c767c03
commit
9630c6c2b4
4 changed files with 339 additions and 2 deletions
17
README.md
17
README.md
|
@ -1,2 +1,15 @@
|
||||||
# FLIP-ESP32-I2C-OLED
|
I wanted to make an ESP32 based 128x64 I2C OLED FLIP simulator,
|
||||||
more details to come, a project for making a FLIP fluid simulation with accelerometer + gyro with a cheap OLED and an ESP32 board
|
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:
|
||||||
|
|
106
fluid.cpp
Normal file
106
fluid.cpp
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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<Particle> 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<vector<char> > grid(SIM_HEIGHT, vector<char>(SIM_WIDTH, ' '));
|
||||||
|
|
||||||
|
// Plot particles
|
||||||
|
for (const auto& p : particles) {
|
||||||
|
int x = static_cast<int>(p.x);
|
||||||
|
int y = static_cast<int>(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;
|
||||||
|
}
|
92
fluid.py
Normal file
92
fluid.py
Normal file
|
@ -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()
|
126
sketch.ino
Normal file
126
sketch.ino
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue