目录
1. 功能需求
- 语音接入控制垃圾分类识别,并触发垃圾桶的开关盖
- 回顾二阶段的Socket编程,实现Sockect发送指令远程控制垃圾分类识别,并触发垃圾桶的开关盖
- 图像识别垃圾分类功能
- 语音播报垃圾物品类型
- OLED显示垃圾物品类型
- 根据垃圾类型开关不同类型垃圾桶
图像处理使用阿里SDK支持Python和Java接口,目的是引入C语言的Python调用,感受大厂做的算法bug,此接口是人工智能接口,阿里云识别模型是通过训练后的模型,精准度取决于训练程度,人工智能范畴,在常规嵌入式设备负责执行居多,说白了就是嵌入式设备负责数据采集,然后转发给人工智能识别后,拿到结果进行执行器动作。
2. Python基础
环境搭建:(备注:在香橙派 3.0.6版本的镜像里已经默认自带了python3.10的版本,不需要安装,只需要后续安装下python3 dev即可。后续统一采用Orangepizero2_3.0.6_ubuntu_jammy_desktop_xfce_linux5.16.17的系统镜像)
Python是一种动态解释型的编程语言。Python可以在Windows、UNIX、MAC等多种操作系统上
使用,也可以在Java、.NET开发平台上使用。
2.1 特点
- Python使用C语言开发,但是Python不再有C语言中的指针等复杂的数据类型。
- Python具有很强的面向对象特性,而且简化了面向对象的实现。它消除了保护类型、抽象类、接口等面向对象的元素。
- Python代码块使用空格或制表符缩进的方式分隔代码。
- Python仅有31个保留字,而且没有分号、begin、end等标记。
- Python是强类型语言,变量创建后会对应一种数据类型,出现在统一表达式中的不同类型的变量需要做 类型转换。
2.2 Python基础知识
简单的基础知识推荐阅读www.runoob.com官网的Python3课程内容
地址如下:
https://www.runoob.com/python3/python3-reg-expressions.html
2.3 dict嵌套简单说明
字典(Dictionary)是Python里非常常见的一种数据结构,如果是在其他语言里,一般称做map。是由键(key)和值(value)成对组成,键和值中间以冒号":“隔开,键值对之间用”,“隔开,整个字典由大括号”{}"括起来。
格式如下:
dict = {key1 : value1, key2 : value2 }
如:
tinydict = {'name': 'runoob', 'likes': 123, 'url': 'www.runoob.com'}
这里面,键一般是唯一的,如果重复了, 最后的一个键值对(Key:value)会替换前面的。 而且键可以用数字,字符串或元组充当,用列表不行。而且值就不需要唯一,而且形式多样,比如可以以列表或者dict的形式出现。
dict的使用非常灵活, 甚至可以和列表组合使用, 列表里能嵌套列表,也能嵌套字典。同样的,字典里能嵌套字典,字典里也能嵌套列表。 如下面这个例子:
garbage_dict = {'Data': {'Elements': [{'Category': '干垃圾', 'CategoryScore':
0.8855999999999999, 'Rubbish': '', 'RubbishScore': 0.0}], 'Sensitive': False},
'RequestId': '1AB9E813-3781-5CA2-95A0-1EA334E80663'}
这个例子里的dict内容是就是一个嵌套的结构,也就是说,它包含了其他的dict或列表作为值。我们可以用以下的方式来理解它:
- 'Elements’对应的值是一个列表,它包含了一个元素,也就是另一个内层的dict。
- 这个内层的dict有四个键:‘Category’、‘CategoryScore’、‘Rubbish’和’RubbishScore’。
- ‘Category’对应的值是一个字符串,表示垃圾分类的类别,例如’干垃圾’。
- 'CategoryScore’对应的值是一个浮点数,表示垃圾分类的置信度,例如0.8856。
- ‘Rubbish’对应的值是一个字符串,表示垃圾的具体名称,例如’'(空字符串)。
- 'RubbishScore’对应的值是一个浮点数,表示垃圾名称的置信度,例如0.0。
- 'Sensitive’对应的值是一个布尔值,表示是否涉及敏感信息,例如False。
- ‘RequestId’对应的值是一个字符串,表示请求的唯一标识符,例如’1AB9E813-3781-5CA2-95A0-1EA334E80663’。
想要取到内层垃圾的类型,可以这样取:
str = garbage_dict['Data']['Elements'][0]['Category']
3. C语言调用Python
3.1 搭建编译环境
通过C语言调用Python代码,需要先安装libpython3的 dev依赖库(不同的ubuntu版本下,python版本可能会有差异, 比如ubuntu 22.04里是libpython3.10-dev)。
首先可以通过以下命令验证是否是否已经存在python3的dev包
dpkg -l | grep libpython3
正常会有类似如下的输出,出现"libpython3"和 “dev”,如libpython3.10-dev即可:
pg@pg-Default-string:~$ dpkg -l | grep libpython
ii libpython3-dev:amd64 3.10.6-1~22.04
amd64 header files and a static library for Python (default)
ii libpython3-stdlib:amd64 3.10.6-1~22.04
amd64 interactive high-level object-oriented language (default
python3 version)
ii libpython3.10:amd64 3.10.12-1~22.04.2
amd64 Shared Python runtime library (version 3.10)
ii libpython3.10-dev:amd64 3.10.12-1~22.04.2
amd64 Header files and a static library for Python (v3.10)
ii libpython3.10-minimal:amd64 3.10.12-1~22.04.2
amd64 Minimal subset of the Python language (version 3.10)
ii libpython3.10-stdlib:amd64 3.10.12-1~22.04.2
amd64 Interactive high-level object-oriented language (standard
library, version 3.10)
如果没有, 可以通过apt命令安装相关的dev包:
sudo apt install libpython3.10-dev
3.2 直接调用python语句
先看这么一个简单的例子:
#include "Python.h"
int main()
{
Py_Initialize(); // 初始化
PyRun_SimpleString("print ('funny')");
Py_Finalize(); //释放资源
}
然要编译和运行这个程序,可以使用以下命令(假设使用的是gcc编译器和Python 3.10版本):
gcc simpledemo.c -o simpledemo -I /usr/include/python3.10 -l python3.10
./simpledemo
gcc后面链接分别为链接库文件目录和链接动态库
输出:
funny
上面代码的解释:
- 首先包含Python.h头文件,这是Python API的头文件,用于访问Python对象和函数
- 其次在程序开始时使用Py_Initialize()函数初始化Python解释器。这样可以在C程序中执行Python代码
- 然后使用PyRun_SimpleString()函数执行一段简单的Python代码,例如打印"funny"。需要传递一个字符串作为参数,表示要执行的Python代码,如print (‘funny’)这么一个Python代码字符串。
函数说明:
int PyRun_SimpleString(const char *command)
这是针对下面 PyRun_SimpleStringFlags() 的简化版接口,将 PyCompilerFlags* 参数设为 NULL;
传参就是python执行语句。
- 最后在程序结束时使用Py_Finalize()函数关闭Python解释器,并释放资源。
3.3 调用无参python函数
现在把某语句放到nopara.py的文件的函数里, 如下:
#nopara.py文件
def say_funny():
print('funny')
接下来用C语言进行调用,一般调用的流程是这样子的:
#if 0
1、包含Python.h头文件,以便使用Python API。
2、使用void Py_Initialize()初始化Python解释器,
3、使用PyObject *PyImport_ImportModule(const char *name)和PyObject
*PyObject_GetAttrString(PyObject *o, const char *attr_name)获取sys.path对象,并利用int PyList_Append(PyObject *list, PyObject *item)将当前路径.添加到sys.path中,以便加载
当前的Python模块(Python文件即python模块)。
4、使用PyObject *PyImport_ImportModule(const char *name)函数导入Python模块,并检查是否
有错误。
5、使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取
Python函数对象,并检查是否可调用。
6、使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用
Python函数,并获取返回值。
7、使用void Py_DECREF(PyObject *o)函数释放所有引用的Python对象。
8、结束时调用void Py_Finalize()函数关闭Python解释器。
相关的函数参数说明参考网站(网站左上角输入函数名即可开始搜索):
https://docs.python.org/zh-cn/3/c-api/import.html
#endif
根据上面的流程写出的示例代码如下:
#include <Python.h>
int main()
{
Py_Initialize(); // 初始化
// 将当前路径添加到sys.path中
PyObject *sys = PyImport_ImportModule("sys");
PyObject *path = PyObject_GetAttrString(sys, "path");
PyList_Append(path, PyUnicode_FromString("."));
// 导入nopara模块/Python文件名
PyObject *pModule = PyImport_ImportModule("nopara");
if (!pModule){
PyErr_Print();
printf("ERROR: failed to load nopara.py\n");
return 1;
}
// 获取say_funny函数对象
PyObject *pFunc = PyObject_GetAttrString(pModule, "say_funny");
if (!pFunc || !PyCallable_Check(pFunc)){
PyErr_Print();
printf("ERROR: function say_funny not found or not callable\n");
return 1;
}
// 调用say_funny函数并获取返回值
PyObject *pValue = PyObject_CallObject(pFunc, NULL);
if (!pValue){
PyErr_Print();
printf("ERROR: function call failed\n");
return 1;
}
// 释放所有引用的Python对象
Py_DECREF(pValue);
Py_DECREF(pFunc);
Py_DECREF(pModule);
// 关闭Python解释器
Py_Finalize();
return 0;
}
然要编译和运行这个程序,可以使用以下命令(假设使用的是gcc编译器和Python 3.10版本):
gcc -o nopara nopara.c -I /usr/include/python3.10/ -l python3.10
./nopara
输出:
funny
3.4 调用有参python函数
C语言调用python有参函数,首先定义一个带参数和返回值的函数:
def say_funny(category):
print(category)
return category
接下来用C语言进行调用,调用的流程无参函数的调用方式几乎是一样的是的:
#if 0
1、包含Python.h头文件,以便使用Python API。
2、使用void Py_Initialize()初始化Python解释器,
3、使用PyObject *PyImport_ImportModule(const char *name)和PyObject
*PyObject_GetAttrString(PyObject *o, const char *attr_name)获取sys.path对象,并利用
int PyList_Append(PyObject *list, PyObject *item)将当前路径.添加到sys.path中,以便加载
当前的Python模块(Python文件即python模块)。
4、使用PyObject *PyImport_ImportModule(const char *name)函数导入Python模块,并检查是否
有错误。
5、使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取
Python函数对象,并检查是否可调用。
6、使用PyObject *Py_BuildValue(const char *format, ...)函数将C类型的数据结构转换成
Python对象,作为Python函数的参数,没有参数不需要调用
7、使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用
Python函数,并获取返回值。
8、使用int PyArg_Parse(PyObject *args, const char *format, ...)函数将返回值转换为C类
型,并检查是否有错误,没有返回值时不需要调用。
9、使用void Py_DECREF(PyObject *o)函数释放所有引用的Python对象。
10、结束时调用void Py_Finalize()函数关闭Python解释器。
相关的函数参数说明参考网站(网站左上角输入函数名即可开始搜索):
https://docs.python.org/zh-cn/3/c-api/import.html
#endif
无非多了两步,第六步传参:
6、使用PyObject *Py_BuildValue(const char *format, ...)函数创建一个Python元组或者对
象,作为Python函数的参数,没有参数势不需要调用
这里要注意的是,Py_BuildValue的第一个参数是类型转换:C对应的Python的数据类型转换对应的格式如下:
和第八步返回值的处理:
8、使用int PyArg_Parse(PyObject *args, const char *format, ...)函数将返回值转换为C类
型,并检查是否有错误,没有返回值时不需要调用。
示例代码如下:
#include <Python.h>
int main()
{
Py_Initialize();
// 将当前路径添加到sys.path中
PyObject *sys = PyImport_ImportModule("sys");
PyObject *path = PyObject_GetAttrString(sys, "path");
PyList_Append(path, PyUnicode_FromString("."));
// 导入para模块
PyObject *pModule = PyImport_ImportModule("para");
if (!pModule){
PyErr_Print();
printf("Error: failed to load nopara.py\n");
}
//获取say_funny函数对象
PyObject *pFunc = PyObject_GetAttrString(pModule, "say_funny");
if (!pFunc){
PyErr_Print();
printf("Error: failed to load say_funny\n");
}
//创建一个字符串作为参数
char *category = "comedy";
PyObject *pArgs = Py_BuildValue("(s)", category);
//调用say_funny函数并获取返回值
PyObject *pValue = PyObject_CallObject(pFunc, pArgs);
if (!pValue){
PyErr_Print();
printf("Error: function call failed\n");
}
//将返回值转换为C类型
char *result = NULL;
if (!PyArg_Parse(pValue, "s", &result)){
PyErr_Print();
printf("Error: parse failed\n");
}
//打印返回值
printf("pValue=%s\n", result);
//释放所有引用的Python对象
Py_DECREF(pValue);
Py_DECREF(pFunc);
Py_DECREF(pModule);
//释放所有引用的Python对象
Py_Finalize();
return 0;
}
然要编译和运行这个程序,可以使用以下命令(假设使用的是gcc编译器和Python 3.10版本):
gcc para.c -o para -I /usr/include/python3.10 -l python3.10
./para
输出:
comedy
4. 阿里云垃圾识别方案
4.1 接入阿里云
在垃圾分类的项目中,我们采用阿里云视觉智能开发平台的接口来做垃圾分类的识别方案,通过上传
本地的拍照下的垃圾图片,通过阿里提供的接口来识别出该垃圾是干垃圾、湿垃圾、回收垃圾还是有害垃圾。
对应官网地址如下:
https://vision.aliyun.com/
然后在上面的输入框输入“垃圾分类”:
可以跳转到对应的垃圾分类的“免费开通"和”技术文档页面“:
https://vision.aliyun.com/experience/detail?
spm=a2c4g.11186623.0.0.6f1b55e2n5cBbZ&tagName=imagerecog&children=ClassifyingRubb ish
可以先选择"技术文档"查看下使用方法:
根据上面描述的指引,蓝色为可点进去的详细说明,完成注册及运行环境的搭建。
重点步骤:
1. 开通阿里云账号及图像识别服务,用自己支付宝即可开通
2. 创建并获取AccessKey ID和Secret
3. 在Linux或开发板上安装所需的SDK
4. 根据示例代码进行修改垃圾分类识别
3. 选择免费开通,通过自己的支付宝或者账号登录即可
4. 登录完成后,即可在产品控制台->点击获取Acce Token获取 对应的AccessKey ID和AccessKey
Secret
当然,在第一次获取到AccessKey ID和AccessKey Secret,需要点击创建AccessKey, 然后最好把
AccessKey.csv下载下来备份,不然会找不到AccessKey Secret就需要重新创建。
5. 在ubuntu 22.04或者全志开发板(orangepi 3.0.6)上安装图像识别(imagerecog)SDK
sudo apt install python3-pip
pip3 install alibabacloud_imagerecog20190930
6. 同时配置Linux环境,根据自己实际的ACCESS_KEY_ID和ACCESS_KEY_SECRET,下面的两行写入到家目录下的.bashrc中
export ALIBABA_CLOUD_ACCESS_KEY_ID=“你的ID” #根据自己实际的ID填写
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="你的SECRET" #根据自己实际的SECRET填写
也可以永久配置这两行生效:
vi ~/.bashrc 和 /etc/profile #然后在末尾输入上面两行后保存
然后退出终端重新登录下,此时再执行export,能看到这两个Key的存在。
7. 抄袭”文件在本地或文件不在同一地域OSS“示例代码,命名为garbage.py。
8. 同时将场景二注释,场景一代码打开,并输入自己测试图片的路径,如下:
# -*- coding: utf-8 -*-
# 引入依赖包
# pip install alibabacloud_imagerecog20190930
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
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',
# 访问的域名对应的region
region_id='cn-shanghai'
)
#场景一:文件在本地
img = open(r'/home/pg/test.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 = img
runtime = RuntimeOptions()
try:
# 初始化Client
client = Client(config)
response = client.classifying_rubbish_advance(classifying_rubbish_request,
runtime)
# 获取整体结果
print(response.body)
except Exception as error:
# 获取整体报错信息
print(error)
# 获取单个字段
print(error.code)
其中“/home/pg/test.jpg”为本地测试用图片(根据在线文档要求:图像类型:JPEG、JPG、PNG,图像大小:不大于3 MB,图像分辨率:不限制图像分辨率,但图像分辨率太高可能会导致API识别超时,超时时间为5秒)
测试图片如下:
9. 然后用python3 garbage.py命令测试运行
orangepi@orangepizero2:~$ python3 garbage.py
{'Data': {'Elements': [{'Category': '干垃圾', 'CategoryScore': 0.8855999999999999,
'Rubbish': '', 'RubbishScore': 0.0}], 'Sensitive': False}, 'RequestId':
'541130FA-92DE-5BCA-8BD4-14154F08BBF0'}
如上,测试成功,说明阿里云垃圾分类方案对接成功。
4.2 C语言调用阿里云Python接口
改造下garbage.py里面的代码,封装成一个函数,供后C程序调用
# -*- coding: utf-8 -*-
# 引入依赖包
# garbage.py
# pip install alibabacloud_imagerecog20190930
import os
import io
import json
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
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',
# 访问的域名对应的region
region_id='cn-shanghai'
)
def alibabacloud_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 = img
runtime = RuntimeOptions()
try:
# 初始化Client
client = Client(config)
response = client.classifying_rubbish_advance(classifying_rubbish_request,
runtime)
print(response.body)
return response.body.to_map()['Data']['Elements'][0]['Category']
except Exception as error:
print(type('获取失败'))
return '获取失败'
参照3.4节有参python函数的做法, 实封装并现以下代码(garbage.c):
//garbage.c
void garbage_init(void)
{
Py_Initialize();
PyObject *sys = PyImport_ImportModule("sys");
PyObject *path = PyObject_GetAttrString(sys, "path");
PyList_Append(path, PyUnicode_FromString("."));
}
void garbage_final(void)
{
Py_Finalize();
}
char *garbage_category(char *category)
{
PyObject *pModule = PyImport_ImportModule("garbage");
if (!pModule){
PyErr_Print();
printf("Error: failed to load garbage.py\n");
goto FAILED_MODULE;
}
PyObject *pFunc = PyObject_GetAttrString(pModule, "alibabacloud_garbage");
if (!pFunc){
PyErr_Print();
printf("Error: failed to load alibabacloud_garbage\n");
goto FAILED_FUNC;
}
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");
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;
}
头文件(garbage.h)如下:
//garbage.h
#ifndef __GARBAGE__H
#define __GARBAGE__H
void garbage_init(void);
void garbage_final(void);
char *garbage_category(char *category);
#endif
测试代码(garbagetest.c)如下:
//garbagetest.c
#include <stdio.h>
#include <stdlib.h>
#include "garbage.h"
int main()
{
char *category = NULL;
garbage_init();
category = garbage_category(category);
printf("category=%s\n", category);
garbage_final();
free(category);
return 0;
}
然要编译和运行这个程序,可以使用以下命令(假设使用的是gcc编译器和Python 3.10版本):
gcc -o garbagetest garbagetest.c garbage.c garbage.h -I /usr/include/python3.10/
-l python3.10
./garbagetest
输出:
category=干垃圾
5. 香橙派使用摄像头
详细可参考《OrangePi_Zero2_H616用户手册v4.0.pdf》 中的3.13.6 USB摄像头测试章节。
操作如下:
- 首先将 USB 摄像头插入到 Orange Pi 开发板的 USB 接口中
- 然后通过 lsmod 命令可以看到内核自动加载了下面的模块
lsmod | grep uvcvideo | grep -v grep
//结果
uvcvideo 106496 0
- 通过 v4l2-ctl 命令可以看到 USB 摄像头的设备节点信息为/dev/videox(x有可能是0 1或者2等数字)
sudo apt update
sudo apt install -y v4l-utils
v4l2-ctl --list-devices
//结果
USB 2.0 Camera (usb-sunxi-ehci-1):
/dev/video1
注意 v4l2 中的 l 是小写字母 l,不是数字1。
另外 video 的序号不一定都是 video1,请以实际看到的为准。
使用 fswebcam 测试 USB 摄像头
a. 安装 fswebcam
sudo apt update sudo apt-get install -y fswebcam
b. 安装完 fswebcam 后可以使用下面的命令来拍照
a) -d 选项用于指定 USB 摄像头的设备节点
b) --no-banner 用于去除照片的水印
c) -r 选项用于指定照片的分辨率
d) -S 选项用设置于跳过前面的帧数
e) ./image.jpg 用于设置生成的照片的名字和路径sudo fswebcam -d /dev/video1 --no-banner -r 1280x720 -S 5 ./image.jpg //注意这里的video1要根据实际的情况修改,
c. 在服务器版的 linux 系统中,拍完照后可以直接通过mobaxterm拖到电脑桌面看或者使用 scp 命令将拍好的图片传到Ubuntu PC 上镜像观看:
scp image.jpg orangepi@192.168.1.55:/home/orangepi/ //根据自己的实际家目录修改pg这个用户名称
使用 mjpg-streamer 测试 USB 摄像头
a. 下载 mjpg-streamer
a) Github 的下载地址:
git clone https://github.com/jacksonliam/mjpg-streamer
b) Gitee 的镜像下载地址为:
git clone https://gitee.com/leeboby/mjpg-streamer
b. 安装依赖的软件包
a) Ubuntu 系统sudo apt-get install -y cmake libjpeg8-dev
c. 编译安装 mjpg-streamer
cd mjpg-streamer/mjpg-streamer-experimental make -j4 sudo make install
d. 然后输入下面的命令启动 mjpg_streamer
注意,video的序号不一定都是 video1,请以实际看到的为准。
export LD_LIBRARY_PATH=. sudo ./mjpg_streamer -i "./input_uvc.so -d /dev/video0 -u -f 30" -o "./output_http.so -w ./www"
e. 然后在和开发板同一局域网的 Ubuntu PC 或者 Windows PC 或者手机的浏览orange Pi器中输入 【开发板的 IP地址:8080】就能看到摄像头输出的视频了
f. 推荐使用 mjpg-streamer 来测试 USB 摄像头,比 motion 流畅很多,使用mjpg-streamer 感觉不到任何卡顿
g.修改 start.sh脚本,将start.sh里的:./mjpg_streamer -i "./input_uvc.so" -o "./output_http.so -w ./www"
字段修改为:
./mjpg_streamer -i "./input_uvc.so -d /dev/video1 -u -f 30" -o "./output_http.so -w ./www" //注意这里的video1需要根据实际情况修改
这样就可以通过执行./start.sh运行摄像头了。
补充设置开机自启动 mjpg-streamer 服务
- 在某目录下创建mjpg.sh脚本文件,写入以下内容并且保存关闭
#!/bin/bash
cd /home/orangepi/mjpg-streamer/mjpg-streamer-experimental/
./start.sh
- 进入/etc/xdg/autostart目录创建一个mjpg.desktop文件并写入以下内容,保存关闭
cd /etc/xdg/autostart
vi mjpg.desktop
//内容
[Desktop Entry]
Name=mjpg
Exec=/home/orangepi/mjpg.sh
Type=Application
NoDisplay=true
- 重启香橙派,并查看该服务是否已经自动启动
6. 语音模块的配置
本项目的语音模块采用智能公元家的SU-03T模块,进入官网创建产品即可傻瓜式配置
1. 首先更改P6,P7口为串口模式
2. 设置唤醒词
3. 设置命令词,语音播报垃圾类型的触发方式为串口输入,并设置唤醒词的回复词
4. 设置命令词的控制,将串口输入唤醒语音播报的设置和语音唤醒向串口发送数据的设置补充完整
这个是垃圾类型播报的唤醒与动作详细设置
这个是语音唤醒并且向串口发送识别指令命令动作的详细设置
5. 剩余内容根据自己爱好进行设置,最后点击右上角生成SDK按钮等待生成
注意:生成固件需要一段时间,下载完毕后要点击下载固件,如果点击下载SDK烧录以后可能会出现一些问题(本人亲身经历找bug找了半天)
最后根据产品说明用烧录软件将固件烧录入语音模块,烧录完成后可以利用串口调试助手进行测试功能的完整性。
7. VSCode安装并通过SSH连接全志开发板
1. 进入VSCode官网 Visual Studio Code - Code Editing. Redefined,下载安装包
2. 安装Remote Development
3. 安装中文插件
4. 配置字体为20
配置文件–>首选项->设置->Font Size为20
5. 配置远程连接
6. 配置远程目录
连接成功后点击打开文件夹并输入文件目录进入指定文件目录,即可看到代码文件
8. 语音模块和阿里云结合
8.1 环境准备
- 将语音模块接在UART5的位置
- 在orange pi 3.0.6上确认已经配置开启了uart5:(overlays=uart5)
orangepi@orangepizero2:~/garbage$ cat /boot/orangepiEnv.txt
verbosity=1
bootlogo=false
console=both
disp_mode=1920x1080p60
overlay_prefix=sun50i-h616
rootdev=UUID=15a0010c-94e1-412f-b030-199e90c16cb1
rootfstype=ext4
overlays=uart5 i2c3
usbstoragequirks=0x2537:0x1066:u,0x2537:0x1068:u
- 同时将USB摄像头接到香橙派上
- 确认已经运行了mjpg-streamer服务
orangepi@orangepizero2:~/garbage$ ps ax | grep mjpg
1704 ? S 0:00 /bin/bash /home/orangepi/mjpg.sh
1710 ? Sl 0:29 ./mjpg_streamer -i ./input_uvc.so -d /dev/video1 -u
-f 30 -o ./output_http.so -w ./www
5594 pts/0 S+ 0:00 grep --color=auto mjpg
orangepi@orangepizero2:~/garbage$
8.2 代码实现
- 首先创建garbage目录,将garbage.c、garbage.h、 garbage.py三个文件拷贝进来。
- 参照之前博文Linux原生串口开发代码实现, 修改uartTool.h、uartTool.c:
//uartTool.h
#ifndef __UARTTOOL_H
#define __UARTTOOL_H
int myserialOpen (const char *device, const int baud);
void serialSendstring (const int fd, const unsigned char *s, int len);
int serialGetstring (const int fd, unsigned char *buffer);
#define SERIAL_DEV "/dev/ttyS5"
#define BAUD 115200
#endif
//uartTool.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "wiringSerial.h"
int mySerialOpen (const char *device, const int baud)
{
struct termios options ;
speed_t myBaud ;
int status, fd ;
switch (baud){
case 9600: myBaud = B9600 ; break ;
case 115200: myBaud = B115200 ; break ;
}
if ((fd = open (device, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK)) == -1)
return -1 ;
fcntl (fd, F_SETFL, O_RDWR) ;
// Get and modify current options:
tcgetattr (fd, &options) ;
cfmakeraw (&options) ;
cfsetispeed (&options, myBaud) ;
cfsetospeed (&options, myBaud) ;
options.c_cflag |= (CLOCAL | CREAD) ;
options.c_cflag &= ~PARENB ;
options.c_cflag &= ~CSTOPB ;
options.c_cflag &= ~CSIZE ;
options.c_cflag |= CS8 ;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG) ;
options.c_oflag &= ~OPOST ;
options.c_cc [VMIN] = 0 ;
options.c_cc [VTIME] = 100 ; // Ten seconds (100 deciseconds)
tcsetattr (fd, TCSANOW, &options) ;
ioctl (fd, TIOCMGET, &status);
status |= TIOCM_DTR ;
status |= TIOCM_RTS ;
ioctl (fd, TIOCMSET, &status);
usleep (10000) ; // 10mS
return fd ;
}
void serialSendString (const int fd, const unsigned char *s,int len)
{
int ret;
ret = write (fd, s, len);
if (ret < 0)
printf("Serial Puts Error\n");
}
int serialGetString (const int fd,unsigned char *buffer)
{
int n_read;
n_read = read(fd,buffer,32);
return n_read;
}
- garbage.h头文件
#ifndef __GARBAGE__H
#define __GARBAGE__H
void 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 <stdlib.h>
#include <string.h>
#include "uartTool.h"
#include "garbage.h"
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;
}
int main(int argc, char *argv[])
{
int serial_fd = -1;
int len = 0;
int ret = -1;
char *category = NULL;
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};
wiringPiSetup();
garbage_init();
ret = detect_process("mjpg_streamer");
if (-1 == ret){
printf("detect process failed\n");
goto END;
}
serial_fd = mySerialOpen(SERIAL_DEV, BAUD);
if (serial_fd == -1){
goto END;
}
while (1){
len = serialGetString(serial_fd, buffer);
if (len > 0 && buffer[2] == 0x46){
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;
}
serialSendString(serial_fd, buffer, 6);
buffer[2] = 0x00;
remove(GARBAGE_FILE);
}
}
close(serial_fd);
END:
gargage_final();
return 0;
}
9. 增加垃圾桶及开关盖功能
实现功能:使用语音模块和摄像头在香橙派上做垃圾智能分类识别, 同时根据识别结果开关不同的垃圾桶的盖子。
9.1 环境准备
在语音模块和阿里云结合搭建环境的基础上, 接上用于开关盖的舵机(舵机模块可以直接粘在垃
圾桶内侧),当前代码里仅用了1个舵机用于示例代码的编写,可以自行多购买4个垃圾桶和舵机用于区分4垃圾类型,接线位置如下:(我只用了Pin5,干垃圾开Pin5对应舵机,其余只发送信号)
实物图:
9.2 代码实现
流程图:
具体代码实现:
- 增加用于实现开光盖(驱动舵机)的源码文件(pwm.c)
#include <wiringPi.h>
#include <softPwm.h>
// 根据公式:PWMfreq = 1 x 10^6 / (100 x range) ,要得到PWM频率为50Hz,则range为200,即周期分为200步,控制精度相比硬件PWM较低。
void pwm_write(int pwm_pin)
{
pinMode(pwm_pin, OUTPUT);
softPwmCreate(pwm_pin, 0, 200); // range设置周期分为200步, 周期20ms
softPwmWrite(pwm_pin, 10); // 1ms 45度
delay(1000);
softPwmStop(pwm_pin);
}
void pwm_stop(int pwm_pin)
{
pinMode(pwm_pin, OUTPUT);
softPwmCreate(pwm_pin, 0, 200); // range设置周期分为200步, 周期20ms
softPwmWrite(pwm_pin, 5); // 0.5ms 0度
delay(1000);
softPwmStop(pwm_pin);
}
- pwm.h代码
#ifndef __PWM__H
#define __PWM__H
#define PWM_GARBAGE 7
#define PWM_RECOVERABLE_GARBAGE 5
void pwm_write(int pwm_pin);
void pwm_stop(int pwm_pin);
#endif
- main.c里增加调用舵机的控制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "uartTool.h"
#include "garbage.h"
#include "pwm.h"
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;
}
int main(int argc, char *argv[])
{
int serial_fd = -1;
int len = 0;
int ret = -1;
char *category = NULL;
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};
wiringPiSetup();
garbage_init();
ret = detect_process("mjpg_streamer");
if (-1 == ret){
printf("detect process failed\n");
goto END;
}
serial_fd = mySerialOpen(SERIAL_DEV, BAUD);
if (serial_fd == -1){
goto END;
}
while (1){
len = serialGetString(serial_fd, buffer);
if (len > 0 && buffer[2] == 0x46){
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;
}
serialSendString(serial_fd, buffer, 6);
if (buffer[2] == 0x41){
pwm_write(PWM_RECOVERABLE_GARBAGE);
delay(2000);
pwm_stop(PWM_RECOVERABLE_GARBAGE);
}else if (buffer[2] != 0x45){
pwm_write(PWM_GARBAGE);
delay(2000);
pwm_stop(PWM_GARBAGE);
}
buffer[2] = 0x00;
remove(GARBAGE_FILE);
}
}
close(serial_fd);
END:
gargage_final();
return 0;
}
10. 项目代码优化
在之前实现的代码中, 主函数是单线程执行的, 导致整个代码的可扩展性非常差,比如想加OLED显示或者添加网络控制变得非常复杂,而且执行一次识别开关盖的流程非常长。因此,调整下代码架构,增加并发功能、提升代码的可扩展性和执行效率。
- 代码大致流程图如下:
- 修改main.c代码,调整整体main函数的代码架构,利用多线程实现具体的功能(用到了线程里的条
件变量控制线程间的数据同步)如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <wiringPi.h>
#include <pthread.h>
#include "uartTool.h"
#include "garbage.h"
#include "pwm.h"
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;
}
int serial_fd = -1;
pthread_cond_t cond;
pthread_mutex_t mutex;
void *pget_voice(void *arg)
{
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};
int len = 0;
if (serial_fd == -1){
printf("%s|%s|%d: open serial failed\n",__FILE__,__func__,__LINE__);
pthread_exit(0);
}
while(1){
len = serialGetString(serial_fd, buffer);
if (len > 0 && buffer[2] == 0x46){
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
pthread_exit(0);
}
void *psend_voice(void *arg)
{
unsigned char *buffer = (unsigned char *)arg;
if (serial_fd == -1){
printf("%s|%s|%d: open serial failed\n",__FILE__,__func__,__LINE__);
pthread_exit(0);
}
pthread_detach(pthread_self());//忽略线程等待,自己释放资源
if(NULL != buffer){
printf("0x%x\n",buffer[2]);
serialSendString(serial_fd, buffer, 6);
}
pthread_exit(0);
}
void *popen_trash(void *arg)
{
unsigned char *buffer = (unsigned char *)arg;
if (serial_fd == -1){
printf("%s|%s|%d: open serial failed\n",__FILE__,__func__,__LINE__);
pthread_exit(0);
}
printf("0x%x\n",buffer[2]);
pthread_detach(pthread_self());//忽略线程等待,自己释放资源
if (buffer[2] == 0x41){
pwm_write(PWM_RECOVERABLE_GARBAGE);
delay(5000);
pwm_stop(PWM_RECOVERABLE_GARBAGE);
}else if (buffer[2] != 0x45){
pwm_write(PWM_GARBAGE);
delay(5000);
pwm_stop(PWM_GARBAGE);
}
pthread_exit(0);
}
void *pcategory(void *arg)
{
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};
char *category = NULL;
pthread_t send_voice_tid,trash_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(&send_voice_tid,NULL,psend_voice,(void *)buffer);
//开垃圾桶线程
pthread_create(&trash_tid,NULL,popen_trash,(void *)buffer);
remove(GARBAGE_FILE);
}
pthread_exit(0);
}
int main(int argc, char *argv[])
{
int ret = -1;
pthread_t get_voice_tid,category_tid;
wiringPiSetup();
garbage_init();
ret = detect_process("mjpg_streamer");
if (-1 == ret){
printf("detect process failed\n");
goto END;
}
serial_fd = mySerialOpen(SERIAL_DEV, BAUD);
if (serial_fd == -1){
goto END;
}
//开语音识别线程
pthread_create(&get_voice_tid,NULL,pget_voice,NULL);
//开阿里云交互线程
pthread_create(&category_tid,NULL,pcategory,NULL);
pthread_join(get_voice_tid,NULL);
pthread_join(category_tid,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
close(serial_fd);
END:
gargage_final();
return 0;
}
此代码中,实现了等待语音模块发送识别指令线程和拍照发送给阿里云识别并修改指令串线程的同步和并发执行,还实现了语音播报和开垃圾桶两个线程的并发执行。
11. 增加OLED显示功能
关于硬件接线,将OLED屏幕分别接香橙派开发板的SDA,SCK,5V,GND四个接线柱。
增加myoled.c和myoled.h两个文件
//oled.c
#include <errno.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;
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;
e = oled_open(&disp, FILENAME);
e = oled_init(&disp);
return e;
}
//myoled.h
#ifndef __MYOLED__H
#define __MYOLED__H
int myoled_init(void);
int oled_show(void *arg);
#endif
修改main.c文件,加入oled屏幕显示线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <wiringPi.h>
#include <pthread.h>
#include "uartTool.h"
#include "garbage.h"
#include "pwm.h"
#include "myoled.h"
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;
}
int serial_fd = -1;
pthread_cond_t cond;
pthread_mutex_t mutex;
void *pget_voice(void *arg)
{
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};
int len = 0;
if (serial_fd == -1)
{
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
while (1)
{
len = serialGetString(serial_fd, buffer);
if (len > 0 && buffer[2] == 0x46)
{
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
pthread_exit(0);
}
void *psend_voice(void *arg)
{
unsigned char *buffer = (unsigned char *)arg;
if (serial_fd == -1)
{
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
pthread_detach(pthread_self()); // 忽略线程等待,自己释放资源
if (NULL != buffer)
{
serialSendString(serial_fd, buffer, 6);
}
pthread_exit(0);
}
void *popen_trash(void *arg)
{
unsigned char *buffer = (unsigned char *)arg;
if (serial_fd == -1)
{
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
pthread_detach(pthread_self()); // 忽略线程等待,自己释放资源
if (buffer[2] == 0x41)
{
pwm_write(PWM_RECOVERABLE_GARBAGE);
delay(3000);
pwm_stop(PWM_RECOVERABLE_GARBAGE);
}
else if (buffer[2] != 0x45)
{
pwm_write(PWM_GARBAGE);
delay(3000);
pwm_stop(PWM_GARBAGE);
}
pthread_exit(0);
}
void *poled_show(void *arg)
{
pthread_detach(pthread_self());
myoled_init();
oled_show(arg);
pthread_exit(0);
}
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(&send_voice_tid, NULL, psend_voice, (void *)buffer);
// 开垃圾桶线程
pthread_create(&trash_tid, NULL, popen_trash, (void *)buffer);
//oled显示线程
pthread_create(&oled_tid, NULL, poled_show, (void *)buffer);
remove(GARBAGE_FILE);
}
pthread_exit(0);
}
int main(int argc, char *argv[])
{
int ret = -1;
pthread_t get_voice_tid, category_tid;
wiringPiSetup();
garbage_init();
ret = detect_process("mjpg_streamer");
if (-1 == ret)
{
printf("detect process failed\n");
goto END;
}
serial_fd = mySerialOpen(SERIAL_DEV, BAUD);
if (serial_fd == -1)
{
goto END;
}
// 开语音识别线程
pthread_create(&get_voice_tid, NULL, pget_voice, NULL);
// 开阿里云交互线程
pthread_create(&category_tid, NULL, pcategory, NULL);
pthread_join(get_voice_tid, NULL);
pthread_join(category_tid, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
close(serial_fd);
END:
gargage_final();
return 0;
}
编译运行
11. 增加网络控制功能
11.1 实现需求
通过Socket远程接入控制垃圾识别功能。
11.2 TCP 心跳机制解决Soket异常断开问题
Socket客户端得断开情形无非就两种情况:
- 客户端能够发送状态给服务器;正常断开,强制关闭客户端等,客户端能够做出反应。
- 客户端不能发送状态给服务器;突然断网,断电,客户端卡死等,客户端根本没机会做出反应,服务器更不了解客户端状态,导致服务器异常等待。
为了解决上述问题,引入TCP心跳包机制:
心跳包的实现,心跳包就是服务器定时向客户端发送查询信息,如果客户端有回应就代表连接正常,
类似于linux系统的看门狗机制。心跳包的机制有一种方法就是采用TCP_KEEPALIVE机制,它是一种用于检测TCP连接是否存活的机制,它的原理是在一定时间内没有数据往来时,发送探测包给对方,如果对方没有响应,就认为连接已经断开。TCP_KEEPALIVE机制可以通过设置一些参数来调整,如探测时间间隔、探测次数等。
Linux内核提供了通过sysctl命令查看和配置TCP KeepAlive参数的方法。
- 查看当前系统的TCP KeepAlive参数
sysctl net.ipv4.tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_probes
sysctl net.ipv4.tcp_keepalive_intvl
- 修改TCP KeepAlive参数
sysctl net.ipv4.tcp_keepalive_time=3600
11.3 C语言实现TCP KeepAlive功能
对于Socket而言,可以在程序中通过socket选项开启TCP KeepAlive功能,并配置参数。对应的Socket选项分别为 SO_KEEPALIVE 、 TCP_KEEPIDLE 、 TCP_KEEPCNT 、 TCP_KEEPINTVL。
int keepalive = 1; // 开启TCP KeepAlive功能
int keepidle = 5; // tcp_keepalive_time 3s内没收到数据开始发送心跳包
int keepcnt = 3; // tcp_keepalive_probes 每次发送心跳包的时间间隔,单位秒
int keepintvl = 3; // tcp_keepalive_intvl 每3s发送一次心跳包
setsockopt(client_socketfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive,sizeof(keepalive));
setsockopt(client_socketfd, SOL_TCP, TCP_KEEPIDLE, (void *) &keepidle, sizeof(keepidle));
setsockopt(client_socketfd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof(keepcnt));
setsockopt(client_socketfd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof(keepintvl));
11.4 网络控制功能代码实现
socket.h代码实现如下:
#ifndef __SOCKET__H
#define __SOCKET__H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <errno.h>
#define IPADDR "192.168.0.10" //填写自己实际的ip地址
#define IPPORT "8192"
#define BUF_SIZE 6
int socket_init(const char *ipaddr, const char *port);
#endif
socket.c代码实现如下:
#include "socket.h"
int socket_init(const char *ipaddr, const char *port)
{
int s_fd = -1;
int ret = -1;
struct sockaddr_in s_addr;
memset(&s_addr, 0, sizeof(struct sockaddr_in));
// 1.socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1)
{
perror("socket");
return -1;
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(port));
inet_aton(ipaddr, &s_addr.sin_addr);
// 2. bind
ret = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
if (-1 == ret)
{
perror("bind");
return -1;
}
// 3. listen
ret = listen(s_fd, 1); // 只监听1个连接,排队扔垃圾
if (-1 == ret)
{
perror("listen");
return -1;
}
return s_fd;
}
main.c代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <wiringPi.h>
#include <pthread.h>
#include "uartTool.h"
#include "garbage.h"
#include "pwm.h"
#include "myoled.h"
#include "socket.h"
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;
}
int serial_fd = -1;
pthread_cond_t cond;
pthread_mutex_t mutex;
void *pget_voice(void *arg)
{
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0x55, 0xAA};
int len = 0;
if (serial_fd == -1)
{
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
while (1)
{
len = serialGetString(serial_fd, buffer);
if (len > 0 && buffer[2] == 0x46)
{
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
pthread_exit(0);
}
void *psend_voice(void *arg)
{
unsigned char *buffer = (unsigned char *)arg;
if (serial_fd == -1)
{
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
pthread_detach(pthread_self()); // 忽略线程等待,自己释放资源
if (NULL != buffer)
{
serialSendString(serial_fd, buffer, 6);
}
pthread_exit(0);
}
void *popen_trash(void *arg)
{
unsigned char *buffer = (unsigned char *)arg;
if (serial_fd == -1)
{
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
pthread_detach(pthread_self()); // 忽略线程等待,自己释放资源
if (buffer[2] == 0x41)
{
pwm_write(PWM_RECOVERABLE_GARBAGE);
delay(3000);
pwm_stop(PWM_RECOVERABLE_GARBAGE);
}
else if (buffer[2] != 0x45)
{
pwm_write(PWM_GARBAGE);
delay(3000);
pwm_stop(PWM_GARBAGE);
}
pthread_exit(0);
}
void *poled_show(void *arg)
{
pthread_detach(pthread_self());
myoled_init();
oled_show(arg);
pthread_exit(0);
}
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(&send_voice_tid, NULL, psend_voice, (void *)buffer);
// 开垃圾桶线程
pthread_create(&trash_tid, NULL, popen_trash, (void *)buffer);
// oled显示线程
pthread_create(&oled_tid, NULL, poled_show, (void *)buffer);
remove(GARBAGE_FILE);
}
pthread_exit(0);
}
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; // tcp_keepalive_time 3s内没收到数据开始发送心跳包
int keepcnt = 3; // tcp_keepalive_probes 发送3次
int keepintvl = 3; // tcp_keepalive_intvl 每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 ret = -1;
pthread_t get_voice_tid, category_tid,get_socket_tid;;
wiringPiSetup();
garbage_init();
ret = detect_process("mjpg_streamer");
if (-1 == ret)
{
printf("detect process failed\n");
goto END;
}
serial_fd = mySerialOpen(SERIAL_DEV, BAUD);
if (serial_fd == -1)
{
goto END;
}
// 开语音识别线程
pthread_create(&get_voice_tid, NULL, pget_voice, NULL);
// 开阿里云交互线程
pthread_create(&category_tid, NULL, pcategory, NULL);
//开网络线程
pthread_create(&get_socket_tid, NULL, pget_socket, 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:
gargage_final();
return 0;
}