תקציר
"זוֹרֵעַ צְדָקוֹת. מַצְמִיחַ יְשׁוּעוֹת" (סידור התפילה).
השאלה המיידית שעולה מהתבוננות בציטוט הנ"ל: היא מה הקשר בין מצוות הצדקה לזריעה?
הסיבה ללשון 'זריעה' בהקשר של מצוות הצדקה: ידוע שכאשר זורעים גרעין באדמה, צומח צמח גדול יותר מהגרעין בכמות ובאיכות. צמיחה זו נעשית על ידי הזריעה, בה הגרעין נרקב ונבלע באדמה. על ידי כניסת הגרעין לאדמה, הוא מעורר את כח הצומח של האדמה, שמצמיח דברים בריבוי גדול. כך גם בצדקה: הצדקה שאנו נותנים עולה למעלה למקור הנשמות, בו כלולים כל המדות של ה', כולל החסד. על ידי נתינת הצדקה, נעורר כח הצמח של הארץ העליונה (השכינה), שמצמיחה חסד ואור הרבה יותר גדול מהצדקה שזורעים בה. כך, על ידי נתינת הצדקה, ניתן להביא גילוי אלוקי ואור אין סוף לתפילה, ולחיים מלאים של עולם הבא.
בנוסף, על בסיס הפסוק :
"וילבש צדקה כשריון וכובע ישועה בראשו"(ישעיה נ"ט).
מסביר האדמו"ר הזקן - "ודרשו רז"ל מה שריון זה כל קליפה וקליפה מצטרפת לשריון גדול אף צדקה כל פרוטה ופרוטה מצטרפת לחשבון גדול".
בהשראת המקורות הנל בניתי תיבת צדקה הכוללת רכיב 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
קוד המקור
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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