קרבות הרובוטים נמשכים שלוש דקות, שלוש דקות שלעיתים למשתתפים נראות כמו נצח (מניסיון). אמנם לא היה לי ניסיון מוקדם בתוך משתתף אבל הייתה לי מוטיבציה להכין שעון כך שהמשתתפים והקהל יוכלו לראות את הזמן. מה תכננתי, במה השתמשנו ומחשבות להמשך.
על התחרות והחלק שלי בה כמשתתף כתבתי כבר כאן וכאן. אבל היה לי גם חלק בארגון האירוע וההכנות לזירה, וכחלק מתפקידי החלטתי שראוי שיהיה שעון גדול שיקצוב את שלוש הדקות של הקרב. בהמשך גם רציתי שהשעון יכריז על מה הקרב הבא אבל תוך כדי התחרות לא השתמשנו בחלק הזה ממש.
מטרת המדריך הזה היא לספר לקהילה מה עשיתי כאן, אבל גם כדי שאזכור בעצמי ואוכל לשפר את תפקוד המסך/שעון לתחרויות הבאות. בשביל זה צריך לזכור מה הGround zero. אז זה זה.
ציוד וחומרים
לפרויקט זה נצטרך את החומרים הבאים:
- שני לוחות P4 – לוחות לד RGB במתח 5V, וזרם מוצהר של עד 3A ׁ(אם כי מדדתי את הזרם הנצרך תוך כדי הפעלה והצריכה הכוללת הייתה נמוכה מ1A)
- בקר esp32 – הבקר עליו אנחנו טוענים את התוכנה והוא זה שמפעיל את הכל.
- ספק כוח 5V ׁׁׁ(לפחות 1A עדיף יותר) ׁ- אני פרקתי את הפלסטיק מהספק שלי אבל אפשר גם פשוט לזרוק אותו בתוך החלל שנשאר בין שני הלוחות.
- חוטי חיבור בין הבקר ללוחות ובין הלוחות (בין הלוחות אפשר להשתמש בכבל idc של 16 פינים. זה קצת עוזר)
- ושני מכסים מודפסים (להלן הקישור לקובץ) שמחזיקים את הלוחות בזווית הנכונה משני הצדדים.

מסך לד P5 64X32

esp32
חיבורים - בין הesp ללוח
אחד משני הלוחות יהיה הלוח הראשון, והשני יהיה שכפול שלו. לכן נתחיל בחיבור הesp32 אל אחד מהלוחות. שימו לב שחוץ מהחיבורים הללו, צריך לחבר גם את הesp32 וגם את הלוח עצמו למתח 5v ואפס משותף.
LED Matrix Pin | Function | ESP32-WROOM-32D Pin |
---|---|---|
R1 | Red Data Row 1 | GPIO25 |
G1 | Green Data Row 1 | GPIO26 |
B1 | Blue Data Row 1 | GPIO27 |
R2 | Red Data Row 2 | GPIO14 |
G2 | Green Data Row 2 | GPIO12 |
B2 | Blue Data Row 2 | GPIO13 |
A | Row Select A | GPIO21 |
B | Row Select B | GPIO22 |
C | Row Select C | GPIO19 |
D | Row Select D | GPIO23 |
CLK | Clock | GPIO18 |
LAT | Latch | GPIO5 |
OE | Output Enable | GPIO15 |
GND | Ground | GND |
בין שני הלוחות השתמשתי בכבל IDC מה ששיכפל את שני המסכים להציג אותו הדבר.
בתמונה השניה ניתן לראות את הלוח האלקטרוני של ספק הכוח אותו הדבקתי לאחד השלטים מאחור. אני אוהב לפרק את הפלסטיק ולהלחים את הכניסות ישירות ליציאות של הלוח האלקטרוני. זה מאפשר להשאיר את הספק מאורר, לתפוס פחות מקום ולהעניק את השקט שבחיבור מולחם.
הקונקטור השקוף שמימין למטה בתמונה השניה הוא חיבור ל230v (ללא הארקה) והוא לא הכרחי אבל הוא מאפשר לחבר כבלים שונים ולנתק לפי הצורך בלי לחשוש מניתוקים.

חיבור שני המסכים אחד לשני ולבקר

חיבור ל230v דרך ספק של 1a (מפורק, ללא הפלסטיק) שמזין את הבקר והמסכים
מבנה מודפס בתלת מימד
שיקולי התכנון למארז היו תלייה מלמעלה וזווית למסכים כלפי מטה.
על כן לא היה טעם לסגור את כל הקופסא, מה גם שאלקטרוניקה תמיד עדיף שתהיה מאווררת.
את החלקים הצהובים כאן אפשר להדפיס בכל מדפסת ומכל חומר. לחלקים הצהובים שני חורים בכל צד כדי לתלות את השלט מארבע נקודות וכך הוא לא יתנדנד.



החלק הצהוב המודפס
התוכנה - כולל קצת הסברים
נתחיל עם כל הקוד בבת אחת, ואז נפרק אותו קצת כדי להבין מה עשינו.
נ#include <WiFi.h> #include <WebServer.h> #include <ESPmDNS.h> #include <ESP32-HUB75-MatrixPanel-I2S-DMA.h> #include <ElegantOTA.h> const char* ssid = "ESP32_Countdown"; const char* password = "12345678"; WebServer server(80); #define PANEL_WIDTH 64 #define PANEL_HEIGHT 32 #define PANEL_CHAIN 1 MatrixPanel_I2S_DMA* matrix = nullptr; int countdownSeconds = 180; bool running = false; bool paused = false; unsigned long previousMillis = 0; String messages[4] = {"", "", "",""}; bool messageSet = false; bool blankScreen = true; int messageIndex = 0; unsigned long messageTimer = 0; unsigned long messageInterval = 3200; const char webpage[] PROGMEM = R"rawliteral( <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ESP32 Countdown</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body{font-family:sans-serif;text-align:center;padding-top:20px;} button,input{width:80%;height:60px;font-size:20px;margin:10px;} a {display:block; margin-top:20px; font-size:18px; color:blue;} </style> </head> <body> <button onclick="fetch('/start')">Start</button><br> <button onclick="fetch('/reset')">Reset</button><br> <button onclick="fetch('/stop')">Stop/Resume</button><br> <button onclick="fetch('/blank')">Blank Page</button> <br/><br/> Four lines message:<br/> <input type="text" id="message1" placeholder="Message 1"><br> <input type="text" id="message2" placeholder="Message 2"><br> <input type="text" id="message3" placeholder="Message 3"><br> <input type="text" id="message4" placeholder="Message 4"><br> <button onclick="sendMessage()">Set Message</button><br> <script> function sendMessage(){ let msg1 = document.getElementById('message1').value; let msg2 = document.getElementById('message2').value; let msg3 = document.getElementById('message3').value; let msg4 = document.getElementById('message4').value; fetch(`/setmessage?msg1=${encodeURIComponent(msg1)}&msg2=${encodeURIComponent(msg2)}&msg3=${encodeURIComponent(msg3)}&msg4=${encodeURIComponent(msg4)}`); } </script> </body> </html> )rawliteral"; void setupWiFi() { WiFi.softAP(ssid, password); Serial.println("WiFi Access Point started"); Serial.print("IP Address: "); Serial.println(WiFi.softAPIP()); } void setupWebServer() { server.on("/", []() { server.send_P(200, "text/html", webpage); }); server.on("/start", []() { running = true; paused = false; blankScreen = false; previousMillis = millis(); server.send(200, "text/plain", "Started"); }); server.on("/reset", []() { countdownSeconds = 180; running = false; paused = false; blankScreen = false; displayTime(countdownSeconds); server.send(200, "text/plain", "Reset"); }); server.on("/stop", []() { if (running) paused = !paused; server.send(200, "text/plain", paused ? "Paused" : "Resumed"); }); server.on("/setmessage", []() { messages[0] = server.arg("msg1"); messages[1] = server.arg("msg2"); messages[2] = server.arg("msg3"); messages[3] = server.arg("msg4"); messageSet = (messages[0] != "" || messages[1] != "" || messages[2] != "" || messages[3] != ""); blankScreen = false; messageIndex = 0; messageTimer = millis(); server.send(200, "text/plain", "Messages Set"); }); server.on("/blank", []() { for (int i = 0; i < 3; i++) messages[i] = ""; messageSet = false; running = false; blankScreen = true; matrix->fillScreen(0); matrix->flipDMABuffer(); server.send(200, "text/plain", "Screen Cleared"); }); ElegantOTA.begin(&server); server.begin(); Serial.println("Web Server running with ElegantOTA."); } void setupMatrix() { HUB75_I2S_CFG mxconfig(PANEL_WIDTH, PANEL_HEIGHT, PANEL_CHAIN); mxconfig.gpio.e = -1; mxconfig.driver = HUB75_I2S_CFG::SHIFTREG; mxconfig.double_buff = true; matrix = new MatrixPanel_I2S_DMA(mxconfig); matrix->begin(); matrix->setTextWrap(false); matrix->fillScreen(0); matrix->flipDMABuffer(); } void showMessage(String msg) { matrix->setTextSize(1); matrix->setTextColor(matrix->color565(0, 0, 255)); matrix->fillScreen(0); int16_t x1, y1; uint16_t w, h; matrix->getTextBounds(msg.c_str(), 0, 0, &x1, &y1, &w, &h); int16_t cursorX = (PANEL_WIDTH - w) / 2; int16_t cursorY = (PANEL_HEIGHT - h) / 2; matrix->setCursor(cursorX, cursorY); matrix->print(msg); matrix->flipDMABuffer(); } void displayTime(int secondsLeft) { int minutes = secondsLeft / 60; int seconds = secondsLeft % 60; char timeStr[6]; sprintf(timeStr, "%02d:%02d", minutes, seconds); matrix->setTextSize(2); matrix->fillScreen((secondsLeft <= 10) ? matrix->color565(255,0,0) : 0); matrix->setTextColor((secondsLeft <= 10) ? matrix->color565(0,0,0) : matrix->color565(255,0,0)); int16_t x1, y1; uint16_t w, h; matrix->getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h); int16_t cursorX = (PANEL_WIDTH - w) / 2; int16_t cursorY = (PANEL_HEIGHT - h) / 2; matrix->setCursor(cursorX, cursorY); matrix->print(timeStr); matrix->flipDMABuffer(); } void setup() { Serial.begin(115200); setupWiFi(); setupWebServer(); setupMatrix(); } void loop() { server.handleClient(); ElegantOTA.loop(); if (blankScreen) return; if (messageSet && !running && !paused) { unsigned long now = millis(); if (now - messageTimer >= messageInterval) { messageTimer = now; showMessage(messages[messageIndex]); messageIndex = (messageIndex + 1) % 4; } } if (running && !paused) { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= 1000) { previousMillis = currentMillis; if (countdownSeconds > 0) { countdownSeconds--; displayTime(countdownSeconds); } else { running = false; countdownSeconds = 180; displayTime(countdownSeconds); matrix->fillScreen(0); matrix->flipDMABuffer(); } } } }
אז נפרק אותו לכמה חתיכות:
#include <WiFi.h> #include <WebServer.h> #include <ESPmDNS.h> #include <ESP32-HUB75-MatrixPanel-I2S-DMA.h> #include <ElegantOTA.h>
שלושת הספריות הראשונות נחוצות להפעלת הesp כשרת אינטרנט שמייצר רשת WiFi משלו.
הספריה הרביעית היא ספריית ההפעלה של שלטי הלד (Matrix Panel)
הספריה החמישית (ElegantOTAׂׂׂ) מאפשרת לנו, לאחר שתלינו את השלט ואנחנו רוצים לעשות שינויים בקוד, לגשת לhttp://<IPAddress>/update ולעדכן את הקוד של הבקר דרך חיבור הWiFi
const char* ssid = "ESP32_Countdown"; const char* password = "12345678"; WebServer server(80);
כאן אנחנו מגדירים את שם רשת הWiFi, במקרה הזה ESP32_Countdown ואת הסיסמה שלה. כדאי לשנות סיסמא למקרה שמישהו ירצה להשתלט על השעון (מי שמזהה את הesp ומכיר את הרעיון הזה של דף אינטרנט, יגיע לשם בלי בעיה, מניסיון)
#define PANEL_WIDTH 64 #define PANEL_HEIGHT 32 #define PANEL_CHAIN 1 MatrixPanel_I2S_DMA* matrix = nullptr;
אלו הן הגדרות הלוח – 32 לדים בגובה על 64 לדים ברוחב (זה כי יש תצורות נוספות ללוחות הללו. השורה השלישית מסבירה שהלוחות לא משורשרים. אם היו משורשרים היינו מתייחסים אליהם כלוח גדול יותר.
int countdownSeconds = 180; bool running = false; bool paused = false; unsigned long previousMillis = 0; String messages[4] = {"", "", "",""}; bool messageSet = false; bool blankScreen = true; int messageIndex = 0; unsigned long messageTimer = 0; unsigned long messageInterval = 3200;
כאן מוגדרים כמה משתנים בסיסיים. אלו שניתנים לשינוי ורלוונטיים הם:
countdownSeconds – כמה שניות צריך לספור לאחור בשעון (180 זה שלוש דקות כמובן)
string mesages – מה יהיה כתוב במשתנים שמחזיקים את ארבעת שורות ההודעות (בהם המשתמשתי בסרטון כדי לספר מה הקרב הבא)
3200 – אלפיות שניה בין ההודעות המתחלפות.
הקוד ארוך, אעצור כאן – אבל אם תגיבו בתגובות שאתם צריכים עוד הבהרות אמשיך לפרק אותו. אחרת, אשאיר אותו כך (:
הדגמת המערכת
אפס משותף זה חשוב
בבית הכל עבד פיקס. לקחתי אותו לאולם בו הייתה הזירה, תלינו אותו מהתקרה, יש לו מתח והכל. אני מוצא את הרשת, אבל… לא מצליח להתחבר. וזה ממש מעל הראש שלי. זאת לא בעיית קליטה. מאוד התבאסתי. מזל שהיה לי עוד לילה אחד לפני הטורניר.
לקחתי את השלט חזרה הבייתה. חיברתי לחשמל, פתחתי את הרשת והקישור של דף השליטה. והנה עובד! תוך רגע. שברתי את הראש… ואז הבנתי מה קרה (למרות שאני עדיין קצת מזועזע מזה).
אפס משותף. אז מסתבר שגם במצב של העברת מידע בוויפיי בין המחשב לבקר, היה צורך באפס משותף. כלומר שהם יהיו מחוברים לשקע צמוד, כך שהאפס שלהם יהיה משותף. עדיין זה נשמע לי הזוי, כי מה יקרה כשהבקר יהיה מחובר בסוללה, אלחוטי? זה גם לא יעבוד? מוזר. לא היה לי זמן לחקור את זה, אבל שמחתי שעליתי על זה בזמן, גם עם ההסבר שלי לא מהודק מאוד.
אם למישהו יש דרך טובה יותר להסביר את זה, וגם להסביר לי איך זה אמור לעבוד אם השלט היה מחובר על סוללה (שלה, כמובן, אין אפס משותף עם כלום), אשמח!
איך לשפר את המסך-שעון כדי שיציג את הקרב הבא בתור עד ההזנקה
כמה מחשבות אחרונות: במשך הקרב עצמו השעון היה ממש חשוב. מה שכן – לא השתמשנו בכלל בפיצ’ר של הטקסט, מאחר והיה צריך כל הזמן להקליד את שמות הרובוטים ללוח ההודעות וזה היה סזיפי אז ירדתי מזה מהר, וגם אני הייתי עסוק בהפעלת הרובוט שלי כך שלא יכולתי לדאוג לזה.
אז לפעם הבאה, יש לי רעיון לשיפור-תיקון – לארגן שדות לכל שמות הרובוטים, את כולם נזין בתחילת התחרות. ליד כל שדה יהיה תיבת סימון. ואז בכל פעם אסמן שני רובוטים בתיבת הסימון בלבד (בלי להקליד כלום) והשלט ידפיס הודעת קרב משתי תיבות הסימון. קל ואפקטיבי. למה זה חשוב? זה לא בהכרח חשוב. אבל רציתי לנצל עד תום את האפשרויות.
אפשרות נוספת היא להוסיף אינדיקציות קוליות (עם זמזם או בהקלטה) בדקה האחרונה, ולכל שניה בעשר השניות האחרונות. כך המשתתפים יכולים להבין מה קורה גם אם הם לא מרימים את הראש.


לתוכן זה נכתבו 0 תגובות