這是一個比較冷門的通訊軟體Telegram (台灣比較少人用),大家還是習慣用Line,它有一個機器人的功能還不錯用,可以雙向溝通,Line的部分只能單向傳送,雖然Line 也有bot,但是設定比較繁瑣,Telegram只要在app申請一個機器人它就會有一個token,利用token就能做個專屬的機器人。
今天是用ESP32-Cam做測試,打造一個自己的”哨兵”,只要在板子加一個PIR sensor,放在門禁管制位置,可以設置是否開啟警戒,只要有人進出,觸動PIR,機器人就能即時傳照片回報,目前沒有限制,(Line也可以,但是限制50張/1hr),當然你可以隨時按拍照上傳目前的即時照片,這個後續可以串到鐵門...只要開門就觸發拍照,我還做了一個密碼管控,因為只有有人搜尋到你的機器人還是可以對它下指令,不用其他系統搭配,只要用Telegram app。
 
STEP 1. 先安裝Telegram 通訊 APP,網路上很多教學請自行參考
 
STEP 2.進入Telegram 搜尋 BotFather,

IMG_2027.PNG

輸入/newbot申請一個機器人

IMG_2028.PNG

接下來幫機器人取個名字,中英文都可以我取個 “哨兵”

IMG_2029.PNG

接下來輸入機器人的帳號,他是有命名規則,必須是英數字,最後結尾必須是Bot 或是 _bot,要注意一下

IMG_2030.jpg

如果帳號重複就必須換個名字,成功了就會出現上面的畫面,紅色框框處就是機器人的API Token,把他複製起來,先存在其他地方,待會程式會用到。

注意不要洩漏你的Token,因為只要有這串Token就可以控制你的機器人。

IMG_2031.jpg

最後搜尋你的機器人帳號,應該就可以找到,輸入一些文字,目前不會有反應,還需要寫一些程式去控制。

IMG_2032.PNG

 

STEP 3. 將你的ESP32cam 線連接起來,因為ESP32cam板子需要用TTL寫入程式,另外我有外加一個PIR sensor 作為感測器。

PIR 是連接在GPIO 13

螢幕快照 2021-04-04 下午6.02.13.png

STEP 4. 上傳程式

首先Arduino IDE 要先安裝CTBOT的程式庫,

螢幕快照 2021-04-04 下午7.08.00.png

程式如下:


#include "WiFi.h"
#include "WiFiClientSecure.h"
#include "ArduinoOTA.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_camera.h"
#include "EEPROM.h"
#include "WiFiManager.h"
#define BOT_TOKEN_LENGTH 50
#define PassWord_LENGTH 12

//CAMERA_MODEL_AI_THINKER
#define CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

#define FLASH_LED_PIN 4 //閃光燈
bool flashState = LOW; //閃光燈狀態

char botToken[BOT_TOKEN_LENGTH] = "";
char PassWordd[PassWord_LENGTH] = "";
char new_pass[PassWord_LENGTH] = "";
char orgpass[12] = "123456";
char masterid[12] = "";
const char TEXT_START[] =
 "***歡迎進入看哨兵系統***"
 "\n"
 "您可以輸入指令或是點擊按鈕"
 "\n"
 "啟動或關閉哨兵"
 "\n"
 "/startwatch -啟動哨兵"
 "\n"
 "/stopwatch -關閉哨兵"
 "\n"
 "/status -哨兵狀態"
 "\n"
 "/photo -拍照"
 "\n"
 "/setpass -變更密碼"
 "\n"
 "/help -指令列表";
WiFiClientSecure clientTCP;
#include "CTBot.h"
CTBot myBot;
CTBotReplyKeyboard myKbd; // reply keyboard object helper
bool isKeyboardActive;

//flag for saving data
bool shouldSaveConfig = false;
bool firstime = true;
bool watchdog = false;
bool pass_status = true;
bool changepass = false;
bool check_pass = false;
bool check_again = false;
bool ota_flag = false;

uint8_t ledr = 33;
const int OTA_Pin = 16;//OTA
const int pir = 13;
const int BotresetPin = 14;
unsigned long ttime;
unsigned long senderID;
unsigned long masterID;
String chatId = ""; //設定傳圖片的ID
String BOTtoken = ""; //設定傳圖的token
String sendPhotoTelegram();
String Router_SSID;
String Router_Pass;
String testpass = "";
String Oldpass = "";

void saveConfigCallback () {
 Serial.println("Should save config");
 shouldSaveConfig = true;
}
void readBotTokenFromEeprom(int offset) {
 for (int i = offset; i < BOT_TOKEN_LENGTH; i++ ) {
 botToken[i] = EEPROM.read(i);
 }
 //EEPROM.commit();
}
void readPassWord() {
 for (int i = 0; i < 12; i++ ) {
 PassWordd[i] = EEPROM.read(i + 60);
 }
 //EEPROM.commit();
}
void readmasterid() {
 for (int i = 0; i < 12; i++ ) {
 masterid[i] = EEPROM.read(i + 80);
 }
 //EEPROM.commit();
}

void writeBotTokenToEeprom(int offset) {
 for (int i = offset; i < BOT_TOKEN_LENGTH; i++ ) {
 EEPROM.write(i, botToken[i]);
 }
 Serial.println("Write EEPROM.....");
 for (int i = 0; i < 10; i++) {
 digitalWrite(ledr, HIGH);
 delay(200);
 digitalWrite(ledr, LOW);
 delay(100);
 }
 EEPROM.commit();
}


void setup() {
 // initialize the Serial
 Serial.begin(115200);
 Serial.println("Starting TelegramBot...");
 EEPROM.begin(128);
 pinMode(pir, INPUT);
 pinMode(BotresetPin, INPUT_PULLUP);
 pinMode(ledr, OUTPUT);
 pinMode(FLASH_LED_PIN, OUTPUT);
 pinMode(OTA_Pin, INPUT_PULLUP);
 digitalWrite(FLASH_LED_PIN, flashState);
 Serial.println("read bot token");
 readBotTokenFromEeprom(0);
 Serial.println(botToken);
 Serial.println("password");
 readPassWord();
 Serial.println(PassWordd);
 readmasterid();
 Serial.println(masterid);


 //wifimanager
 WiFi.mode(WIFI_STA);
 WiFiManager wm;
 wm.setDebugOutput(true);
 wm.setSaveConfigCallback(saveConfigCallback);
 WiFiManagerParameter custom_bot_id("botid", "Bot Token", botToken, 50);
 wm.addParameter(&custom_bot_id);
 wm.autoConnect("WatchDog");
 Serial.println("WiFi connected");
 Serial.println("IP address: ");
 IPAddress ip = WiFi.localIP();
 Serial.println(ip);
 strcpy(botToken, custom_bot_id.getValue());
 if (shouldSaveConfig) {
 writeBotTokenToEeprom(0);
 }
 //Start OTA 按住OTA鈕 燈閃3下放開進入OTA模式
 if (digitalRead(OTA_Pin) == LOW) {
 for (int i = 0; i < 3 ; i++) {
 digitalWrite(ledr, HIGH);
 delay(500);
 digitalWrite(ledr, LOW);
 delay(500);
 }
 delay(3000);
 if (digitalRead(OTA_Pin) == HIGH) {
 Serial.println("OTA.....");
 ota_flag = true;
 ArduinoOTA.onStart([]() {
 String type;
 if (ArduinoOTA.getCommand() == U_FLASH) {
 type = "sketch";
 } else { // U_FS
 type = "filesystem";
 }

 // NOTE: if updating FS this would be the place to unmount FS using FS.end()
 Serial.println("Start updating " + type);
 });
 ArduinoOTA.onEnd([]() {
 Serial.println("\nEnd");
 });
 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
 Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
 });
 ArduinoOTA.onError([](ota_error_t error) {
 Serial.printf("Error[%u]: ", error);
 if (error == OTA_AUTH_ERROR) {
 Serial.println("Auth Failed");
 } else if (error == OTA_BEGIN_ERROR) {
 Serial.println("Begin Failed");
 } else if (error == OTA_CONNECT_ERROR) {
 Serial.println("Connect Failed");
 } else if (error == OTA_RECEIVE_ERROR) {
 Serial.println("Receive Failed");
 } else if (error == OTA_END_ERROR) {
 Serial.println("End Failed");
 }
 });
 ArduinoOTA.begin();
 Serial.println("Ready");
 Serial.print("IP address: ");
 Serial.println(WiFi.localIP());
 }
 delay(1000);
 if (digitalRead(OTA_Pin) == LOW) {//長按OTA鈕直到快閃6下,清除所有設定
 Serial.println("Reset.....");
 wm.resetSettings();
 for (int i = 0; i < 128; i++ ) {
 EEPROM.write(i, '\0');
 }
 EEPROM.commit();
 for (int i = 0; i < 6 ; i++) {
 digitalWrite(ledr, HIGH);
 delay(100);
 digitalWrite(ledr, LOW);
 delay(200);
 }
 ESP.restart();
 }
 }
 else {
 // set the telegram bot token
 myBot.setTelegramToken(botToken);
 myBot.enableUTF8Encoding(true); //UTF8顯示中文
 // check if all things are ok
 if (myBot.testConnection()) {
 Serial.println("\ntestConnection OK");
 for (int i = 0; i < 5 ; ++i) {
 digitalWrite(ledr, HIGH);
 delay(200);
 digitalWrite(ledr, LOW);
 delay(200);
 }
 digitalWrite(ledr, HIGH);
 delay(1000);
 digitalWrite(ledr, LOW);
 }
 else {
 Serial.println("\ntestConnection NOK");
 // set the pin connected to the LED to act as output pin
 digitalWrite(ledr, HIGH); // turn off the led (inverted logic!)
 }
 myKbd.addButton("啟動哨兵");
 myKbd.addButton("關閉哨兵");
 myKbd.addButton("哨兵狀態");
 myKbd.addRow();
 myKbd.addButton("拍照");
 myKbd.addButton("指令列");
 myKbd.addButton("變更密碼");
 myKbd.enableResize();
 isKeyboardActive = false;

 //camra setting
 camera_config_t config;
 config.ledc_channel = LEDC_CHANNEL_0;
 config.ledc_timer = LEDC_TIMER_0;
 config.pin_d0 = Y2_GPIO_NUM;
 config.pin_d1 = Y3_GPIO_NUM;
 config.pin_d2 = Y4_GPIO_NUM;
 config.pin_d3 = Y5_GPIO_NUM;
 config.pin_d4 = Y6_GPIO_NUM;
 config.pin_d5 = Y7_GPIO_NUM;
 config.pin_d6 = Y8_GPIO_NUM;
 config.pin_d7 = Y9_GPIO_NUM;
 config.pin_xclk = XCLK_GPIO_NUM;
 config.pin_pclk = PCLK_GPIO_NUM;
 config.pin_vsync = VSYNC_GPIO_NUM;
 config.pin_href = HREF_GPIO_NUM;
 config.pin_sscb_sda = SIOD_GPIO_NUM;
 config.pin_sscb_scl = SIOC_GPIO_NUM;
 config.pin_pwdn = PWDN_GPIO_NUM;
 config.pin_reset = RESET_GPIO_NUM;
 config.xclk_freq_hz = 20000000;
 config.pixel_format = PIXFORMAT_JPEG;

 //init with high specs to pre-allocate larger buffers
 if (psramFound()) {
 config.frame_size = FRAMESIZE_UXGA;
 config.jpeg_quality = 10; //0-63 lower number means higher quality
 config.fb_count = 2;
 Serial.println("UXGA");
 } else {
 config.frame_size = FRAMESIZE_SVGA;
 config.jpeg_quality = 12; //0-63 lower number means higher quality
 config.fb_count = 1;
 Serial.println("SVGA");
 }

 // camera init
 esp_err_t err = esp_camera_init(&config);
 if (err != ESP_OK) {
 Serial.printf("Camera init failed with error 0x%x", err);
 digitalWrite(ledr, HIGH);
 delay(1000);
 ESP.restart();
 }

 // Drop down frame size for higher initial frame rate
 sensor_t * s = esp_camera_sensor_get();
 s->set_framesize(s, FRAMESIZE_SVGA); // UXGASXGAXGASVGAVGACIFQVGAHQVGAQQVGA

 //end camra setting
 //開機傳送訊息
 masterID = strtoul(masterid, NULL, 10);
 Serial.println(masterID);
 myBot.sendMessage(masterID, "哨兵系統啟動...請輸入密碼");
 Serial.println("哨兵開機完成");
 }
}

void loop() {
 // a variable to store telegram message data
 if (ota_flag) {
 ArduinoOTA.handle();
 }
 else {
 TBMessage msg;
 botreset();
 // 若接收到訊息
 if (myBot.getNewMessage(msg)) {
 // step1========================================================
 if (pass_status) { //啟動輸入密碼
 if (msg.text.equalsIgnoreCase(PassWordd)) {//驗證密碼
 pass_status = false;
 isKeyboardActive = true;
 senderID = msg.sender.id;
 String testpass = String(PassWordd); //設定傳圖ID
 String Oldpass = String(orgpass);//設定傳圖token
 Serial.println(testpass);
 Serial.println(Oldpass);
 myBot.sendMessage(msg.sender.id, TEXT_START, myKbd);
 if (String(masterid) != String(msg.sender.id)) {
 String(msg.sender.id).toCharArray(masterid, 12);
 for (int i = 80, j = 0; i < 92 , j < 12; ++i, ++j) {
 EEPROM.write(i, masterid[j]);
 }
 EEPROM.commit();
 delay(100);
 Serial.println(String(masterid));
 }
 }
 else if (testpass == Oldpass)
 myBot.sendMessage(msg.sender.id, "請輸入密碼 (預設值:123456)");

 else
 myBot.sendMessage(msg.sender.id, "請輸入密碼");
 }
 // step2========================================================
 if (senderID == msg.sender.id) {
 if (msg.text.equalsIgnoreCase("/startwatch") msg.text.equalsIgnoreCase("啟動哨兵")) {
 digitalWrite(ledr, HIGH);
 myBot.sendMessage(msg.sender.id, "哨兵啟動");
 watchdog = true;
 }
 else if (msg.text.equalsIgnoreCase("/stopwatch") msg.text.equalsIgnoreCase("關閉哨兵")) {
 digitalWrite(ledr, LOW);
 myBot.sendMessage(msg.sender.id, "哨兵關閉");
 watchdog = false;
 }
 else if (msg.text.equalsIgnoreCase("/status") msg.text.equalsIgnoreCase("哨兵狀態")) {
 if (watchdog)
 myBot.sendMessage(msg.sender.id, "哨兵防護中...");
 else
 myBot.sendMessage(msg.sender.id, "哨兵休息中...");
 }
 else if (msg.text.equalsIgnoreCase("/help") msg.text.equalsIgnoreCase("指令列")) {
 myBot.sendMessage(msg.sender.id, TEXT_START);
 }
 else if (msg.text.equalsIgnoreCase("/photo") msg.text.equalsIgnoreCase("拍照")) {
 sendPhotoTelegram();
 }

 // step3========================================================
 else if (check_again) {
 if (msg.text.equalsIgnoreCase(new_pass)) {
 myBot.sendMessage(senderID, "密碼變更成功,請重新啟動");
 pass_status = true;
 for (int i = 60, j = 0; i < 72 , j < 12; ++i, ++j) {
 EEPROM.write(i, new_pass[j]);
 }
 EEPROM.commit();
 delay(100);
 ESP.restart();
 }
 else
 myBot.sendMessage(senderID, "密碼錯誤,請重新操作");
 check_again = false;
 }
 // step4========================================================
 else if (check_pass) {
 msg.text.toCharArray(new_pass, 12);
 myBot.sendMessage(senderID, "請再次輸入密碼");
 check_pass = false;
 check_again = true;
 }
 // step5========================================================
 else if (changepass) {
 if (msg.text.equalsIgnoreCase(PassWordd)) {
 myBot.sendMessage(senderID, "密碼正確,請輸入新密碼 (英數字612碼)");
 check_pass = true;
 }
 else
 myBot.sendMessage(senderID, "密碼錯誤,請重新操作");
 changepass = false;
 }
 else if (msg.text.equalsIgnoreCase("/setpass") msg.text.equalsIgnoreCase("變更密碼")) {
 changepass = true;
 myBot.sendMessage(msg.sender.id, "請輸入舊密碼");
 }
 //else{
 // myBot.sendMessage(msg.sender.id, "汪汪...");
 // }
 }
 } //mybot msg
 if (watchdog) {
 byte p = digitalRead(pir);
 if (p == HIGH) {
 sendPhotoTelegram();
 for (int i = 0; i < 10 ; ++i) {
 digitalWrite(ledr, HIGH);
 delay(200);
 digitalWrite(ledr, LOW);
 delay(200);
 }
 digitalWrite(ledr, HIGH);
 myBot.sendMessage(senderID, "注意注意...有人入侵");
 delay(1000);
 }
 }

 // wait 500 milliseconds
 delay(500);
 }
}

//************************
String sendPhotoTelegram() {
 const char* myDomain = "api.telegram.org";
 String getAll = "";
 String getBody = "";
 String chatId = String(senderID); //設定傳圖ID
 String BOTtoken = String(botToken);//設定傳圖token
 Serial.println(chatId);
 Serial.println(BOTtoken);
 camera_fb_t * fb = NULL;
 fb = esp_camera_fb_get();
 if (!fb) {
 Serial.println("Camera capture failed");
 delay(1000);
 ESP.restart();
 return "Camera capture failed";
 }

 Serial.println("Connect to " + String(myDomain));

 if (clientTCP.connect(myDomain, 443)) {
 Serial.println("Connection successful");

 String head = "--RandomNerdTutorials\r\nContent-Disposition: form-data; name=\"chat_id\"; \r\n\r\n" + chatId + "\r\n--RandomNerdTutorials\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
 String tail = "\r\n--RandomNerdTutorials--\r\n";
 Serial.println(head);
 Serial.println(tail);
 uint16_t imageLen = fb->len;
 uint16_t extraLen = head.length() + tail.length();
 uint16_t totalLen = imageLen + extraLen;

 clientTCP.println("POST /bot" + BOTtoken + "/sendPhoto HTTP/1.1");
 Serial.println("POST /bot" + BOTtoken + "/sendPhoto HTTP/1.1");
 clientTCP.println("Host: " + String(myDomain));
 Serial.println("Host: " + String(myDomain));
 clientTCP.println("Content-Length: " + String(totalLen));
 Serial.println("Content-Length: " + String(totalLen));
 clientTCP.println("Content-Type: multipart/form-data; boundary=RandomNerdTutorials");
 Serial.println("Content-Type: multipart/form-data; boundary=RandomNerdTutorials");
 clientTCP.println();
 clientTCP.print(head);
 Serial.print(head);

 uint8_t *fbBuf = fb->buf;
 size_t fbLen = fb->len;
 for (size_t n = 0; n < fbLen; n = n + 1024) {
 if (n + 1024 < fbLen) {
 clientTCP.write(fbBuf, 1024);
 fbBuf += 1024;
 }
 else if (fbLen % 1024 > 0) {
 size_t remainder = fbLen % 1024;
 clientTCP.write(fbBuf, remainder);
 }
 }

 clientTCP.print(tail);

 esp_camera_fb_return(fb);

 int waitTime = 10000; // timeout 10 seconds
 long startTimer = millis();
 boolean state = false;

 while ((startTimer + waitTime) > millis()) {
 Serial.print(".");
 delay(100);
 while (clientTCP.available()) {
 char c = clientTCP.read();
 if (c == '\n') {
 if (getAll.length() == 0) state = true;
 getAll = "";
 }
 else if (c != '\r') {
 getAll += String(c);
 }
 if (state == true) {
 getBody += String(c);
 }
 startTimer = millis();
 }
 if (getBody.length() > 0) break;
 }
 clientTCP.stop();
 Serial.println(getBody);
 }
 else {
 getBody = "Connected to api.telegram.org failed.";
 Serial.println("Connected to api.telegram.org failed.");
 }
 return getBody;
}
//***************

void botreset() {
 if (digitalRead(BotresetPin) == LOW)//重設密碼123456
 delay(1000);
 if (digitalRead(BotresetPin) == LOW) {
 //orgpass.toCharArray(PassWordd,12);
 for (int j = 0; j < 12; j++ ) {
 EEPROM.write(j + 60, orgpass[j]);
 }
 EEPROM.commit();
 WiFiManager wm;
 wm.resetSettings();
 Serial.println("Finished");
 for (int j = 0; j < 20; j++ ) {
 digitalWrite(ledr, HIGH);
 delay(100);
 digitalWrite(ledr, LOW);
 delay(100);
 }
 ESP.restart();
 }
}

==========================================================
程式上傳完成後,將ESP32cam reset,打開手機搜尋WiFi,找Watchdog,Configure WiFi
螢幕快照 2021-04-04 下午7.47.17.png
將WiFi密碼及Telegram 的Token填入
螢幕快照 2021-04-04 下午7.47.54.png
等待ESP32cam 重新啟動就完成了!
IMG_1875.jpg
IMG_1876.PNG
IMG_1877.PNG

程式說明:

預設密碼 123456

螢幕快照 2021-04-04 下午8.43.55.png

若要修改指令列的內容可以直接修改

螢幕快照 2021-04-04 下午7.44.42.png

ESP32cam 背面有一個預設的led 連接GPIO33,設定當作指示燈用

若不想每次都要接TTL上傳程式,可用OTA的方式,需另外再加一個按鈕,按住OTA鈕 ,然後按一下reset, 燈慢閃3下放開進入OTA模式

若繼續長按OTA鈕直到快閃6下,清除所有設定

PIR sensor 連接GPIO13

若忘記密碼 GPIO 14 短接,當指示燈快閃可以清除密碼,(第一次上傳程式後請執行一次,將密碼改為預設值123456,要不然怎麼輸入都不行的)

螢幕快照 2021-04-04 下午8.41.36.png

WiFi SSID 名稱,可自行修改

螢幕快照 2021-04-04 下午8.43.08.png

Telegram 自訂按鈕,按鈕名稱可自行修改,254行是將按鈕分行,也可以變成3行

螢幕快照 2021-04-04 下午8.42.07.png

指令列跟程式內的文字要對應到,要不然機器人無法判讀

螢幕快照 2021-04-04 下午8.42.37.png

最後就發揮大家的創意,變成自己的監控機器人

IMG_1878.jpg

 

 

 

文章標籤
全站熱搜
創作者介紹
創作者 terrywu5 的頭像
terrywu5

IoT 智慧生活的異想世界

terrywu5 發表在 痞客邦 留言(8) 人氣(6,584)