Автоматизированная система уведомлений о выходе новых серий сериалов и аниме
Компоненты и материалы
| Печатная плата JLCPCB (своя разработка) | ×1 |
| Arduino Nano 33 IoT | ×1 |
| Raspberry Pi 3 Model B (или Pi 4) | ×1 |
| Raspberry Pi 4 Model B (альтернатива) | ×1 |
| Экран Nokia 5110 (SparkFun) | ×1 |
| Serial MP3 Player (OPEN-SMART) | ×1 |
| RGB LED (общий анод) | ×1 |
| SD-карта (Flash Memory) | ×2 |
| Резистор 220 Ом | ×12 |
| Тактильная кнопка 6×6×5 мм | ×4 |
| DC Jack 2.1 мм (PCB mount) | ×1 |
Инструменты
- Паяльник
- Клеевой пистолет
Программное обеспечение и сервисы
- Arduino IDE
- KiCad (разводка платы)
- Notepad++
О проекте
Описание
Существует множество веб/мобильных приложений для отслеживания дат выхода серий, но я хотел создать автономное устройство «с нуля» — железо и софт — с уникальными функциями.
Сначала разработано веб-приложение TV Series / Anime Release Date Tracker на PHP. Оно формирует список сериалов/аниме и получает даты выходов через TVMAZE REST API (endpoint /shows/:id/episodesbydate?date=:date). API возвращает полную информацию об эпизоде или 404, если в эту дату ничего не вышло.
Веб-сервер (LAMP + MariaDB) развёрнут на Raspberry Pi. Для тех, у кого нет Pi или не хотят его использовать — опубликована бесплатная версия с удобным интерфейсом на моём сайте TheAmplituhedron. Исходный код приложения приведён ниже.
Arduino Nano 33 IoT опрашивает веб-приложение. Если есть новая серия — парсит ответ с разделителем % (формат: %Название%Сезон%Эпизод%Имя_эпизода%), выводит информацию на Nokia 5110 и воспроизводит опенинг через Serial MP3 Player. Интерфейс меню на экране с 4 кнопками.
Плейлист опенингов хранится на microSD в папке 01/ как 001xxx.mp3, 002xxx.mp3 и т.д.
После тестов на макетке разработана собственная PCB в форме ретро-телефона (JLCPCB спонсировал изготовление).
Шаг 1: Проектирование и пайка PCB
Плата разработана в KiCad. Gerber-файлы в разделе файлов. Компоненты на плате:
- A1 — разъёмы Arduino Nano 33 IoT
- J1 — разъёмы Serial MP3 Player
- J2 — разъёмы DS3231 (не обязательно)
- J3 — разъёмы Nokia 5110
- J4 — DC Jack
- D1 — 5 мм RGB LED (общий анод)
- K1–K4 — кнопки
- R1–R12 — резисторы 220 Ом
Шаг 2: Настройка LAMP-сервера (MariaDB) на Raspberry Pi
sudo apt-get install apache2 -y- Проверка:
https://localhostилиhttps://<IP_Pi>(hostname -I) sudo apt-get update && sudo apt-get install php -ysudo apt-get install mariadb-server php-mysql -ysudo mysql_secure_installation— root password: bot, ответы Y на все вопросы- Создание БД:
sudo mysql -uroot -p→create database tvseries; GRANT ALL PRIVILEGES ON tvseries.* TO 'root'@'localhost' IDENTIFIED BY 'bot'; FLUSH PRIVILEGES; sudo apt-get install php-curl→sudo service apache2 restart
Шаг 3: Получение данных из TVMAZE REST API
Поиск шоу на TVMAZE → берем TV ID из URL.
Пример: https://api.tvmaze.com/shows/1505/episodesbydate?date=2023-10-15 (One Piece).
Если эпизод есть — возвращается JSON с полями season, number, name. Иначе — 404.
Мой список для тестов:
- One Piece — TV ID: 1505
- My Hero Academia — TV ID: 13615
- Westworld — TV ID: 1371
- The Simpsons — TV ID: 83
- The Late Late Show — TV ID: 2831 (ежедневный, удобен для тестов)
Шаг 4: Веб-приложение (PHP) и таблица series
Скачайте TV-Series-Anime-Episode-Tracker.zip (index.php, query.php), распакуйте, перенесите в /var/www/html/:
sudo mv /home/pi/Downloads/TV-Series-Anime-Episode-Tracker/ /var/www/html/
Откройте https://localhost/TV-Series-Anime-Episode-Tracker/ — создастся таблица series.
Добавьте шоу в БД:
INSERT INTO `series` (`series_name`, `tv_id`, `status`) VALUES ('One Piece', '1505', 'Pending');
query.php опрашивает TVMAZE для каждого шоу. При находке эпизода — выводит строку с разделителями % и меняет статус на Released (чтобы не повторяться). Пример ответа:
%The Late Late Show%2020%91%Jason Schwartzman, Alicia Key%
Если ничего — No Entry Found!
Работа без Raspberry Pi
Подписчики TheAmplituhedron могут использовать хостингую версию: веб-интерфейс.
Шаг 5: Настройка Arduino Nano 33 IoT
- Установите в Arduino IDE: Arduino SAMD Boards и библиотеку WiFiNINA.
- Для Nokia 5110 — библиотека LCD5110_Basic.
- Второй.serial порт (RX0/TX1) —
Serial1для MP3-плеера.
Шаг 6: Команды Serial MP3 Player (OPEN-SMART)
Плейлист опенингов (5 треков) в папке 01/ на microSD (FAT16/32). Имена: 001One Piece-Opening.mp3, 002My Hero Academia-Opening.mp3 и т.д.
UART команды (все начинаются с 7E, заканчиваются EF, второй байт — длина):
// Выбор SD-карты
{0x7E,0x03,0x35,0x01,0xEF}
// Воспроизведение трека 1..5
{0x7E,0x04,0x41,0x00,0x01,0xEF} ... {0x7E,0x04,0x41,0x00,0x05,0xEF}
// Play / Pause / Next / Prev
{0x7E,0x02,0x01,0xEF} / {0x7E,0x02,0x02,0xEF} / {0x7E,0x02,0x03,0xEF} / {0x7E,0x02,0x04,0xEF}
Функция send_command_to_MP3_player(cmd, len) шлёт байты в Serial1.
Шаг 7: Программирование Arduino Nano 33 IoT
- Настройки WiFi (SSID, пароль), IP Raspberry Pi, путь к
query.php. - Инициализация экрана, графики (TV, музыка), команды MP3.
- Меню: A.Init Tracker, B.MP3 Player, C.Sleep (переключение ←/→, выбор OK).
- Режим Tracker: HTTP GET к
query.php, парсинг ответа по%, вывод инфо, воспроизведение опенинга, RGB-индикация цвета шоу (One Piece=Red, MHA=Green, Westworld=Blue, Simpsons=Yellow, Late Late=Purple). Ожидание кнопки Exit. - Режим MP3 Player: управление треками кнопками, иконка ноты.
- Режим Sleep: засыпание через 10 сек с обратным отсчётом, RGB-ночник, выход по Exit.
Подключение
Nokia 5110: D2-SCK, D3-MOSI, D4-DC, D5-RST, D6-CS
RGB: D9-R, D10-G, D11-B
Кнопки: A0-Left, A1-OK, A2-Right, A3-Exit
DS3231 (опц): A4-SDA, A5-SCL
Режимы и функции
- Init Tracker — подключение к серверу, опрос каждые 10 сек. При новой серии: инфо + опенинг + цвет. Без серии: «No Released Episode Detected :(» + иконка TV.
- MP3 Player — проигрывание плейлиста, управление кнопками.
- Sleep — сон через 10 сек, RGB-ночник, выход по Exit.
Видео и итог
Устройство закреплено на книжном шкафу клеевым пистолетом. Работает отлично!
Код
Series_Release_Date_Notifier.ino (Arduino)
////////////////////////////////////////////////////
// TV Series / Anime New Episode Release Date Notifier
// Arduino Nano 33 IoT
// by Kutluhan Aktar
////////////////////////////////////////////////////
#include
#include
#include
char ssid[] = "[_SSID_]";
char pass[] = "[_PASSWORD_]";
int keyIndex = 0;
int status = WL_IDLE_STATUS;
IPAddress server(192, 168, 1, 22); // IP Raspberry Pi
String application = "/TV-Series-Anime-Episode-Tracker/query.php";
WiFiClient client;
LCD5110 myGLCD(2,3,4,5,6);
extern uint8_t SmallFont[], MediumNumbers[], tv[], music[];
static int8_t select_SD_card[] = {0x7e,0x03,0x35,0x01,0xef};
static int8_t play_song_1[] = {0x7e,0x04,0x41,0x00,0x01,0xef};
static int8_t play_song_2[] = {0x7e,0x04,0x41,0x00,0x02,0xef};
static int8_t play_song_3[] = {0x7e,0x04,0x41,0x00,0x03,0xef};
static int8_t play_song_4[] = {0x7e,0x04,0x41,0x00,0x04,0xef};
static int8_t play_song_5[] = {0x7e,0x04,0x41,0x00,0x05,0xef};
static int8_t play[] = {0x7e,0x02,0x01,0xef};
static int8_t pause[] = {0x7e,0x02,0x02,0xef};
static int8_t next_song[] = {0x7e,0x02,0x03,0xef};
static int8_t previous_song[] = {0x7e,0x02,0x04,0xef};
volatile boolean TV=false, Music=false, Sleep=false, Activated=false;
#define B_Exit A3; #define B_Right A2; #define B_OK A1; #define B_Left A0
#define redPin 9; #define greenPin 10; #define bluePin 11
int Right, OK, Left, Exit, selected=0;
void setup() {
pinMode(B_Exit,INPUT); pinMode(B_Right,INPUT); pinMode(B_OK,INPUT); pinMode(B_Left,INPUT);
pinMode(redPin,OUTPUT); pinMode(greenPin,OUTPUT); pinMode(bluePin,OUTPUT);
adjustColor(0,0,0);
myGLCD.InitLCD(); myGLCD.setFont(SmallFont);
Serial.begin(9600); Serial1.begin(9600);
send_command_to_MP3_player(select_SD_card,5);
if(WiFi.status()==WL_NO_MODULE) { Serial.println("WiFi module failed!"); while(true); }
while(status!=WL_CONNECTED) {
Serial.print("Connecting to "); Serial.println(ssid);
myGLCD.print("Waiting...",0,8); myGLCD.print("Attempting to",0,16); myGLCD.print("connect to",0,24); myGLCD.print("WiFi !!!",0,32);
status = WiFi.begin(ssid,pass); delay(10000);
}
Serial.println("Connected to wifi");
myGLCD.clrScr(); myGLCD.print("Connected to",0,8); myGLCD.print("WiFi!!!",0,16); delay(2000); myGLCD.clrScr();
}
void loop() {
read_buttons(); change_menu_options(); interface();
if(TV) { do { myGLCD.invertText(true); myGLCD.print("A.Init Tracker",0,16); myGLCD.invertText(false); delay(100);
if(OK==HIGH) { myGLCD.clrScr(); Activated=true;
while(Activated) {
if(client.connect(server,80)) {
Serial.println("connected to server");
myGLCD.print("Connected to",0,8); myGLCD.print("the server!!!",0,16);
client.println("GET "+application+" HTTP/1.1");
client.println("Host: 192.168.1.22"); client.println("Connection: close"); client.println();
} else { myGLCD.print("Connection",0,8); myGLCD.print("Error!!!",0,16); }
delay(2000);
String response="";
while(client.available()) { char c=client.read(); response+=c; }
if(response!="" && response.endsWith("%")) {
int d=response.indexOf("%"), d1=response.indexOf("%",d+1), d2=response.indexOf("%",d1+1), d3=response.indexOf("%",d2+1), d4=response.indexOf("%",d3+1);
String Series_Name=response.substring(d+1,d1), Season=response.substring(d1+1,d2), Episode=response.substring(d2+1,d3), Episode_Name=response.substring(d3+1,d4);
myGLCD.clrScr(); myGLCD.print(Series_Name,0,0); myGLCD.print(Season+" x "+Episode,0,16); myGLCD.print(Episode_Name,0,32);
if(Series_Name=="One Piece") { send_command_to_MP3_player(play_song_1,6); adjustColor(255,0,0); }
else if(Series_Name=="My Hero Academia") { send_command_to_MP3_player(play_song_2,6); adjustColor(0,255,0); }
else if(Series_Name=="Westworld") { send_command_to_MP3_player(play_song_3,6); adjustColor(0,0,255); }
else if(Series_Name=="The Simpsons") { send_command_to_MP3_player(play_song_4,6); adjustColor(255,255,0); }
else if(Series_Name=="The Late Late Show") { send_command_to_MP3_player(play_song_5,6); adjustColor(80,0,80); }
boolean song=true;
while(song) { read_buttons(); if(Exit==HIGH) { song=false; send_command_to_MP3_player(pause,4); adjustColor(0,0,0); } }
myGLCD.clrScr();
} else {
myGLCD.clrScr(); myGLCD.print("No Released",0,0); myGLCD.print("Episode",0,16); myGLCD.print("Detected :(",0,32); delay(5000); myGLCD.clrScr();
}
myGLCD.drawBitmap(8,0,tv,60,48); delay(10000); myGLCD.clrScr();
read_buttons(); if(Exit==HIGH) { Activated=false; myGLCD.clrScr(); }
}
}
} while(TV==false); }
if(Music) { do { myGLCD.invertText(true); myGLCD.print("B.MP3 Player",0,24); myGLCD.invertText(false); delay(100);
if(OK==HIGH) { myGLCD.clrScr(); Activated=true;
while(Activated) { read_buttons(); myGLCD.drawBitmap(8,0,music,60,48);
if(Right) send_command_to_MP3_player(next_song,4);
if(Left) send_command_to_MP3_player(previous_song,4);
if(OK) send_command_to_MP3_player(pause,4);
if(Exit==HIGH) { Activated=false; myGLCD.clrScr(); send_command_to_MP3_player(pause,4); }
}
}
} while(Music==false); }
if(Sleep) { do { myGLCD.invertText(true); myGLCD.print("C.Sleep",0,32); myGLCD.invertText(false); delay(100);
if(OK==HIGH) {
myGLCD.clrScr(); myGLCD.print("Entering",CENTER,0); myGLCD.print("Sleep Mode",CENTER,8); myGLCD.print("in",CENTER,16); myGLCD.print("Seconds",CENTER,40);
myGLCD.setFont(MediumNumbers);
for(int s=10;s>=0;s--) { myGLCD.printNumI(s,CENTER,24,2,'0'); delay(1000); }
myGLCD.enableSleep(); Activated=true;
while(Activated) {
adjustColor(255,0,0); delay(500); adjustColor(0,255,0); delay(500);
adjustColor(0,0,255); delay(500); adjustColor(255,255,0); delay(500); adjustColor(80,0,80); delay(500);
read_buttons(); if(Exit==HIGH) { Activated=false; myGLCD.clrScr(); myGLCD.disableSleep(); myGLCD.setFont(SmallFont); adjustColor(0,0,0); }
}
}
} while(Sleep==false); }
}
void read_buttons() { Right=digitalRead(B_Right); OK=digitalRead(B_OK); Left=digitalRead(B_Left); Exit=digitalRead(B_Exit); }
void send_command_to_MP3_player(int8_t cmd[], int len) {
Serial.print("\nMP3 Command => ");
for(int i=0;i3) selected=1; delay(100);
switch(selected) {
case 1: TV=true; Music=false; Sleep=false; break;
case 2: TV=false; Music=true; Sleep=false; break;
case 3: TV=false; Music=false; Sleep=true; break;
}
}
void adjustColor(int r,int g,int b) {
r=255-r; g=255-g; b=255-b;
analogWrite(redPin,r); analogWrite(greenPin,g); analogWrite(bluePin,b);
}
index.php
0;
}
function createTable($table,$conn) {
if(!checkDatabase($table,$conn)) {
$sql="CREATE TABLE `$table`(id int(11) AUTO_INCREMENT PRIMARY KEY NOT NULL, series_name varchar(255) NOT NULL, tv_id varchar(255) NOT NULL, status varchar(255) NOT NULL);";
mysqli_query($conn,$sql) ? print("Database Table Created!") : print("Error!");
} else print("Database Table Found!");
}
createTable("series",$conn);
?>
query.php
conn=$conn; $this->table_name=$table; }
private function check_ep_release_date($id,$name,$status) {
$date=date("Y-m-d");
$url=self::ROOT.$id.self::PATH.$date;
if($data=json_decode(file_get_contents($url))) {
if($status!="Released") {
echo "%".$name."%".$data[0]->season."%".$data[0]->number."%".$data[0]->name."%";
mysqli_query($this->conn,"UPDATE `$this->table_name` SET `status`='Released' WHERE series_name='$name'");
}
} else {
if($status!="Pending") mysqli_query($this->conn,"UPDATE `$this->table_name` SET `status`='Pending' WHERE series_name='$name'");
}
}
public function track_db_entries() {
$sql="SELECT * FROM `$this->table_name` ORDER BY `id` DESC";
if($res=mysqli_query($this->conn,$sql)) {
if(mysqli_num_rows($res)>0) while($row=mysqli_fetch_assoc($res)) $this->check_ep_release_date($row['tv_id'],$row['series_name'],$row['status']);
else echo "No Entry Found!";
} else echo "No Database Found!";
}
}
$conn=mysqli_connect("localhost","root","bot","tvseries");
$q=new query(); $q->define_user($conn,"series"); $q->track_db_entries();
?>
Файлы проекта
- Gerber PCB:
gerber_ycEfXzZq6h.zip - Веб-приложение:
tv-series-anime-episode-tracker_gCfN0nFiA8.zip - Опенинги:
opening_songs_y2LCEw79tg.zip - Графика экрана:
tv_Iz2isO8nGI.c,music_SA2xR86cZw.c
Схемы
Схемы подключения (4 варианта) в разделе файлов.
Производственный процесс
- Создание профессионального универсального пульта на Raspberry Pi с LIRC
- Преобразование RF в DC с Raspberry Pi: сборка и отладка детектора огибающей
- Cycle Chaser: превратите свой велосипед в ночной световый шоу с Raspberry Pi
- Создание детектора вора на базе Raspberry Pi — пошаговое руководство
- Создайте домашний монитор температуры на Raspberry Pi с MCP9808, InfluxDB и Grafana
- Создайте систему домашней автоматизации на Raspberry Pi для удаленного управления
- Raspberry Pi 3 B+ — Обновленные характеристики, спецификации и руководство по покупке
- DIY робот в стиле Wall‑E на Raspberry Pi из коробки от CD
- RasPiRobot Board V2: Расширьте Raspberry Pi в мощный робот-контроллер
- Построение ровера на Windows IoT Core с Raspberry Pi 2 — Руководство от новичка к профи