Интерактивная 3D-инсталляция по мотивам «Звёздных войн»

в 14:57, , рубрики: arduino, microsoft, Windows 10, Блог компании Microsoft, звёздные войны, Интенет вещей, Разработка для интернета вещей, разработка под windows

Где-то в далёкой-далёкой галактике (хотя и в нашей тоже) неофициально день «Звёздных войн» 4 мая отмечается. Не случайно он выбран, из фильма цитата тому подтверждение «May the Force be with you» («Да пребудет с тобой Сила»), как намёк звучит она для поклонников «May the fourth be with you». В посте этом поздравить хотим, на тёмную сторону перешедших. Рассказать хотим про 3D-инсталяции создание. Интерактивная будет она и тематическая.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 1

Введение

Предыстория

На этот проект меня вдохновила моя жена, когда мы вместе ходили по магазинам перед Рождеством. Как раз вышел очередной эпизод «Звёздных войн», и полки в магазинах ломились от игрушек из этого фильма. Жена окликнула меня и спросила, не закончился ли ещё конкурс Hackster на тему «Звёздных войн». Она сказала, что увидела несколько интересных вещей, из которых можно было бы собрать что-нибудь классное.

В итоге мы купили несколько игрушек и придумали забавную интерактивную инсталляцию на стену.

За основу мы взяли небольшую панель, разместив на ней звуковую карту и, конечно, лампочки, чтобы было много света. Также мы установили датчик движения и ультразвуковой датчик расстояния для обнаружения людей поблизости. Датчик освещенности тоже присутствует.

Наша панель — интерактивная, она реагирует на человека, стоящего перед ней или проходящего мимо. Глаза Дарта Вейдера загораются красным, активация и деактивация светового меча сопровождаются звуками, которые всем знакомы с детства. Звезда Смерти также используется как ночной светильник. Она включается в темноте, когда срабатывает датчик движения. Наконец, иногда Дарт Вейдер разговаривает.

Происходит это спонтанно, но не постоянно. Основная идея — это должно случаться, «когда меньше всего ждёшь». Таким образом Дарт Вейдер уже напугал нескольких членов нашей семьи.

Для управления инсталляцией также можно использовать универсальное приложение Windows 10 и облачный веб-интерфейс API Particle.io.

Вот так это выглядит сейчас (панель целиком (122 х 91 см)).

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 2

Конструктивное исполнение

Инсталляция состоит из четырёх модулей, которые взаимодействуют с помощью шины I2C. Главный контроллер реализован на базе платформы Particle Photon, он отвечает за опрос датчиков, управление другими платами и коммуникации через облако. Есть ещё три вспомогательных контроллера на базе Adafruit Trinket Pro 5V, они управляют световым мечом, Звездой Смерти, глазами Дарта Вейдера и звуковыми эффектами.

Благодаря белой светодиодной подсветке Звезда Смерти становится отличным ночным светильником. В глазах Дарта Вейдера установлены светодиодные модули Adafruit NeoPixel Jewels (по семь светодиодов NeoPixels в каждом). Световой меч — это 100 светодиодов RGBW NeoPixels (кусок ленты со 144 светодиодами на метр). В шлеме Дарта Вейдера (в прорези для рта) и в Звезде Смерти установлено по динамику.

Процесс сборки

Панель

Фанера обычно продается в листах размером 122 х 244 см, я обрезал её до 122 х 91 см. Чтобы отделка смотрелась чисто и аккуратно, были выбраны листы более высокого качества. Тем не менее любую фанеру необходимо предварительно слегка обработать по краям и на лицевой стороне. Для этого я использовал наждачную бумагу зернистостью 220. После чего протёр обработанные поверхности смоченной тряпкой и дал им полностью высохнуть.

Свою панель мы покрыли краской Glidden Blue Grey Sky (30BB 32/067).

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 3

Рамка

Рамка сделана из сосновой рейки размером 2,5 х 5 см (1,9 к 4,45 см). Пазы получились 122 и 91 см для длинной и короткой сторон соответственно. По краям реек добавлены срезы под углом 45°, чтобы получить рамку для полноценного подрамника.

Подготовив рейки и плотно посадив их на место с каждой стороны доски, необходимо окончательно закрепить их с помощью клея для дерева и угловых зажимов. Склеиваем края таким образом, чтобы смежные стороны находились под прямым углом.

Чтобы усилить рамку, я использовал 8 проволочных гвоздей # 18 x 3/4" (по 2 шт. на каждый угол). Нужно просверлить направляющее отверстие для каждого гвоздя, иначе мы рискуем расколоть древесину. Кончик гвоздя необходимо утопить с помощью небольшого пробойника.
После того как клей высохнет, заполняем углы деревянной шпатлевкой, чтобы избавиться от зазоров. Также необходимо зашпаклевать места, где были вбиты гвозди.

Перед покраской пройдёмся по всей поверхности рамки наждачкой, а затем протрём обработанные поверхности смоченной тряпкой и полностью высушим.

Рамку мы покрыли краской Glidden Grey Metal (00NN 10/000).

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 4

Крепление рамки

Рамка крепится к панели с помощью небольших латунных шарниров, которые продаются в любом хозяйственном магазине. Незначительное перекрытие спереди можно устранить с помощью большой палочки для перемешивания краски толщиной около 4–5 мм. Далее переворачиваем панель лицевой стороной вниз и подкладываем палочку, чтобы добиться желаемой высоты. Закрепим шарниры на панели с помощью прилагаемых шурупов. Направляющее отверстие необходимо просверлить для каждого шурупа, иначе рискуем расколоть древесину. Важно соблюдать осторожность и каждый раз оставлять зазор с лицевой стороны панели.

У меня всего 12 шарниров, соединяющих рамку с доской (по 3 вдоль каждой стороны). Два боковых шарнира расположены на расстоянии около 13 см от внутреннего угла, а третий — по центру соответствующей стороны. Ниже лицевая сторона панели, на ней виден зазор между панелью и рамкой.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 5

Размещение наклеек и моделей

Дарт Вейдер, световой меч и Звезда Смерти (это все светильники) продавались сразу с наклейками, создающими особый визуальный эффект: они как будто пробили основание насквозь. Мы сделали фон серым специально, чтобы подчеркнуть это эффект.

Далее выбираем оптимальное размещение этих трёх элементов на панели. Здесь вы можете положиться на свой художественный вкус. Определившись с расположением, закрепляем наклейки в соответствии с прилагаемой инструкцией. Обратите внимание, что в инструкции сказано, что окрашенной стене необходимо дать до двух недель, чтобы хорошенько высохнуть. Мы действительно ждали две недели, прежде чем наклеивать наклейки. Вот так выглядят все три наклейки.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 6

Дополнительные наклейки мы купили на сайте Amazon, причём из набора пригодилось далеко не всё. Думаю, что каждый сам для себя решит, какое количество и какой тип наклеек захочет сделать.

Я усилил 3D-эффект с помощью моделей штурмовиков (звездолеты Hot Wheels TIE Fighters), прикрепив их к панели с помощью клеевого пистолета. Этот шаг можно пропустить.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 7

В комплекте настенных светильников есть все необходимые шурупы для закрепления светильников на панели. Как обычно, рекомендую просверлить направляющие отверстия.

Шлем Дарта Вейдера

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

Сначала снимаем заднюю крышку у головы Дарта Вейдера. Далее откручиваем винты, удерживающие светодиоды и выключатель, отрезаем провода, ведущие к батарейному отсеку. Эти компоненты, возможно, пригодятся для другого проекта. :)

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 8

Потом, для подготовки светодиодных модулей NeoPixel Jewels, подкючаем провода 22 AWG к каждой из трех клемм. Рекомендую использовать красный провод для 5 В, черный для заземления и зелёный для интерфейса. Отрезаем куски длиной около 90 см и припаиваем сначала красный провод к контакту 5 В первого модуля Jewel, затем чёрный провод. Наконец, припиаваем зелёный провод к контакту Data In.

Следующий модуль Jewel будет подключаться к первому. Снова отрезаем провода трёх цветов, на этот раз по 25 см. Припаиваем красный провод к контактам 5 В на первом и втором модуле Jewel. Красный провод припаиваем к контактам GND на первом и втором модуле Jewel. Наконец, зелёный провод — к контакту Data Out на первом модуле и контакту Data In на втором модуле.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 9

Чтобы прикрепить модули к пластине шлема, используем трубочки для напитков. Они нужны для того, чтобы свет был виден через тёмные глазницы. Сначала нарезаем куски по 5 см (в этом примере использованы более толстые трубочки, но обычные тоже подойдут). Далее размещаем модули ближе к верхней части шлема. Для их крепления к задней части модулей NeoPixel и к пластине шлема используем клеевой пистолет. Одной трубочки вполне достаточно для надёжной фиксации, но, на всякий случай, я решил использовать вторую.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 10

Наконец, нам нужно установить динамик в шлем. Приелкиваем его к пластине на уровне рта. В шлеме есть небольшой кусок металла, который приглушал бы звук, поэтому его нужно убрать. Для этого достаточно открутить несколько винтов. Далее, с помощью клеевого пистолета закрепляем динамик на пластине шлема.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 11

Чтобы вывести провода, сверлим в центре пластины отверстие диаметром 6 мм (1/4"). Не забудьте, вам нужно будет прикрепить пластину к панели без передней части шлема. Поэтому стоит просверлить новые отверстия в панели через отверстия в пластине. Таким образом, провода от шлема пройдут через отверстие в пластине и отверстие в панели, чтобы их можно было собрать в один пучок сзади.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 12

Подключив светодиоды и динамик, пропускаем провода через отверстие и ставим пластину шлема на место. Не забудьте закрутить винты. :)

Световой меч

Разобрать световой меч немного сложнее. В итоге у вас должно получиться четыре отдельных элемента.

Сначала снимаем красную пластиковую трубку, имитирующую луч. Она просто отвинчивается и вытаскивается. Внутри неё вы найдёте вторую часть — длинную прозрачную пластиковую трубку. Это свернутый кусок пластика, на котором мы потом закрепим полосу светодиодов NeoPixel. Отложите этот элемент в сторону.

Следующий элемент, который нужно снять, — это пластина-основание. Отвинчиваем три винта. Должно получиться вот так.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 13

Далее снимаем боковую часть рукояти светового меча и удаляем всю электронную начинку.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 14

После этого обрезаем ленту NeoPixel, оставив на ней 100 светодиодов. Если вы никогда этого не делали, вам будет полезно руководство Adafruit Uberguide.

Отрезаем три куска провода 22 AWG примерно по 60 см. Берём красный, чёрный и зелёный провода (кстати, лично я часто использую зелёный, белый или жёлтый провода в качестве интерфейсного провода для модулей NeoPixel), припаиваем красный провод к контакту 5 В, чёрный — к контакту GND, а зелёный — к контакту Data In.

Дальше очень аккуратно протягиваем ленту NeoPixel в прозрачную пластиковую трубку, стараясь сильно её не гнуть.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 15

После чего пропускаем провода через нижнюю пластину, как показано на фото ниже.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 16

Протянув провода, закрепляем боковую пластину с помощью пяти винтов. Затем прикрепляем заднюю панель и протягиваем провода через отверстие, в котором находился переключатель. Там, где провода будут проходить через основную панель, необходимо просверлить отверстие диаметром 6 мм (1/4").

Макетирование

Составляем прототип каждой схемы, используя изображения в спойлерах.

Главные контроллер и контроллер светового меча

Схема (главный контроллер)
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 17

Схема (контроллер светового меча)
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 18

Как получилось у меня
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 19

Контроллер звука

Схема
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 20

Как получилось у меня
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 21

Контроллер Звезды Смерти/глаз Дарта Вейдера

Схема
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 22

Как получилось у меня
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 23

Контроллер Звезды Смерти крупным планом
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 24

Контроллер глаз Дарта Вейдера крупным планом
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 25

Дополнительно

Так выглядят остальные части.

Сенсор движений, ультразвуковой датчик и кольцо NeoPixel (сверху вниз)
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 26

Один модуль NeoPixel для тестирования глаз Дарта Вейдера
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 27

Миниатюрная монтажная плата для разводки 12 В на все платы
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 28

Обратите внимание, что вы должны уметь собирать макеты плат по схемам.
Примечание. Я использовал:

  • дополнительные высокие однорядные монтажные платы с выводами в своём макете, а затем стандартные монтажные платы с выводами при пайке на печатных платах;
  • кольцо NeoPixel вместо ленты NeoPixel (для светового меча);
  • один модуль NeoPixel вместо двух (для глаз);
  • миниатюрную монтажную плату для разводки 12 В на все остальные платы.

В итоге, у нас получается вот такая cхема.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 29

Для реализации этой части проекта потребуется значительное время (хотя, конечно не 2 недели :) ), кроме того, вы должны обратить пристальное внимание на схемы и размещение компонентов на макетах. Кстати, я использовал макетные платы Adafruit Perma-Proto.

Если всё прошло успешно, у вас получится такой результат.

Печатные платы

После сборки и тестирования макетов, схемы можно переносить на печатные платы (в моём случае это Perma-Proto).

Поочередно переносим схемы, припаивая детали. Лучше, чтобы у вас были запасные детали, чтобы скопировать схему, а не переносить её. Это поможет избежать ошибок.

Примечание:

  • Я использовал стандартные однорядные монтажные платы с выводами для силовых соединений и соединений I2C. Для разводки питания отделил по два контакта. Для I2C отделил по три контакта.
  • Взял 3-контактные клеммные колодки для двух соединений NeoPixel. Это детали размером 3,5 мм. Они должны быть припаяны к плате по диагонали.
  • Также, использовал 28-контактные разъёмы IC Sockets размером 1,5 см (0,6") для Trinket Pro.
  • Для размещения модуля Photon я взял 0,25-сантиметровые (0,1") 36-контактные монтажные разъёмы с выводами. Отрезал два комплекта по 12 контактов для модуля Photon.
  • Для подключения батареи монетного типа к контактам GND и VBAT модуля Photon я взял два контакта от монтажного разъёма с выводами.
  • Как минимум для двух монтажных схем потребуется двойной комплект монтажных плат для перемычек 12 В, либо вы можете использовать 3 набора для одной из схем. Питание будет подаваться с тыльной стороны панели, и его необходимо развести по всем платам.
  • Шину I2C необходимо подключить к каждой монтажной плате. Для этого я припаял два комплекта монтажных плат как минимум для двух схем.

В спойлерах ниже вы найдёте фото получившихся схем.

Общий вид

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 30

Вид сбоку крупным планом

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 31

Контроллер Звезды Смерти/глаз Дарта Вейдера

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 32

Основной контроллер

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 33

Контроллер светового меча

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 34

Если всё прошло успешно, у вас получится такой результат.

Звезда Смерти

Я выбрал Звезду Смерти, чтобы разместить в ней все электронные компоненты. Она идеально вписывается в концепцию и предоставляет достаточно свободного места.

Для начала необходимо её разобрать и удалить всю базовую электронику. Сначала откручиваем винты с задней стороны, потом винты, крепящие электронные компоненты, после чего отрезаем провода от батарейного отсека.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 35

Собранные ранее схемы закрепляем на панели Звезды Смерти. Для этого используем распорки (или штифты) M3, винты M3, гайки M3 и по две шайбы M3 для каждой распорки. Всего распорок будет восемь (по две на каждую печатную плату).

Начинаем с крепления латунных штифтов к каждой плате с помощью гайки M3 (для этого вам, возможно, потребуется снять модули Trinket и Photon).

Три платы меньшего размера можно разместить с использованием уже имеющихся пазов на пластине. Для большой платы нужно просверлить отверстия. Далее, с внешней стороны пластины вкручиваем винт и шайбу, размещаем большую плату и карандашом помечаем точки, где будут закреплены штифты. Убираем плату и просверливаем отверстия для винтов. Порядок плат не имеет значения, поэтому каждый может подобрать оптимальную компоновку для себя и затем закрепить платы.

По аналогии со шлемом Дарта Вейдера нам понадобится центровочное отверстие, которое нужно сопоставить с отверстием в панели. Через него провода пройдут к задней стороне панели, куда будет подключен световой меч и шлем Дарта Вейдера.

У меня получилась вот такая компоновка плат. Обратите внимание на отверстие, просверленное вверху слева.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 36

Далее подключаем двухпроводной разъём светодиодной подсветки и закрепляем его с помощью термоусадочной трубки. На фото ниже вы можете увидеть подготовку светодиода.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 37

Второй динамик будет установлен внутри Звезды Смерти. Вышло так, что монтажные отверстия на динамике совпали с винтами внутри Звезды Смерти, как показано ниже. Поэтому подключаем динамик к клеммам звуковой карты. После чего крепим светодиодную подсветку с помощью клеевого пистолета над зелёным пластиковым куполом внутри Звезды Смерти.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 38

По аналогии с процессом крепления светодиода, делаем выводы фоторезистора и проводим провода через отверстия на правой стороне купола (в сторону от светового меча). С внутренней стороны подключаем двухпроводной разъём к фоторезистору и помещаем его в термоусадочную трубку, как показано ниже.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 39

В нижней части купола сверлим отверстия диаметром 1,3 см (1/2") для датчика движения и ультразвукового датчика расстояния. Подключаем четырёхпроводной разъём к каждому датчику и протягиваем датчики в отверстия, после чего закрепляем их с внешней стороны купола с помощью клеевого пистолета.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 40

Далее нарезаем два комплекта из трёх проводов 22 AWG длиной примерно 60 см для соединений NeoPixel и подключаем их к клеммам на плате. Красный провод — для 5В, чёрный — для GND и зелёный (или жёлтый) — для интерфейса. Цветовая кодировка в дальнейшем упростит подключение компонентов на задней панели платы. С помощью изоленты и маркера я пометил эти провода, чтобы легко идентифицировать их позже (я написал «световой меч» и «Дарт Вейдер»).

Далее отрезаем ещё два провода для динамика: красный и чёрный (лучше взять 26 AWG, но 22 AWG тоже подойдет), и подключаем их к клеммам. Двухпроводную розетку подключаем к разъёму 12 В на плате Photon.

На этом этапе провода можно зачистить и закрепить с помощью хомутиков и изоленты. Сначала заводим шесть проводов от NeoPixel, провода от двух динамиков и провод питания через отверстие в пластине. С помощью изоленты их так, чтобы нельзя было вытащить с обратной стороны платы, после чего объединяем в пучки и стягиваем хомутиками.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 41

Теперь собираем Звезду Смерти и пропускаем все провода через отверстие в панели.

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 42

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

Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 43

Программное обеспечение и приложения

Программное обеспечение для этого проекта можно разделить на три категории.

Во-первых, это встроенное ПО для Photon. Это программное обеспечение управляет другими платами и поддерживает с ними связь через шину I2C. Во-вторых, это код Arduino, который загружается в каждый из трёх модулей Trinket Pro. В-третьих, это программное обеспечение Windows 10 UWP, которое используется на устройствах Windows 10 для удалённого управления нашей интерактивной инсталляцией.

Код в спойлере ниже — это встроенное ПО для Photon. Файлы классов, ссылки на которые присутствуют в коде, можно найти в репозитории GitHub для этого проекта.

Немного кода :)

#include "Controller.h"
#include "HC-SR04.h"
#include "SensorSmoothing.h"

// ***
// *** Два контакта, к которым подключается HC-SR04.
// ***
#define TRIGGER_PIN D2
#define ECHO_PIN D3

// ***
// *** Два контакта для подключения датчика движения
// ***
#define MOTION_OUT D4
#define MOTION_ENABLED D5

// ***
// *** Контакт фоторезистора
// ***
#define PHOTORESISTOR_PIN A0

// ***
// *** Количество измерений для усреднения
// ***
#define NUMBER_OF_SAMPLES 5

// ***
// *** Громкость по умолчанию
// ***
#define DEFALT_VOLUME 15

// ***
// *** Главный контроллер, который отправляет
// *** команды всем устройствам.
// ***
retained int _volume = DEFALT_VOLUME;

// ***
// *** Определяем контроллер.
// ***
Controller _controller = Controller();

// ***
// *** Создаем объект HcSr04 для
// *** HC-SR04 (ультразвуковой датчик расстояния).
// ***
HcSr04 _ranging = HcSr04();

// ***
// *** Эта переменная сигнализирует об
// *** обнаружении движения.
// ***
bool _motion = false;

// ***
// *** Эта переменная сигнализирует об
// *** обнаружении движения.
// ***
double _distance = 0.0;
// ***
// *** Аналоговая величина на контакте фоторезистора.
// ***
int _light = 0;

// ***
// *** В этом объекте будем хранить текущее среднее
// *** для выборки размером NUMBER_OF_SAMPLES.
// *** Это необходимо для предотвращения ложных или 
// *** слишком частых срабатываний датчика движения.
// ***
SensorSmoothing<double> _distanceSample = SensorSmoothing<double>(NUMBER_OF_SAMPLES);

// ***
// *** Таймер для опроса датчика
// ***
Timer _sensorTimer(1000, onSensorTimer);

// ***
// *** Время возникновения последнего события
// ***
int _lastEventTime = 0;

// ***
// *** Порог освещенности
// ***
#define DEFAULT_LIGHT_THRESHOLD 1000
retained int _lightThreshold = DEFAULT_LIGHT_THRESHOLD;
bool _lightBelowThreshold = false;
bool _deathStarIsOn = false;

// ***
// *** Параметры для датчика движения
// ***
retained int _minimumEventTime = 20;    // секунды
retained int _minimumDistance = 127;    // сантиметры

void setup()
{
  // ***
  // *** Вывод сообщения о ходе настройки
  // ***
  Particle.publish("Setup", "Started");
  // ***
  // *** Активация ОЗУ для резервного копирования
  // ***
  STARTUP(System.enableFeature(FEATURE_RETAINED_MEMORY));
  // ***
  // *** Инициализация модуля HC-SR04 (ультразвуковой датчик расстояния)
  // ***
  _ranging.begin(TRIGGER_PIN, ECHO_PIN);
  // ***
  // *** Настройка шины I2C
  // ***
  Wire.begin();
  // ***
  // *** Инициализация контроллера
  // ***
  _controller.begin(_volume);
  // ***
  // *** Включение индикатора D7 при активации
  // *** светового меча
  // ***
  pinMode(D7, OUTPUT);
  digitalWrite(D7, LOW);
  // ***
  // *** Набор общедоступных переменных
  // ***
  Particle.variable("motion", _motion);
  Particle.variable("distance", _distance);
  Particle.variable("light", _light);
  // ***
  // *** Публикуем функции.
  // ***
  Particle.function("controller", controllerWebApi);
  Particle.function("parameter", parameterWebApi);
  Particle.function("test", testWebApi);
  // ***
  // *** Устанавливаем в качестве времени появления последнего события
  // *** текущее время.
  // ***
  _lastEventTime = Time.now();
  // ***
  // *** Запускаем таймер.
  // ***
  _sensorTimer.start();
  // ***
  // *** Даем другим платам время для запуска,
  // *** на случай если мы только что включили питание.
  // ***
  delay(3000);
  // ***
  // *** Сброс
  // ***
  _controller.darthVaderOff();
  _controller.lightSaberOff();
  _controller.deathStarOff();
  // ***
  // *** Вывод сообщения о ходе настройки
  // ***
  Particle.publish("Setup", "Completed");
}

void loop()
{
  // ***
  // *** Временное сохранение значения _lightBelowThreshold
  // ***
  bool previousLightBelowThreshold = _lightBelowThreshold;
  // ***
  // *** Проверка наличия движения
  // ***
  if (_motion)
  {
    // ***
    // *** Проверка уровня освещенности и принятие решения
    // *** о необходимости включения
    // *** подсветки.
    // ***
    if (_light < _lightThreshold)
    {
      if (!previousLightBelowThreshold)
      {
        Particle.publish("Light", "Below Threshold");
      }
      _lightBelowThreshold = true;
      // ***
      // *** Включить светодиод Звезды смерти.
      // ***
      _deathStarIsOn = true;
      _controller.deathStarOn();
    }
    else
    {
      if (previousLightBelowThreshold)
      {
        Particle.publish("Light", "Above Threshold");
      }
      _lightBelowThreshold = false;
    }
    // ***
    // *** Проверим, что объект находится на расстоянии менее метра.
    // ***
    if (_distance < _minimumDistance)
    {
      // ***
      // *** Убедимся, что с момента последнего обновления
      // *** прошло больше минимального
      // *** промежутка времени.
      // ***
      if ((Time.now() - _lastEventTime) > _minimumEventTime)
      {
        Particle.publish("Object", "Detected");
        _lastEventTime = Time.now();
        _controller.randomEvent();
      }
    }
  }
  else
  {
    // ***
    // *** Движения не обнаружено.
    // ***
    if (_deathStarIsOn)
    {
      // ***
      // *** Если уровень освещенности больше пороговой величины, выключить
      // *** светодиод Звезды смерти.
      // ***
      _deathStarIsOn = false;
      _controller.deathStarOff();
    }
  }
  // ***
  // *** Задержка
  // ***
  delay(150);
}

void onSensorTimer()
{
  // ***
  // *** Опрос датчика движения
  // ***
  bool previousMotion = _motion;
  // ***
  // *** Получить текущий статус датчика движения.
  // ***
  _motion = (digitalRead(MOTION_OUT) == HIGH);
  // ***
  // *** Публикация события происходит только при изменении состояния
  // *** датчика движения.
  // ***
  if (!previousMotion && _motion)
  {
    Particle.publish("Motion", "Detected");
    if (_lightBelowThreshold)
    {
      Particle.publish("Light Level", "Below Threshold: " + String(_light) + "/" + String(_lightThreshold));
    }
    else
    {
      Particle.publish("Light Level", "Above Threshold: " + String(_light) + "/" + String(_lightThreshold));
    }
    // ***
    // *** Публикация события с информацией о расстоянии до
    // *** объекта, находящегося напротив панели.
    // ***
    Particle.publish("Distance", String(String(_distance / 2.54).toInt()) + " in / " + String(String(_minimumDistance / 2.54).toInt()) + " in");
    // ***
    // *** Публикация события с информацией о периоде времени 
    // *** до инициации следующего события.
    // ***
    Particle.publish("Time", "Last Event: " + String(Time.now() - _lastEventTime) + " second(s) ago");
  }
  else if (previousMotion && !_motion)
  {
    Particle.publish("Motion", "Reset");
  }
  // ***
  // *** Получение данных о расстоянии от ультразвукового
  // *** датчика
  // ***
  _distance = getRangeEx();
  // ***
  // *** Опрос фоторезистора
  // ***
  _light = analogRead(PHOTORESISTOR_PIN);
}

// ***
// *** Веб-API
// ***
int testWebApi(String command)
{
  // ***
  // *** Тестирование
  // ***
  Particle.publish("Test", "Running");
  _controller.setVolume(10);
  delay(500);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(0);
  delay(18000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(1);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(2);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(3);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(4);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(5);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(6);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(7);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.darthVaderOn();
  delay(1000);
  _controller.darthVaderVoice(8);
  delay(5000);
  _controller.darthVaderOff();
  delay(3000);
  _controller.lightSaberOn(7000);
  delay(2000);
  _controller.deathStarOn();
  delay(5000);
  _controller.deathStarOff();
  Particle.publish("Test", "Completed");
  return 1;
}

// ***
// *** Веб-API
// ***
int parameterWebApi(String command)
{
  int returnValue = 0;
  // ***
  // *** Если команда содержит аналогичный
  // *** признак, будем считать, что система пытается
  // *** задать значение параметра.
  // ***
  int equalIndex = command.indexOf("=");
  // ***
  // *** Проверка значения команды для типа
  // *** вызова параметра.
  // ***
  if (equalIndex > 0)
  {
    // ***
    // *** Эта команда устанавливает значение переменной.
    // ***
    String parameterName = command.substring(0, equalIndex);
    String value = command.substring(equalIndex + 1, command.length());
    if (parameterName == "volume")
    {
      _volume = value.toInt();
      _controller.setVolume(_volume);
    }
    else if (parameterName == "minimumDistance")
    {
      _minimumDistance = value.toInt();
    }
    else if (parameterName == "lightThreshold")
    {
      _lightThreshold = value.toInt();
    }
    else if (parameterName == "minimumEventTime")
    {
      _minimumEventTime = value.toInt();
    }
    // ***
    // *** Опубликовать параметр.
    // ***
    Particle.publish("Set Parameter", parameterName + " = " + value);
    // ***
    // *** Вернуть значение.
    // ***
    returnValue = value.toInt();
  }
  else if (equalIndex == -1)
  {
    // ***
    // *** Если команда имеет аналогичное имя,
    // *** вернуть значение.
    // ***
    if (command == "volume")
    {
      returnValue = _volume;
    }
    else if (command == "minimumDistance")
    {
      returnValue = _minimumDistance;
    }
    else if (command == "lightThreshold")
    {
      returnValue = _lightThreshold;
    }
    else if (command == "minimumEventTime")
    {
      returnValue = _minimumEventTime;
    }
    // ***
    // *** Опубликовать параметр.
    // ***
    Particle.publish("Get Parameter", command);
  }
  else if (command == "reset")
  {
    // ***
    // *** Вернуть значения параметров по умолчанию.
    // ***
    _volume = DEFALT_VOLUME;
    _controller.setVolume(_volume);
    _minimumDistance = 50 * 2.54;
    _lightThreshold = 1000;
    _minimumEventTime = 20;
    _lightThreshold = DEFAULT_LIGHT_THRESHOLD;
    // ***
    // *** Опубликовать параметр.
    // ***
    Particle.publish("Reset Parameters", "");
    returnValue = 1;
  }
  return returnValue;
}

// ***
// *** Веб-API
// ***
int controllerWebApi(String command)
{
  int returnValue = 0;
  // ***
  // *** Опубликовать параметр.
  // ***
  Particle.publish("Controller", command);
  // ***
  // *** Передать команду в контроллер.
  // ***
  returnValue = _controller.executeCommand(command);
  return returnValue;
}

double getRangeEx()
{
  double returnValue = 0;
  // ***
  // *** Получить расстояние в сантиметрах.
  // ***
  Range range;
  if (_ranging.getRange(&range))
  {
    // ***
    // *** Добавить замер.
    // ***
    _distanceSample.addSample(range.distance);
    // ***
    // *** Вернуть среднее расстояние.
    // ***
    returnValue = _distanceSample.average();
  }
  else
  {
    // ***
    // *** Проверить результат.
    // ***
    if (range.result == timeFailed)
    {
      switch (range.pingTime.result)
      {
      case alreadyActive:
        Particle.publish("Result", "The sensor is busy.");
        break;
      case timeout:
        Particle.publish("Result", "The sensor did not respond.");
        break;
      }
    }
    else if (range.result == underRange)
    {
      switch (range.result)
      {
      case underRange:
        Particle.publish("Result", "The object is too close.");
        break;
      case overRange:
        Particle.publish("Result", "No object is in range.");
        break;
      }
    }
    else
    {
      Particle.publish("Result", "Unknown Error.");
    }
  }
  return returnValue;
}

Ниже скрины того, что у нас получилось.

Главный экран для управления эффектами и меню.
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 44

Настройка облачного API для Photon и настройка параметров.
Интерактивная 3D-инсталляция по мотивам «Звёздных войн» - 45

Код Windows 10 обращается к облачному интерфейсу API Particle.io с помощью HttpClient для выполнения определенных функций, используя Particle.function(), как показано далее.

protected async virtual Task<int> Execute(string functionName, string command)
{
	int returnValue = 0;
	HttpClient httpClient = new HttpClient();
	HttpRequestMessage request = new HttpRequestMessage();
	var values = new List<KeyValuePair<string, string>>
	{
		new KeyValuePair<string, string>("args", command)
	};
	FormUrlEncodedContent content = new FormUrlEncodedContent(values);
	request.Headers.Authorization = this.Identity.Authentication;
	request.Method = this.Method;
	request.RequestUri = new Uri(this.Identity.BaseUri, string.Format(this.Resource, functionName));
	request.Content = content;
	HttpResponseMessage response = await httpClient.SendAsync(request);
	if (response.IsSuccessStatusCode)
	{
		string json = await response.Content.ReadAsStringAsync();
		dynamic obj = JsonConvert.DeserializeObject(json);
		string value = obj.return_value;
		returnValue = Convert.ToInt32(value);
	}
	else
	{
		throw new HttpRequestException(response.ReasonPhrase);
	}
	return returnValue;
}

В приведённом ниже коде показано, как получить значение переменной, определенной с использованием Particle.variable().

protected async override Task<T> Execute(params string[] args)
{
	T returnValue = default(T);
	HttpClient httpClient = new HttpClient();
	HttpRequestMessage request = new HttpRequestMessage();
	request.Headers.Authorization = this.Identity.Authentication;
	request.Method = HttpMethod.Get;
	request.RequestUri = new Uri(this.Identity.BaseUri, string.Format(this.Resource, args));
	HttpResponseMessage response = await httpClient.SendAsync(request);
	if (response.IsSuccessStatusCode)
	{
		string json = await response.Content.ReadAsStringAsync();
		dynamic obj = JsonConvert.DeserializeObject(json);
		string value = obj.result;
		returnValue = (T)Convert.ChangeType(value, typeof(T));
	}
	else
	{
		await this.OnFailed(response);
	}
	return returnValue;
}

Всё программное обеспечение доступно в репозитории GitHub для этого проекта. Список всех компонентов для проекта и ссылки на их покупку, можно найти на сайте.

Как это работает

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

Автор: Schvepsss

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js