# 自製Wifi物聯網存錢箱
* 搭載街機常用之投幣孔模組以及mp3播放模組。
## 照片
[影片Demo](https://youtu.be/Ekf3MOymUUg)
![](https://i.imgur.com/jNEKNJb.jpg)
![](https://i.imgur.com/ysVI4uo.jpg)
![](https://i.imgur.com/DX2rPAO.jpg)
## 硬體零件
| 材料 | 示意圖 |
| -------- | -------- |
| WeMos D1 WiFi Arduino UNO 開發板ESP8266| ![](https://i.imgur.com/r71nrkS.png)|
| DFPlayer Mini Player Module | ![](https://i.imgur.com/JvQxQYf.png)|
|SD記憶卡| ![](https://i.imgur.com/mUzLACQ.jpg)|
| 多币值出口款投币器 | ![](https://i.imgur.com/83i6y9I.png )|
| 8Ω 1W 單體 | ![](https://i.imgur.com/x1hzGQb.png) |
| DC5V 3W+3W數位功率音效放大器 | ![](https://i.imgur.com/MFPGKNp.png)|
| AC ADAPTER 交換式電子變壓器 12V/1A | ![](https://i.imgur.com/GFOjBQ4.png) |
## 外觀設計
![](https://i.imgur.com/exv1kwQ.png)
* 使用CorelDraw設計,3mm木板雷射切割。
## 軟體設計
* 先在Arduino內新增ESP8266之開發板資訊,參見[教學](https://sites.google.com/site/wenyumaker/10-esp8266/02memos-d1?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)。
* 若找不到函式庫,請自行搜尋下載。
* 修改想要連線的網路ssid與password。
* 程式執行後若有連接到網路則會在Serial Port顯示IP位址,可以根據上面的IP位址去連線,記得要在同一個網域底下。
#### Main Program
```c=
#include "Arduino.h"
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"
#include <ESP8266WiFi.h>
#include <EEPROM.h>
#define M_ADDRESS 0x00
#define EEPROM_SIZE 100
// Replace with your network credentials
const char* ssid = "stu00608";
const char* password = "kuyastu5220";
WiFiServer server(80);
SoftwareSerial mySoftwareSerial(D6, D7); // RX, TX
DFRobotDFPlayerMini myDFPlayer;
// Variable to store the HTTP request
String header;
String moneyString;
long money;
int coin_1,coin_5,coin_10,coin_50;
int last_coin_1,last_coin_5,last_coin_10,last_coin_50;
int resetFlag;
unsigned long currentTime = millis();
unsigned long countTime = millis();
unsigned long previousTime = 0;
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;
void setup() {
Serial.begin(115200);
EEPROM.begin(EEPROM_SIZE);
MP3_setup();
WifiSetup();
money = EEPROMReadlong(M_ADDRESS);
Serial.print("money now is : ");
Serial.println(String(money));
pinMode(D5,INPUT);
pinMode(D2,INPUT);
pinMode(D4,INPUT);
pinMode(D3,INPUT);
countTime = millis();
}
void loop(){
ClientHandler();
if( (coin_1=digitalRead(D5)) || (coin_5=digitalRead(D2)) || (coin_10=digitalRead(D4)) || (coin_50=digitalRead(D3)) ){
money = EEPROMReadlong(M_ADDRESS);
if(coin_1 && !last_coin_1){
myDFPlayer.play(1);
Serial.println("您投入了1元");
money += 1;
}else if(coin_5 && !last_coin_5){
myDFPlayer.play(1);
Serial.println("您投入了5元");
money += 5;
}else if(coin_10 && !last_coin_10){
myDFPlayer.play(1);
Serial.println("您投入了10元");
money += 10;
}else if(coin_50 && !last_coin_50){
myDFPlayer.play(1);
Serial.println("您投入了50元");
money += 50;
}
EEPROMWritelong(M_ADDRESS,money);
}
last_coin_1 = coin_1;
last_coin_5 = coin_5;
last_coin_10 = coin_10;
last_coin_50 = coin_50;
}
```
#### DFRobotDFPlayerMini
* 參考投幣音效:https://www.youtube.com/watch?v=iA6XpqaZvCU
```c=
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"
void MP3_setup(){
mySoftwareSerial.begin(9600);
if (!myDFPlayer.begin(mySoftwareSerial)) {
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);
}
myDFPlayer.volume(10);
myDFPlayer.EQ(DFPLAYER_EQ_NORMAL);
myDFPlayer.outputDevice(DFPLAYER_DEVICE_SD);
Serial.println(F("DFPlayer Mini online."));
myDFPlayer.setTimeOut(500);
}
```
#### ESP8266WiFi
* 對網頁程式不熟的我這邊真的是個難關,花了一段時間才理解裡面的原理。Wifi+網頁UI的部分共有3個函式負責。
```c=
void WifiSetup(){
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
server.begin();
}
```
* `WifiSetup()`函式用來在Arduino初始化時連接到指定Wifi網路,其中的`WiFi.begin(ssid, password)`就是連線的函式,輸入值分別是Wifi名稱和密碼。最後會印出這個Wifi的IP位址,在Client端就依靠這個IP進入底下的網頁。
```c=
void ClientHandler(){
unsigned long startTime = millis();
WiFiClient client = server.available();
if (client) {
Serial.println("New Client.");
String currentLine = "";
currentTime = millis();
previousTime = currentTime;
while (client.connected() && currentTime - previousTime <= timeoutTime) {
currentTime = millis();
if (client.available()) {
char c = client.read();
Serial.write(c);
header += c;
if (c == '\n'){
if (currentLine.length() == 0) {
showUI(client);
break;
}else{
currentLine = "";
}
}else if (c != '\r'){
currentLine += c;
}
}
}
header = "";
client.stop();
Serial.println("Client disconnected.");
Serial.print("Used Time : ");
Serial.println(String(millis()-startTime));
Serial.print("money now : ");
money = EEPROMReadlong(M_ADDRESS);
Serial.println(String(money));
Serial.println("");
}
}
```
* `ClientHandler()`函式負責處理有Client連線到這個位址時的操作,我們用`server.available()`來偵測,在確認沒有Timeout後就會進入到`showUI()`來顯示網頁內容。
```c=
void showUI(WiFiClient client){
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
// and a content-type so the client knows what's coming, then a blank line:
moneyString = String(money);
resetFlag = (money)? false:true;
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// turns the GPIOs on and off
if (header.indexOf("GET /RESET") >= 0) {
Serial.println("GET Reset Button");
resetFlag = true;
money = 0;
EEPROMWritelong(M_ADDRESS,money);
money = EEPROMReadlong(M_ADDRESS);
client.print("<HEAD>");
client.print("<meta http-equiv=\"refresh\" content=\"0;url=/\">");
client.print("</head>");
}else if (header.indexOf("GET /REFRESH") >= 0) {
Serial.println("GET Refresh Button");
client.print("<HEAD>");
client.print("<meta http-equiv=\"refresh\" content=\"0;url=/\">");
client.print("</head>");
}
//client.println("");
// Display the HTML web page
client.println("<!DOCTYPE html><html>");
client.println("<head>");
client.println("<title>投幣機</title>");
client.println("<meta charset=\"utf-8\">");
client.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<style>");
//client.println("body {margin: 0;font-family: Arial, Helvetica, sans-serif;}");
client.println("* { box-sizing: border-box; font-family: 微軟正黑體, Helvetica, sans-serif;}");
client.println("body { margin: 0; font-family: 微軟正黑體, Helvetica, sans-serif;}");
client.println(".button { display: inline-block; border-radius: 3px; background-color: #f4511e; border: none; color: #FFFFFF; text-align: center; font-size: 20px;padding: 12px; width: 200px; transition: all 0.5s; cursor: pointer; margin: 4px;}");
client.println(".content { background-color: #FFFFFF; padding: 10px;}");
client.println(".footer { background-color: #f1f1f1; padding: 10px;}");
client.println("</style>");
client.println("</head>");
client.println("<body>");
client.println("<h1>投幣機</h1>");
client.println("<h2>目前金額 : " + moneyString + "</h2>");
client.println("<button class=\"button\" style=\"vertical-align:middle\"><a href=\"/RESET\">重置</a></button>");
client.println("<button class=\"button\" style=\"vertical-align:middle\"><a href=\"/REFRESH\">重新整理</a></button>");
if(resetFlag){
client.println("<p>money reset!</p>");
}
client.println("</body></html>");
client.println();
}
```
* `showUI()`函式負責處理網頁內容顯示。8~11行是html的描述,接下來的判斷式則是判斷有沒有進到網頁的某個index,而我們在底下會用按鈕超連結導向到指定index執行重設和重新整理,再來就跟一般html的寫法一樣,這裡的樣式採用css。
#### EEPROM 寫入
```c=
void EEPROMWritelong(int address, long value){
//Decomposition from a long to 4 bytes by using bitshift.
//One = Most significant -> Four = Least significant byte
byte four = (value & 0xFF);
byte three = ((value >> 8) & 0xFF);
byte two = ((value >> 16) & 0xFF);
byte one = ((value >> 24) & 0xFF);
//Write the 4 bytes into the eeprom memory.
EEPROM.write(address, four);
EEPROM.write(address + 1, three);
EEPROM.write(address + 2, two);
EEPROM.write(address + 3, one);
EEPROM.commit();
}
long EEPROMReadlong(long address){
//Read the 4 bytes from the eeprom memory.
long four = EEPROM.read(address);
long three = EEPROM.read(address + 1);
long two = EEPROM.read(address + 2);
long one = EEPROM.read(address + 3);
//Return the recomposed long by using bitshift.
return ((four << 0) & 0xFF) + ((three << 8) & 0xFFFF) + ((two << 16) & 0xFFFFFF) + ((one << 24) & 0xFFFFFFFF);
}
```
* 我們預期金額會是一筆相當大的數字(雖然不太可能),所以我們將金額以`long`的形式儲存,在寫入記憶體時就得要把每個byte拆分開來儲存,值得注意的地方是Wemos這一塊若要將資料寫進硬體(斷電保存),只有4K的空間,而且必須執行`EEPROM.commit()`才會真正存進去。
## 總結
* Wemos D1這塊板子的腳位非常不好找,不知道是不是我買的版本問題,腳位上的位置和實際取用的位置不同,有的還有重複(暈),建議想要實作的人可以用一般Arduino外接ESP8266就好。
* 投幣機的訊號是用脈衝的方式傳遞,在Arduino內可以用`pulsein()`來抓取,但是如果連續投幣,會抓不到投的硬幣種類,所以我直接在指示燈上外接一條線input到Arduino的腳位來解決這個問題,就不使用投幣機本身的訊號線了。
## 參考
* https://randomnerdtutorials.com/esp8266-web-server-with-arduino-ide/
* https://www.w3schools.com/css/default.asp
* https://zhuanlan.zhihu.com/p/103756212
:pancakes: Author : 沈奕辰
##### tags: `Arduino`