項目簡介
本項目旨在開發一個基于嵌入式系統的智能垃圾分類裝置。該裝置能夠通過串口通信、語音播報、網絡通信等多種方式,實現垃圾的自動識別和分類投放。系統采用多線程設計,確保各功能模塊高效并行工作。
項目功能
- 垃圾分類識別
系統使用攝像頭拍攝垃圾圖片,并將其上傳到阿里云進行分類識別。
識別結果包括干垃圾、濕垃圾、可回收垃圾、有害垃圾等類別。
- 語言播報
系統通過串口與語言模塊通信,根據垃圾分類結果進行語音播報。
語音模塊接收指令并播放相應的提示音。
- OLED顯示
系統通過OLED屏幕顯示當前垃圾分類結果。
4.垃圾桶蓋控制
系統根據識別結果控制相應垃圾桶的開關。
使用PWM信號控制舵機開關垃圾桶蓋。
5.網絡通信
系統支持通過網絡接收客戶端指令,實現遠程控制。
通過TCP/IP協議接收來自客戶端的“open”指令,觸發垃圾分類操作。
硬件需求
Orange Pi Zero2
攝像頭模塊
語音播報模塊SU-03T
128*64 OLED顯示屏
SG90舵機
串口通信模塊
運行環境
操作系統:Linux
編程語言:C語言、Python
功能實現
garbage.py
調用阿里云SDK API
# -*- coding: utf-8 -*-
# 引入依賴包
# pip install alibabacloud_imagerecog20190930
//引入必要的Python包,包括阿里云圖像識別服務的SDK包。
import os
import io
from urllib.request import urlopen
from alibabacloud_imagerecog20190930.client import Client
from alibabacloud_imagerecog20190930.models import ClassifyingRubbishAdvanceRequest
from alibabacloud_tea_openapi.models import Config
from alibabacloud_tea_util.models import RuntimeOptions//通過讀取環境變量中的AccessKey ID和AccessKey Secret,配置阿里云客戶端的認證信息和訪問域名。
config = Config(# 創建AccessKey ID和AccessKey Secret,請參考https://help.aliyun.com/document_detail/175144.html。# 如果您用的是RAM用戶的AccessKey,還需要為RAM用戶授予權限AliyunVIAPIFullAccess,請參考https://help.aliyun.com/document_detail/145025.html# 從環境變量讀取配置的AccessKey ID和AccessKey Secret。運行代碼示例前必須先配置環境變量。access_key_id=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID'),access_key_secret=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),# 訪問的域名endpoint='imagerecog.cn-shanghai.aliyuncs.com',# 訪問的域名對應的regionregion_id='cn-shanghai'
)//垃圾分類函數,提供了從本地文件讀取和從URL讀取,兩種方式
def alibaba_garbage():#場景一:文件在本地img = open(r'/tmp/garbage.jpg', 'rb') //打開文件所在路徑#場景二:使用任意可訪問的url#url = 'https://viapi-test-bj.oss-cn-beijing.aliyuncs.com/viapi-3.0domepic/imagerecog/ClassifyingRubbish/ClassifyingRubbish1.jpg'#img = io.BytesIO(urlopen(url).read())classifying_rubbish_request = ClassifyingRubbishAdvanceRequest()classifying_rubbish_request.image_urlobject = imgruntime = RuntimeOptions()try:# 初始化Clientclient = Client(config)//調用阿里云圖像識別服務,將上傳的垃圾圖片進行分類,并返回分類結果。response = client.classifying_rubbish_advance(classifying_rubbish_request, runtime)# 獲取整體結果print(response.body.to_map()['Data']['Elements'][0]['Category'])return response.body.to_map()['Data']['Elements'][0]['Category']except Exception as error:return 'gat failed'在腳本被直接執行時,調用‘alibaba_garbage’函數。
if __name__ == "__main__":alibaba_garbage()
garbage.c
在C語言中嵌入Python解釋器以調用Python函數。
#include <Python.h>
#include "garbage.h"void garbage_init(void)
{ //初始化Python解釋器Py_Initialize();//導入‘sys’模塊PyObject *sys = PyImport_ImportModule("sys");//獲取‘sys.path’模塊PyObject *path = PyObject_GetAttrString(sys, "path");//將當前目錄‘.’添加到'sys.path'中,以便導入當前目錄下的Python腳本。PyList_Append(path, PyUnicode_FromString("."));
}void garbage_final(void)
{//終止Python解釋器。 Py_Finalize();
}char *garbage_category(char *category)
{//導入名為‘garbage.py’的python模塊。//如果導入失敗,打印錯誤信息并跳轉到'FAILED_MODULE'表簽進行清理。PyObject *pModule = PyImport_ImportModule("garbage");if(!pModule){PyErr_Print();printf("ERROR: faild to load garbage.py\n");goto FAILED_MODULE;}//獲取'garbage'模塊中的'alibaba_garbage'函數。//如果獲取失敗或函數不可調用,打印錯誤信息并跳轉到 FAILED_FUNC 標簽進行清理。PyObject *pFunc = PyObject_GetAttrString(pModule, "alibaba_garbage");if(!pFunc || !PyCallable_Check(pFunc)){PyErr_Print();printf("ERROR: function alibaba_garbage not found or not callable\n");goto FAILED_FUNC;}//調用 alibaba_garbage 函數,不傳遞參數。//如果調用失敗,打印錯誤信息并跳轉到 FAILED_VALUE 標簽進行清理。PyObject *pValue = PyObject_CallObject(pFunc, NULL);if(!pValue){PyErr_Print();printf("ERROR: function call failed\n");goto FAILED_VALUE;}char *result = NULL;if(!PyArg_Parse(pValue, "s", &result)){PyErr_Print();printf("ERROR: parse failed\n");goto FAILED_RESULT;}category = (char *)malloc(sizeof(char *)*(strlen(result)+1));memset(category, 0 , (strlen(result)+1));strncpy(category, result, (strlen(result)+1));// 釋放發生錯誤之前分配的內存空間
FAILED_RESULT:Py_DECREF(pValue);
FAILED_VALUE:Py_DECREF(pFunc);
FAILED_FUNC:Py_DECREF(pModule);
FAILED_MODULE:return category;
}
內存泄漏
內存泄漏是指在計算機程序中,動態分配的內存未被正確釋放,導致這些內存塊不能被重新分配和使用。隨著程序的運行,未釋放的內存不斷累積,最終可能耗盡可用內存資源,導致系統性能下降,甚至使程序或系統崩潰。
內存泄漏的成因
- 未釋放動態分配的內存:
程序在使用 malloc、calloc 或 realloc 分配內存后,沒有使用 free 函數釋放內存。
- 循環或遞歸調用中未釋放內存:
在循環或遞歸調用中,動態分配的內存未被釋放。
- 異常退出或提前返回:
程序在處理錯誤或異常時,沒有正確釋放已經分配的內存。
- 丟失指針引用:
動態分配的內存地址被覆蓋或丟失,沒有其他指針指向該內存塊。
棧上分配和堆上分配
棧上分配
棧上分配的內存(例如局部變量)在函數調用結束后會自動釋放。
void function() {int local_variable = 10; // 棧上分配// local_variable 在函數結束時自動釋放
}
堆上分配
堆上分配的內存需要程序員手動釋放,否則會導致內存泄漏。
void function() {int *heap_variable = (int *)malloc(sizeof(int) * 10); // 堆上分配// 使用完 heap_variable 后需要手動釋放內存free(heap_variable);
}
garbage.h
#ifndef __GARBAGE__H
#define __GARBAGE__Hvoid garbage_init(void);
void garbage_final(void);
char *garbage_category(char *category);//宏定義拍照指令和圖片地址
#define WGET_CMD "wget http://127.0.0.1:8080/?action=snapshot -O /tmp/garbage.jpg"
#define GARBAGE_FILE "/tmp/garbage.jpg"#endif
main.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <wiringPi.h>
#include <pthread.h>#include "uartTool.h"
#include "garbage.h"
#include "pwm.h"
#include "myoled.h"
#include "socket.h"int serial_fd = -1;
pthread_cond_t cond;
pthread_mutex_t mutex;//該函數通過執行 shell 命令 ps 檢測特定進程是否正在運行。
static int detect_process(const char *process_name)
{int n = -1;FILE *strm;char buf[128] = {0};sprintf(buf, "ps -ax | grep %s | grep -v grep", process_name);if ((strm = popen(buf, "r")) != NULL) {if (fgets(buf, sizeof(buf), strm) != NULL) {n = atoi(buf);}}else{return -1;}pclose(strm);return n;
}//從串口讀取語音命令,并根據特定條件觸發條件變量信號。
void *pget_voice(void *arg)
{unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};int len = 0;if (-1 == serial_fd) {printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);pthread_exit(0);}printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);while(1){len = serialGetstring(serial_fd, buffer);if (len > 0 && buffer[2] == 0x46) {pthread_mutex_lock(&mutex);buffer[2] = 0x00;pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}}pthread_exit(0);
}//發送語音命令到串口。
void *psend_voice(void *arg)
{pthread_detach(pthread_self());unsigned char *buffer = (unsigned char *)arg;if (-1 == serial_fd){printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);pthread_exit(0);}if (NULL != buffer){serialSendstring(serial_fd, buffer, 6);}pthread_exit(0);
}//控制垃圾桶打開和關閉。
void *popen_trash_can(void *arg)
{pthread_detach(pthread_self());unsigned char *buffer = (unsigned char *)arg;if (buffer[2] == 0x43) {printf("%s|%s|%d: buffer[2]=0x%x\n", __FILE__, __func__, __LINE__, buffer[2]);pwm_write(PWM_RECOVERABLE_GARBAGE);delay(2000);pwm_stop(PWM_RECOVERABLE_GARBAGE);}else if (buffer[2] != 0x45){printf("%s|%s|%d: buffer[2]=0x%x\n", __FILE__, __func__, __LINE__, buffer[2]);pwm_write(PWM_GARBAGE);delay(2000);pwm_stop(PWM_GARBAGE);}pthread_exit(0);
}//初始化 OLED 顯示屏并顯示垃圾分類結果。
void*poled_show(void *arg)
{pthread_detach(pthread_self());myoled_init();oled_show(arg);pthread_exit(0);
}//等待條件變量信號,執行垃圾分類邏輯,并觸發語音、垃圾桶和 OLED 顯示。
void *pcategory(void *arg)
{unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};char *category = NULL;pthread_t send_voice_tid, trash_tid, oled_tid;while(1){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex);pthread_mutex_unlock(&mutex);buffer[2] = 0x00;system(WGET_CMD);if (0 == access(GARBAGE_FILE, F_OK)){category = garbage_category(category);if (strstr(category, "干垃圾")){buffer[2] = 0x41;}else if (strstr(category, "濕垃圾")){buffer[2] = 0x42;}else if (strstr(category, "可回收垃圾")){buffer[2] = 0x43;}else if (strstr(category, "有害垃圾")){buffer[2] = 0x44;}else {buffer[2] = 0x45;}}else{buffer[2] = 0x45;}//開語音播報線程pthread_create(&trash_tid, NULL, psend_voice, (void *)buffer);//開垃圾桶開關pthread_create(&send_voice_tid, NULL, popen_trash_can, (void *)buffer);//oled顯示線程pthread_create(&oled_tid, NULL, poled_show, (void *)buffer);remove(GARBAGE_FILE);}}void *pget_socket(void *arg)
{int s_fd = -1;// 服務器套接字文件描述符int c_fd = -1;// 客戶端套接字文件描述符char buffer[6];// 接收數據緩沖區int nread = -1;// 接收到的數據長度struct sockaddr_in c_addr;// 客戶端地址結構memset(&c_addr,0,sizeof(struct sockaddr_in));// 初始化客戶端地址結構s_fd = socket_init(IPADDR, IPPORT);// 初始化服務器套接字printf("%s|%s|%d:s_fd=%d\n", __FILE__, __func__, __LINE__, s_fd);if (-1 == s_fd){pthread_exit(0);}sleep(3);int clen = sizeof(struct sockaddr_in);// 客戶端地址結構的長度while(1){c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);// 接受客戶端連接int keepalive = 1; // 開啟TCP KeepAlive功能int keepidle = 5; // 5s內沒收到數據開始發送心跳包int keepcnt = 3; // 每次發送心跳包的次數int keepintvl = 3; // 每3s發送一次心跳包setsockopt(c_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive, sizeof(keepalive));setsockopt(c_fd, SOL_TCP, TCP_KEEPIDLE, (void *) &keepidle, sizeof(keepidle));setsockopt(c_fd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof(keepcnt));setsockopt(c_fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof(keepintvl)); printf("%s|%s|%d: Accept a connection from %s:%d\n", __FILE__, __func__, __LINE__, inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port));if(c_fd == -1){perror("accept");continue;}while(1){memset(buffer, 0, sizeof(buffer));nread = recv(c_fd, buffer, sizeof(buffer), 0); //n_read = read(c_fd,buffer, sizeof(buffer));printf("%s|%s|%d:nread=%d, buffer=%s\n", __FILE__, __func__,__LINE__, nread, buffer);if (nread > 0){if (strstr(buffer, "open")){pthread_mutex_lock(&mutex);pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}}else if(0 == nread || -1 == nread){break;}}close(c_fd);}pthread_exit(0);
}int main(int argc, char *argv[])
{ int len = 0;int ret = -1;char *category = NULL;pthread_t get_voice_tid, category_tid, get_socket_tid;wiringPiSetup();garbage_init();ret = detect_process("mjpg_streamer"); //判斷mjpg_streamer是否已經在運行if (-1 == ret){goto END;}else{printf("已運行mjpg_streamer,可以開始垃圾分類識別\n");}serial_fd = myserialOpen(SERIAL_DEV, BAUD);if (-1 == serial_fd) {goto END;}//開語音線程printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);pthread_create(&get_voice_tid, NULL, pget_voice, NULL);//開網絡線程pthread_create(&get_socket_tid, NULL, pget_socket, NULL);//開阿里云交互線程printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);pthread_create(&category_tid, NULL, pcategory, NULL);pthread_join(get_voice_tid, NULL);pthread_join(category_tid, NULL);pthread_join(get_socket_tid, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);close(serial_fd);
END:garbage_final;return 0;
}
關于TCP KeepAlive
通過設置 TCP KeepAlive 選項,可以在客戶端異常斷開連接時檢測到失效連接,并在必要時關閉它。它會自動發生在操作系統內核層面。
- KeepAlive 探測: 當啟用了 TCP KeepAlive 并設置了相關參數后,操作系統內核會在連接空閑指定時間(TCP_KEEPIDLE)后開始發送 KeepAlive 探測包。
- 檢測響應: 內核會等待客戶端對探測包的響應。如果在指定的次數(TCP_KEEPCNT)內沒有收到客戶端的響應,內核會認為該連接已經失效。
- 關閉連接: 當探測包沒有得到響應時,內核會自動關閉該連接,并通知應用程序。此時,你在應用程序中的 recv 調用會返回 -1,表示連接已經斷開。
oled.c
這段代碼實現了一個 OLED 顯示功能,用于在一個指定位置上顯示垃圾分類的結果。具體來說,代碼分為兩個主要部分:oled_show 函數和 myoled_init 函數。
#include <error.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>#include "oled.h"
#include "font.h"
#include "myoled.h"#define FILENAME "/dev/i2c-3"static struct display_info disp;int oled_show(void *arg)
{unsigned char *buffer = (unsigned char *)arg;//在屏幕上的指定位置顯示指定內容oled_putstrto(&disp, 0, 9+1, "This garbage is:");// 設置字體disp.font = font2;//根據 buffer[2] 的值顯示相應的垃圾分類結果switch(buffer[2]){case 0x41:oled_putstrto(&disp, 0, 20, "dry waste");break;case 0x42:oled_putstrto(&disp, 0, 20, "wet waste");break;case 0x43:oled_putstrto(&disp, 0, 20, "recyclable waste");break;case 0x44:oled_putstrto(&disp, 0, 20, "hazardous waste");break;case 0x45:oled_putstrto(&disp, 0, 20, "recognition failed");break;}// 再次設置字體disp.font = font2;//把需要顯示的內容刷新到屏幕上oled_send_buffer(&disp); return 0;
}int myoled_init(void)
{int e;// 設置顯示屏地址和字體disp.address = OLED_I2C_ADDR;disp.font = font2;// 打開和初始化 OLED 顯示屏e = oled_open(&disp, FILENAME);e = oled_init(&disp);return e;
}