Linux应用项目之量产工具(二)——输入系统

前言

        前面我们完成了量产工具显示系统部分,见量产工具(一)——显示系统

e53f34b7c0734cd3b988bec34b7d30d2.png

        本篇实现输入系统。我们这个项目支持不同的输入设备,比如触摸屏和网络输入,这些输入设备都会向上层提供数据,那么这些数据的结构要统一;另外,对于不同的输入设备,我们也应该抽象出一个统一的结构体来表示。


数据结构抽象

        我们要抽象出两个结构体,一个用来描述上报的数据,一个用来描述输入设备。

37b77a2c49b142af931564ea127e9aa8.png

1.数据相关结构体

ef9e1cae4fcb4ff69e3306e6ad5e2a32.png

        我们这个项目,肉眼观察到模块测试通过后可以用手指点击屏幕,对于无法手动测试的模块,需要通过网络输入发送字符串表示测试通过,所以我们抽象出的数据结构体既要支持触摸屏也要支持网络。

struct InputEvent

3080a750b9414113a6f5216d204b3783.png

eq?%5Cbullet INPUT_TYPE_TOUCH 表示是触摸屏数据
eq?%5Cbullet INPUT_TYPE_NET 表示是网络数据

eq?%5Cbullet iType表示是触摸屏数据还是网络数据
eq?%5Cbullet iX表示触摸屏数据的x坐标
eq?%5Cbullet iY同理
eq?%5Cbullet iPressure表示触摸屏数据的压力值,判断按下还是松开
eq?%5Cbullet str存放网络数据发送的字符串
eq?%5Cbullet tTime可以存放时间(可能会用到)

2.设备相关结构体

        现在来抽象出输入设备相关结构体,对于不同的输入设备,他们都应该实现什么函数?

struct InputDevice

9da3275e059a4611a644abf4386b68b4.png

        如果你学习了量产工具(一)显示系统的内容,会发现抽象出来的结构体和显示系统类似,代码思路也是类似的,只不过为了代码的复用性,封装的比较复杂。

eq?%5Cbullet name:可以通过设备名字从管理链表中得到设备句柄,调用底层函数
eq?%5Cbullet GetInputEvent:调用底层函数得到一个个 InputEvent 结构体
eq?%5Cbullet DeviceInit:底层初始化函数,比如打开设备节点等操作
eq?%5Cbullet DeviceExit:有打开就有退出函数,关闭文件等等
eq?%5Cbullet 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

65fcf5ccf10f4990b2f71d2d9682d728.png

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

008d76e8c4fa4ba094941cbcf473c57c.png

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)nm

STRIP        = $(CROSS_COMPILE)strip
OBJCOPY        = $(CROSS_COMPILE)objcopy
OBJDUMP        = $(CROSS_COMPILE)objdump

export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

LDFLAGS := -lts

export CFLAGS LDFLAGS

TOPDIR := $(shell pwd)
export TOPDIR

TARGET := 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目录下的文件

运行结果

18069ed0a63b4432b5187c1ffc41bcf4.png

        成功打印类别,xy坐标和压力值。


网络输入编程

        网络编程相关知识见:Linux网络编程

        和触摸屏编程步骤十分类似,实现如下结构体并实现里面的函数:

98b4cb0e515b4379b9ef36441248a053.png

        在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

1e2bc5e0ef50498fa4672e686f0c8094.png

        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地址+要发送的字符串即可。

运行结果

90f9c2851170435cad68bc48b22ea659.png

        服务端在后台执行后,再来执行client程序

2f0df3af9aca4bc3a523a74b5246f015.png


输入管理

        为什么需要输入管理?我们想支持同时从多个输入设备里面得到数据,就需要提供一个输入管理。

        怎么同时读取多个输入设备?如果使用轮询的方式来读取,一个设备在休眠的时候,另一个设备发来的数据可能会丢失,所以需要为每一个输入设备创建一个线程,就像单片机引入操作系统FreeRTOS一样,要用到线程编程。相关知识见:Linux线程编程

框架

e844d5ef97fc4d7cae77f5eeed15b652.png

核心在于:GetInputEvent

怎么同时读取多个设备?
        读取触摸屏时,可能会休眠,那么网络输入就会丢失;
        读取网络数据时,可能会休眠,那么触摸屏数据就会丢失
所以不能使用“先后轮询的方式”,不能使用下面的代码:
while (1)
{
        g_tTouchscreenDev.GetInputEvent(&event1);
        g_tInputDev.GetInputEvent(&event2);
}
要想支持多个输入设备,只能使用线程:
        为每个InputDevice都创建一个“读取线程”

引入环形缓冲区

怎么避免数据丢失?
        比如触摸屏,它会一下子上报很多数据,对于网络输入,也有可能同时又多个client发来数据。
        所以,不能使用单一的变量来保存数据,而是使用一个数组来保存数据,使用“环形缓冲区”。

        一想到环形缓冲区,学过FreeRTOS的队列的同学应该很熟悉,他就是一个数组,有一个读指针和一个写指针,写满后指针会从尾部指向头部,这也是“环形”的由来。

5ded02f870534271af42bc1cbe883919.png

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;
}

输入管理单元测试

75ee1eeb4a20448d9a16823dcc3f4115.png

        在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文件 

运行结果

8b6b53e0d231496cb6359a6279e16edb.png

        服务端程序在后台运行,触摸屏能发送数据,client端也能给server端发送数据,至此量产工具的输入系统就完成了。


        后续会更新量产工具的第三部分——文字系统,希望大家点赞关注支持一下。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sakabu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值