前言
前面我们完成了量产工具显示系统部分,见量产工具(一)——显示系统
本篇实现输入系统。我们这个项目支持不同的输入设备,比如触摸屏和网络输入,这些输入设备都会向上层提供数据,那么这些数据的结构要统一;另外,对于不同的输入设备,我们也应该抽象出一个统一的结构体来表示。
数据结构抽象
我们要抽象出两个结构体,一个用来描述上报的数据,一个用来描述输入设备。
1.数据相关结构体
我们这个项目,肉眼观察到模块测试通过后可以用手指点击屏幕,对于无法手动测试的模块,需要通过网络输入发送字符串表示测试通过,所以我们抽象出的数据结构体既要支持触摸屏也要支持网络。
struct InputEvent
INPUT_TYPE_TOUCH 表示是触摸屏数据
INPUT_TYPE_NET 表示是网络数据
iType表示是触摸屏数据还是网络数据
iX表示触摸屏数据的x坐标
iY同理
iPressure表示触摸屏数据的压力值,判断按下还是松开
str存放网络数据发送的字符串
tTime可以存放时间(可能会用到)
2.设备相关结构体
现在来抽象出输入设备相关结构体,对于不同的输入设备,他们都应该实现什么函数?
struct InputDevice
如果你学习了量产工具(一)显示系统的内容,会发现抽象出来的结构体和显示系统类似,代码思路也是类似的,只不过为了代码的复用性,封装的比较复杂。
name:可以通过设备名字从管理链表中得到设备句柄,调用底层函数
GetInputEvent:调用底层函数得到一个个 InputEvent 结构体
DeviceInit:底层初始化函数,比如打开设备节点等操作
DeviceExit:有打开就有退出函数,关闭文件等等
ptNext:结构体指针,用于管理输入设备
input_manager.h
#ifndef _INPUT_MANAGER_H
#define _INPUT_MANAGER_H
#include <sys/time.h>
#ifndef NULL
#define NULL (void *)0
#endif
#define INPUT_TYPE_TOUCH 1
#define INPUT_TYPE_NET 2
typedef struct InputEvent{
struct timeval tTime;
int iType;
int iX;
int iY;
int iPressure;
char str[1024];
}InputEvent, *PInputEvent;
typedef struct InputDevice{
char *name;
int (*GetInputEvent)(PInputEvent ptInputEvent);
int (*DeviceInit)(void);
int (*DeviceExit)(void);
struct InputDevice *ptNext;
}InputDevice, *PInputDevice;
/*以下函数后面会实现*/
void RegisterInputDevice(PInputDevice ptInputDev);
void InputInit(void);
int GetInputEvent(PInputEvent ptInputEvent);
void InputDeviceInit(void);
#endif
触摸屏编程
触摸屏相关知识见:输入系统应用编程
我们要给触摸屏构造这么一个结构体并且实现里面的函数,结构体如下:使用tslib
touchscree.c
#include <input_manager.h>
#include <tslib.h>
#include <stdio.h>
/*触摸屏句柄*/
static struct tsdev *g_ts;
/*触摸屏初始化函数*/
static int TouchScreenGetDeviceInit(void)
{
/*使用tslib库开发,初始化调用ts_setup就行了*/
g_ts = ts_setup(NULL, 0);
if (!g_ts)
{
printf("ts_setup err\n");
return -1;
}
return 0;
}
/*得到触摸屏的输入数据*/
static int TouchScreenGetInputEvent(PInputEvent ptInputEvent)
{
struct ts_sample samp;
int ret;
/*调用ts_read就可以得到数据,数据存放在samp里*/
ret = ts_read(g_ts, &samp, 1);
if(ret != 1)
return -1;
/*iType为触摸屏数据,其他的值从samp里拿*/
ptInputEvent->iType = INPUT_TYPE_TOUCH;
ptInputEvent->iX = samp.x;
ptInputEvent->iY = samp.y;
ptInputEvent->iPressure = samp.pressure;
ptInputEvent->tTime = samp.tv;
return 0;
}
/*触摸屏关闭退出函数*/
static int TouchScreenGetDeviceExit(void)
{
ts_close(g_ts);
return 0;
}
static InputDevice g_tTouchScreenDev = {
.name = "touchscreen",
.GetInputEvent = TouchScreenGetInputEvent,
.DeviceInit = TouchScreenGetDeviceInit,
.DeviceExit = TouchScreenGetDeviceExit,
};
/*这个函数是把自己注册进管理链表里,后面输入管理那部分会讲,但原理和之前显示系统一样*/
void TouchScreenRegister(void)
{
/*调用管理层的函数,把自己注册到链表里,RegisterInputDevice函数后面才给,这里先忽略*/
RegisterInputDevice(&g_tTouchScreenDev);
}
只需要调用三个函数就能完成初始化、获得数据和退出的功能,非常方便。
触摸屏单元测试
要实现单元测试,只需要在touchscreen.c里添加一个main函数
/*单元测试用,测试完就可以删掉了,一个程序只能有一个main函数*/
#if 1
int main(int argc, char **argv)
{
InputEvent event;
int ret;
/*初始化*/
g_tTouchScreenDev.DeviceInit();
while(1)
{
/*循环读*/
ret = g_tTouchScreenDev.GetInputEvent(&event);
if(ret){
printf("GetInputEvent err\n");
return -1;
}
else
{
/*打印*/
printf("Type : %d\n", event.iType);
printf("iX : %d\n", event.iX);
printf("iY : %d\n", event.iY);
printf("iPressure : %d\n", event.iPressure);
}
}
return 0;
}
#endif
unittest目录下的makefile
将之前测试显示系统时的makefile注释掉(#表示注释这一行)
EXTRA_CFLAGS :=
CFLAGS_file.o :=#obj-y += disp_test.o
input目录下的makefile
touchscreen.c文件在input目录下,在该目录下添加一个makefile文件,如下:
EXTRA_CFLAGS :=
CFLAGS_file.o :=obj-y += touchscreen.o
顶层目录的makefile
CROSS_COMPILE ?=
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nmSTRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdumpexport AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMPCFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/includeLDFLAGS := -lts
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIRTARGET := test
obj-y += display/
obj-y += input/all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
看加粗部分,编译的时候要连接tslib库,加上-lts,还要编译input目录下的文件
运行结果
成功打印类别,xy坐标和压力值。
网络输入编程
网络编程相关知识见:Linux网络编程
和触摸屏编程步骤十分类似,实现如下结构体并实现里面的函数:
在input目录下创建netinput.c文件
netinput.c
我们使用UDP模式,我们的程序作为server端,其他程序要给我们发数据,其他程序就是client端。
#include <input_manager.h>
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
/* socket
* bind
* sendto/recvfrom
*/
/*端口号为8888*/
#define SERVER_PORT 8888
static int g_iSocketServer;
/*获得网络数据的函数*/
static int NetInputGetInputEvent(PInputEvent ptInputEvent)
{
struct sockaddr_in tSocketClientAddr;
int iRecvLen;
char aRecvBuf[1000];
unsigned int iAddrLen = sizeof(struct sockaddr);
/*直接调用recvfrom函数接收网络数据*/
iRecvLen = recvfrom(g_iSocketServer, aRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
if (iRecvLen > 0)
{
/*给字符串加上结束符*/
aRecvBuf[iRecvLen] = '\0';
/*设置数据类型为网络数据*/
ptInputEvent->iType = INPUT_TYPE_NET;
/*使用gettimeofday函数给结构体里的tTime赋值*/
gettimeofday(&ptInputEvent->tTime, NULL);
/*使用strncpy将字符串保存到数据结构体里面*/
strncpy(ptInputEvent->str, aRecvBuf, 1000);
ptInputEvent->str[999] = '\0';
return 0;
}
else
return -1;
}
/*网络输入的初始化函数*/
static int NetInputGetDeviceInit(void)
{
struct sockaddr_in tSocketServerAddr;
int iRet;
/*socket函数得到文件描述符*/
g_iSocketServer = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == g_iSocketServer)
{
printf("socket error!\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
memset(tSocketServerAddr.sin_zero, 0, 8);
iRet = bind(g_iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
return 0;
}
/*退出函数*/
static int NetInputGetDeviceExit(void)
{
close(g_iSocketServer);
return 0;
}
static InputDevice g_tNetInputDev = {
.name = "touchscreen",
.GetInputEvent = NetInputGetInputEvent,
.DeviceInit = NetInputGetDeviceInit,
.DeviceExit = NetInputGetDeviceExit,
};
/*把结构体注册到链表里,后面会讲*/
void NetinputRegister(void)
{
RegisterInputDevice(&g_tNetInputDev);
}
流程和触摸屏编程大差不差,网络编程相关知识我就不加注释了。
网络输入单元测试
要实现单元测试,只需要在netinput.c里添加一个main函数,并且把touchscreen.c里的main函数注释掉。
#if 1
int main(int argc, char **argv)
{
InputEvent event;
int ret;
g_tNetinputDev.DeviceInit();
while (1)
{
ret = g_tNetinputDev.GetInputEvent(&event);
if (ret) {
printf("GetInputEvent err!\n");
return -1;
}
else
{
/*打印类型和字符串*/
printf("Type : %d\n", event.iType);
printf("str : %s\n", event.str);
}
}
return 0;
}
#endif
input目录下的makefile
input目录下现在有两个.c文件
EXTRA_CFLAGS :=
CFLAGS_file.o :=obj-y += touchscreen.o
obj-y += netinput.o
加上最后一行,表示编译netput.c文件
client程序
我们还要编写出客户端来给服务端发送数据,运行的时候,服务端加上 & 让他在后台运行。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
/* socket
* connect
* send/recv
*/
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
int iSendLen;
int iAddrLen;
if (argc != 3)
{
printf("Usage:\n");
printf("%s <server_ip> <str>\n", argv[0]);
return -1;
}
iSocketClient = socket(AF_INET, SOCK_DGRAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);
iAddrLen = sizeof(struct sockaddr);
iSendLen = sendto(iSocketClient, argv[2], strlen(argv[2]), 0,
(const struct sockaddr *)&tSocketServerAddr, iAddrLen);
close(iSocketClient);
return 0;
}
这里不多解释了,用法为 %s <server_ip> <str> 输入可执行文件+服务端的IP地址+要发送的字符串即可。
运行结果
服务端在后台执行后,再来执行client程序
输入管理
为什么需要输入管理?我们想支持同时从多个输入设备里面得到数据,就需要提供一个输入管理。
怎么同时读取多个输入设备?如果使用轮询的方式来读取,一个设备在休眠的时候,另一个设备发来的数据可能会丢失,所以需要为每一个输入设备创建一个线程,就像单片机引入操作系统FreeRTOS一样,要用到线程编程。相关知识见:Linux线程编程。
框架
核心在于:GetInputEvent
怎么同时读取多个设备?
读取触摸屏时,可能会休眠,那么网络输入就会丢失;
读取网络数据时,可能会休眠,那么触摸屏数据就会丢失
所以不能使用“先后轮询的方式”,不能使用下面的代码:
while (1)
{
g_tTouchscreenDev.GetInputEvent(&event1);
g_tInputDev.GetInputEvent(&event2);
}
要想支持多个输入设备,只能使用线程:
为每个InputDevice都创建一个“读取线程”
引入环形缓冲区
怎么避免数据丢失?
比如触摸屏,它会一下子上报很多数据,对于网络输入,也有可能同时又多个client发来数据。
所以,不能使用单一的变量来保存数据,而是使用一个数组来保存数据,使用“环形缓冲区”。
一想到环形缓冲区,学过FreeRTOS的队列的同学应该很熟悉,他就是一个数组,有一个读指针和一个写指针,写满后指针会从尾部指向头部,这也是“环形”的由来。
input_manager.c
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include <input_manager.h>
/* 全局输入设备链表头指针 */
static PInputDevice g_InputDevs = NULL;
/* 互斥锁和条件变量,用于线程同步 */
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;
/* 环形缓冲区相关定义 */
#define BUFFER_LEN 20
static int g_iRead = 0;
static int g_iWrite = 0;
static InputEvent g_atInputEvents[BUFFER_LEN];
/* 环形缓冲区操作函数 */
/* 判断唤醒缓冲区是否为满 */
//满了返回1,没满返回0
static int isInputBufferFull(void)
{
return (g_iRead == ((g_iWrite + 1) % BUFFER_LEN));
}
/* 判断唤醒缓冲区是否为空 */
//空了返回1,没空返回0
static int isInputBufferEmpty(void)
{
return (g_iRead == g_iWrite);
}
//储存输入事件到环形缓冲区里面
static void PutInputEventToBuffer(PInputEvent ptInputEvent)
{
if (!isInputBufferFull())//数组不满,就储存
{
g_atInputEvents[g_iWrite] = *ptInputEvent;
g_iWrite = (g_iWrite + 1) % BUFFER_LEN;
}
}
//在环形缓冲区里面读取输入事件
static int GetInputEventFromBuffer(PInputEvent ptInputEvent)
{
if (!isInputBufferEmpty())//数组不空就读取
{
*ptInputEvent = g_atInputEvents[g_iRead];
g_iRead = (g_iRead + 1) % BUFFER_LEN;
return 1;
}
else
{
return 0;
}
}
/* 注册输入设备函数,这个就是我们之前说的注册函数,其实就是放进链表里 */
void RegisterInputDevice(PInputDevice ptInputDev)
{
ptInputDev->ptNext = g_InputDevs;
g_InputDevs = ptInputDev;
}
/* 初始化输入系统 */
void InputInit(void)
{
/* 注册触摸屏设备 */
extern void TouchscreenRegister(void);
TouchscreenRegister();
/* 注册网络输入设备 */
extern void NetInputRegister(void);
NetInputRegister();
}
/* 输入接收线程函数 */
static void *input_recv_thread_func(void *data)
{
PInputDevice ptInputDev = (PInputDevice)data;
InputEvent tEvent;
int ret;
while (1)
{
/* 从输入设备读取数据 */
ret = ptInputDev->GetInputEvent(&tEvent);
if (!ret)
{
/* 保存数据到环形缓冲区 */
pthread_mutex_lock(&g_tMutex);
PutInputEventToBuffer(&tEvent);
/* 唤醒等待数据的线程 */
pthread_cond_signal(&g_tConVar);
pthread_mutex_unlock(&g_tMutex);
}
}
return NULL;
}
/* 初始化输入设备 */
void IntpuDeviceInit(void)
{
int ret;
pthread_t tid;
/* 遍历所有输入设备,进行初始化和创建接收线程 */
PInputDevice ptTmp = g_InputDevs;
while (ptTmp)
{
/* 初始化设备 */
ret = ptTmp->DeviceInit();
/* 创建接收线程 */
if (!ret)
{
ret = pthread_create(&tid, NULL, input_recv_thread_func, ptTmp);
}
ptTmp = ptTmp->ptNext;
}
}
/* 获取输入事件 */
int GetInputEvent(PInputEvent ptInputEvent)
{
InputEvent tEvent;
int ret;
/* 无数据则休眠 */
pthread_mutex_lock(&g_tMutex);
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
pthread_mutex_unlock(&g_tMutex);
return 0;
}
else
{
/* 休眠等待 */
pthread_cond_wait(&g_tConVar, &g_tMutex);
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
ret = 0;
}
else
{
ret = -1;
}
pthread_mutex_unlock(&g_tMutex);
}
return ret;
}
输入管理单元测试
在unittest里添加input_test.c文件
input_test.c
#include <sys/mman.h> // 内存管理声明
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态定义
#include <unistd.h> // 提供通用的文件、目录、程序及进程操作的函数
#include <linux/fb.h> // 帧缓冲设备的定义
#include <fcntl.h> // 文件控制选项定义
#include <stdio.h> // 标准输入输出定义
#include <string.h> // 字符串操作函数定义
#include <sys/ioctl.h> // IO控制设备的函数定义
#include <input_manager.h> // 自定义输入管理的头文件
int main(int argc, char **argv)
{
int ret; // 用于函数返回值
InputEvent event; // 定义一个输入事件的结构体变量
InputInit(); // 初始化输入系统
IntpuDeviceInit(); // 初始化输入设备,注意这里应该是 InputDeviceInit
while (1) // 主循环
{
// 打印当前文件名、函数名和行号
printf("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
ret = GetInputEvent(&event); // 从输入设备获取一个事件
// 再次打印,并显示GetInputEvent函数的返回值
printf("%s %s %d, ret = %d\n", __FILE__, __FUNCTION__, __LINE__, ret);
if (ret) { // 如果返回值不是0,表示获取事件时出错
printf("GetInputEvent err!\n");
return -1; // 出错返回-1
}
else // 如果成功获取事件
{
// 打印事件类型
printf("%s %s %d, event.iType = %d\n", __FILE__, __FUNCTION__, __LINE__, event.iType );
if (event.iType == INPUT_TYPE_TOUCH) // 如果是触摸屏事件
{
// 打印触摸屏事件的详细信息
printf("Type : %d\n", event.iType);
printf("iX : %d\n", event.iX);
printf("iY : %d\n", event.iY);
printf("iPressure : %d\n", event.iPressure);
}
else if (event.iType == INPUT_TYPE_NET) // 如果是网络事件
{
// 打印网络事件的详细信息
printf("Type : %d\n", event.iType);
printf("str : %s\n", event.str);
}
}
}
return 0; // 正常退出程序
}
input目录下的makefile
EXTRA_CFLAGS :=
CFLAGS_file.o :=
obj-y += touchscreen.o
obj-y += netinput.o
obj-y += input_manager.o
要新编译input_manager.c文件
uinttest目录下的Makefile
EXTRA_CFLAGS :=
CFLAGS_file.o :=
#obj-y += disp_test.o
obj-y += input_test.o
编译input_test.c文件
运行结果
服务端程序在后台运行,触摸屏能发送数据,client端也能给server端发送数据,至此量产工具的输入系统就完成了。
后续会更新量产工具的第三部分——文字系统,希望大家点赞关注支持一下。