欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 产业 > 基于Linux C语言多线程服务器+Qt客户端+STM32客户端实现的无人超市项目

基于Linux C语言多线程服务器+Qt客户端+STM32客户端实现的无人超市项目

2025/4/5 20:12:55 来源:https://blog.csdn.net/sakabu/article/details/146562011  浏览:    关键词:基于Linux C语言多线程服务器+Qt客户端+STM32客户端实现的无人超市项目

目录

一、前言

二、项目演示

三、项目概述

3.1项目流程图

3.2项目简介

四、项目难点

Linux 多线程服务器

Qt管理员端

Qt用户端

STM32客户端


一、前言

        这个是前段时间做的一个项目,综合性很强,还是值得学习的。技术栈有Linux系统编程(线程编程、进程间通信、网络编程作为socket服务端)、MySQL数据库(服务端和数据库进行交互,存储和读取数据)、Qt上位机开发(作为socket客户端)、STM32使用8266WIFI模块也作为socket客户端。这些内容我之前都有写过博客,可以去看相关博文:Linux网络编程,Qt网络调试助手、MySQL数据库开发。

二、项目演示

无人超市项目

三、项目概述

3.1项目流程图

3.2项目简介

        其中Qt我写了两个程序都作为socket的客户端(client),其中Qt管理员端主要负责商品和会员信息的注册和数据的修改工作。Qt用户端就是用户的结算界面,通过识别商品卡完成商品加入购物车这一动作,并计算总价格,结算的时候识别会员卡进行结算操作,并给Linux服务端发送数据以执行下一步操作,同时MySQL数据库的数据实时更新。STM32通过8266WiFi模块也作为socket的客户端(client),主要是给Linux服务端发送唯一的卡号,并接收开柜门的信息控制舵机旋转模拟商品出柜。
        Linux服务端其实相当于数据的中转站,服务端程序创建三个线程对应三个客户端程序。MySQL数据库就是存储会员和商品的数据(其实也可以用文件来代替,通过读取文件来操作数据;但是用文件管理数据会比较麻烦,如果会员或商品数据一多,就不知道要从哪里读,一口气全部读出来再查找数据费时费力)

四、项目难点

        大佬的源码是用C++写的服务器,对于字符串的管理都是由std命名空间里的string来管理的,std::string是一个封装了动态内存管理的类,通常不需要手动释放内存。我用C语言实现服务端代码就需要严格对内存进行管理,避免出现段错误(由于不会用gdb,因此只会在可能出现段错误的地方加上打印信息,然后又要重新将程序上传至虚拟机再编译执行,十分麻烦)。不过写完这么一个大型项目,能大大锻炼自己的调试能力以及学习面向对象的编程思想。
        还有一个难点是单片机只负责上传卡号,我要怎么分辨这个卡号要执行什么操作?是已经注册的商品要加入购物车?还是要注册的会员或商品的ID?亦或者是结算时刷的会员卡?这里就需要一个全局变量,用到类似状态机的方法,通过进程间通信里的信号:Linux进程间通信:信号,通过改变要发送的信号执行对应的信号响应函数就好了。什么时候改变要发送的信号?例如Qt管理员端,点击商品注册页面的请求资源按键,会向Linux服务端发送数据,进而改变要发送的信号(全局变量),通过这种方式就能实现单片机只刷卡,但会执行该执行的操作。至于难点主要就这两个,其他都是一些细节的操作,比如客户端会有很多操作,要采用什么方式来处理这些数据?我采用了类似于数据报的格式,就是"操作码+数据",服务端接收到数据后先提取操作码,再将数据分发到对应的函数,每个线程都是类似的操作。
        在main函数的while循环里,会阻塞在accept这里等待客户端的连接请求,但是刷卡的时候会触发软件中断,导致accept函数退出,有可能会导致程序崩溃,我是用goto语句,一旦跳出accept函数就退出立马跳回来,代码如下:

// 接受客户端连接
ret_client myserver_get_client_socket(Myserver* server) 
{ret_client ret;
reboot:server->client_socket = accept(server->server_socket, (struct sockaddr*)&ret.client_struct, &server->len);if (server->client_socket == -1) {perror("accept interrupted by signal, retrying...\n-----------------\n");goto reboot; // 关键:立即重试}ret.client_socket = server->client_socket;return ret;
}

Linux 多线程服务器

        学过网络编程的对于socket服务端的编写应该很熟悉了,我只是把它封装了一层,再结合Linux线程编程,每当有新的客户端连接请求,就创建一个新的线程与它接应,并传入必需的操作句柄,如何传入句柄这些数据?。我定义了一个Mythread结构体,模拟C++中的基类,里面只有一个函数指针,如下:

#ifndef __MYTHREAD_H_
#define __MYTHREAD_H_#include <stdio.h>
#include "myserver.h"// 定义Mythread结构体,模拟C++中的基类
typedef struct {void (*thread_start)(ret_client *param);
} Mythread;#endif

        在每个线程的处理函数中都定义对应的线程结构体,其中就定义了Mythread的对象,这些线程结构体就模拟C++中的派生类;在main函数里,接收到了客户端的连接请求,就调用对应线程的初始化函数,传入线程结构体的指针,将结构体里定义的Mythread函数指针指向实现好的线程启动函数,再在main函数里通过传入的线程结构体指针调用线程启动函数即可。演示如下:

        总之服务端就像一个消息的中转站,通过和MySQL服务器进行连接,服务器通过客户端发送的数据的操作码来执行对应的函数,main.c如下:

#include "myserver.h"
#include "mythread.h"
#include "managerthread.h"
#include "customerthread.h"
#include "mcuthread.h"#define SERVER_IP "192.168.254.128"
#define SERVER_PORT 8888int RES = 34;int main() 
{/*--------------------------*//*数据库初始化相关操作*/// 初始化数据库连接对象SQLifconfig *MySQL_Handler = SQLifconfig_init();if (!MySQL_Handler) {printf("初始化数据库失败\n");return -1;}// 连接数据库if (!SQLifconfig_SQL_init(MySQL_Handler, "127.0.0.1", "root", "123456", "supermarket")) {printf("连接数据库失败\n");SQLifconfig_destroy(MySQL_Handler);return -1;}/*--------------------------*//*--------------------------*//*服务器初始化相关操作*///初始化服务器Myserver *server = myserver_init(AF_INET, SOCK_STREAM, 0);// 启动服务器myserver_start(server, SERVER_IP, SERVER_PORT, AF_INET);/*--------------------------*/while(1){// 接受客户端连接ret_client client_ret = myserver_get_client_socket(server);printf("New client connected: client socket fd = %d\n", client_ret.client_socket);client_ret.MySQL_Handler = MySQL_Handler;// 读取客户端数据char* buf = myserver_readbuf(server);unsigned long num = -1;if (buf){printf("Received data: %s\n", buf);num = atoi(buf);  // 将字符串转换为整数printf("Converted number: %ld\n", num);free(buf);} else{printf("Failed to read data\n");}switch (num){case 100101:{Managerthread *manager_thread = (Managerthread *)malloc(sizeof(Managerthread));managerthread_init(manager_thread);manager_thread->base.thread_start(&client_ret);printf("Qt管理员端连接成功: %ld\n", num);break;}case 100111:{Customerthread *customer_thread = (Customerthread *)malloc(sizeof(Customerthread));customerthread_init(customer_thread);customer_thread->base.thread_start(&client_ret);printf("Qt用户端连接成功: %ld\n", num);break;}case 101001:{Mcuthread *mcu_thread = (Mcuthread *)malloc(sizeof(Mcuthread));mcuthread_init(mcu_thread);mcu_thread->base.thread_start(&client_ret);printf("STM32客户端连接成功: %ld\n", num);break;}default:printf("Unknown case: %ld\n", num);   break;}}// 销毁服务器myserver_destroy(server);//释放数据库连接对象SQLifconfig_destroy(MySQL_Handler);return 0;
}

Qt管理员端

        界面:

        功能:会员注册、查询、充值、注销操作;商品的添加、删除操作;查看销售记录(以文本的形式记录在Linux服务端);查看操作日志(当管理员端停止运行后消失,只记录启动后的会员和商品的操作)

        启动后每两秒尝试连接到服务器端,连接失败继续尝试重连。连接成功后先向服务器发送一段数据,创建与其对接的线程,当点击会员卡管理和添加新商品页面的请求资源按键时,单片机刷卡后会将卡号填充到对应的 lineEdit 文本框内(上面说到的发送"信号"),后续可以给销售记录文本做一个大小的限制,避免占用过多的内存;还可以在STM32下位机添加一个GPS模块,得到经纬度信息后在Qt管理员端显示地图,查看是哪一家店完成了销售。

Qt用户端

        界面:

        功能:识别商品卡进行商品入购物车的操作;结算;删除不要的商品;从服务器获得STM32客户端上传的温湿度信息。

        客户端就比较简单了,就是平时展示给消费者的页面,点击结算案件后等待用户刷会员卡,然后将购买信息上传到服务端,服务端从数据库中读取数据,进行余额的比较后再进行下一步操作;可能会因为余额不足结算失败,这时候就要去管理员端充值后再来结算;结算成功会打印用户消费信息。

STM32客户端

        这个项目我一开始是用的网络调试助手来模拟STM32的,因为关键步骤就是刷卡嘛,就提前写好几个卡号然后发给服务器就好了。因此STM32的代码我还没写,这里就展示一下部分代码,DHT11温湿度模块和舵机的操作就很简单了,就是while循环读,RFID刷卡模块也是放在while循环里面读取卡号。

        main.c的while循环如下:

while(1) 
{			if(!RC522_cardScan(cardID)){send_idcard(cardID);} Delay_ms(250);if(GPS_Flag==1)  {     send_gps();}Delay_ms(350);if(Read_DHT11(&DHT11_Data) == SUCCESS){send_wunshidu();}Delay_ms(250);if(Serial_Flag==1){My_Servo_SetAngle();}
}

        8266WiFi模块配置如下,直接连接到Tcp服务端:

#include "8266wifi.h"
#include <stdio.h>void Wifi_TCP_Init(void)
{Serial_Init();Serial_SendString("AT+RST\r\n");Delay_s(1);Serial_SendString("AT+CWMODE=3\r\n");Delay_s(1);Serial_SendString("AT+CWJAP=\"sakabu\",\"12345678\"\r\n");//连接热点Delay_s(5);Serial_SendString("AT+CIPSTART=\"TCP\",\"192.168.254.128\",8888\r\n");Delay_s(4); Serial_SendString("AT+CIPMODE=1\r\n");Delay_s(1); Serial_SendString("AT+CIPSEND\r\n");Delay_s(1); Serial_SendString("101001");Delay_s(1); 
}

        后续我会写一下STM32的代码,到时候更新一篇博客把源码展示出来。

        需要源码的点个免费的关注,评论邮箱直接发!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词