Создайте своего монстра в коробке: проект робототехники на Arduino
Компоненты и материалы
| Деревянный ящик/коробка | × | 1 |
| Arduino UNO | × | 1 |
| Adafruit Wave Shield | × | 1 |
| Модуль реле 4-канальный SainSmart | × | 1 |
| Мотор стеклоочистителя | × | 1 |
| Красные светодиодные гирлянды (100 шт.) | × | 1 |
| Дымовая машина | × | 1 |
| Усилитель/колонка с питанием | × | 1 |
О проекте
Обновление до версии 4
Код Arduino полностью переписан. Основные изменения:
- Главный цикл реализован как конечный автомат
- Вся логика таймингов построена на сравнении времени событий с текущим временем
- Убрана зависимость от кадров и функции
delay()в основном цикле - Исправлена проблема «занятого порта» при подключении к Arduino IDE
- Реле теперь используют нормально открытые (NO) контакты — обновите подключение реле!
- Добавлены индикаторы: «Движение обнаружено», «Готов к пробуждению», «Пауза»
- Убран переключатель триггера действия
- Добавлена возможность увеличить время работы красной подсветки и дыма
- Код обильно прокомментирован
Актуальный код доступен на GitHub.
Монстр в коробке
Я обожаю хэллоуинские реквизиты и, вдохновившись чужими проектами «Монстр в коробке», решил построить свой. Идея проста: создать иллюзию, что в контейнере скрывается страшное существо, готовое вырваться на свободу. Эффект достигается сочетанием движения, звука, света, дыма и внезапности.
Что внутри коробки?!
Коробка должна быть достаточно большой, чтобы казаться способной скрыть что-то огромное и страшное. Вид старого, изношенного ящика, едва сдерживающего монстра, усиливает эффект. Не нашел подходящего варианта в секонд-хендах — собрал сам.
Использовал 15 досок из сосны (180 см) из Home Depot. Собрал коробку примерно 90×75×75 см. Доски распилил на циркулярной пиле, сцепил пневматическим гвоздезабивателем 18G. Позже понял, что гвозди не выдерживают нагрузок от ударов крышки — усилил конструкцию сухими шурупами 25 мм (закрутил изнутри, чтобы головки не виднелись).
Дерево искусственно состарил: настоем черного чая, уксуса и стальной шерсти. Украсил шаблонами и этикетками, найденными в сети или напечатанными самостоятельно.

Мозги!!!
У реквизита два режима: «Спит» и «Бодрствует». В режиме сна коробка тихая, издает только храп и легкое рычание. Пробуждение происходит при срабатывании PIR-датчика движения. Монстр рявкает, выдыхает дым, крышка прыгает, внутри загорается красный свет.
Управление: Arduino Uno + Wave Shield (звук) + модуль реле (исполнительные механизмы). На Wave Shield загружены файлы со звуками сна и рёва. PIR-датчик переключает режимы. В режиме бодрствования Arduino через реле включает мотор крышки, красную подсветку, дымогенератор и запускает звуки. Добавлена кнопка «Пауза» для временного отключения при появлении маленьких детей.

Оживает!
Подъемник крышки — устройство, которое быстро поднимает и опускает крышку, имитируя попытки монстра выбраться. Популярны пневматические приводы и вращающиеся эксцентрики. Пневматика давала бы более случайные движения, но я выбрал вращающийся эксцентрик — проще и дешевле.
Купил б/у мотор стеклоочистителя на свалке за $15. Продавец удивился, когда я сказал, что марка машины не важна. После объяснений он сам захотел собрать такой реквизит :)
Эти моторы работают от 12 В, имеют хороший крутящий момент и подходящую скорость. Важно: ищите мотор с остатками проводной разъемной колодки — так легче подпаять свои провода. Нужен также кривошип и шейка; если есть только главная шейка, эксцентрик будет проскальзывать.
Мотор закреплен на боковой стенке внутри коробки. Учтите, что вращающийся эксцентрик не должен задевать провода и другие детали. На YouTube много уроков по работе с такими моторами.

Эксцентрик диаметром 30 см вырезал из фанеры 12 мм. Сначала вырезал идеальный круг, установил на мотор, подогнал высоту мотора так, чтобы крышка поднималась примерно на 5 см. Затем лобзиком вырезал на эксцентрике зубчики случайной ширины и глубины.

Ролик на нижней стороне крышки закреплен на двух металлических уголках, втулке из нейлона 25 мм и каретном болте. Ролик касается зубчиков эксцентрика.

Питание мотора — 12 В от блока ATX через реле под управлением Arduino. Выравнивание потребовало нескольких попыток (хорошо, три раза), но в итоге крышка начала прыгать как надо!
Дыхание огня: дым и свет
Дымогенератор 400 Вт (радиоуправляемый). Для управления от Arduino разобрал пульт, припаял два провода к контактам кнопки «пуск» и подключил их к каналу реле. Простой хак, подсмотренный на YouTube. Также надел старый шланг от пылесоса, чтобы дым выходил плотным потоком из-под крышки — эффект лучше, и конденсат не скапливается на электронике.
Подсветка: обычная гирлянда из 100 красных светодиодов, подключенная через удлинитель, перерезанный и включенный в цепь реле. Arduino включает/выключает гирлянду.



Голос снаружи
Wave Shield имеет 3,5-мм джек для наушников. Подключил его патч-кордом к 30-ваттному бас-гитарному комбо (взял в репетиционной базе). Подойдут любые активные колонки, включая компьютерные, но с хорошим низами звук впечатлянее.

Прикупи цепями
Купил пластиковые цепи в магазине хэллоуин-аксессуаров и напечатал на 3D-принтере накладной замок. Обернул коробку — добавило глубины и реализма иллюзии.

Хэллоуин!!!
Выставил коробку во дворе, PIR-датчик спрятал в кустах рядом. Соединил датчик с коробкой длинным динамиковым проводом, экспериментировал с позиционированием: монстр просыпался, когда кто-то подходил примерно на 1,5 м.
С наступлением темноты детали оформления коробки стали плохо видны. Подсветил стробоскопическим прожектором — эффект получился отличным.
Дети, подходящие за конфетами, замечали рычащую коробку: некоторые с любопытством, другие — с испугом. Когда монстр оживал с ревом, они вскрикивали, смеялись, некоторые даже аплодировали. Огромный успех!
Улучшения
Надоело прятать датчик в кустах — вклеил его в пластиковую крысу, которую можно класть вдоль дорожки или под кустом. На задней стенке коробки установил разъем RJ11 для подключения «крысиного» датчика по 15-метровому телефонному шнуру — намного удобнее, чем два провода динамикового кабеля.


На той же панели добавил переключатель и индикаторные светодиоды. Переключатель ставит все функции на паузу без выключения питания. Главный выключатель — сетевой фильтр внутри коробки — неудобен для доступа, а здесь используется только сигнальное напряжение (5 В). Эта кнопка спасала, когда нужно было успокоить монстра для малышей или показать внутренности любопытным.
Пластиковые цепи мешали, когда требовалось залезть в коробку вечером. Решил: на концах цепей закрепил черные карабины с пружиной (тяжелоте на стяжки). Теперь цепи мгновенно отцепляются от кольцевых крючков, вкрученных в нижние углы коробки. Не хай-тек, но очень удобно.
Планы на будущее
- РУ-пульт для дистанционного запуска/паузы монстра
- Обратная связь от подъемника крышки, чтобы Arduino «знал», открыта крышка или закрыта. Иногда механизм останавливался в верхнем положении, и «начинка» была видна до следующего цикла пробуждения
- Подключить внешнюю стробоскопическую подсветку к Arduino для программного управления
Репозиторий на GitHub
Актуальный код и схемы подключения — здесь на GitHub.
Код
Monster In A Box Sketch (C/C++)
/* Wave Shield использует пины: 2, 3, 4, 5, 10, 11, 12, 13
Пины 13, 12, 11 — SPI для SD-карты (аппаратный SPI).
По умолчанию библиотека использует пин 10 (CS SD) и пины 2,3,4,5 (DAC).
Изменение требует правки библиотеки (ссылки на порты PORTD и т.д.).
Свободны пины 6, 7, 8, 9 и 6 аналоговых входов (цифровые 14-20).
https://learn.adafruit.com/adafruit-wave-shield-audio-shield-for-arduino/faq */
#include "MonsterSounds.h"
#define RESERVED_00 0 // Serial RX
#define RESERVED_01 1 // Serial TX
#define RESERVED_02 2 // Wave Shield
#define RESERVED_03 3 // Wave Shield
#define RESERVED_04 4 // Wave Shield
#define RESERVED_05 5 // Wave Shield
#define FOG_MACHINE 6 // Реле: дымогенератор
#define RED_LEDS 7 // Реле: красные светодиоды
#define LID_BOUNCER 8 // Реле: мотор крышки
#define RESERVED_09 9 // Резерв реле
#define RESERVED_10 10 // Wave Shield
#define RESERVED_11 11 // Wave Shield
#define RESERVED_12 12 // Wave Shield
#define RESERVED_13 13 // Wave Shield
#define PIR_SENSOR A0 // Вход PIR
#define MOTION_LED A1 // LED: движение обнаружено
#define PAUSED_LED A2 // LED: система на паузе
#define READY_LED A3 // LED: готов к пробуждению
#define PAUSE_BUTTON A4 // Кнопка паузы
#define DEBUG_BUTTON A5 // Кнопка отладки
// Настройки таймеров эффектов — в секундах (РЕДАКТИРУЕТЕ)
#define WAKE_DELAY 30 // Мин. пауза между пробуждениями (сек)
#define WAKE_DELAY_DEBUG 10 // Переопределение WAKE_DELAY в режиме отладки
#define SLEEP_SOUND_DELAY 1 // Пауза между звуками сна (сек)
#define WAKE_MIN 3 // Мин. длительность бодрствования (сек)
#define WAKE_MAX 5 // Макс. длительность бодрствования (сек)
#define RED_LIGHT_EXTRA_TIME 1 // Доп. время красной подсветки после остановки крышки
#define SMOKE_EXTRA_TIME 2 // Доп. время дыма после остановки крышки
// В миллисекундах (НЕ МЕНЯТЬ)
#define WAKE_DELAY_MILLIS WAKE_DELAY * 1000
#define WAKE_DELAY_DEBUG_MILLIS WAKE_DELAY_DEBUG * 1000
#define SLEEP_SOUND_DELAY_MILLIS SLEEP_SOUND_DELAY * 1000
#define WAKE_MIN_MILLIS WAKE_MIN * 1000
#define WAKE_MAX_MILLIS WAKE_MAX * 1000
#define RED_LIGHT_EXTRA_TIME_MILLIS RED_LIGHT_EXTRA_TIME * 1000
#define SMOKE_EXTRA_TIME_MILLIS SMOKE_EXTRA_TIME * 1000
MonsterSounds sounds;
static unsigned long timeSinceLastSnore = 0;
static unsigned long wakeAllowedTimer = 0;
static unsigned long lidBounceTimer = 0, lidBounceDuration = 0;
static unsigned long smokeTimer = 0, smokeDuration = 0;
static unsigned long redLightTimer = 0, redLightDuration = 0;
enum States {
STATE_INITIALIZE,
STATE_PAUSED,
STATE_SLEEPING,
STATE_READY_TO_WAKE,
STATE_AWAKE
};
States state = STATE_INITIALIZE;
void setup() {
Serial.begin(9600);
pinMode(LID_BOUNCER, OUTPUT);
pinMode(RED_LEDS, OUTPUT);
pinMode(FOG_MACHINE, OUTPUT);
pinMode(PAUSED_LED, OUTPUT);
pinMode(MOTION_LED, OUTPUT);
pinMode(READY_LED, OUTPUT);
stopAllEffects();
pinMode(PIR_SENSOR, INPUT);
pinMode(PAUSE_BUTTON, INPUT_PULLUP);
pinMode(DEBUG_BUTTON, INPUT_PULLUP);
sounds.initialize();
sounds.playSystemReady();
delay(1000);
Serial.println();
Serial.println("*** System Ready ***");
}
void loop() {
boolean pauseSwitchClosed = digitalRead(PAUSE_BUTTON) == LOW;
boolean motionDetected = digitalRead(PIR_SENSOR) == HIGH;
digitalWrite(MOTION_LED, digitalRead(PIR_SENSOR));
switch (state) {
case STATE_INITIALIZE:
if (pauseSwitchClosed) goToPause(); else goToSleep();
break;
case STATE_PAUSED:
if (!pauseSwitchClosed) { goToSleep(); digitalWrite(PAUSED_LED, LOW); }
else digitalWrite(PAUSED_LED, HIGH);
break;
case STATE_SLEEPING:
if (pauseSwitchClosed) goToPause();
else if (isAllowedToWake()) goToReadyToWake();
else processSleeping();
break;
case STATE_READY_TO_WAKE:
if (pauseSwitchClosed) { goToPause(); digitalWrite(READY_LED, LOW); }
else if (motionDetected) { goToAwake(); digitalWrite(READY_LED, LOW); }
else processSleeping();
break;
case STATE_AWAKE:
if (pauseSwitchClosed) goToPause();
else if (processAwakeAnimation()) goToSleep();
break;
default: Serial.println("UNKNOWN STATE"); break;
}
}
inline void goToPause() {
Serial.println("PAUSED");
state = STATE_PAUSED;
stopAllEffects();
sounds.stopAll();
}
inline void goToSleep() {
Serial.println("GOING TO SLEEP");
state = STATE_SLEEPING;
wakeAllowedTimer = millis();
}
inline void goToReadyToWake() {
Serial.println("READY TO WAKE");
state = STATE_READY_TO_WAKE;
}
inline void goToAwake() {
Serial.println("AWAKE");
state = STATE_AWAKE;
wakeMonster();
}
inline void processSleeping() {
if ((millis() - timeSinceLastSnore) > SLEEP_SOUND_DELAY_MILLIS) {
sounds.playSnore();
timeSinceLastSnore = millis();
}
}
inline boolean isAllowedToWake() {
boolean isDebug = digitalRead(DEBUG_BUTTON) == LOW;
unsigned long requiredDelay = isDebug ? WAKE_DELAY_DEBUG_MILLIS : WAKE_DELAY_MILLIS;
boolean isAllowed = (millis() - wakeAllowedTimer) > requiredDelay;
if (isAllowed) digitalWrite(READY_LED, HIGH);
return isAllowed;
}
void wakeMonster() {
int activityDuration = random(WAKE_MIN_MILLIS, WAKE_MAX_MILLIS);
Serial.print(" wake duration: "); Serial.print(activityDuration); Serial.println(" ms");
bounceLid(activityDuration);
flashRedLight(activityDuration + RED_LIGHT_EXTRA_TIME_MILLIS);
activateSmoke(activityDuration + SMOKE_EXTRA_TIME_MILLIS);
sounds.playRoar();
}
boolean processAwakeAnimation() {
printTimersToLog();
boolean done1 = false, done2 = false, done3 = false;
if (millis() - lidBounceTimer > lidBounceDuration) { bounceLid(0); done1 = true; }
if (millis() - redLightTimer > redLightDuration) { flashRedLight(0); done2 = true; }
if (millis() - smokeTimer > smokeDuration) { activateSmoke(0); done3 = true; }
return done1 && done2 && done3;
}
inline void bounceLid(unsigned long duration) {
if (duration <= 0) { energizeRelay(LID_BOUNCER); lidBounceDuration = 0; }
else { de_energizeRelay(LID_BOUNCER); lidBounceTimer = millis(); lidBounceDuration = duration; }
}
inline void flashRedLight(unsigned long duration) {
if (duration <= 0) { energizeRelay(RED_LEDS); redLightDuration = 0; }
else { de_energizeRelay(RED_LEDS); redLightTimer = millis(); redLightDuration = duration; }
}
inline void activateSmoke(unsigned long duration) {
if (duration <= 0) { energizeRelay(FOG_MACHINE); smokeDuration = 0; }
else { de_energizeRelay(FOG_MACHINE); smokeTimer = millis(); smokeDuration = duration; }
}
inline void stopAllEffects() {
bounceLid(0); flashRedLight(0); activateSmoke(0);
}
inline void printTimersToLog() {
static unsigned long timeofLastTimerLog = 0;
if (millis() - timeofLastTimerLog >= 1000) {
Serial.print(" lid: ");
Serial.print((millis()-lidBounceTimer) > lidBounceDuration ? 0 : (lidBounceDuration - (millis()-lidBounceTimer)));
Serial.print(" lights: ");
Serial.print((millis()-redLightTimer) > redLightDuration ? 0 : (redLightDuration - (millis()-redLightTimer)));
Serial.print(" smoke: ");
Serial.println((millis()-smokeTimer) > smokeDuration ? 0 : (smokeDuration - (millis()-smokeTimer)));
timeofLastTimerLog = millis();
}
}
inline void energizeRelay(int channel) { digitalWrite(channel, HIGH); } // NO — разомкнут
inline void de_energizeRelay(int channel) { digitalWrite(channel, LOW); } // NO — замкнут
MonsterSounds.h (C/C++)
/* ПИНЫ ARDUINО
Пины 13, 12, 11 — всегда используются SD-картой (аппаратный SPI).
Еще 5 пинов для DAC и SD, по умолчанию: 10 (CS SD) и 2,3,4,5 (DAC).
Изменение требует правки библиотеки (обращение к портам PORTD и т.д.).
Свободны: 6, 7, 8, 9 и 6 аналоговых входов (цифровые 14-20). */
#include
#include
#include
#include
static const char roar0[] = "ROAR0000.wav";
static const char roar1[] = "ROAR0001.wav";
static const char roar2[] = "ROAR0002.wav";
static const char * const roarSounds[] = {roar0, roar1, roar2};
static const char sleep0[] = "SNORE000.wav";
static const char sleep1[] = "SNORE001.wav";
static const char sleep2[] = "SNORE002.wav";
static const char * const sleepSounds[] = {sleep0, sleep1, sleep2};
int previousRoarSound = -1;
class MonsterSounds {
private:
SdReader card;
FatVolume vol;
FatReader root;
FatReader file;
WaveHC wave;
void playfile(char *name);
public:
void initialize();
void playSystemReady();
void playRoar();
void playSnore();
void stopAll();
};
MonsterSounds.ino (C/C++)
void MonsterSounds::initialize() {
Serial.println("Initializing Sounds...");
if(!card.init()) Serial.println(F("Card init. failed!"));
if(!vol.init(card)) Serial.println(F("No partition!"));
if(!root.openRoot(vol)) Serial.println(F("Couldn't open dir"));
Serial.println(F("Files found:"));
root.ls();
randomSeed(analogRead(0));
}
void MonsterSounds::playSystemReady() { this->playfile("WELCOME0.WAV"); }
void MonsterSounds::playRoar() {
int index = random(3);
while (index == previousRoarSound) index = random(3);
previousRoarSound = index;
this->playfile(roarSounds[index]);
}
void MonsterSounds::playSnore() {
if (!wave.isplaying) {
int index = random(3);
this->playfile(sleepSounds[index]);
}
}
void MonsterSounds::stopAll() { wave.stop(); }
void MonsterSounds::playfile(char *name) {
PgmPrint("Playing sound: "); Serial.println(name);
if (wave.isplaying) wave.stop();
if (!file.open(root, name)) { PgmPrintln("File not found"); return; }
if (!wave.create(file)) { PgmPrintln("Not a valid WAV"); return; }
wave.play();
}
Схемы

Производственный процесс
- ЭЭГ-аппараты: дизайн, производство и новые применения
- Революция соковых коробок: история, дизайн и экологический след
- Торговые автоматы: индустрия США на $36,6 млрд
- Экспертное руководство по современным обменным автоматам: дизайн, компоненты и будущие тренды
- Эволюция и производство швейных машин: от изобретений XIX века к современной автоматизации
- Колготки: полное руководство по элегантному женскому хоузири
- Сборка домашней машины для написания домашних заданий (плоттер-писатель)
- Giftduino: идеальный набор Arduino для подарков — проект для хоббистов и педагогов
- Создание ультразвукового левитатора на Arduino: пошаговое руководство
- Компактная метеостанция: Arduino Nano + ESP8266, BMP280, OLED / Nokia 5110 LCD