Помогите реализовать проект

спс, вот это цена, у меня столько батарейка одна стоит))) если магазин не специализированный (и то вроде дороже на 25-75 рублей, не помню… хотя может там и нет батареек внутри…) и они работают от 2 батареек…
https://aliexpress.ru/item/1005010145239802.html не знаю толще ли это будет, но как вариант, и смену цвета делать можно, но лучше адресную тогда если что…

Это и есть адресная, но она не изящна.

вроде нельзя каждым пикселем управлять, а просто подача шима нужной частоты на 1 или все 3 контакта,(так что по аккуратнее, а то может не то приехать))) ) но каюсь у меня нехватка времени, могу и ошибаться)) цены могут еще и дешевле быть, но не намного… цена в разы отличается…

Эта картинка очень сильно намекает на адресность. Именно из такой, но 10 метровой переделывал под ёлочку.

они могли показать на фото подключение нескольких отрезков лент, с разными цветами… надо читать описание, вроде это просто лента которая как раз не управляет каждым диодом, хотя если почитать и посмотреть фото ниже, действительно 3 пина… кажется я нашел адресную ленту дешево))) относительно дешево, по сравнению с другими…

p.s. а на других фото вижу 4 провода… в общем лучше все перепроверить)))
в описании RGBIC скорее всего все таки адресная))

Если Вы про вот это

То там в описании прямо написано WS2812B, так что всё можно :slight_smile:

:slight_smile:

Она не изящная.

Да уж…
Моя вот такие предпочитает https://www.ozon.ru/product/platok-sherstyanoy-1256260713
И доставка не на следующий день…

Это разные вещи для разных применений. Т.е. это не “логан против мерина”, “легковушка простив самосвала”

@MrDooku нате вам с кодом “сердечка“ симуляция

uint8_t light1=9;
uint8_t light2=10;
uint8_t light3=11;
uint8_t music=7;
uint8_t vibro=8;
uint8_t button1=5;
uint8_t button2=4;

void setup() 
{
  pinMode(light1, OUTPUT);
  pinMode(light2, OUTPUT);
  pinMode(light3, OUTPUT);
  pinMode(music, OUTPUT);
  pinMode(vibro, OUTPUT);
  pinMode(button1, INPUT_PULLUP);
  pinMode(button2, INPUT_PULLUP);

  analogWrite(light1, 0);
  analogWrite(light2, 0);
  analogWrite(light3, 0);
  digitalWrite(music, LOW);
  digitalWrite(vibro, LOW);
}

void loop() 
{
  //ожидание активации
  waitActivation();

  //включение музыки
  digitalWrite(music, HIGH);
  //поочередное включение гирлянд за 5 секунд каждая
  for (int i=1; i<=255; i++)
  {
    analogWrite(light1, i);
    delay(20);
  }
  for (int i=1; i<=255; i++)
  {
    analogWrite(light2, i);
    delay(20);
  }
  for (int i=1; i<=255; i++)
  {
    analogWrite(light3, i);
    delay(20);
  }
  //моторчик, имитация периодического включения на 4 секунды
  for (int i=0; i < 20; i++)
  {
    digitalWrite(vibro, HIGH);
    delay(100);
    digitalWrite(vibro, LOW);
    delay(100);
  }
  //одновременное выключение гирлянд за 5 секунд
  for (int i=255; i>=0; i--)
  {
    analogWrite(light1, i);
    analogWrite(light2, i);
    analogWrite(light3, i);
    delay(20);
  }
  //выключение музыки
  digitalWrite(music, LOW);
}

void waitActivation()
{
  int buttonState1 = 0;
  int buttonState2 = 0;
  int lastButtonState1 = LOW;
  int lastButtonState2 = LOW;

  unsigned long pressTime = 0;
  unsigned long blinkTime = 0;
  int activationStep = 0;
  int blink = 0;

  int codeDelay=5000; //допустимая задержка между нажатиями кнопок, мс

  while (activationStep < 4)
  {
    buttonState1 = digitalRead(button1);
    buttonState2 = digitalRead(button2);
  
    if (buttonState1 == LOW && lastButtonState1 == HIGH) 
    {
      if (   activationStep == 0 
          || activationStep == 2 )
      {
        pressTime = millis();
        activationStep++;
      }
    }
  
    if (buttonState2 == LOW && lastButtonState2 == HIGH) 
    {
      if (   activationStep == 1 
          || activationStep == 3 )
      {
        pressTime = millis();
        activationStep++;
      }
    }
  
    if (   activationStep != 0 
        && (millis() - pressTime > codeDelay) ) 
    {
      activationStep = 0;
    }
  
    lastButtonState1 = buttonState1;
    lastButtonState2 = buttonState2;
    delay(10);

    //подсказка ввода кода моргает
    if (millis() - blinkTime >= 100)
    {
      if (blink == 0)
      {
        //чем ближе к концу тем слабее подсветка
        blink = ( 255*(codeDelay - (millis()-pressTime)) ) / codeDelay;
      }
      else
      {
        blink = 0;
      }
      blinkTime = millis();
    }

    //чем ближе к правильному коду, тем больше лампочек мигают
    switch (activationStep)
    {
      case 0:
        analogWrite(light1, 0);
        analogWrite(light2, 0);
        analogWrite(light3, 0);
        break;
      case 1:
        analogWrite(light1, blink);
        analogWrite(light2, 0);
        analogWrite(light3, 0);
        break;
      case 2:
        analogWrite(light1, blink);
        analogWrite(light2, blink);
        analogWrite(light3, 0);
        break;
      case 3:
        analogWrite(light1, blink);
        analogWrite(light2, blink);
        analogWrite(light3, blink);
        break;
    }
  } //while

  //выключение подсказки кода
  analogWrite(light1, 0);
  analogWrite(light2, 0);
  analogWrite(light3, 0);
}

Главное чек приложить… ))

1 лайк

Чек то зачем?

Для повышения «ценности подарка» )))

Ты думаешь она не в курсе насчет “ценности”?
Она мне говорит, как называется, и что не может найти, т.к. везде закончились. Но возможно, мне где-то еще удастся найти остатки за двойную цену.

Коллекционеры - они такие…

Ну, я же говорю: “для разных применений” :slight_smile:

Речь шла о ТС.

Ну вот, исчез… Наверное очень торопится спаять чудо. Неее, так он и до следующего НГ не успеет. Не вижу блеска в глазах. Я тогда все, благотворительность в данном месте кончилась. Если че, зовите, я за попкорном пошел.

1 лайк

не знаю проще ли этот путь для него будет, с использованием веб сервера, но вроде должен иметь не плохие шансы сделать сам даже за 2вое суток, вот что может взять за основу)))

#include <WiFi.h>
#include <WebServer.h>
#include <vector>
#include <math.h>

const char* ssid = "ESP32-Heart";
const char* password = "12345678";

WebServer server(80);

struct Point {
float x;
float y;
};

std::vector<Point> generateHeartPoints() {
std::vector<Point> points;
int steps = 100;

for (int i = 0; i <= steps; i++) {
float t = (float)i / steps * 2 * M_PI;
float x = 16 * pow(sin(t), 3);
float y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t);

Point p;
p.x = 50 + (x * 1.5);
p.y = 50 - (y * 1.5);

points.push_back(p);
}

return points;
}

std::vector<Point> heartPoints;
const float THRESHOLD = 6.0;
const int REQUIRED_PERCENTAGE = 90;
const int MIN_POINTS = 80;

bool* visitedPoints = nullptr;
std::vector<Point> drawnPoints;
bool isDrawing = false;
bool heartCompleted = false;
unsigned long completionTime = 0;
const int LIGHT_DURATION = 10000;

int lastVisitedIndex = -1;
int consecutiveMisses = 0;
const int MAX_CONSECUTIVE_MISSES = 3;
unsigned long lastTouchTime = 0;

const int LED_PIN = 2;

const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}

body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}

.container {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 100%;
max-width: 500px;
text-align: center;
}

h1 {
color: #ff4081;
margin-bottom: 10px;
font-size: 28px;
}

.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 16px;
}

.canvas-container {
position: relative;
margin: 20px auto;
width: 100%;
max-width: 400px;
}

canvas {
width: 100%;
height: 400px;
border: 3px solid #ff4081;
border-radius: 15px;
background: white;
touch-action: none;
display: block;
}

.status-container {
margin: 25px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
border-left: 5px solid #ff4081;
}

.status {
font-size: 18px;
font-weight: bold;
color: #333;
min-height: 27px;
}

.progress-container {
margin-top: 15px;
}

.progress-bar {
width: 100%;
height: 10px;
background: #e0e0e0;
border-radius: 5px;
overflow: hidden;
}

.progress-fill {
height: 100%;
background: linear-gradient(90deg, #ff4081, #ff80ab);
width: 0%;
transition: width 0.3s ease;
}

.progress-text {
margin-top: 5px;
font-size: 14px;
color: #666;
}

.instructions {
color: #666;
font-size: 14px;
margin-top: 20px;
line-height: 1.5;
padding: 15px;
background: #f0f0f0;
border-radius: 10px;
}

.led-status {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #ccc;
margin-right: 8px;
transition: background 0.3s;
}

.led-status.on {
background: #4CAF50;
box-shadow: 0 0 10px #4CAF50;
}

.heart-emoji {
font-size: 24px;
margin: 0 5px;
}

@media (max-width: 480px) {
.container {
padding: 20px;
}

canvas {
height: 350px;
}

h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="container">
<h1><span class="heart-emoji">❤️</span> Обведите сердечко <span class="heart-emoji">❤️</span></h1>
<p class="subtitle">Обведите контур пальцем, чтобы включить лампочку</p>

<div class="canvas-container">
<canvas id="heartCanvas"></canvas>
</div>

<div class="status-container">
<div class="status" id="status">Готово к рисованию...</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">0% обведено</div>
</div>
</div>

<div class="instructions">
<div style="margin-bottom: 10px;">
<span class="led-status" id="ledStatus"></span>
Статус лампочки: <span id="ledText">Выключена</span>
</div>
Начните обводить сердечко в любом месте. Лампочка включится только когда вы обведёте <b>90% контура</b> и будет гореть 10 секунд.
</div>
</div>

<script>
const canvas = document.getElementById('heartCanvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const ledStatus = document.getElementById('ledStatus');
const ledText = document.getElementById('ledText');

canvas.width = 400;
canvas.height = 400;

let heartPoints = [];
let visitedPoints = [];
let isDrawingMode = false;
let isCompleted = false;
let lastUpdate = 0;

function loadInitialData() {
fetch('/get-data')
.then(response => response.json())
.then(data => {
heartPoints = data.heartPoints;
visitedPoints = data.visited;
isDrawingMode = data.drawing;
isCompleted = data.completed;
updateDisplay();
drawHeart();
});
}

function drawHeart() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

if (heartPoints.length === 0) return;

ctx.beginPath();
ctx.moveTo(heartPoints[0].x * 4, heartPoints[0].y * 4);

for (let i = 1; i < heartPoints.length; i++) {
ctx.lineTo(heartPoints[i].x * 4, heartPoints[i].y * 4);
}

ctx.closePath();
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 3;
ctx.stroke();

ctx.beginPath();
let hasPath = false;
let lastVisited = -1;

for (let i = 0; i < heartPoints.length; i++) {
if (visitedPoints[i]) {
if (!hasPath) {
ctx.moveTo(heartPoints[i].x * 4, heartPoints[i].y * 4);
hasPath = true;
lastVisited = i;
} else if (Math.abs(i - lastVisited) <= 5) {
ctx.lineTo(heartPoints[i].x * 4, heartPoints[i].y * 4);
lastVisited = i;
} else {
ctx.moveTo(heartPoints[i].x * 4, heartPoints[i].y * 4);
lastVisited = i;
}
}
}

if (hasPath) {
ctx.strokeStyle = '#ff4081';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
}

for (let i = 0; i < heartPoints.length; i++) {
if (visitedPoints[i]) {
ctx.beginPath();
ctx.arc(heartPoints[i].x * 4, heartPoints[i].y * 4, 3, 0, Math.PI * 2);
ctx.fillStyle = '#ff4081';
ctx.fill();
}
}
}

function updateDisplay() {
if (isCompleted) {
statusEl.textContent = '🎉 Сердечко полностью обведено!';
ledStatus.className = 'led-status on';
ledText.textContent = 'Включена (горит 10 сек)';
} else if (isDrawingMode) {
statusEl.textContent = '✏️ Продолжайте обводить...';
ledStatus.className = 'led-status';
ledText.textContent = 'Выключена';
} else {
statusEl.textContent = '👆 Коснитесь сердечка, чтобы начать';
ledStatus.className = 'led-status';
ledText.textContent = 'Выключена';
}

const visitedCount = visitedPoints.filter(v => v).length;
const totalPoints = heartPoints.length;
const percentage = totalPoints > 0 ? Math.round((visitedCount / totalPoints) * 100) : 0;

progressFill.style.width = percentage + '%';
progressText.textContent = percentage + '% обведено';

if (percentage < 50) {
progressFill.style.background = '#ff4081';
} else if (percentage < 90) {
progressFill.style.background = '#ff80ab';
} else {
progressFill.style.background = '#4CAF50';
}
}

function handleTouch(x, y) {
fetch('/touch?x=' + x + '&y=' + y + '&t=' + Date.now())
.then(response => response.json())
.then(data => {
visitedPoints = data.visited;
isDrawingMode = data.drawing;
isCompleted = data.completed;

drawHeart();
updateDisplay();

if (isDrawingMode && !isCompleted) {
const now = Date.now();
if (now - lastUpdate > 300) {
lastUpdate = now;
checkStatus();
}
}
});
}

function checkStatus() {
fetch('/get-data')
.then(response => response.json())
.then(data => {
visitedPoints = data.visited;
isDrawingMode = data.drawing;
isCompleted = data.completed;

if (!isCompleted) {
drawHeart();
updateDisplay();
}
});
}

function setupCanvasEvents() {
let isTouching = false;
let lastX = 0, lastY = 0;

function processInteraction(x, y) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const canvasX = (x - rect.left) * scaleX / 4;
const canvasY = (y - rect.top) * scaleY / 4;

const dist = Math.sqrt(Math.pow(canvasX - lastX, 2) + Math.pow(canvasY - lastY, 2));
if (dist > 2) {
lastX = canvasX;
lastY = canvasY;
handleTouch(canvasX, canvasY);
}
}

canvas.addEventListener('mousedown', (e) => {
isTouching = true;
processInteraction(e.clientX, e.clientY);
});

canvas.addEventListener('mousemove', (e) => {
if (isTouching) {
processInteraction(e.clientX, e.clientY);
}
});

canvas.addEventListener('mouseup', () => {
isTouching = false;
});

canvas.addEventListener('mouseleave', () => {
isTouching = false;
});

canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isTouching = true;
const touch = e.touches[0];
processInteraction(touch.clientX, touch.clientY);
});

canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isTouching) {
const touch = e.touches[0];
processInteraction(touch.clientX, touch.clientY);
}
});

canvas.addEventListener('touchend', () => {
isTouching = false;
});
}

setInterval(checkStatus, 300);

loadInitialData();
setupCanvasEvents();

window.addEventListener('resize', () => {
drawHeart();
});
</script>
</body>
</html>
)rawliteral";

float distance(Point p1, Point p2) {
float dx = p1.x - p2.x;
float dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
}

int findNearestPoint(float x, float y) {
if (heartPoints.empty()) return -1;

Point currentPoint = {x, y};
int nearestIndex = -1;
float minDistance = THRESHOLD;

if (lastVisitedIndex != -1) {
int startIdx = lastVisitedIndex - 15;
int endIdx = lastVisitedIndex + 15;

for (int offset = 0; offset <= 30; offset++) {
int i = (lastVisitedIndex - 15 + offset + heartPoints.size()) % heartPoints.size();

if (visitedPoints[i]) continue;

float dist = distance(currentPoint, heartPoints[i]);
if (dist < minDistance) {
minDistance = dist;
nearestIndex = i;
}
}
}

if (nearestIndex == -1) {
for (size_t i = 0; i < heartPoints.size(); i++) {
if (visitedPoints[i]) continue;

float dist = distance(currentPoint, heartPoints[i]);
if (dist < minDistance) {
minDistance = dist;
nearestIndex = i;
}
}
}

return nearestIndex;
}

void fillSmallGaps(int newIndex) {
if (lastVisitedIndex == -1) return;

int gapSize = abs(newIndex - lastVisitedIndex);

if (gapSize > 1 && gapSize <= 10) {
int step = (newIndex > lastVisitedIndex) ? 1 : -1;
for (int i = lastVisitedIndex + step; i != newIndex; i += step) {
int idx = (i + heartPoints.size()) % heartPoints.size();
if (!visitedPoints[idx]) {
visitedPoints[idx] = true;
}
}
}

visitedPoints[newIndex] = true;
}

bool checkHeartCompleted() {
if (heartPoints.empty()) return false;

int visitedCount = 0;
for (int i = 0; i < (int)heartPoints.size(); i++) {
if (visitedPoints[i]) visitedCount++;
}

float percentage = (float)visitedCount / heartPoints.size() * 100;

return (percentage >= REQUIRED_PERCENTAGE && visitedCount >= MIN_POINTS);
}

void handleRoot() {
server.send(200, "text/html", htmlPage);
}

void handleTouch() {
if (server.hasArg("x") && server.hasArg("y")) {
float x = server.arg("x").toFloat();
float y = server.arg("y").toFloat();

unsigned long currentTime = millis();

if (currentTime - lastTouchTime < 50) {
lastTouchTime = currentTime;
} else {
lastTouchTime = currentTime;

int nearestIndex = findNearestPoint(x, y);

if (nearestIndex != -1) {
if (!isDrawing) {
isDrawing = true;
Serial.println("Начало рисования");
}

if (lastVisitedIndex != -1) {
fillSmallGaps(nearestIndex);
} else {
visitedPoints[nearestIndex] = true;
}

lastVisitedIndex = nearestIndex;
consecutiveMisses = 0;

if (drawnPoints.size() < 50) {
drawnPoints.push_back({x, y});
}

if (!heartCompleted && checkHeartCompleted()) {
heartCompleted = true;
completionTime = millis();
digitalWrite(LED_PIN, HIGH);
Serial.println("Сердечко завершено! Лампочка включена.");
}
} else {
consecutiveMisses++;
if (consecutiveMisses > MAX_CONSECUTIVE_MISSES) {
lastVisitedIndex = -1;
Serial.println("Сброс последней точки - не попали по контуру");
}
}
}
}

String json = "{";
json += "\"visited\":[";
for (int i = 0; i < (int)heartPoints.size(); i++) {
json += visitedPoints[i] ? "true" : "false";
if (i < (int)heartPoints.size() - 1) json += ",";
}
json += "],";
json += "\"drawing\":" + String(isDrawing ? "true" : "false") + ",";
json += "\"completed\":" + String(heartCompleted ? "true" : "false");
json += "}";

server.send(200, "application/json", json);
}

void handleGetData() {
String json = "{";

json += "\"heartPoints\":[";
for (size_t i = 0; i < heartPoints.size(); i++) {
json += "{\"x\":" + String(heartPoints[i].x) + ",\"y\":" + String(heartPoints[i].y) + "}";
if (i < heartPoints.size() - 1) json += ",";
}
json += "],";

json += "\"visited\":[";
for (int i = 0; i < (int)heartPoints.size(); i++) {
json += visitedPoints[i] ? "true" : "false";
if (i < (int)heartPoints.size() - 1) json += ",";
}
json += "],";

json += "\"drawing\":" + String(isDrawing ? "true" : "false") + ",";
json += "\"completed\":" + String(heartCompleted ? "true" : "false");
json += "}";

server.send(200, "application/json", json);
}

void setup() {
Serial.begin(115200);
delay(1000);

Serial.println("\n\n=== ESP32 Heart Drawing (Improved) ===");

heartPoints = generateHeartPoints();

visitedPoints = new bool[heartPoints.size()];
for (size_t i = 0; i < heartPoints.size(); i++) {
visitedPoints[i] = false;
}

Serial.print("Сгенерировано точек сердечка: ");
Serial.println(heartPoints.size());
Serial.print("Требуется для завершения: ");
Serial.print(REQUIRED_PERCENTAGE);
Serial.print("% (минимум ");
Serial.print(MIN_POINTS);
Serial.println(" точек)");

pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);

Serial.print("Создание точки доступа... ");
WiFi.softAP(ssid, password);
Serial.println("Готово!");

Serial.print("IP адрес: ");
Serial.println(WiFi.softAPIP());

server.on("/", handleRoot);
server.on("/touch", handleTouch);
server.on("/get-data", handleGetData);

server.begin();
Serial.println("HTTP сервер запущен");
Serial.println("Подключитесь к Wi-Fi: " + String(ssid));
Serial.println("Пароль: " + String(password));
Serial.println("Откройте браузер и перейдите по IP адресу выше\n");
}

void loop() {
server.handleClient();

if (heartCompleted) {
unsigned long currentTime = millis();
if (currentTime - completionTime >= LIGHT_DURATION) {
digitalWrite(LED_PIN, LOW);
heartCompleted = false;
isDrawing = false;
lastVisitedIndex = -1;
consecutiveMisses = 0;

for (size_t i = 0; i < heartPoints.size(); i++) {
visitedPoints[i] = false;
}
drawnPoints.clear();

Serial.println("Сброс: можно обводить заново");
}
}
}

пусть подлагивает, иногда дважды приходится обводить, но работает!)))

вот ссылка на видео https://wdfiles.ru/3wr5y вроде можно скачать…

да точно, но я как то не заметил))) а самое главное парадокс в том, что когда искал дешевую адресную ленту, находил 4 пиновые, или дорогие, начал искать просто ргб, сразу нашел адресные и дешевые)))

1 лайк

Если что я живу не по Московскому времени) за очередной скетч спасибо)

как успехи ?))) если вы сделали проект, самое время опубликовать что получилось…