спс, вот это цена, у меня столько батарейка одна стоит))) если магазин не специализированный (и то вроде дороже на 25-75 рублей, не помню… хотя может там и нет батареек внутри…) и они работают от 2 батареек…
https://aliexpress.ru/item/1005010145239802.html не знаю толще ли это будет, но как вариант, и смену цвета делать можно, но лучше адресную тогда если что…
Это и есть адресная, но она не изящна.
вроде нельзя каждым пикселем управлять, а просто подача шима нужной частоты на 1 или все 3 контакта,(так что по аккуратнее, а то может не то приехать))) ) но каюсь у меня нехватка времени, могу и ошибаться)) цены могут еще и дешевле быть, но не намного… цена в разы отличается…
Эта картинка очень сильно намекает на адресность. Именно из такой, но 10 метровой переделывал под ёлочку.
они могли показать на фото подключение нескольких отрезков лент, с разными цветами… надо читать описание, вроде это просто лента которая как раз не управляет каждым диодом, хотя если почитать и посмотреть фото ниже, действительно 3 пина… кажется я нашел адресную ленту дешево))) относительно дешево, по сравнению с другими…
p.s. а на других фото вижу 4 провода… в общем лучше все перепроверить)))
в описании RGBIC скорее всего все таки адресная))
Если Вы про вот это
То там в описании прямо написано WS2812B, так что всё можно ![]()
![]()
Она не изящная.
Да уж…
Моя вот такие предпочитает 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);
}
Главное чек приложить… ))
Чек то зачем?
Для повышения «ценности подарка» )))
Ты думаешь она не в курсе насчет “ценности”?
Она мне говорит, как называется, и что не может найти, т.к. везде закончились. Но возможно, мне где-то еще удастся найти остатки за двойную цену.
Коллекционеры - они такие…
Ну, я же говорю: “для разных применений” ![]()
Речь шла о ТС.
Ну вот, исчез… Наверное очень торопится спаять чудо. Неее, так он и до следующего НГ не успеет. Не вижу блеска в глазах. Я тогда все, благотворительность в данном месте кончилась. Если че, зовите, я за попкорном пошел.
не знаю проще ли этот путь для него будет, с использованием веб сервера, но вроде должен иметь не плохие шансы сделать сам даже за 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 пиновые, или дорогие, начал искать просто ргб, сразу нашел адресные и дешевые)))
Если что я живу не по Московскому времени) за очередной скетч спасибо)
как успехи ?))) если вы сделали проект, самое время опубликовать что получилось…



