Zaprezentowany projekt składa się z kontrolowanych przez Internet lampek choinkowych, które dodatkowo odtwarzają muzykę. Kolendy można wybrać on-line i dorzucić do listy odtwarzania. Muzyka nadawana jest przez moduł kieszonkowego nadajnika FM, który pozwala odtworzyć ją na dowolnym radioodbiorniku w obrębie domu.
Urządzenie Xmas-box posiada 8 kanałów (gniazdek zasilających 230 V), które mogą sterować oświetleniem w różnym trybie. Potrafi emulować wskaźnik wysterowania, zapalać światła w różnych sekwencjach lub losowo. Tryby są częściowo synchronizowane z muzyką – podczas odtwarzania kolęd, co 10 sekund zmieniany jest tryb zapalania świateł, aby animacje nie były zbyt monotonne.
Autor w swoim systemie umieścił imponującą ilość oświetlenia. Łącznie na wszystkich gałęziach znajduje się 3300 miniaturowych światełek, trzy reflektory, linijka LED, cztery komplety ledowego oświetlenia drzew (po 40 diod każda) i jeden podświetlany renifer.
Potrzebne elementy
Do budowy urządzenia potrzebne będą następujące moduły:
- Arduino Duemilanove,
- Wave Shield v1.1 dla Arduino od Adafruit,
- moduł ioBridge IO-204 Monitor & Control,
- moduł ioBridge Smart Serial,
- karta SD o pojemności do 1 GB,
- cyfrowy transmiter FM PPA Digital,
- mostek bezprzewodowy Linksys WET11 lub podobny.
Ponadto potrzebne będą:
- rozdzielacz zasilania 230 V, z co najmniej ośmioma wyjściami,
- opornik 10 kΩ,
- tranzystor 2N2222 lub podobny,
- osiem przekaźników półprzewodnikowych o odpowiedniej mocy, dobranej do obciążenia,
- obudowa – autor użył w tym celu plastikową skrzynkę na narzędzia (fotografia 1),
- kable, kostki do wykonywania połączeń itp.
Budowa systemu
Autor konstrukcji badał kilka wariantów realizacji tego zadania, ale finalnie zdecydował się na zastosowanie modułu Arduino z nakładką WaveShield oraz modułem ioBridge, który zapewnia komunikację sieciową. Za sterowanie światłami odpowiadają przekaźniki półprzewodnikowe (SSR), kluczujące napięcie sieciowe. System jest zamknięty w małej plastikowej obudowie – skrzynce na narzędzia. Został zamontowany na poddaszu – zastosowana obudowa nie jest całkowicie odporna na warunki atmosferyczne. Skrzynka narzędziowa ma trzy poziomy. Na dole znajdują się wszystkie przekaźniki i okablowanie 230 V. Na środkowym umieszczone są zasilacze dla Arduino (9 V), ioBridge (5 V) oraz mostek Wi-Fi z jego zasilaniem.
Na najwyższym poziomie znajdują się moduły – Arduino, ioBridge i nadajnik FM.
Ogromną zaletą stosowania przekaźników elektronicznych zamiast mechanicznych jest to, że nie trzeba dodawać diod, tranzystorów ani rezystorów do sterowania czy zabezpieczania.
Ponieważ nie są one mechaniczne zależności, przełączanie jest płynniejsze. Podczas prototypowania przekaźniki można zamienić kolorowymi diodami LED z opornikami 1 kΩ.
Po uruchomieniu układu można bezpiecznie podłączyć przekaźniki.
Połączenia są dość proste. Wyjścia cyfrowe z Arduino zostały podłączone do dodatnich pinów sterujących każdego z 8 przekaźników elektronicznych. Do wyjść 230 V przekaźników podłączone są gniazdka sieciowe z rozdzielaczy. Należy pamiętać, aby połączyć ze sobą wszystkie przewody neutralne i uziemienia. Przewody fazowe, podłączone są do każdego z ośmiu przekaźników, a następnie faza z gniazdka podłączana jest do drugiego terminala przekaźnika. Instalacja została zasilona z systemu dwóch gniazdek – na każdą wtyczkę, podłączono po cztery przekaźniki, co pozwala na zmniejszenie poboru prądu z gniazdka zasilającego cały system, a także umożliwiło estetyczniejsze poprowadzenie kabli w obudowie.
Finalnie musimy podłączyć wszystkie wszystkie linie GPIO do sterowania poszczególnymi modułami. Połączenia podzielono na sekcje (w kolejności narzucanej przez konwencję numeracji GPIO Arduino) i rozpisano poniżej:
- Komunikacja
- D0(RX) → ioBridge TX;
- D1(TX) → ioBridge RX;
- Moduł Wave Shield
- D2 → LCS;
- D3 → CLK;
- D4 → DI;
- D5 → LAT;
- D10 → CCS;
- wyjście modułu Wave → wejście transmitera FM;
- D11, D12, D13 → interfejs karty SD;
- Przekaźniki świateł
- D6 → Kanał 1;
- D7 → Kanał 2;
- D8 → Kanał 3;
- A1 (D15) → Kanał 4;
- A2 (D16) → Kanał 5;
- A3 (D17) → Kanał 6;
- A4 (D18) → Kanał 7;
- A5 (D19) → Kanał 8;
- Transmiter FM
- D9 → do bazy tranzystora 2N2222 przez opornik 10 kΩ, kolektor i emiter do przełącznika zasilania na transmiterze;
- Zasilanie
- GND[0] → masa sterowania przekaźników;
- 5 V → zasilanie transmitera FM;
- GND[1] → masa transmitera FM;
- GND[2] → masa płytki ioBridge
- Vu Meter
- A0 → opornik R7 (1,5 kΩ) na module WAVE, do pomiaru wyjściowego poziomu dźwięku.
Oprogramowanie
Szkic dla Arduino IDE można pobrać ze strony projektu na portalu Instructables (link na końcu artykułu). Do zadziałania będą potrzebne trzy zewnętrzne biblioteki, które należy doinstalować w Arduino IDE:
- AF Wave (https://bit.ly/3fuIksw);
- Wave (jak wyżej);
- String, dawniej TextString (https://bit.ly/3lYzI00).
#include <AF_Wave.h>
#include <avr/pgmspace.h>
#include "util.h"
#include "wave.h"
#define maxLength 16
#define ID 0
#define CMD 2
#define AR1 4
#define AR2 8
#define FILTER_SHIFT 3
AF_Wave card;
File f;
Wavefile wave;
// Zmień poniższe parametry, aby dopasować system do swoich wymagań
long ligthDelay = 200;
boolean debug = false;
int channels[]={6,7,8,15,16,17,18,19}; // Wyjścia cyfrowe sterujące przekaźnikami.
int channelsLenght = 8;
int32_t filter_reg;
int16_t filter_input;
int16_t filter_output;
uint16_t samplerate;
int volPin = 0;
int val;
long randChannel;
long randLightmode;
long volume;
long v2;
int queue;
int mode = 0;
int queueMode = 0;
char *track;
int count = 0;
unsigned long time;
unsigned long randDuration = 0;
int inCount;
boolean answer = true;
boolean commandComplete = false;
String inString = String(maxLength);
String tmpTrack = String(maxLength);
String command = String(maxLength);
void setup() {
Serial.begin(9600);
for (int i = 2; i <= 5; ++i)
pinMode(i, OUTPUT);
pinMode(9, OUTPUT); // pin załączania transmittera FM
for (int i = 0; i < channelsLenght; i++)
pinMode(channels[i], OUTPUT);
if (!card.init_card()) {
if (debug)putstring_nl("Card init. failed!");
return;
}
if (!card.open_partition()) {
if (debug)putstring_nl("No partition!");
return;
}
if (!card.open_filesys()) {
if (debug)putstring_nl("Couldn't open filesys");
return;
}
if (!card.open_rootdir()) {
if (debug)putstring_nl("Couldn't open dir");
return;
}
if (debug)putstring_nl("Files found:");
ls();
if (debug)putstring_nl("turn on FM transmiter");
delay(500);
digitalWrite(9, HIGH);
delay(200);
digitalWrite(9, LOW);
command = "";
}
void ls() {
char name[13];
card.reset_dir();
if (debug)putstring_nl("Files found:");
while (1) {
if (!card.get_next_name_in_dir(name)) {
card.reset_dir();
return;
}
if (debug)Serial.println(name); // wypisanie listy plików
}
}
uint8_t tracknum = 0;
void loop() {
uint8_t i, r;
char c, name[15];
card.reset_dir();
for (i=0; i<tracknum+1; i++) { // przechodzenie po kolei przez pliki
r = card.get_next_name_in_dir(name);
if (!r) { // koniec listy plików, zaczynanie od początku
tracknum = 0;
return;
}
}
card.reset_dir(); // resetowanie folderu, by znaleźć pliki
playComplete(name);
tracknum++;
}
void playComplete(char *name) {
uint16_t potval;
uint32_t newsamplerate;
char c;
if (queue == 1){ // sprawdzenie możliwości odtwarzania ze zdalnej kolejki
playFile(track); // odtwarzanie ze zdalnej kolejki
track = "";
queue = 0;
mode = queueMode;
}
else{
playFile(name);
}
samplerate = wave.dwSamplesPerSec; // konfiguracja czętotliwości próbkowania
int isPlayingCounter = 1;
while (wave.isplaying) { // główna pętla
isPlayingCounter++;
if(Serial.available() > 0) { // oczekiwanie na nowe komendy
char inChar = Serial.read();
if(inChar == 59 || inChar == 10 || inChar == 13){ // wykrywanie końca komendy
inString = command;
command = "";
mode = 0;
commandComplete = true;
}
else {
command.append(inChar);
}
}
if (commandComplete == true || mode > 0) {
if(inString.charAt(0) == 'n' && mode == 0){ // tryb pinu
if (wave.isplaying) { // jeśli coś jest odtwarzane, należy to zatrzymać
if (debug)putstring("Next track");
commandComplete = false;
inString = "";
wave.stop(); // zatrzymanie odtwarzania
}
}
if(inString.charAt(0) == 'd'){ // tryb pinu
char *tmpligthDelay;
tmpligthDelay[0] = inString.charAt(1);
tmpligthDelay[1] = inString.charAt(2);
tmpligthDelay[2] = inString.charAt(3);
if (isNumeric(inString.charAt(4))){
tmpligthDelay[3] = inString.charAt(4);
}
ligthDelay = atoi(tmpligthDelay);
}
if(inString.charAt(0) == 'q' && isNumeric(inString.charAt(1)) && mode == 0){
tmpTrack = "";
track = "";
tmpTrack.append(inString.charAt(1));
if (isNumeric(inString.charAt(2)))
tmpTrack.append(inString.charAt(2));
tmpTrack.append(".WAV");
track = tmpTrack;
if (inString.charAt(3) == 'm'){
char modeChar = inString.charAt(4);
queueMode = atoi(&modeChar);
}
commandComplete = false;
inString = "";
queue = 1;
}
if((inString.charAt(3) == 'm' && inString.charAt(4) == '1') || mode == 1){
doVuMeter();
mode = 1;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '2') || mode == 2){
doRandomChannel();
mode = 2;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '3') || mode == 3){
doSequence();
mode = 3;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '4') || mode == 4){
doMerge();
mode = 4;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '5') || mode == 5){
doAscend();
mode = 5;
}
else if((inString.charAt(3) == 'm' && inString.charAt(6) == '6') || mode == 6){
doDescend();
mode = 6;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '7') || mode == 7){
doSplit();
mode = 7;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '8' && isNumeric(inString.charAt(5))) || mode == 8 ){
doAllChannels(inString.charAt(5));
mode = 8;
}
else if((inString.charAt(3) == 'm' && inString.charAt(4) == '9' && isNumeric(inString.charAt(5)) && isNumeric(inString.charAt(6))) || mode == 9){
doChannel(inString.charAt(5),inString.charAt(6));
mode = 9;
}else{
randomMode(isPlayingCounter);
}
}else{
randomMode(isPlayingCounter);
}
delay(100);
}
card.close_file(f);
}
void randomMode(int i){
if (debug)Serial.println("Random Channel");
time = millis();
if (time > randDuration){
randDuration = time + (10 * 1000) ;
randLightmode = random(1,8);
}
switch (randLightmode){
case 1:
doVuMeter();
break;
case 2:
doRandomChannel();
break;
case 3:
doSequence();
break;
case 4:
doAscend();
break;
case 5:
doDescend();
break;
case 6:
doSplit();
break;
case 7:
doMerge();
break;
}
}
void playFile(char *name) {
f = card.open_file(name);
if (!f) {
if (debug)putstring_nl(" Couldn't open file");
return;
}
if (!wave.create(f)) {
if (debug)putstring_nl(" Not a valid WAV");
return;
}
wave.play(); // odtwarzanie muzyki
Serial.print(name[0]);
if (isNumeric(name[1])){
Serial.print(name[1]);
}
}
void doAllChannels(char state){
if (debug)Serial.println("ALL Channels");
int intState = atoi(&state);
for (int i = 0; i < channelsLenght; i++) {
digitalWrite(channels[i], intState);
}
}
void doChannel(char channel, char state){
if (debug)Serial.println("Single Channel");
int intChannel = atoi(&channel);
int intState = atoi(&state);
digitalWrite(channels[intChannel], intState);
}
void doRandomChannel(){
if (debug)Serial.println("Random Channel");
for (int i = 0; i < 3; i++) {
randChannel = random(0,8);
digitalWrite(channels[randChannel], HIGH);
delay(ligthDelay);
digitalWrite(channels[randChannel], LOW);
}
}
void doVuMeter(){
if (debug)Serial.println("Vu Meter");
volume = 0;
for (int i=0; i<8; i++) {
v2 = analogRead(0) - 350;
if (v2 < 0)
v2 *= -1;
if (v2 > volume)
volume = v2;
delay(5);
}
volume = volume / 30;
for (int i = 0; i < channelsLenght; i++) {
if (volume > i +3 ){
digitalWrite(channels[i], HIGH);
}
else{
digitalWrite(channels[i], LOW);
}
}
}
void doSequence(){
if (debug)Serial.println("Sequence");
for (int i = 0; i < channelsLenght; i++) {
digitalWrite(channels[i], HIGH);
delay(ligthDelay);
digitalWrite(channels[i], LOW);
delay(ligthDelay);
}
for (int i = channelsLenght - 1; i >= 0; i--) {
digitalWrite(channels[i], HIGH);
delay(ligthDelay);
digitalWrite(channels[i], LOW);
delay(ligthDelay);
}
}
void doMerge(){
if (debug)Serial.println("Merge");
int rev = 7;
for (int i = 0; i < 4; i++) {
digitalWrite(channels[i], HIGH);
digitalWrite(channels[rev - i], HIGH);
delay(ligthDelay);
digitalWrite(channels[i], LOW);
digitalWrite(channels[rev - i], LOW);
delay(ligthDelay);
}
}
void doSplit(){
if (debug)Serial.println("Split");
int rev = 3;
int inc = 1;
for (int i = 3; i >= 0; i--) {
digitalWrite(channels[i], HIGH);
digitalWrite(channels[rev + inc], HIGH);
delay(ligthDelay);
digitalWrite(channels[i], LOW);
digitalWrite(channels[rev + inc], LOW);
delay(ligthDelay);
inc++;
}
}
void doDescend(){
if (debug)Serial.println("Descend");
for (int i = channelsLenght - 1; i >= 0; i--) {
digitalWrite(channels[i], HIGH);
delay(ligthDelay);
digitalWrite(channels[i], LOW);
delay(ligthDelay);
}
}
void doAscend(){
if (debug)Serial.println("Ascend");
for (int i = 0; i < channelsLenght; i++) {
digitalWrite(channels[i], HIGH);
delay(ligthDelay);
digitalWrite(channels[i], LOW);
delay(ligthDelay);
}
}
boolean isNumeric(char character){
boolean ret = false;
if(character >= 48 && character <= 75){
ret = true;
}
return ret;
}
Listing 1 dostępny jest również w materiałach dodatkowych do artykułu. Po pobraniu powyższych bibliotek można załadować go do Arduino IDE, skompilować i załadować do Arduino.
Konfiguracja IoBridge
Konfiguracja ioBridge jest bardzo łatwa. Wszystko, co trzeba zrobić, aby poprawnie komunikować się z systemem, to wysyłać poprzez port szeregowy proste komunikaty. Głównie chodzi o numer ścieżki, który będzie do pobrania przez interfejs API w formacie JSON (LastSerialOutput).
Oto składnia komunikacji z Xmas-boxem poprzez port szeregowy. Średnik jest elementem komendy i oznacza koniec polecenia.
- Następna ścieżka
- n;
- Dodaj do kolejki
- q(1...99);
- Dodaj do kolejki z konkretnym trybem oświetlania
- q(1...99)m(1...8);
- Zmień tryb oświetlania
- xxxm(1...7);
- Zapal/zgaś wszystkie światła
- xxxm8{1/0);
- Zapal/zgaś konkretne kanały świateł
- xxxm9(1...8)(1/0);
Transmiter FM
Początkowo w roli transmitera autor chciał zastosować Belkin TuneCast II, ponieważ znalazł wiele informacji w Internecie, dotyczących integracji go z Arduino. Niestety nie udało się znaleźć takiego urządzenia. Zastosowany inny moduł sprawdza się doskonale. Jedyne modyfikacje modułu, jakie trzeba było wykonać to wylutowanie gniazda mini-jack i podłączenie 2 przewodów wychodzących z wyjścia audio modułu WAVE oraz wylutowanie gniazda zasilania, aby podłączyć zasilanie bezpośrednio z Arduino.
Nadajnik FM zasilany jest napięciem 5 V wychodzącym z Arduino. Nie jest to najlepszy pomysł, gdyż Arduino może być zasilane również wyższym napięciem, a transmiter potrzebuje 5 V.
W tej aplikacji Arduino zasilane jest z zasilacza 5 V, więc nie ma ryzyka przekroczenia napięcia.
Zamiast włącznika należy w układzie wlutować tranzystor 2N2222 z bazą podłączoną do rezystora 10 kΩ, którą steruje wyjście cyfrowe Arduino. Aby włączyć transmiter, należy emulować naciśnięcie przycisku poprzez ustawienie na chwilę stanu wysokiego na tym pinie. Jako antena służy fragment przewodu, dolutowany drugą stroną do masy wejścia zasilania transmitera.
Interfejs webowy
Kolejkowanie utworów realizowane jest w aplikacji internetowej. Do stworzenia interfejsu webowego autor korzysta z Oracle Application Express (APEX), ponieważ jest to jego ulubione narzędzie do szybkiego tworzenia aplikacji internetowych. Ale system może być również zarządzany przez dowolne inne rozwiązanie internetowe wykorzystujące bazę danych (np. Linux z Apache, MySQL i PHP, tak zwany pakiet LAMP). Na rysunku 1 pokazano panel aplikacji webowej, jaka steruje omawianym systemem.
Aplikacja przechowuje w bazie 2 główne tabele:
- SONGS – przechowuje nazwę i wykonawcę utworu;
- QUEUE – przechowuje żądania piosenek.
Aplikacja internetowa używa jQuery do pobierania z interfejsu API ioBridge JSON Data Feeed w celu określenia aktualnie odtwarzanego utworu. Używając JavaScript, pobierany jest node LastSerialOutput, który przechowuje numer bieżącej ścieżki. Następnie kolejne jQuery wywołuje usługę REST w aplikacji internetowej i wysyła numer ścieżki. Usługa REST zwraca tytuł utworu, wykonawców i nazwisko osoby, która zażądała utworu. Strona docelowo wzbogacona ma być wzbogacona o podgląd na żywo wraz z odtwarzaniem dźwięku, ale autor nadal nad tym pracuję.
Nikodem Czechowski, EP
Źródło: https://bit.ly/3fwEVcP