9 בדצמבר 2023

קופת צדקה זורע צדקות - Charity Sower

 

מבוא

כל פעם שאנו נותנים צדקה, אנו משפיעים לטובה - לא רק על הזולת אלא גם על עצמנו ועל החברה כולה. צדקה היא הזדמנות לעשיית טוב וליצירת שינוי חיובי בעולמנו. 

כל תרומה, גדולה כקטנה, מסוגלת להאיר ולשמח. יש בכוחה לסייע ברגעי מצוקה, לעודד בשעת ייאוש ואף לשנות גורלות.  

מעבר לתרומה חד-פעמית, חשוב לטפח תרבות של נתינה קבועה ומתמשכת. נתינה מחזורית מאפשרת סיוע עקבי לנזקקים, ומסייעת לנו לסגל לעצמנו הרגלים של נדיבות ואכפתיות.

כך נוכל להצטרף למועדון יקר הערך של "מחוללי השינוי" - אנשים התורמים בקביעות לשיפור חייהם של אחרים וליצירת עולם טוב יותר. מתקן "זורע צדקות" מאפשר את התנועה של נתינה מתמשכת.

הביטוי "זורע צדקות"

"זוֹרֵעַ צְדָקוֹת. מַצְמִיחַ יְשׁוּעוֹת" (סידור התפילה).

השאלה המיידית שעולה מהתבוננות בציטוט הנ"ל: היא מה הקשר בין מצוות הצדקה לזריעה?
הסיבה ללשון 'זריעה' בהקשר של מצוות הצדקה: ידוע שכאשר זורעים גרעין באדמה, צומח צמח גדול יותר מהגרעין בכמות ובאיכות. צמיחה זו נעשית על ידי הזריעה, בה הגרעין נרקב ונבלע באדמה. על ידי כניסת הגרעין לאדמה, הוא מעורר את כח הצומח של האדמה, שמצמיח דברים בריבוי גדול. כך גם בצדקה: הצדקה שאנו נותנים עולה למעלה למקור הנשמות, בו כלולים כל המדות של ה', כולל החסד. על ידי נתינת הצדקה, נעורר כח הצמח של הארץ העליונה (השכינה), שמצמיחה חסד ואור הרבה יותר גדול מהצדקה שזורעים בה. כך, על ידי נתינת הצדקה, ניתן להביא גילוי אלוקי ואור אין סוף לתפילה, ולחיים מלאים של עולם הבא.

בנוסף, על בסיס הפסוק :
"וילבש צדקה כשריון וכובע ישועה בראשו"(ישעיה נ"ט).

מסביר האדמו"ר הזקן - "ודרשו רז"ל מה שריון זה כל קליפה וקליפה מצטרפת לשריון גדול אף צדקה כל פרוטה ופרוטה מצטרפת לחשבון גדול".

בהשראת המקורות הנ"ל עיצבנו* קופת צדקה חדשנית שמאפשרת שימוש חוזר במטבעות (שקשה יותר ויותר להשיג בימים אלה) כדי:
1. לאפשר תרומה יום יומית של מטבעות בתפילה 
2. למנוע את הצורך לחפש כל פעם מטבעות מחדש

* אסף מתן - עיצוב סכמתי,  בועז מתן - עיצוב פרטני





קופת צדקה - זורע צדקות









תאי אחסון

תא תרומה - התא בו תורמים במהלך התפילה (לפני הודו/ב"ויברך דוד")
תא אחסון מטבעות - משמש לאחסון מספר קבוע של מטבעות. מתא אחסון זה לוקחים מטבעות בזמן התפילה ותורמים בתא הימני - "תא התרומה" 
תא אחסון שטרות - בתא זה ניתן לאחסן שטרות כסף בשווי הכולל של "תא אחסון המטבעות"

אופן השימוש

בכל יום מימי החול בזמן התפילה לוקחים מטבע או מטבעות מתא האחסון הנשלף ותורמים בחריץ בתרומה.כאשר המטבעות בתא האחסון מסתיימים. תורמים שטר בשווי כל תכולת תא התרומה לקופת בית הכנסת (ניתן לשים שטרות בתא אחסון השטרות בשווי הכולל של תא התרומה). באופן דומה, ניתן להעביר את סכום התרומה ב"ביט" או "פייבוקס" לתרומה עבור מוסד צדקה נבחר. לאחר התרומה יש להחזיר את כל המטבעות מתא התרומה לתא האחסון האמצעי.

באופן זה לעולם לא "תתקע" בלי בטבעות לתרומה.

תמונות נוספות


מודל 3 תאים




מודל 2 תאים




אריזה

מילות סיכום

שמחתי מאד לבצע את הפרוייקט בשיתוף עם אחי בועז מתן. יהיו אולי כאלו שיתהו על השילוב של רוחניות וטכנולוגיה, אבל זה חלק ממי שאני. אני להוט להתחבר, לשתף ולדון עם אחרים שחולקים נקודות מבט דומות.
ניתן ליצור קשר בתגובות וכן בקישור הזה: http://linktr.ee/asafmatan

רשיון





The license code "CC BY-NC-ND" refers to one of the Creative Commons licenses, specifically:

CC (Creative Commons): This is the organization that offers various standardized licenses to enable content creators to share their work with specific conditions.


BY (Attribution): This element requires that the original creator must be credited whenever the work is used or shared. The user must acknowledge the authorship in a manner specified by the creator or licensor.


NC (Non-Commercial): The work can be used and shared, but only for non-commercial purposes. This means it cannot be used in ways that are primarily intended for or directed toward commercial advantage or monetary compensation.


ND (No Derivatives): The work can be shared, but it must remain unchanged and whole, meaning no derivatives or adaptations of the work are allowed. This means you cannot edit, alter, or build upon the work in any way.









20 באפריל 2023

"זורע צדקות" - "Charity Sower" - אביב 2023

תקציר

"זוֹרֵעַ צְדָקוֹת. מַצְמִיחַ יְשׁוּעוֹת" (סידור התפילה).

השאלה המיידית שעולה מהתבוננות בציטוט הנ"ל: היא מה הקשר בין מצוות הצדקה לזריעה?
הסיבה ללשון 'זריעה' בהקשר של מצוות הצדקה: ידוע שכאשר זורעים גרעין באדמה, צומח צמח גדול יותר מהגרעין בכמות ובאיכות. צמיחה זו נעשית על ידי הזריעה, בה הגרעין נרקב ונבלע באדמה. על ידי כניסת הגרעין לאדמה, הוא מעורר את כח הצומח של האדמה, שמצמיח דברים בריבוי גדול. כך גם בצדקה: הצדקה שאנו נותנים עולה למעלה למקור הנשמות, בו כלולים כל המדות של ה', כולל החסד. על ידי נתינת הצדקה, נעורר כח הצמח של הארץ העליונה (השכינה), שמצמיחה חסד ואור הרבה יותר גדול מהצדקה שזורעים בה. כך, על ידי נתינת הצדקה, ניתן להביא גילוי אלוקי ואור אין סוף לתפילה, ולחיים מלאים של עולם הבא.

בנוסף, על בסיס הפסוק :
"וילבש צדקה כשריון וכובע ישועה בראשו"(ישעיה נ"ט).

מסביר האדמו"ר הזקן - "ודרשו רז"ל מה שריון זה כל קליפה וקליפה מצטרפת לשריון גדול אף צדקה כל פרוטה ופרוטה מצטרפת לחשבון גדול".

בהשראת המקורות הנל בניתי תיבת צדקה הכוללת רכיב GPS וחיישן מרחק אשר מתעדת על מפת העולם את תהליך זריעת הצדקות. בסוף כל מחזור תרומה של סט מטבעות שנמצא בתא נפרד, הכסף מועבר לצדקה בPayBox. תהליך "זריעת הצדקות" הפיזי במקומות גאוגרפיים שונים מהווה פעולה חינוכית חשובה, המפנימה את מושג הנתינה שבמצווה.




תאור הפרוייקט

תאור מפורט של הפרוייקט ורשימת רכיבים, כלים, תוכנות וקישורים, ניתן למצוא כאן:

כאן, בבלוג בשפת הקודש אתאר בקצרה את רשימת הרכיבים וצילומי מסך של תהליך הבניה והמוצר המוגמר.

התכנון מבוסס על מערכת מוכללת של חברת Seeed בשם Wio Terminal.

מדריך מעולה על הפלטפורמה:

Wio Terminal כולל רכיבים רבים בנוסף למעבד SAMD51, ובינהם שני שקעי Grove לחיבור התקנים פריפריאליים סטנדרטיים של חברת Seeed וכן התקנים המתבססים על תקשורת I2C של חברות אחרות כמו חיישן המרחק של חברת Adafruit. בפרוייקט זה השתמשתי בשני רכיבים פריפראיליים:
1. GPS Air 530 - לשם איתור מקום התרומה
2. חיישן מרחק VCNL4040 - זיהוי של הכנסת מטבע לקופה

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

כפי שניתן לראות בתרשים הזרימה בהמשך, ישנם שני אופציות הפעלה. עם חיבור Wifi וללא. כאשר לוחצים על ה-Joystick בקדמת המסך תוך כדי הפעלת המחשב, מתבצע חיבור לרשת הWifi. חיבור לאינטרנט נדרש רק כאשר רוצים לשדר את כל הקורדינטות שנאספו עד כה לשירות הענן Wappsto, שמראה בצורה וזואלית באיזה מקום נתרם הכסף. כדי לשדר את כל הקורדינטות שנאספו לWappsto יש ללחוץ על כפתור B (הכפתור האמצעי מעל wio terminal) כאשר המחשב נמצא במוד Wifi.

מידע נוסף ורלוונטי על שלבי החיבור של Wio Terminal לשרות הענן Wappsto:

כאשר המחשב מאותחל בצורה רגילה. הWifi כבוי, חיישני הGPS והמרחק פועלים וניתן לראות את הקורדינטות המיקום הנוכחיות במסך אחד, ובמסך השני ניתן לראות כמה מטבעות נתרמו. כדי לדפדף בין המסכים לוחצים על כפתור C, השמאלי. מעל Wio Terminal

פרוייקט שהתבססתי עליו עבור קריאת נתוני הGPS:

תרשים זרימה:


תרשים זרימה


רכיבים

  • Seeed Wio Terminal Main controller
  • Seeed GPS Module Air 530
  • חיישן מרחק VCNL4040 של חברת Adafruit
  • כבל USB Type A- TypeC עם מחבר זוויתי
כאמור, קישורים לחלקים מופיעים בפרסום באתר Hackster בקישור למעלה.

הדפסות תלת מימד והרכבה

זורע צדקות - עיצוב כללי 



פאנל קדמי



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



הרכבת התקני הנעילה

תא צדדי שמכיל מטבעות שלא נתרמו




תא צדדי ותא ראשי

חומרה, חיווט ותוכנה

שלושת רכיבי החומרה:

Wio Terminal - Picture from Seeed website


Air 530 GPS module - Picture from Seeed website



VCNL4040 - Picture from Adafruit website


חיווט חשמלי


פרויקט זה אין צורך בהלחמות. החיבורים לרכיב הGPS וחיישן הקרבה התאפשרו ע"י שימוש במחברי Grove סטנדרטיים.


דיאגרמת חיבורים




חיבור התומכים ל Wio Terminal




הרכבת הGPS וחיישן המרחק בתא במרכזי



בדיקה ראשונית של מסך הGPS




מסך מונה המטבעות





מסך הפתיחה




מוכנים לנסיעה (בטיחותית) של "זריעת צדקות"



ציון מקומות התרומה לאחר העלאת הקורדינטות לאתר Wappsto


קוד המקור


// Written by Asaf Matan
// GPS code is based on
// https://www.hackster.io/SeeedStudio/wio-terminal-gps-ad70e2 Arduino/Wappsto
// example
#include <Adafruit_VCNL4040.h>
#include <SPI.h>
#include "TFT_eSPI.h"
#include <Seeed_FS.h>
#include <SoftwareSerial.h>
#include <TinyGPS++.h>
#include <WiFiClientSecure.h>
#include <WiFiMulti.h>
#include "Free_Fonts.h"
#include "RawImage.h"
#include "SD/Seeed_SD.h"
#include "Wappsto.h"
#include "WappstoValueTypes.h"
#include "wappsto_config.h"
#include "my_secrets.h"
WiFiMulti wifiMulti;
WiFiClientSecure client;
Wappsto wappsto(&client);
#define BUZZER_PIN WIO_BUZZER
Network *myNetwork;
Device *myDevice;
Value *myLatitudeValue;
Value *myLongitudeValue;
DeviceDescription_t myDeviceDescription = {
.name = "Location Device1",
.product = "",
.manufacturer = "",
.description =
"Example sending location shown on map in the Wappsto dashboard",
.version = "1.0",
.serial = "00001",
.protocol = "Json-RPC",
.communication = "WiFi",
};
double myLatitude = MY_LAT;
double myLongitude = MY_LON;
void updateLocation(double valLatitude, double valLongitude) {
myLatitudeValue->report(valLatitude);
myLongitudeValue->report(valLongitude);
}
void refreshLocationCallback(Value *value) {
// Nothing to read from wappsto no nothing to refresh
}
bool skip_wifi = false;
const char *ssid = MY_SSID;
const char *password = MY_PASS;
Adafruit_VCNL4040 vcnl4040 = Adafruit_VCNL4040();
SoftwareSerial mySerial(0, 1); // RX, TX
TinyGPSPlus gps;
TFT_eSPI tft;
TinyGPSCustom ExtLat(gps, "GPGGA", 3); // N for Latitude
TinyGPSCustom ExtLng(gps, "GPGGA", 5); // E for Longitude
const float pi = 3.1415;
int menu = 0, p_menu = 3;
int logging = 0, sat_n = 0;
int coin_counter = 0;
int wappsto_transmited_lines = 0;
String F_name = "db.txt";
String p_hour, p_lat, p_lng, p_alt, p_sat, p_date;
String p_coin_counter;
// for Satellites position
static const int MAX_SATELLITES = 40;
TinyGPSCustom totalGPGSVMessages(gps, "GPGSV",
1); // $GPGSV sentence, first element
TinyGPSCustom messageNumber(gps, "GPGSV",
2); // $GPGSV sentence, second element
TinyGPSCustom satsInView(gps, "GPGSV", 3); // $GPGSV sentence, third element
TinyGPSCustom satNumber[4]; // to be initialized later
TinyGPSCustom elevation[4];
TinyGPSCustom azimuth[4];
TinyGPSCustom snr[4];
struct {
bool active;
int elevation;
int azimuth;
int snr;
int dsp;
} sats[MAX_SATELLITES];
String N_date, hour0;
bool sd;
String lat0, lng0;
void disp_title() {
if (menu == 0) {
tft.setFreeFont(FF0);
tft.fillScreen(TFT_BLACK);
tft.setTextSize(3);
tft.drawString("GPS", 120, 3);
tft.setTextSize(2);
tft.drawString("Date", 30, 42);
tft.drawString("Time", 30, 74);
tft.drawString("LAT", 30, 106);
tft.drawString("LONG", 30, 138);
tft.drawString("ALT", 30, 170);
tft.drawString("Satellites", 30, 202);
if (sd != true) {
tft.drawChar(295, 223, 'S', TFT_WHITE, TFT_RED, 2);
tft.drawChar(307, 223, 'D', TFT_WHITE, TFT_RED, 2);
}
p_hour = " ";
p_lat = " ";
p_lng = " ";
p_alt = " ";
p_sat = " ";
p_date = " ";
} else if (menu == 1) {
tft.setFreeFont(FF0);
tft.fillScreen(TFT_BLACK);
tft.setTextSize(3);
tft.drawString("Stats", 120, 3);
tft.setTextSize(2);
tft.drawString("Coins", 30, 42);
p_coin_counter = " ";
}
}
String doubleToString(double value, int precision) {
char buffer[20];
dtostrf(value, 0, precision, buffer);
return String(buffer);
}
void displayInfo() {
if (gps.location.isValid()) {
lat0 = doubleToString(gps.location.lat(), 6);
lng0 = doubleToString(gps.location.lng(), 6);
} else {
lat0 = "0";
lng0 = "0";
}
if (gps.date.isValid()) {
N_date = String(gps.date.day()) + "/" + String(gps.date.month()) + "/" +
String(gps.date.year());
} else {
N_date = "00/00/0000";
}
if (gps.time.isValid()) {
hour0 = "";
if (gps.time.hour() < 10) hour0 = "0";
hour0 += gps.time.hour();
hour0 += ":";
if (gps.time.minute() < 10) hour0 += "0";
hour0 += gps.time.minute();
hour0 += ":";
if (gps.time.second() < 10) hour0 += "0";
hour0 += gps.time.second();
} else {
hour0 = "00:00:00";
}
}
void displayInfoData(void) {
if (menu == 0) { // menu 0
if (menu != p_menu) {
disp_title();
p_menu = menu;
}
if (N_date != p_date) { // Date
tft.fillRect(100, 42, 120, 16, TFT_BLACK);
tft.drawString(N_date, 100, 42);
p_date = N_date;
}
if (hour0 != p_hour) { // Time
tft.fillRect(100, 74, 120, 16, TFT_BLACK);
tft.drawString(hour0, 100, 74);
p_hour = hour0;
}
if (lat0 != p_lat) { // Latitude
tft.fillRect(100, 106, 132, 16, TFT_BLACK);
tft.drawString(lat0, 100, 106);
p_lat = lat0;
}
if (lng0 != p_lng) { // Longitude
tft.fillRect(100, 138, 156, 16, TFT_BLACK);
tft.drawString(lng0, 100, 138);
p_lng = lng0;
}
if (String(gps.altitude.meters()) != p_alt) { // Altimeter
tft.fillRect(100, 170, 60, 16, TFT_BLACK);
tft.drawString(String(gps.altitude.meters()), 100, 170);
p_alt = String(gps.altitude.meters());
}
if (String(gps.satellites.value()) != p_sat) { // N of Satellites
tft.fillRect(160, 202, 32, 16, TFT_BLACK);
tft.drawString(String(gps.satellites.value()), 160, 202);
p_sat = String(gps.satellites.value());
}
} // end of menu=0
else if (menu == 1) {
if (menu != p_menu) {
disp_title();
p_menu = menu;
}
if (String(coin_counter) != p_coin_counter) { // Coin Counter
tft.fillRect(100, 42, 120, 16, TFT_BLACK);
tft.drawString(String(coin_counter), 100, 42);
p_coin_counter = String(coin_counter);
}
}
}
void playTone(int tone, int duration) {
for (long i = 0; i < duration * 1000L; i += tone * 2) {
digitalWrite(BUZZER_PIN, HIGH);
delayMicroseconds(tone);
digitalWrite(BUZZER_PIN, LOW);
delayMicroseconds(tone);
}
}
void writeIntToSD(const char *filename, int value) {
File file = SD.open(filename, FILE_WRITE);
if (file) {
file.print(value);
file.close();
} else {
Serial.println("Error writing to file");
}
}
int readIntFromSD(const char *filename) {
File file = SD.open(filename, FILE_READ);
int value = 0;
if (file) {
value = file.parseInt();
file.close();
} else {
Serial.println("Error reading from file");
}
return value;
}
String *parseCSV(String data, int &fieldCount) {
String *fields =
new String[data.length()]; // Allocate memory for the maximum
// expected number of fields
fieldCount = 0;
// Split the data string into fields
int lastIndex = 0;
int commaIndex = data.indexOf(',');
while (commaIndex != -1) {
fields[fieldCount] = data.substring(lastIndex, commaIndex);
fieldCount++;
lastIndex = commaIndex + 1;
commaIndex = data.indexOf(',', lastIndex);
}
fields[fieldCount] = data.substring(lastIndex);
fieldCount++;
return fields;
}
void initializeWifi(void) {
Serial.println("Initializing WiFi");
wifiMulti.addAP(ssid, password);
while (wifiMulti.run() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void proximity_handler(void) {
int proximity_value;
proximity_value = vcnl4040.getProximity();
if (proximity_value > 50) {
if (sd == true) {
Serial.println(F_name);
File myFile = SD.open(F_name, FILE_APPEND);
if (myFile) {
myFile.print(N_date);
myFile.print(",");
myFile.print(hour0);
myFile.print(",");
myFile.print(lat0);
myFile.print(",");
myFile.print(lng0);
myFile.print(",");
myFile.println(gps.altitude.meters());
myFile.close();
}
}
Serial.println("Proximity Value:");
Serial.println(proximity_value);
coin_counter += 1;
// Store Coin Counter to SD
writeIntToSD("coin_counter.txt", coin_counter);
delay(1500);
}
}
void wappsto_transmit(void) {
if (digitalRead(WIO_KEY_B) == LOW) {
int line_counter = 0;
// Log to SD card
if (sd == true) {
File myFile = SD.open(F_name, FILE_READ);
if (myFile) {
while (myFile.available()) {
int fieldCount;
String line = myFile.readStringUntil('\n');
if (line.length() > 0) {
Serial.println(line);
String *fields = parseCSV(line, fieldCount);
// Print the fields
for (int i = 0; i < fieldCount; i++) {
Serial.print("Field ");
Serial.print(i);
Serial.print(": ");
Serial.println(fields[i]);
if (i == 2) {
myLatitude = fields[i].toDouble();
} else if (i == 3) {
myLongitude = fields[i].toDouble();
}
}
delete[] fields; // Deallocate the memory used for the fields array
if (line_counter >= wappsto_transmited_lines) {
if (myLatitude > 0) {
Serial.print("Transmited line: ");
Serial.println(line_counter);
updateLocation(myLatitude, myLongitude);
wappsto.dataAvailable();
}
wappsto_transmited_lines++;
}
line_counter++;
}
}
myFile.close();
Serial.print("Updating transmitted lines to: ");
Serial.println(wappsto_transmited_lines);
writeIntToSD("wappsto_transmitted_lines.txt", wappsto_transmited_lines);
} else {
Serial.println("Failed to open file");
}
while (digitalRead(WIO_KEY_B) == LOW) {
}
}
}
}
void setup() {
Serial.begin(57600);
pinMode(WIO_KEY_A, INPUT_PULLUP);
pinMode(WIO_KEY_B, INPUT_PULLUP);
pinMode(WIO_KEY_C, INPUT_PULLUP);
pinMode(WIO_5S_PRESS, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
if (digitalRead(WIO_5S_PRESS) == HIGH) {
mySerial.begin(9600);
}
// Let things stablize (better than !Serial for this case)
delay(2000);
tft.begin();
tft.setRotation(3);
Serial.print("Initializing SD card...");
if (!SD.begin(SDCARD_SS_PIN, SDCARD_SPI)) {
Serial.println("initialization failed!");
sd = false;
tft.drawChar(295, 223, 'S', TFT_WHITE, TFT_RED, 2);
tft.drawChar(307, 223, 'D', TFT_WHITE, TFT_RED, 2);
} else {
Serial.println("initialization done.");
sd = true;
}
// Display splash screen
tft.setFreeFont(&FreeSerifBoldItalic12pt7b);
tft.fillScreen(TFT_WHITE);
tft.setTextColor(TFT_RED);
tft.drawString("Charity Sower", 90, 10);
drawImage<uint16_t>("coin.bmp", 100, 50);
delay(5000);
tft.fillScreen(TFT_BLACK); // Black background
tft.setTextColor(TFT_YELLOW);
// read the stored integer from the SD card
coin_counter = readIntFromSD("coin_counter.txt");
// for satellites position
// Initialize all the uninitialized TinyGPSCustom objects
for (int i = 0; i < 4; ++i) {
satNumber[i].begin(gps, "GPGSV", 4 + 4 * i); // offsets 4, 8, 12, 16
elevation[i].begin(gps, "GPGSV", 5 + 4 * i); // offsets 5, 9, 13, 17
azimuth[i].begin(gps, "GPGSV", 6 + 4 * i); // offsets 6, 10, 14, 18
snr[i].begin(gps, "GPGSV", 7 + 4 * i); // offsets 7, 11, 15, 19
}
if (!vcnl4040.begin()) {
Serial.println("Couldn't find VCNL4040 chip");
while (1)
;
}
Serial.println("Found VCNL4040 chip");
if (digitalRead(WIO_5S_PRESS) == HIGH) {
Serial.println("Skip Wifi Setup");
skip_wifi = true;
} else {
initializeWifi();
initializeNtp();
wappsto.config(network_uuid, ca, client_crt, client_key, 5, NO_LOGS);
if (wappsto.connect()) {
Serial.println("Connected to Wappsto");
} else {
Serial.println("Could not connect");
}
Serial.println("Create network");
myNetwork = wappsto.createNetwork("Location Example");
Serial.println("Create Device");
myDevice = myNetwork->createDevice(&myDeviceDescription);
Serial.println("Create Default val");
myLatitudeValue = myDevice->createNumberValue(&defaultLatitudeParameter);
myLongitudeValue = myDevice->createNumberValue(&defaultLongitudeParameter);
Serial.println("Register callbacks");
myLatitudeValue->onRefresh(&refreshLocationCallback);
myLongitudeValue->onRefresh(&refreshLocationCallback);
Serial.println("read last entry transmitted to wappsto");
wappsto_transmited_lines = readIntFromSD("wappsto_transmitted_lines.txt");
Serial.print("Last transmitted line:");
Serial.println(wappsto_transmited_lines);
}
}
void loop() {
if (skip_wifi == false) {
Serial.println('.');
wappsto.dataAvailable();
delay(500);
}
while (mySerial.available() > 0) {
char c = mySerial.read();
// Serial.print(c);
gps.encode(c);
}
displayInfo();
displayInfoData();
proximity_handler();
// Screen toggle
if (digitalRead(WIO_KEY_C) == LOW) { // Page(menu) change
menu++;
if (menu > 1) menu = 0;
while (digitalRead(WIO_KEY_C) == LOW) {
}
}
if (coin_counter > 10) {
playTone(500, 1000);
coin_counter = 0;
}
// Handle transmitting of stored coordinates to wappstop incase we are in wifi
// on mode.
wappsto_transmit();
}

התוצאה הסופית












מילות סיכום

שמחתי מאד לבצע את הפרוייקט בשיתוף עם חברת Seeed. יהיו אולי כאלו שיתהו על השילוב של רוחניות וטכנולוגיה, אבל זה חלק ממי שאני. אני להוט להתחבר, לשתף ולדון עם אחרים שחולקים נקודות מבט דומות.
ניתן ליצור קשר בתגובות וכן בקישור הזה: http://linktr.ee/asafmatan




10 במרץ 2023

FreedomBox פסח 2023

תקציר

"מִמִּצְרַיִם גְּאַלְתָּנוּ, מִבֵּית עֲבָדִים פְּדִיתָנוּ".

מדי יום, אנו מזכירים קטע זה בתפילת שחרית לאחר קריאת שמע, לזכר המאורע המופלא של יציאת מצרים.

פסוק זה הוא גם חלק בולט מניגון ידוע המושר במהלך חג הפסח היהודי.

בהשראת המסורת הזו, יצרתי "Freedom Box", שנועדה להטעין ולרענן אנשים כשהם מרגישים לחוצים ורוצים לפרוץ מהמיצר. כל שצריך לעשות הוא ללחוץ על הכפתור האדום ולהתחדש!

צורת המחומש מעוטרת בציורים וקטוריים שנרכשו מחברת ShutterStock, כולם קשורים למסורת חג הפסח. פאה אחת העיצוב כולל גריל רמקול 3W, בעוד שהכיסוי מצויד בכפתור Arcade אדום וענק מבית Adafruit.



תאור הפרוייקט

תאור מפורט של הפרוייקט ורשימת רכיבים, כלים, תוכנות וקישורים, ניתן למצוא כאן:

כאן, בבלוג בשפת הקודש אתאר בקצרה את רשימת הרכיבים וצילומי מסך של תהליך הבניה והמוצר המוגמר.

התכנון מבוסס על מעבד אודיו ומעבד ESP32 זעיר מבית Seeed.

רכיבים

  • Seeed Studio XIAO ESP32C3 - Main processor
  • DFRobot, DFPlayer mini - Music player/Amplifier
  • לוח MDF 3mm. במידות 40X70 ס"מ
  • כפתור ארקייד גדול אדום מAdafruit
  • רמקול 3W מAdafruit + פלוס מכסה מותאם בהדפסת תלת מימד.
  • כבל USB Type A-TypeC. 
  • כבלי גישור מסוג Dupont נקבה לנקבה.
כאמור, קישורים לחלקים מופיעים בפרסום באתר Hackster בקישור למעלה.

חיתוך ובניה

היות וקניתי את התמונות הווקטוריות ShutterStock, אני יכול לשתף רק את המבנה הגולמי של הקופסא שבוצע באתר https://en.makercase.com


קבצי DXF. ניתן קישור לפרסום לHackster.

חיתוך לייזר באמצעות מכונת Ortur Laser Master 3.


תחילת הרכבת הפאות


הרכבת הרמקול



הרכבת הלחצן



הרכבת המעבדים 


התוצאה הסופית





תוכנה, חומרה וחיווט

השתמשתי בשני המדריכים הנהדרים האלה במדריכים קדימה כדי לחבר את מעבד xiao ESP32 ל-DFPlayer.


מעבד XIAO של חברת Seeed

מקור: אתר Seeed

מעבד האודיו והמגבר

מקור: אתר DFrobot


חיווט חשמלי






יצרתי גרסה קצרה לשיר הפסח (ששר זבולון נתנוב בניגון החב"די) ושמרתי אותו כקובץ mp3.00001 בכרטיס  mini SD של DFPlayer. 
כדי להשמיע את השיר, השתמשתי בפונקציית UART של פינים 6 ו-7 של מעבד Xiao.
בכל פעם שהלחצן נלחץ, נשלחת פקודה סיריאלית ל-DFPlayer כדי להפעיל את קובץ השמע המתאים. הסבר מפורט מאד על הרכיב ניתן למצוא במדריכים של DFRobot שצירפתי למעלה.

קוד המקור


/***************************************************
This code utilize Freedom Box design based on example by
By [Angelo qiao](Angelo.qiao@dfrobot.com)
Adaptations and xiao code by Asaf Matan
GNU Lesser General Public License.
****************************************************/
#include "Arduino.h"
#include <HardwareSerial.h>
#include "DFRobotDFPlayerMini.h"
//Define two Serial devices mapped to the two internal UARTs
HardwareSerial MySerial0(0);
DFRobotDFPlayerMini myDFPlayer;
const int buttonPin = D2;
// State of the push button
int buttonState = 0;
void printDetail(uint8_t type, int value);
void setup()
{
delay(5000);
MySerial0.begin(9600, SERIAL_8N1, -1, -1);
Serial.begin(115200);
pinMode(buttonPin, INPUT_PULLUP);
Serial.println();
Serial.println(F("DFRobot DFPlayer Mini Demo"));
Serial.println(F("Initializing DFPlayer ... (May take 3~5 seconds)"));
if (!myDFPlayer.begin(MySerial0)) { //Use softwareSerial to communicate with mp3.
Serial.println(F("Unable to begin:"));
Serial.println(F("1.Please recheck the connection!"));
Serial.println(F("2.Please insert the SD card!"));
while(true);
}
Serial.println(F("DFPlayer Mini online."));
myDFPlayer.volume(20); //Set volume value. From 0 to 30
myDFPlayer.play(1); //Play the first mp3
}
void loop()
{
buttonState = digitalRead(buttonPin);
if (buttonState == LOW)
{
myDFPlayer.play(1);
if (myDFPlayer.available())
{
printDetail(myDFPlayer.readType(), myDFPlayer.read()); //Print the detail message from DFPlayer to handle different errors and states.
}
}
delay(500);
}
void printDetail(uint8_t type, int value){
switch (type) {
case TimeOut:
Serial.println(F("Time Out!"));
break;
case WrongStack:
Serial.println(F("Stack Wrong!"));
break;
case DFPlayerCardInserted:
Serial.println(F("Card Inserted!"));
break;
case DFPlayerCardRemoved:
Serial.println(F("Card Removed!"));
break;
case DFPlayerCardOnline:
Serial.println(F("Card Online!"));
break;
case DFPlayerPlayFinished:
Serial.print(F("Number:"));
Serial.print(value);
Serial.println(F(" Play Finished!"));
break;
case DFPlayerError:
Serial.print(F("DFPlayerError:"));
switch (value) {
case Busy:
Serial.println(F("Card not found"));
break;
case Sleeping:
Serial.println(F("Sleeping"));
break;
case SerialWrongStack:
Serial.println(F("Get Wrong Stack"));
break;
case CheckSumNotMatch:
Serial.println(F("Check Sum Not Match"));
break;
case FileIndexOut:
Serial.println(F("File Index Out of Bound"));
break;
case FileMismatch:
Serial.println(F("Cannot Find File"));
break;
case Advertise:
Serial.println(F("In Advertise"));
break;
default:
break;
}
break;
default:
break;
}
}
view raw freedombox.ino hosted with ❤ by GitHub

מילות סיכום

שמחתי מאד לבצע את הפרוייקט בשיתוף עם חברת Seeed. יהיו אולי כאלו שיתהו על השילוב של רוחניות וטכנולוגיה, אבל זה חלק ממי שאני. אני להוט להתחבר, לשתף ולדון עם אחרים שחולקים נקודות מבט דומות.
ניתן ליצור קשר בתגובות וכן בקישור הזה: http://linktr.ee/asafmatan

1 במרץ 2023

משלוח מנות CrackTheCode-Purim!

תקציר

משלוח מנות ייחודי אשר מהווה חדר בריחה קטן על בסיס המשחק המפורסם Mastermind (או בעברית "בול פגיעה")

כדי לפתוח את  משלוח המנות (או כדי להגיע ל-"אוצר" :-), ישנם שני מסלולים

א. פתרון חידת הגיון הקשורה לפורים.

ב. משחק בול פגיעה אלקטרוני.












תאור הפרוייקט

היצירה מבוססת על העיצוב של MICHAEL KLEMENTS   - קישור

השיפורים שעשיתי
  • תכנון מחודש של החלל 
  • שימוש במעבד ESP32
  • התאמת העיצוב לפורים (תמונות שנקנו בShutterStock)

מסלולי פתרון


הראשון - חידת הגיון

"בשורשם של דברים, הטוב והרע חד הם, לכאורה..."

תזכורת: הפתרון הוא מספרי.

השני - בול פגיעה 

בתחילת המשחק בוחר כל שחקן מספר בן כמה ספרות, כאשר נהוג לבחור מספר בן ארבע ספרות, אסורות חזרות על ספרות. לדוגמה, 1234 הוא מספר חוקי (גם '0' נחשב כספרה חוקית). 

המשחק מתנהל ע"י סדרה של ניחושים, לאחר הניחוש המחשב מגלה לשחקן ע"י נורות לד אדומות את מספר ה"פגיעות" ונורות לד ירוקות, את מספר ה"בול" (הלדים לא מגלים את מיקום הספרות של הבול או הפגיעה).

בול - מצב בו אחת הספרות במספר המנוחש זהה לספרה במספר המקורי, וממוקמת נכון.

פגיעה - מצב בו אחת הספרות במספר המנוחש זהה לספרה במספר המקורי, אך ממוקמת במיקום שונה.

הוראות הפעלה

  • חבר את שקע לתקע (איך אפשר בלי??) 

  • העבר את מתג ההפעלה בצד ימין כלפי מעלה.

  • לאחר הופעת מסך הפתיחה ותצוגת הלדים הזן ספרה

    • שינוי הספרה ע"י סיבוב החוגה

    • אישור הבחירה ע"י לחיצה על החוגה

    • חזור על הפעולה עד להשלמת 4 ספרות

    • המתן לחיווי בול/פגיעה

    • אם ישנו "בול" המנעול הפנימי יפתח

בניה

חיתוך לייזר
הרכבה
















רכיבים

  • לדים אדומים - 4
  • לדים ירוקים - 4
  • לחצן בחירה / Encoder
  • מעבד ESP32
  • מסך OLED SSD1306
  • מפסק - On/Off

חיווט

Number

Left Pinout

Name

Right Pinout

Name

1

EN


23

EncSW

2

VP


SCL(22)

Oled-Clock

3

VN


TXD


4

34


RXD


5

35


SDA(21)

Oled-Data

6

32

LedGreen4

19

ServoSigna

7

33

LedRed3

18

LedRed4

8

25

LedGreen3

5

EncA

9

26

LedGreen2

TX2


10

27

LedRed1

RX2


11

14

LedRed2

4

OledReset

12

12

LedGreen1

2


13

13


15

EncB

14

GND


GND

Oled/Enc-Sw/Ser

15

VIN

ServoPower

3.3

OledVin


קוד





//Code Breaker
//Source Michael Klements Arduino Uno code
//Converted to ESP32 - Asaf Matan
#include <SPI.h> //Import libraries to control the OLED display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Servo.h> //Import library to control the servo
#include "AiEsp32RotaryEncoder.h"
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define SCREEN_ADDRESS 0x3D ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Servo lockServo; // create servo object to control a servo
#define ROTARY_ENCODER_A_PIN 5
#define ROTARY_ENCODER_B_PIN 15
#define ROTARY_ENCODER_BUTTON_PIN 23
#define ROTARY_ENCODER_STEPS 4
byte correctNumLEDs[4] = {27,14,33,18}; //Pin numbers for correct number LEDs (Indicate a correct digit)
byte correctPlaceLEDs[4] = {12,26,25,32}; //Pin numbers for correct place LEDs (Indicate a correct digit in the correct place)
byte code[4] = {0,0,0,0}; //Create an array to store the code digits
byte codeGuess[4] = {0,0,0,0}; //Create an array to store the guessed code digits
byte guessingDigit = 0; //Tracks the current digit being guessed
byte numGuesses = 0; //Tracks how many guesses it takes to crack the code
boolean correctGuess = true; //Variable to check whether the code has been guessed correctly, true initially to generate a new code on startup
byte encoderPos = 0; //Current value of encoder position, digit being input form 0 to 9
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);
int buttonState = 0;
void IRAM_ATTR readEncoderISR()
{
rotaryEncoder.readEncoder_ISR();
}
/* Local Functions decleration */
void startupAni ();
void updateDisplayCode();
void generateNewCode();
void inputCodeGuess();
void checkCodeGuess();
void updateLEDs (int corNum, int corPla);
void setup()
{
Serial.begin(115200); //Starts the Serial monitor for debugging
Serial.println(F("Starting..."));
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, 9, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.setAcceleration(250);
// Let Oled diplay power up
delay(3000);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
display.clearDisplay(); //Clear display
display.setTextColor(SSD1306_WHITE); //Set the text colour to white
lockServo.attach(19);
for(int i=0 ; i<=3 ; i++) //Define pin modes for the LEDs
{
pinMode(correctNumLEDs[i], OUTPUT);
pinMode(correctPlaceLEDs[i], OUTPUT);
digitalWrite(correctPlaceLEDs[i], LOW);
digitalWrite(correctNumLEDs[i], LOW);
}
pinMode(ROTARY_ENCODER_A_PIN, INPUT_PULLUP); //Set pinA as an input, pulled HIGH to the logic voltage
pinMode(ROTARY_ENCODER_B_PIN, INPUT_PULLUP); //Set pinB as an input, pulled HIGH to the logic voltage
delay(2000); // Pause for 2 seconds
startupAni(); //Display the startup animation
}
void loop()
{
if(correctGuess) //Code between games to reset if the guess is correct, initially true to open safe and then generate new code
{
lockServo.write(45); //Unlock the safe
delay(300);
updateLEDs (0,4); //Flashing LED sequence
delay(300);
updateLEDs (4,0);
delay(300);
updateLEDs (0,4);
delay(300);
updateLEDs (4,0);
delay(300);
updateLEDs (4,4); //Turn all LEDs on
if(numGuesses >= 1) //Check that its not the start of the game
{
display.clearDisplay(); //Clear the display
display.setTextSize(1); //Set the display text size to small
display.setCursor(35,10); //Set the display cursor position
display.print(F("In ")); //Set the display text
display.print(numGuesses); //Set the display text
display.setCursor(35,20); //Set the display cursor position
display.print(F("Attempts")); //Set the display text
display.display(); //Output the display text
delay(5000);
}
display.clearDisplay(); //Clear the display
display.setTextSize(1); //Set the display text size to small
display.setCursor(35,10); //Set the display cursor position
display.print(F("Push To")); //Set the display text
display.setCursor(35,20); //Set the display cursor position
display.print(F("Lock Safe")); //Set the display text
display.display(); //Output the display text
display.setTextSize(2); //Set the display text size back to large
boolean lock = false; //Safe is initially not locked
while(!lock) //While button is not pressed, wait for it to be pressed
{
if (rotaryEncoder.isEncoderButtonClicked())
{
Serial.println("button pressed");
lockServo.write(140); //Lock the safe
display.clearDisplay(); //Clear the display
display.setCursor(30,10); //Set the display cursor position
display.print(F("Locked")); //Set the display text
display.display(); //Output the display text
lock = true;
}
}
generateNewCode(); //Calls function to generate a new random code
updateLEDs (0,0);
correctGuess = false; //The code guess is initially set to incorrect
numGuesses = 0; //Reset the number of guesses counter
}
inputCodeGuess(); //Calls function to allow the user to input a guess
numGuesses++; //Increment the guess counter
checkCodeGuess(); //Calls function to check the input guess
guessingDigit = 0; //Reset the digit being guessed
codeGuess[0] = 0; //Reset the first digit of the code
updateDisplayCode(); //Update the displayed code
}
void updateDisplayCode() //Function to update the display with the input code
{
String temp = ""; //Temporary variable to concatenate the code string
if(!correctGuess) //If the guess is not correct then update the display
{
for (int i=0 ; i<guessingDigit ; i++) //Loops through the four digits to display them
{
temp = temp + codeGuess[i];
}
temp = temp + encoderPos;
for (int i=guessingDigit+1 ; i<=3 ; i++)
{
temp = temp + "0";
}
Serial.println(temp); //Output to Serial monitor for debugging
display.setTextSize(2); //Set the display text size
display.clearDisplay(); //Clear the display
display.setCursor(40,10); //Set the display cursor position
display.println(temp); //Set the display text
display.display(); //Update the display
}
}
void generateNewCode() //Function to generate a new random code
{
Serial.print("Code: ");
code[0]=0;
code[1]=5;
code[2]=0;
code[3]=2;
for (int i=0 ; i<= 3 ; i++) //Loops through the four digits and assigns a random number to each
{
// esp_random();
// code[i] = random(0,9); //Generate a random number for each digit
Serial.print(code[i]); //Display the code on Serial monitor for debugging
}
Serial.println();
}
void inputCodeGuess() //Function to allow the user to input a guess
{
for(int i=0 ; i<=3 ; i++) //User must guess all four digits
{
guessingDigit = i;
boolean confirmed = false; //Both used to confirm button push to assign a digit to the guess code
//rotaryEncoder.setEncoderValue(0);
updateDisplayCode();
while(!confirmed) //While the user has not confirmed the digit input
{
if (rotaryEncoder.encoderChanged())
{
Serial.println(rotaryEncoder.readEncoder());
encoderPos = rotaryEncoder.readEncoder(); //Store encoder position
codeGuess[i] = encoderPos; //If the button is pressed, accept the current digit into the guessed code
updateDisplayCode();
}
if (rotaryEncoder.isEncoderButtonClicked())
{
codeGuess[i] = encoderPos; //If the button is pressed, accept the current digit into the guessed code
updateDisplayCode();
confirmed=true; //Update the code being displayed
}
}
}
}
void checkCodeGuess() //Function to check the users guess against the generated code
{
int correctNum = 0; //Variable for the number of correct digits in the wrong place
int correctPlace = 0; //Variable for the number of correct digits in the correct place
int usedDigits[4] = {0,0,0,0}; //Mark off digits which have been already identified in the wrong place, avoids counting repeated digits twice
for (int i=0 ; i<= 3 ; i++) //Loop through the four digits in the guessed code
{
for (int j=0 ; j<=3 ; j++) //Loop through the four digits in the generated code
{
if (codeGuess[i]==code[j]) //If a number is found to match
{
if(usedDigits[j]!=1) //Check that it hasn't been previously identified
{
correctNum++; //Increment the correct digits in the wrong place counter
usedDigits[j] = 1; //Mark off the digit as been identified
break; //Stop looking once the digit is found
}
}
}
}
for (int i=0 ; i<= 3 ; i++) //Compares the guess digits to the code digits for correct digits in correct place
{
if (codeGuess[i]==code[i]) //If a correct digit in the correct place is found
correctPlace++; //Increment the correct place counter
}
Serial.print("Correct Place: ");
Serial.println(correctPlace);
updateLEDs(correctNum, correctPlace); //Calls a function to update the LEDs to reflect the guess
if(correctPlace==4) //If all 4 digits are correct then the code has been cracked
{
display.clearDisplay(); //Clear the display
display.setCursor(20,10); //Set the display cursor position
display.print(F("Cracked")); //Set the display text
display.display(); //Output the display text
correctGuess = true;
}
else
correctGuess = false;
}
void updateLEDs (int corNum, int corPla) //Function to update the LEDs to reflect the guess
{
for(int i=0 ; i<=3 ; i++) //First turn all LEDs off
{
digitalWrite(correctNumLEDs[i], LOW);
digitalWrite(correctPlaceLEDs[i], LOW);
}
for(int j=0 ; j<=corNum-1 ; j++) //Turn on the number of correct digits in wrong place LEDs
{
digitalWrite(correctNumLEDs[j], HIGH);
}
for(int k=0 ; k<=corPla-1 ; k++) //Turn on the number of correct digits in the correct place LEDs
{
Serial.print("Turn correct place led number: ");
Serial.println(correctPlaceLEDs[k]);
digitalWrite(correctPlaceLEDs[k], HIGH);
}
}
void startupAni ()
{
display.setTextSize(2); //Set the display text size
display.setCursor(35,10); //Set the display cursor position
display.println(F("Crack")); //Set the display text
display.display(); //Output the display text
delay(500);
display.clearDisplay(); //Clear the display
display.setCursor(45,10);
display.println(F("The"));
display.display();
delay(500);
display.clearDisplay();
display.setCursor(40,10);
display.println(F("Code"));
display.display();
delay(500);
display.clearDisplay();
}
סיכום

כמו בפרוייקטים הקודמים, גם כאן, מה שמנחה אותי הוא השילוב בין רוחניות וטכנולוגיה.

מי שדם דומה זורם בעורקיו, מוזמן ליצור קשר asaf.matan@gmail.com או בתגובות למטה.