本文主要是介绍使用香橙派并基于Linux实现最终版智能垃圾桶项目 --- 下,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
最终完成效果视频:
使用香橙派并基于Linux实现最终版带图像识别的可回收垃圾桶 — 下_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1bc411o7zS/?spm_id_from=333.999.0.0&vd_source=16380169fa415d2d056dd877a8d8c1b7
硬件接线 & 最终实现图
目录
项目需求
如何使用C语言执行Python代码
ubuntu22.04系统重装
遇到的一些问题
环境搭建
C语言执行一小段python代码
python_test.c:
编译语句:
实现结果:
C语言执行python函数
代码示例:
python_function_test.py:
python_function_test.c:
编译语句:
实现效果:
阿里云的垃圾识别方案
编辑大体步骤:
账号创建 & 开启服务
获取AccessKey ID & Secret
安装SDK & 环境配置
图像识别代码试运行
aliyun_classify.py:
使用C语言调用阿里云识别
aliyun_classify.py:
classify.c:
classify.h: (固定格式)
ali_test.c:
香橙派的摄像头接入
首先,将USB摄像头插入,我使用的是一枚720P的广角USB摄像头:
然后运行以下指令可以看到内核自动加载了下面的模块:
然后通过以下命令可以看到 USB 摄像头的设备节点信息
使用 fswebcam 测试 USB 摄像头拍照
使用 mjpg-streamer 测试 USB 摄像头视频流
修改/home/orangepi/mjpg-streamer/mjpg-streamer-experimental/下的start.sh:
将视频拍照并保存本地:
编写脚本使得mjpg-streamer可以在香橙派开机时自动后台启动:
需求8 --- 使用第三方库结合python实现图像识别垃圾分类
实现思路
新的线程统计图:
新的程序运行图:
新的接线图:
语音模块的重新烧写
引脚设置:
唤醒词:
命令设置:
控制详情:编辑
设置免唤醒:
其他配置:
实现效果
代码的重新编写
注意事项
v6_server.c:
编译语句:
运行语句:
回顾需求:
项目需求
- 靠近时,垃圾桶开启2秒,2秒后关闭
- 垃圾桶开启带滴滴声
- 垃圾桶开启超过10秒,滴滴声警报
- 实现Sockect客户端发送指令远程打开/关闭垃圾桶,并显示垃圾桶状态
- 语音控制垃圾桶开关盖
- 记录一段时间内垃圾桶开关盖动作及开关盖指令来源并记录在文件中
- 统计一段时间内垃圾桶开关盖次数及开关盖指令来源并记录在数据库中
- 图像识别垃圾分类功能
在上节已经完成了对于前7个功能的编写,本节完成最后一个需求!
如何使用C语言执行Python代码
很无语的是,我目前香橙派上安装的UBUNTU18.04的python3和阿里云并不兼容,也就意味着,我需要先重装系统,安装更高版本的ubuntu,使用更高版本的python3.10来完成和阿里云的交互。
ubuntu22.04系统重装
由于之前有过装系统的经验,这里就不再次详细演示了,注意重装前的重要资料备份!!
要将新烧写的系统恢复成我目前的状态,以下是要做的事:
- 格式化SD卡并写入新的光盘映像文件
- 使用CH340连接香橙派和电脑,设置波特率115200,等待boot完成后使用默认账户登录
- 修改密码,sudo reboot
- 网络设置连接WIFI
- 修改开发板内核启动日志级别
- 安装外设驱动库
(Orangepi Zero2 全志H616 的初识_mjmmm的博客-CSDN博客)
- 设置手动对齐和自动对齐的空格数
(香橙派使用外设驱动库wiringOP来驱动蜂鸣器_mjmmm的博客-CSDN博客)
- 安装IIC工具
(香橙派配合IIC驱动OLED & 使用SourceInsight解读源码_mjmmm的博客-CSDN博客)
- 安装adb工具
- /etc/udev/rules.d下写规则文件匹配手机USB
(基于香橙派和SU-03T 使用Linux实现语音控制刷抖音-CSDN博客)
- /etc/udev/rules.d下写规则文件匹配U盘
- 安装tree指令
(使用香橙派学习Linux udev的rules 并实现U盘的自动挂载-CSDN博客)
- 安装嵌入式数据库SQLite3
(使用香橙派学习 嵌入式数据库---SQLite-CSDN博客)
安装完后发现,这一次香橙派的IP变为了192.168.2.23
遇到的一些问题
第二次安装后,运行“ls /dev”:
发现,i2c-3和uart5并没有被开启,经过查证,这是内核版本的问题,输入“uname -r” :
![]()
可见,内核版本是5.16.17,而这个版本并不会自动开启12c-3,只有老版本4.9才会!
解决办法:
输入“sudo vim /boot/orangepiEnv.txt” ; 并加入一句“overlays=i2c3 uart5”:
![]()
保存退出后,reboot,然后再次运行“ls /dev”:
问题解决 !
环境搭建
如果想要在C语言中调用python的代码,需要安装libpython3的dev依赖库:
通过以下命令查看是否已经存在依赖包:
dpkg -l | grep libpython3
可见并不存在后缀为“-dev” 的库,因此需要执行以下指令安装:
sudo apt install libpython3.10-dev
安装完成后再次查看就可以看到“libpython3.10-dev”了:
C语言执行一小段python代码
python_test.c:
#include "Python.h" //Python API的头文件,用于访问Python对象和函数int main()
{Py_Initialize(); //初始化Python解释器,这样可以在C程序中执行Python代码PyRun_SimpleString("print('funny')"); //执行一段简单的Python代码Py_Finalize(); //关闭Python解释器,并释放资源}
编译语句:
gcc python_test.c -I /usr/include/python3.10 -l python3.10
//“-I”指定python头文件路径;“-l”指定python库文件路径
实现结果:
C语言执行python函数
步骤大体如下:
- 包含Python.h头文件,以便使用Python API
- 使用void Py_Initialize()初始化Python解释器
- 使用 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模块)
- 使用PyObject *PyImport_ImportModule(const char *name)函数导入Python模块,并检查是否有错误
- 使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取 Python函数对象,并检查是否可调用
- 使用PyObject *Py_BuildValue(const char *format, ...)函数将C类型的数据结构转换成Python对 象,作为Python函数的参数, 没有参数则不需要调用
- 使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用Python 函数,并获取返回值
- 使用int PyArg_Parse(PyObject *args, const char *format, ...)函数将返回值转换为C类型, 并检查是否有错误, 没有返回值时则不需要调用
- 使用void Py_DECREF(PyObject *o)函数释放所有引用的Python对象
- 结束时调用void Py_Finalize()函数关闭Python解释器
以上出现的这些粉色的函数可以通过这个网站来查询详细的说明:
(网站左上角输入函数名搜索即可)
导入模块 — Python 3.12.0 文档
关于第三步,一般是这样写:
PyObject *sys = PyImport_ImportModule("sys"); PyObject *path = PyObject_GetAttrString(sys, "path"); PyList_Append(path, PyUnicode_FromString("."));
关于第六步,Py_BuildValue(const char *format, ...)函数创建的是一个Python元组或者对象,作为Python函数的参数。这个函数的第一个参数是类型转换,C对应的Python的数据类型转换对应的格式如下:
代码示例:
首先,单独写一个python文件,其中定义一个带参数和返回值的函数:
python_function_test.py:
def say_something(something):print(something)return something
然后在写一个C程序来调用它:
python_function_test.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("python_function_test");if (!pModule){PyErr_Print();printf("Error: failed to load module\n");}//获取say_funny函数对象PyObject *pFunc = PyObject_GetAttrString(pModule, "say_something");if (!pFunc){PyErr_Print();printf("Error: failed to load function\n");}//创建一个字符串作为参数char *something = "mjm hahahaha";PyObject *pArgs = Py_BuildValue("(s)", something); //(s)代表有一个字符串元素的元组//调用函数并获取返回值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_function_test.c -I /usr/include/python3.10 -l python3.10
实现效果:
可见,C程序成功调用了python函数并传入了参数,同时也成功获得了python函数的返回值!
阿里云的垃圾识别方案
在学习了C语言调用python的方法,现在就可以开始了解阿里云提供的算法,从而使得我写的C代码可以将数据发送给阿里云处理并从阿里云获得结果:
- 先进入阿里云的官网:阿里云|达摩院视觉智能开放平台
- 进入后搜索“垃圾分类”,点击第一个进入:能力展示-阿里云视觉智能开放平台
- 点击“技术文档”即可查看官方提供的接入指引,图片限制,输入输出参数等:调用ClassifyingRubbish进行垃圾分类识别_视觉智能开放平台-阿里云帮助中心
大体步骤:
- 开通阿里云账号及图像识别服务,用自己支付宝即可开通
- 创建并获取AccessKey ID和Secret
- 在Linux或开发板上安装所需的SDK
账号创建 & 开启服务
开通后就会出现以下画面,点击“进入控制台” :
工作台地址:阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台
获取AccessKey ID & Secret
在控制台中,就可以获得Acess Token了,第一次点击需要创建AccessKey:
安装SDK & 环境配置
在获取完AccessKey的ID和Secret之后,就可以回到香橙派,安装阿里云的SDK了:
sudo apt install python3-pip
pip3 install alibabacloud_imagerecog20190930
安装完成后,根据自己实际的ACCESS_KEY_ID 和 ACCESS_KEY_SECRET,将以下内容写入到家目录下的.bashrc中:
export ALIBABA_CLOUD_ACCESS_KEY_ID="XXX" #根据自己实际的ID填写
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="XXXX" #根据自己实际的SECRET填写
vi ~/.bashrc #然后在末尾输入上面两行后保存
bashrc :
然后输入“export”,可以看到我的ID和Secret已经存在:
图像识别代码试运行
示例代码地址:
垃圾分类识别常用语言和示例有哪些_视觉智能开放平台-阿里云帮助中心
找到“文件在本地或文件不在同一地域OSS”的示例代码,将它拷贝到香橙派的smart_bin下,将场景一打开,场景二注释
aliyun_classify.py:
# -*- coding: utf-8 -*-
# 引入依赖包
# pip install alibabacloud_imagerecog20190930import 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 RuntimeOptionsconfig = 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')#场景一:文件在本地
img = open(r'/home/orangepi/smart_bin/pic/1.png', '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:# 初始化Clientclient = Client(config)response = client.classifying_rubbish_advance(classifying_rubbish_request, runtime)# 获取整体结果print(response.body)
except Exception as error:# 获取整体报错信息print(error)# 获取单个字段print(error.code)
其中,“/home/orangepi/smart_bin/pic/1.png” 就是我在香橙派本地存储的一张照片:
然后运行“python3 aliyun_classify.py”:
可见,识别正确,的确是塑料饮料瓶,并且成功分类到了可回收垃圾!
使用C语言调用阿里云识别
修改aliyun_classify.py,将核心代码定义成一个函数,且返回垃圾分类的结果:
aliyun_classify.py:
# -*- coding: utf-8 -*-
# 引入依赖包
# pip install alibabacloud_imagerecog20190930import 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 RuntimeOptionsconfig = 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')def ali_classify():#场景一:文件在本地img = open(r'/home/orangepi/smart_bin/pic/1.png', '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)return response.body.to_map()['Data']['Elements'][0]['Category']
#response.body的类型是class,所以需要先转换成字典在进行提取,此处的to_map函数是阿里云SDK在实现这个class时封装的一个函数(models.py中),作用就是转化成dictexcept Exception as error:# 获取整体报错信息print(error)# 获取单个字段print(error.code)return '获取失败'
然后写一个C程序封装调用上面python代码可能需要的函数:
classify.c:
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <Python.h>
#include "classify.h"void classify_init(void)
{Py_Initialize();PyObject *sys = PyImport_ImportModule("sys");PyObject *path = PyObject_GetAttrString(sys, "path");PyList_Append(path, PyUnicode_FromString("."));
}void classify_final(void)
{Py_Finalize();
}char *classify_category(char *category)
{PyObject *pModule = PyImport_ImportModule("aliyun_classify");if (!pModule){PyErr_Print();printf("Error: failed to load module\n");goto FAILED_MODULE; //goto的意思就是如果运行到这里就直接跳转到FAILED_MODULE}PyObject *pFunc = PyObject_GetAttrString(pModule, "ali_classify");if (!pFunc){PyErr_Print();printf("Error: failed to load function\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) ); //开辟一个新的字符串常量。+1是为了留出空间给\0memset(category, 0, (strlen(result) + 1)); //初始化字符串strncpy(category, result, (strlen(result) + 1)); //将result的结果复制给新的字符串FAILED_RESULT:Py_DECREF(pValue);
FAILED_VALUE:Py_DECREF(pFunc);
FAILED_FUNC:Py_DECREF(pModule);
FAILED_MODULE:return category;
}
👆此处注意到,result的值是pValue赋予的,且他们都是字符串指针变量,而pValue在最后返回前就会被free掉,pValue的值一旦被free,result的值也会被free,所以必须在pValue被free之前再定义一个字符串,使用strcpy来保存需要返回的结果!
classify.h: (固定格式)
#ifndef __CLASSIFY__H
#define __CLASSIFY__H
void classify_init(void);
void classify_final(void);
char *classify_category(char *category);
#endif
#ifndef __XXX__H #define __XXX__H#endif
这个格式的使用是为了避免重复调用
最后利用刚刚写的h文件作为头文件,写一个调用阿里云接口的C程序:
ali_test.c:
#include <stdio.h>
#include <stdlib.h>
#include "classify.h"int main()
{char *category = NULL;classify_init();category = classify_category(category);printf("category=%s\n", category);classify_final();free(category);return 0;
}
编译:
gcc -o ali_test ali_test.c classify.c classify.h -I /usr/include/python3.10/ -l python3.10
运行:
调用成功!!
香橙派的摄像头接入
详细可参考《OrangePi_Zero2_H616用户手册v4.0.pdf》 中的3.13.6 USB摄像头测试章节
-
首先,将USB摄像头插入,我使用的是一枚720P的广角USB摄像头:
-
然后运行以下指令可以看到内核自动加载了下面的模块:
lsmod | grep uvcvideo | grep -v grep
-
然后通过以下命令可以看到 USB 摄像头的设备节点信息
v4l2-ctl --list-devices
此处我提示没有识别这个命令,所以要先运行以下命令进行安装:
sudo apt update sudo apt install -y v4l-utils
后经过测试,可以使用的驱动应为“/dev/video1”
-
使用 fswebcam 测试 USB 摄像头拍照
还是先安装fswebcam:
sudo apt-get install -y fswebcam
安装完成后,运行以下代码让接入的摄像头拍照:
sudo fswebcam -d /dev/video1 --no-banner -r 1280x720 -S 5 ./image.jpg
// -d 选项用于指定 USB 摄像头的设备节点
// --no-banner 用于去除照片的水印
// -r 选项用于指定照片的分辨率
// -S 选项用设置于跳过前面的帧数
// ./image.jpg 用于设置生成的照片的名字和路径
然后在当前目录下打开image.jpg查看:
可见,拍照成功!
-
使用 mjpg-streamer 测试 USB 摄像头视频流
还是先下载mjpg-streamer:
git clone https://github.com/jacksonliam/mjpg-streamer //github下载地址 git clone https://github.com/jacksonliam/mjpg-streamer //gitee下载地址
然后下载UBUNTU对应的依赖包:
sudo apt-get install -y cmake libjpeg8-dev
最后编译安装 mjpg-streamer:
cd mjpg-streamer/mjpg-streamer-experimental make -j4 sudo make install
安装完成后,运行以下命令启动mjpg_streamer:
export LD_LIBRARY_PATH=.
sudo ./mjpg_streamer -i "./input_uvc.so -d /dev/video1 -u -f 30" -o "./output_http.so -w ./www"
然后在和开发板同一局域网的 Ubuntu PC 或者 Windows PC 或者手机的浏览orange Pi器中输入 【开发板的 IP地址:8080】就能看到摄像头输出的视频了,此处我使用了同一局域网的windowsPC, 地址是:192.168.2.23:8080,进入后点击“Stream”:
成功看到了摄像头拍到的实时影像!
-
修改/home/orangepi/mjpg-streamer/mjpg-streamer-experimental/下的start.sh:
先打开start.sh,可见这是一个自动开启摄像头的脚本:
将下面一句修改为:
./mjpg_streamer -i "./input_uvc.so -d /dev/video1 -u -f 30" -o "./output_http.so -w ./www"
此时,运行start.sh就可以直接运行摄像头了:
-
将视频拍照并保存本地:
在使用mjpg-streamer摄像头开启后,另开一个终端,可以使用以下命令在视频流中拍照并保存本地:
wget http://192.168.2.23:8080/?action=snapshot -O /home/orangepi/smart_bin/pic/garbage.jpg
此时,照片的路径为:/home/orangepi/smart_bin/pic/garbage.jpg
-
编写脚本使得mjpg-streamer可以在香橙派开机时自动后台启动:
为了让流程进一步简化,不需要每一次需要时都运行start.sh然后再开一个终端,可以将start.sh写成开机自启且后台运行的形式:
在家目录创建脚本文件:
touch mjpg.sh
内容:
#!/bin/bashcd /home/orangepi/mjpg-streamer/mjpg-streamer-experimental/
./start.sh
保存退出后,赋予权限:
chmod +x mjpg.sh
然后CD到开启自启的程序目录下:
cd /etc/xdg/autostart/
创建一个名为“mjpg.desktop” 的文件,内容如下:
[Desktop Entry]
Name=mjpg
Exec=/home/orangepi/mjpg.sh
Type=Application
NoDisplay=true
保存退出后,reboot重启香橙派,然后再进入192.168.2.23:8080查看:
可见,此时mjpg-streamer已经后台开机自启了!
start.sh:在当前路径下开启mjpg-streamer服务的脚本
mjpg.sh:在其他路径,运行start.sh脚本的脚本
mjpg.desktop:将mjpg设置为开机自启动的程序
Q:为什么不用之前学习守护进程的知识来解决这个问题,即将mjpg.sh的路径或start.sh的路径写在/etc/rc.local下,并加上“&”后台运行呢?
(使用香橙派学习 Linux的守护进程_mjmmm的博客-CSDN博客)
A:我的理解是,写在/etc/rc.local下的路径对应的只能是可执行文件,不能是脚本,通过观察start.sh里的内容可以发现,mjpg-streamer的运行需要在可执行文件后加上很多后缀,在之前还要export,所以也不能直接把可执行文件写在这个路径下。综上,使用了上文的这种方法实现了开机后台自启动。
需求8 --- 使用第三方库结合python实现图像识别垃圾分类
实现思路
在上文的学习中,我们学习了:
- 封装了使用C语言调用阿里云图像识别的代码:aliyun_classify.py; classify.c; classify.h
- 学会了使用“wget”指令来通过mjpg-streamer来对视频拍照并保存到本地
所以,思路就是首先通过system函数调用wget指令,将一张实时拍下的照片存到本地,然后通过调用classify.c中封装的函数来对这个本地的照片进行处理,发送到阿里云识别并得到最终结果!
同时,因为增加了图像识别模块,功能是先识别物品类型在开关盖,而之前实现的功能都是直接收到指令或距离接近直接就开盖,所以需要对于代码的整体结构做出调整
- 由于添加了物品识别,所以将物品的识别单独新开一个线程7,而之前其他涉及开关盖的线程都将“开关盖指令”改为“向线程7发送信号”,由线程7先物品识别,然后根据其他线程发来的消息统一进行开关盖和语音播报
而线程7识别之后的开关盖也可以写成新的线程,并且为了让线程7不用使用pthread_join来等待这线程返回导致的效率问题,可以让子线程使用pthread_detach来和父线程分离,自动退出
新的线程统计图:
新的程序运行图:
新的接线图:
语音模块的重新烧写
(注意!!只要涉及到串口输入或GPIO输入,就必须下载固件而不是SDK,烧录固件文件里的“jx_su_03t_release_update.bin”!!!!)
由于代码思路的调整,语音模块也需要相应的重新烧写,具体烧写步骤见我以前的博文,此处只展示关键的指令设计:
引脚设置:
唤醒词:
命令设置:
控制详情:
设置免唤醒:
(有时候就想直接开关盖,如果此时还必须来一句“你好小智”进行唤醒就会很鸡肋)
其他配置:
实现效果
- 开机播报“小智随时准备着 ”
- 当说出“你好小智 ”可以唤醒模块,模块回复“我在 ”
- 当超过30s没有指令或说出“退下 ”时,模块会进入休眠模式,并回复“有需要再叫我 ”
- 当说出“打开盖子 ”时,模块回复“好的稍等 ”,并串口发送“6F 70 65 6E”
- 当说出“关闭盖子 ”时,模块回复“好的稍等 ”,并串口发送“63 6C 6F 73 65”
- 当GPIO A25由低电平跳转到高电平时时,模块说“这是干垃圾 ,不开盖 ”
- 当GPIO A26由低电平跳转到高电平时时,模块说“这是湿垃圾,不开盖 ”
- 当GPIO A27由低电平跳转到高电平时时,模块说“检测到可回收垃圾,开盖 ”
- 当GPIO B2由低电平跳转到高电平时时,模块说“识别错误 ”
代码的重新编写
注意事项
- 当函数很多代码很长时,可以采用以下代码来进行调式,这个代码会打印所属的文件,函数,行号:
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
- 代码中使用了线程的互斥锁和条件变量,具体使用:
详见:线程_mjmmm的博客-CSDN博客
- 在使用摄像头拍照时,有一步是需要检测照片是否成功拍下并保存本地,使用到的是access函数,而拍完照之后删除照片使用的是remove函数:
详见:C语言:access函数的用法_access 函数_R-QWERT的博客-CSDN博客
- 使用串口时,需要先打开串口的驱动文件ttyS5,这就导致了每次运行时需要sudo权限才行,然而阿里云的SDK是装到当前用户(即普通用户)下的,这就导致了sudo运行会提示找不到阿里云的SDK,解决办法之一是通过在运行时加上“-E”选项来保留当前用户的环境变量。
v6_server.c:
#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
#include <stdlib.h>
#include <wiringPi.h>
#include <pthread.h>
#include <unistd.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>#include <stdint.h>
#include <stdarg.h>
#include <termios.h>
#include <sys/ioctl.h>
#include "mjm_uart_tool.h"#include <sqlite3.h>#include "classify.h"#define PWM 5
#define BEEP 2
#define Trig 9
#define Echo 10#define A25 6
#define A26 7
#define A27 8
#define B2 13int angle;
double dist;
static int i;int sockfd;
int conn_sockfd;
int len = sizeof(struct sockaddr_in);
int ret;
char readbuf[128];
struct sockaddr_in my_addr;
struct sockaddr_in client_addr;
static int conn_flag;
char hist[128] = {0}; //用于存放一条历史记录,注意这个变量一定不能在create_hist里赋值,因为局部指针变量作为函数的返回值没有意义,会报错; 且作为strcat的对象必须是字符串变量
char hist_whole[10000] = {0}; //用于存放所有历史记录; 且作为strcat的对象必须是字符串变量
sqlite3 *db;
char *zErrMsg = 0;
int dist_open_count = 0;
int dist_close_count = 0;
int sock_open_count = 0;
int sock_close_count = 0;
int soun_open_count = 0;
int soun_close_count = 0;
int request = 0;
int request_is = 0;
//unsigned char buffer[5] = {0xAA, 0x55, 0x00, 0x55, 0xAA};
int io_fd;pthread_mutex_t mutex;
pthread_cond_t cond;int callback(void *arg, int column_size, char *column_value[], char *column_name[]) //数据库exec函数对应的callback函数
{int j;//printf("arg=%s\n",(char *)arg);for(j=0;j<column_size;j++){printf("%s = %s\n", column_name[j], column_value[j]);fflush(stdout); //由于callback可能在线程运行中被调用,所以这句话不能忘}printf("=======================\n");fflush(stdout);return 0;//必须返回0,这样数据库中有多少条数据,这个回调函数就会被调用多少次
}void init_table() //用于初始化数据库中表格的值
{char *init_table_sql;init_table_sql = "insert into history values('dist',0,0);";ret = sqlite3_exec(db, init_table_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){printf("Can't init table value: %s\n", sqlite3_errmsg(db));}init_table_sql = "insert into history values('socket',0,0);";ret = sqlite3_exec(db, init_table_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){printf("Can't init table value: %s\n", sqlite3_errmsg(db));}init_table_sql = "insert into history values('sound',0,0);";ret = sqlite3_exec(db, init_table_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){printf("Can't init table value: %s\n", sqlite3_errmsg(db));}}void write2file() //将历史记录写入文件的函数
{int hist_fd; // file descriptionhist_fd = open("./history.txt",O_RDWR|O_CREAT|O_APPEND, 0666); //可读可写可打开的打开历史记录的文件,不存在就创建,且每次都追加写入if(hist_fd < 0){printf("fail to open history file!\n");fflush(stdout);}ret = write(hist_fd, &hist_whole, strlen(hist_whole));if(ret == -1){printf("fail to write history write to file!\n");fflush(stdout);}else{printf("write history to file successfully!\n");/*printf("write the following history to file:\n");printf("------------------------\n");printf("%s",hist_whole);printf("------------------------\n");*/fflush(stdout);}close(hist_fd);memset(hist_whole,'\0',sizeof(hist_whole)); //清空hist_whole!}void write2sql() //将历史记录写入数据库的函数
{char update_sql[128] = {'\0'};sprintf(update_sql,"update history set open = %d, close = %d where cause = 'dist';",dist_open_count, dist_close_count);ret = sqlite3_exec(db, (const char *)update_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){ printf("Can't update date: %s\n", sqlite3_errmsg(db));}sprintf(update_sql,"update history set open = %d, close = %d where cause = 'socket';",sock_open_count, sock_close_count);ret = sqlite3_exec(db, (const char *)update_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){printf("Can't update date: %s\n", sqlite3_errmsg(db));}sprintf(update_sql,"update history set open = %d, close = %d where cause = 'sound';",soun_open_count, soun_close_count);ret = sqlite3_exec(db, (const char *)update_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){printf("Can't update date: %s\n", sqlite3_errmsg(db));}ret = sqlite3_exec(db, "select * from history;", callback, 0, &zErrMsg); //将数据库数据打印到屏幕if(ret != SQLITE_OK){printf("Can't show date: %s\n", sqlite3_errmsg(db));}dist_open_count = 0;dist_close_count = 0;sock_open_count = 0;sock_close_count = 0;soun_open_count = 0;soun_close_count = 0;
}char *create_hist(int type, int action)//用于生成一条历史记录的函数//type的值1;2;3分别对应距离靠近开盖;客户端指令开盖;语音识别开盖//action的值1;2分别对应开盖;关盖
{FILE *fp;char *dist = "||distance close||";char *sock = "||client request||";char *soun = "||voice command||";char *open = "open action||";char *close = "close action||";char *c = "\n";memset(&hist,'\0',sizeof(hist));fp = popen("date +\"%Y-%m-%d %H:%M:%S\"","r");fread(&hist, sizeof(char), 128, fp);if(type == 1){ //如果时距离靠近导致的开关盖strcat(hist,dist); }else if(type == 2){ //如果是客户端指令导致的开关盖strcat(hist,sock);}else if(type == 3){ //如果是语音识别导致的开关盖strcat(hist,soun);}if(action == 1){strcat(hist,open);}else if(action == 2){strcat(hist,close);}strcat(hist,c);//每条历史记录结束加上一个换行键return (char *)hist;}int cmd_handler(int fd, char readbuf[128])
{int ret;char str[128]; //将读到的数据备份在这里strcpy(str,readbuf); //由于字符串的首地址是字符串的名字,所以此时相当于传入的地址,所有对字符串的操作都会影响它,所以需要进行备份,先备份再对备份的数据进行数据处理就不会影响原数据了if(strcmp((char *)str,"open")==0 && angle == 1){ //收到open指令时垃圾桶盖子是关着的ret = write(fd,"open request sent",13);if(ret == -1){perror("write");pthread_exit(NULL);}pthread_mutex_lock(&mutex);request = 3; //开盖请求pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}else if(strcmp((char *)str,"open")==0 && angle == 3){ //收到open指令时垃圾桶盖子是开着的ret = write(fd,"already open!",14);if(ret == -1){perror("write");pthread_exit(NULL);}}else if(strcmp((char *)str,"close")==0 && angle == 3){ //收到close指令时垃圾桶盖子是开着的ret = write(fd,"close request sent",14);if(ret == -1){perror("write");pthread_exit(NULL);}pthread_mutex_lock(&mutex);request = 4; //关盖请求pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}else if(strcmp((char *)str,"close")==0 && angle == 1){ //收到close指令时垃圾桶盖子是关着的ret = write(fd,"already close!",15);if(ret == -1){perror("write");pthread_exit(NULL);}}else if(strcmp((char *)str,"quit")==0){ //如果客户端打出了quitret = write(fd,"Bye",4); //立刻回发一个Bye,目的是让客户端取消接收阻塞然后成功从FIFO读取到退出信息if(ret == -1){perror("write");pthread_exit(NULL);}}}void startHC() //先要给Trig一个至少10us/ms的高电平方波
{digitalWrite (Trig, LOW) ;delay(5); //5msdigitalWrite (Trig, HIGH) ;delay(5);delay(5);digitalWrite (Trig, LOW) ;
}void signal_handler(int signum)
{if(i <= angle){digitalWrite(PWM, HIGH);}else{digitalWrite(PWM, LOW);}if(i == 40){ //40*500 = 20000us = 20msi = 0;}i++;}void *thread1(void *arg) //负责不断检测距离的线程
{struct timeval startTime;struct timeval stopTime;double diffTime;//double distwhile(1){delay(300); //让HC-SR04稳定一下startHC();while(digitalRead(Echo) == 0); //程序会卡在这里直到Echo变高的一瞬间gettimeofday(&startTime,NULL);while(digitalRead(Echo) == 1); //程序会卡在这里直到Echo变低的一瞬间gettimeofday(&stopTime,NULL);diffTime = 1000000*(stopTime.tv_sec - startTime.tv_sec) + (stopTime.tv_usec - startTime.tv_usec);dist = 0.034 * diffTime * 0.5;//printf("dist = %f---",dist);//fflush(stdout);if(dist < 40 && angle == 1){ //当距离小于且盖子未开pthread_mutex_lock(&mutex);request = 1; //开盖请求pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}else if(dist > 40 && angle == 3){//当距离大于且盖子打开pthread_mutex_lock(&mutex);request = 2; //关盖请求pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}}pthread_exit(NULL);
}void *thread2(void *arg) //负责检测开盖时间,超过10s就报警的线程
{struct timeval startTime;struct timeval stopTime;double diffTime;while(1){while(angle == 1);//程序会卡在这里直到盖子打开gettimeofday(&startTime,NULL);while(angle == 3){gettimeofday(&stopTime,NULL);diffTime = (stopTime.tv_sec - startTime.tv_sec) + 1/1000000 *(stopTime.tv_usec - startTime.tv_usec);if(diffTime > 10){ //盖子打开超过10秒digitalWrite (BEEP, LOW) ; //蜂鸣器响delay (1000) ; //一秒长鸣警报digitalWrite (BEEP, HIGH) ; //蜂鸣器不响break;}}}pthread_exit(NULL);
}void *thread3(void *arg) //负责不断等待socket客户端接入的线程
{while(1){//acceptconn_sockfd = accept(sockfd,(struct sockaddr *)&client_addr,&len);if(conn_sockfd == -1){perror("accept");pthread_exit(NULL);;}else{conn_flag = 1; //保证连接成功后才开始接收printf("accept success, client IP = %s\n",inet_ntoa(client_addr.sin_addr));fflush(stdout);}}pthread_exit(NULL);
}void *thread4(void *arg) //负责socket客户端接入后接收处理客户端指令的线程
{while(1){ //切不可直接“while(conn_flag == 1)”,这样在没有接入时会直接退出,要不停的循环查看flag的值while(conn_flag == 1){//readmemset(&readbuf,0,sizeof(readbuf));ret = recv(conn_sockfd, &readbuf, sizeof(readbuf), 0);if(ret == 0){ //如果recv函数返回0表示连接已经断开printf("client has quit\n");fflush(stdout);close(conn_sockfd);break;}else if(ret == -1){perror("recv");conn_flag = 0; //此时打印一遍错误信息就会结束,如果不把flag置1,在一个客户端退出另一个客户端还未接入时就会不停的打印错误信息//pthread_exit(NULL); //此处不能退出,因为因为这样如果有一个客户端接入并退出后这个线程就会退出,为了保证一个客户端退出后,另一个客户端还可以接入并正常工作,此处仅显示错误信息而不退出}//pthread_mutex_lock(&mutex);//对angle值修改需要先上锁cmd_handler(conn_sockfd, readbuf); //对客户端发来的消息进行判断的总函数//pthread_mutex_unlock(&mutex);printf("\nclient: %s\n",readbuf);fflush(stdout);}}pthread_exit(NULL);
}void *thread5(void *arg) //负责通过串口接收语音模块指令的线程
{char io_readbuf[32] = {'\0'};while(1){while(serialDataAvail (*((int *)arg))){serialGetstring (*((int *)arg),io_readbuf) ;printf("-> %s\n",io_readbuf);fflush(stdout);if(strcmp(io_readbuf,"open") == 0 && angle == 1){ //当收到open指令且盖子关闭时pthread_mutex_lock(&mutex);request = 5; //开盖请求pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}else if(strcmp(io_readbuf,"close") == 0 && angle == 3){//当收到close指令且盖子打开时pthread_mutex_lock(&mutex);request = 6; //关盖请求pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}else if(strcmp(io_readbuf,"open") == 0 && angle == 3){printf("already open\n");fflush(stdout);}else if(strcmp(io_readbuf,"close") == 0 && angle == 1){printf("already close\n");fflush(stdout);}else{printf("unkown command\n");fflush(stdout);}memset(io_readbuf,'\0',32);}}pthread_exit(NULL);
}void *thread6(void *arg) //负责每隔一段时间向文件和数据库写入历史记录的线程
{struct timeval startTime;struct timeval stopTime;double diffTime;while(1){gettimeofday(&startTime,NULL);while(1){gettimeofday(&stopTime,NULL);diffTime = (stopTime.tv_sec - startTime.tv_sec) + 1/1000000 *(stopTime.tv_usec - startTime.tv_usec); //单位为秒if(diffTime > 60){//如果时间是1分钟,由于线程的竞争机制,不一定会非常精确,所以使用>号 write2file();//将历史记录写入文件write2sql();//将历史记录写入数据库break;}}}pthread_exit(NULL);
}void *thread7(void *arg) //阿里云交互,统一处理开关盖请求的线程
{char *category = NULL;//char buffer[5] = {0xAA, 0x55, 0x00, 0X55, 0xAA};pthread_t p_openclose;pthread_t p_sayres;int request_action = 0;while(1){//printf("%s|%s|%d: \n", __FILE__, __func__, __LINE__);pthread_mutex_lock(&mutex);// while(request!=1 && request!=2 && request!=3){pthread_cond_wait(&cond, &mutex);request_is = request; //保存request的值request = 0;// }pthread_mutex_unlock(&mutex);if(request_is == 1 || request_is == 3 || request_is == 5){//若是开盖指令system("wget http://192.168.2.23:8080/?action=snapshot -O /home/orangepi/smart_bin/pic/garbage.jpg"); //拍照delay(10);//给一点时间让照片拍出来if(0 == access("/home/orangepi/smart_bin/pic/garbage.jpg", F_OK)){ //如果照片成功拍到了//printf("%s|%s|%d: \n", __FILE__, __func__, __LINE__);category = classify_category(category); //阿里云识别printf("category:%s\n",category);fflush(stdout);if(strcmp(category,"干垃圾") == 0){digitalWrite (A25, HIGH) ;delay(30);}else if(strcmp(category,"湿垃圾") == 0){digitalWrite (A26, HIGH) ;delay(30);}else if(strcmp(category,"获取失败") == 0){digitalWrite (B2, HIGH) ;delay(30);}else if(strcmp(category,"可回收垃圾") == 0){ //开盖指令收到且为可回收垃圾digitalWrite (A27, HIGH) ;delay(30);angle = 3; //开盖digitalWrite (BEEP, LOW) ; //蜂鸣器响delay (100) ;digitalWrite (BEEP, HIGH) ; //蜂鸣器不响delay (100) ;digitalWrite (BEEP, LOW) ; //蜂鸣器响delay (100) ;digitalWrite (BEEP, HIGH) ; //蜂鸣器不响delay(2000);//延时2秒if(request_is == 1){ //距离导致的开盖dist_open_count++;create_hist(1,1);//构建一条历史记录strcat(hist_whole,hist);//在总的历史记录中添加刚刚构建的历史记录}else if(request_is == 3){ //客户端指令导致的开盖sock_open_count++;create_hist(2,1);//构建一条历史记录strcat(hist_whole,hist);//在总的历史记录中添加刚刚构建的历史记录}else if(request_is == 5){ //语音模块导致的开盖soun_open_count++;create_hist(3,1);//构建一条历史记录strcat(hist_whole,hist);//在总的历史记录中添加刚刚构建的历史记录}}}else{ //拍照不成功digitalWrite (B2, HIGH) ; //算作获取失败delay(30);}ret = remove("/home/orangepi/smart_bin/pic/garbage.jpg"); //删除照片if(ret != 0){printf("pic remove fail!\n");fflush(stdout);}}else if(request_is == 2 || request_is == 4 || request_is == 6){ //若是关盖指令angle = 1; //关盖//delay(2000);//延时2秒if(request_is == 2){ //距离导致的关盖dist_close_count++;create_hist(1,2);//构建一条历史记录strcat(hist_whole,hist);//在总的历史记录中添加刚刚构建的历史记录}else if(request_is == 4){ //客户端指令导致的关盖sock_close_count++;create_hist(2,2);//构建一条历史记录strcat(hist_whole,hist);//在总的历史记录中添加刚刚构建的历史记录}else if(request_is == 6){ //语音模块导致的关盖soun_close_count++;create_hist(3,2);//构建一条历史记录strcat(hist_whole,hist);//在总的历史记录中添加刚刚构建的历史记录 }}request_is = 0;digitalWrite (A25, LOW) ;digitalWrite (A26, LOW) ;digitalWrite (A27, LOW) ;digitalWrite (B2, LOW) ; }pthread_exit(NULL);
}int main(int argc, char **argv)
{struct itimerval itv;//int io_fd;char *create_table_sql;pthread_t t1_id;pthread_t t2_id;pthread_t t3_id;pthread_t t4_id;pthread_t t5_id;pthread_t t6_id;pthread_t t7_id;memset(&my_addr,0,sizeof(struct sockaddr_in));memset(&client_addr,0,sizeof(struct sockaddr_in));if(argc != 3){printf("param error! add ip address & port num\n");return 1;}wiringPiSetup () ;pinMode (PWM, OUTPUT);pinMode (Trig, OUTPUT);pinMode (Echo, INPUT);pinMode (BEEP, OUTPUT);pinMode (A25, OUTPUT);pinMode (A26, OUTPUT);pinMode (A27, OUTPUT);pinMode (B2, OUTPUT);digitalWrite (Trig, LOW) ;digitalWrite (Echo, LOW) ;digitalWrite (BEEP, HIGH) ;digitalWrite (B2, LOW) ;digitalWrite (A25, LOW) ;digitalWrite (A26, LOW) ;digitalWrite (A27, LOW) ;//阿里云服务初始化classify_init();//设定定时时间itv.it_interval.tv_sec = 0;itv.it_interval.tv_usec = 500;//设定开始生效,启动定时器的时间itv.it_value.tv_sec = 1;itv.it_value.tv_usec = 0;//设定定时方式if( -1 == setitimer(ITIMER_REAL, &itv, NULL)){perror("error");exit(-1);}//信号处理signal(SIGALRM,signal_handler);angle = 1;//初始化角度为关盖//打开串口驱动文件,配置波特率if ((io_fd = myserialOpen ("/dev/ttyS5", 115200)) < 0) {fprintf (stderr, "Unable to open serial device: %s\n", strerror (errno)) ;return 1 ;}//printf("main:io_fd = %d\n",io_fd);//打开数据库ret = sqlite3_open("history.db", &db);if(ret != SQLITE_OK){printf("Can't open database: %s\n", sqlite3_errmsg(db));exit(0);}else{printf("Open database successfully\n");}//创建表格//create_table_sql = "create table history(cause char,open Integer,close Integer);";create_table_sql = "CREATE TABLE HISTORY(" \"CAUSE CHAR(30) PRIMARY KEY NOT NULL," \"OPEN INT NOT NULL," \"CLOSE INT NOT NULL );" ;ret = sqlite3_exec(db, create_table_sql, callback, 0, &zErrMsg);if(ret != SQLITE_OK){printf("Can't create table: %s\n", sqlite3_errmsg(db));//exit(0);}else{printf("Table create successfully\n");}//初始化表格数据init_table();//socketsockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1){perror("socket");return 1;}else{printf("socket success, sockfd = %d\n",sockfd);}//bindmy_addr.sin_family = AF_INET;my_addr.sin_port = htons(atoi(argv[2]));//host to net (2 bytes)inet_aton(argv[1],&my_addr.sin_addr); //char* format -> net formatret = bind(sockfd, (struct sockaddr *)&my_addr, len);if(ret == -1){perror("bind");return 1;}else{printf("bind success\n");}//listenret = listen(sockfd,10);if(ret == -1){perror("listen");return 1;}else{printf("listening...\n");}ret = pthread_mutex_init(&mutex, NULL);if(ret != 0){printf("mutex create error\n");}ret = pthread_cond_init(&cond, NULL);if(ret != 0){printf("cond create error\n");}ret = pthread_create(&t1_id,NULL,thread1,NULL);if(ret != 0){printf("thread1 create error\n");}ret = pthread_create(&t2_id,NULL,thread2,NULL);if(ret != 0){printf("thread2 create error\n");}ret = pthread_create(&t3_id,NULL,thread3,NULL);if(ret != 0){printf("thread3 create error\n");}ret = pthread_create(&t4_id,NULL,thread4,NULL);if(ret != 0){printf("thread4 create error\n");}ret = pthread_create(&t5_id,NULL,thread5,(void *)&io_fd);if(ret != 0){printf("thread5 create error\n");}ret = pthread_create(&t6_id,NULL,thread6,NULL);if(ret != 0){printf("thread6 create error\n");}ret = pthread_create(&t7_id,NULL,thread7,NULL);if(ret != 0){printf("thread7 create error\n");}pthread_join(t1_id,NULL);pthread_join(t2_id,NULL);pthread_join(t3_id,NULL);pthread_join(t4_id,NULL);pthread_join(t5_id,NULL);pthread_join(t6_id,NULL);pthread_join(t7_id,NULL);pthread_mutex_destroy(&mutex);//摧毁互斥量pthread_cond_destroy(&cond);//摧毁条件变量close(io_fd);//关闭串口驱动文件的套接字classify_final();//关闭阿里云服务return 0;
}
编译语句:
gcc -o v6_server classify.c classify.h mjm_uart_tool.c mjm_uart_tool.h v6_server.c -lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt -lsqlite3 -I /usr/include/python3.10/ -l python3.10
运行语句:
server端:
sudo -E ./v6_server 192.168.2.23 8888
client端:
./v2_client 192.168.2.23 8888
这篇关于使用香橙派并基于Linux实现最终版智能垃圾桶项目 --- 下的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!