[经验分享] 【SUBJECT技术】(MMI, ArkUI, LibInput)实现“输入事件收集、打包、接收以及转换完全日志”的方法 原创 精华

诚迈_雨哥 显示全部楼层 发表于 2023-12-8 16:53:38

实现“输入事件收集、打包、接收以及转换完全日志”的方法

1.问题背景

输入事件是开源系统提供人机交互的纽带。从Linux libinput上报由用户操作产生的各种事件。输入设备包括触摸屏、手势、键盘、鼠标、触摸板、手写笔、游戏手柄、音量电源键等各种设备。MultimodalInput即多模输入子系统(简称MMI),它进行收集,初步转换成自己对应的事件类型。然后经由拦截、消费、监听、订阅等节点逐级对事件打包上报。凡是注册了多模输入的光标事件PointerEvent、按键事件KeyEvent或者轴类事件AxisEvent,都会在输入事件产生后收到回调处理。典型的就是上报给ArkUI子系统,相应地ArkUI收到输入事件会将多模子系统的事件转换成自己的事件类型。 在整个过程中,输入事件的缺失、冗余、转换错误都将导致用户失败的体验。整个排查过程就是层层、处处设置日志进行分析,而每个开发者的思路和对模块的了解程度不尽相同,所以排查过程比较困难。 于是想从Linux输入事件的收集、MMI打包、ARKUI接收以及转换等关键节点输出完全、可阅读的信息,帮助开发者迅速定位问题。

2.特别改进点

  • 之前每处会打印多条,并且各处所关注的参数不尽相同,改进后一条输入事件打印一条完全日志。包含来自 libinput模块所有以libinput_ 开头的函数。

  • 之前多处打印日志,改进后分别在收集、打包、接收和转换节点输出。 如蓝色框所示分别表示几个节点时的日志:

    log-string.PNG

  • 之前输出枚举数值,改进后调用那些能够输出枚举名称的函数,例如事件类型用EventTypeToString() 和GetEventType()代替之前的GetEventType()。代码为:

{
    std::string logPkt;
    logPkt += ",EventType:" + std::string(inpEvt->EventTypeToString(inpEvt->GetEventType())) +
     std::to_string(inpEvt->GetEventType());
}
  • 之前deviceID不知道是什么设备,改进后将设备名称和 deviceID结合输出。 如红框所示设备名称与DeviceId相结合:

    log-deviceId.PNG

  • 之前不能够输出枚举名称的即未实现枚举值到名称转换的switch case 函数,采用 magic_enum::enum_name() 可以输出枚举值名称,并且无须添加switch case函数。 枚举值到枚举名称的转换,如代码所示:

{
    std::string logPkt;
    logPkt +=",action(mmi-ark):["+std::string(evt.pointerEvent->DumpPointerAction())+ std::to_string(evt.pointerEvent->GetPointerAction())+
        "-"+magic_enum::enum_name(evt.action).data()+ std::to_string(static_cast<int32_t>(evt.action))+"]";
}
  • 之前转换前后的参数分属在不同日志条目中,现在整合在一块。例如鼠标按下动作在多模MMI中枚举值为2,名称为down,在ArkUI中枚举值是0,名称为DOWN, 打印的日志为 action(mmi-ark):[down2-DOWN0],如红框所示表示了两个模块的不同枚举转换值:

    log-enumstring.PNG

  • 一条输入事件打印一条完全日志时,有大部分的参数是相同的,字符串会很长。怎么把焦点放在变化的参数上,选择包含'LINE'的日志,并且删除'/'前的日志前缀,按照逗号分隔符将相邻日志的重复参数(字段)去掉,后面有实现的方法说明。

3.日志编写及实现

3.1.收集 Linux上报的输入事件

在多模输入子系统中,处理事件函数的入口进行事件收集,传入的是libinput_event* event。

void EventNormalizeHandler::HandleEvent(libinput_event* event)
{
    // ......
    PrintState_LibinputEvent(event); // sstate1-COL
    // ......
}

收集时的日志实现

int32_t EventNormalizeHandler::PrintState_LibinputEvent(libinput_event *event) // wyyxiu sstate1-COL
{
    //...... 
    std::string logPkt;
    logPkt = "sstate1-COL,";

    switch (type) {
    case LIBINPUT_EVENT_DEVICE_ADDED: {
        //...... 
        logPkt += ",sysName:" + std::string(ssysName) + ",udevTags:" + std::to_string(udevTags) +
            ",bus:" + std::to_string(bus) + ",phys:" + std::string(sphys) + ",uniq:" + std::string(suniq);
        logPkt += ",deviceId:" + std::string(sname) + std::string(ssysName);
        //......
        MMI_HILOGI("$%{public}d__LINE__ %{public}s", __LINE__, logPkt.c_str());
        break;
    }
    case LIBINPUT_EVENT_DEVICE_REMOVED: {
        logPkt += "[EVENT_DEVICE_REMOVED]" + std::to_string(type) + ",deviceId:" + std::string(sname) + std::string(ssysName);
        MMI_HILOGI("$%{public}d__LINE__ %{public}s", __LINE__, logPkt.c_str());
        break;
    }
    case LIBINPUT_EVENT_KEYBOARD_KEY: {
        // ...... 
        logPkt += "[EVENT_KEYBOARD_KEY]" + std::to_string(type) + " time:" + std::to_string(time) +
            ",deviceId:" + std::string(sname) + std::string(ssysName) + ",keyCode:" +
            std::to_string(keyCode) + ",keyAction:" + std::to_string(keyAction);
        MMI_HILOGI("$%{public}d__LINE__ %{public}s", __LINE__, logPkt.c_str());
        break;
    }
    case LIBINPUT_EVENT_POINTER_MOTION:
    case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
    case LIBINPUT_EVENT_POINTER_BUTTON:
    case LIBINPUT_EVENT_POINTER_BUTTON_TOUCHPAD:
    case LIBINPUT_EVENT_POINTER_AXIS:
    case LIBINPUT_EVENT_POINTER_TAP:
    case LIBINPUT_EVENT_POINTER_MOTION_TOUCHPAD: {
        //...... 
        logPkt += "[EVENT_POINTER]" + std::to_string(type) + ",deviceId:" + std::string(sname) + std::string(ssysName) +
            ",dxUnacc:" + std::to_string(static_cast<int32_t>(dxUnacc)) +
            ",dyUnacc:" + std::to_string(static_cast<int32_t>(dyUnacc)) + ",button:" + std::to_string(button) +
            ",state:" + std::to_string(state) + ",source:" + std::to_string(source) + ",hasVerticalAxis:" + std::to_string(hasVerticalAxis) +
            ",axisVerValue:" + std::to_string(static_cast<int32_t>(axisVerValue)) + ",hasHorizontalAxis:" + std::to_string(hasHorizontalAxis) +
            ",axisHorValue:" + std::to_string(static_cast<int32_t>(axisHorValue)) +
            ",dx:" + std::to_string(static_cast<int32_t>(dx)) + ",dy:" + std::to_string(dy) + ",fingerCount:" + std::to_string(fingerCount);
        MMI_HILOGI("$%{public}d__LINE__ %{public}s", __LINE__, logPkt.c_str());
        break;
    }
    case LIBINPUT_EVENT_TOUCHPAD_DOWN:
    case LIBINPUT_EVENT_TOUCHPAD_UP:
    case LIBINPUT_EVENT_TOUCHPAD_MOTION: {
        // ...... 
        break;
    }
    case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN:
    case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE:
    case LIBINPUT_EVENT_GESTURE_SWIPE_END:
    case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN:
    case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE:
    case LIBINPUT_EVENT_GESTURE_PINCH_END: {
        // ...... 
        break;
    }
    case LIBINPUT_EVENT_TOUCH_DOWN:
    case LIBINPUT_EVENT_TOUCH_UP:
    case LIBINPUT_EVENT_TOUCH_MOTION: {
        // ......
        break;
    }
    case LIBINPUT_EVENT_TABLET_TOOL_AXIS:
    case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY:
    case LIBINPUT_EVENT_TABLET_TOOL_TIP: {
        // ...... 
        break;
    }
    case LIBINPUT_EVENT_JOYSTICK_BUTTON:  {
        // ...... 
        break;
    }
    case LIBINPUT_EVENT_JOYSTICK_AXIS: {
        // ...... 
        break;
    }
    case LIBINPUT_EVENT_SWITCH_TOGGLE: {
        // ...... 
        break;
    }
    default: {
        MMI_HILOGW("[default] This device does not support :%{public}d", type);
        break;
    }
    }

    return RET_OK;
}

3.2.事件派发打包前的输入事件

在多模输入子系统中,输入事件数据转换(事件打包)函数的末尾打印事件,传入的是KeyEvent、SwitchEvent、PointerEvent。以打印按键事件为例说明:

int32_t InputEventDataTransformation::KeyEventToNetPacket(
    const std::shared_ptr<KeyEvent> key, NetPacket &pkt)
{
    //......
    std::string logPkt;
    if (PrintState_InputEvent(key, logPkt)) {
        PrintState_KeyEvent(key, logPkt);// sstate2-PKG
    }
    return RET_OK;
}

打包时的日志实现

bool InputEventDataTransformation::PrintState_InputEvent(const std::shared_ptr<InputEvent> inpEvt, std::string &logPkt)
{
    static int32_t lastEvtId = -1;
    if (lastEvtId == inpEvt->GetId()) {
        return false;
    }
    lastEvtId = inpEvt->GetId();
    logPkt ="sstate2-PKG,[InputEvent] mmi_Id:"+std::to_string(inpEvt->GetId())+
    ",ActionTime:"+std::to_string(inpEvt->GetActionTime())+
    ",EventType:"+std::string(inpEvt->EventTypeToString(inpEvt->GetEventType()))+std::to_string(inpEvt->GetEventType())+
    //",ActionStartTime:"+std::to_string(inpEvt->GetActionStartTime())+
    ",Action:"+std::to_string(inpEvt->GetAction())+
    //",SensorInputTime:"+std::to_string(inpEvt->GetSensorInputTime())+
    ",DeviceId:"+std::to_string(inpEvt->GetDeviceId())+",TargetDisplayId:"+std::to_string(inpEvt->GetTargetDisplayId())+
    ",TargetWindowId:"+std::to_string(inpEvt->GetTargetWindowId())+
    ",AgentWindowId:"+std::to_string(inpEvt->GetAgentWindowId())+",Flag:"+std::to_string(inpEvt->GetFlag()); 
    return true;
}
void InputEventDataTransformation::PrintState_KeyEvent(const std::shared_ptr<KeyEvent> evt, std::string &logLkt)
{
    logLkt +=",[KeyEvent] Action:"+std::string(evt->ActionToString(evt->GetKeyAction()))+std::to_string(evt->GetKeyAction())+
        ",Code:"+std::string(evt->KeyCodeToString(evt->GetKeyCode()))+std::to_string(evt->GetKeyCode()) + 
        ",Intention:"+std::to_string(evt->GetKeyIntention());
    auto keys = evt->GetKeyItems();
    int32_t size = static_cast<int32_t>(keys.size());
    logLkt += ",keys.size:"+std::to_string(size);
    for (const auto &item : keys) {
        logLkt += ",Code:"+std::to_string(item.GetKeyCode())+ ",DownTime:"+std::to_string(item.GetDownTime())+
           ",DeviceId:"+std::to_string(item.GetDeviceId())+ ",IsPressed:"+std::to_string(item.IsPressed())+
            ",Unicode:"+std::to_string(item.GetUnicode());
    }
    logLkt += ",NUMFunctionKey:"+std::to_string(evt->GetFunctionKey(KeyEvent::NUM_LOCK_FUNCTION_KEY))+
        ",CAPSFunctionKey:"+std::to_string(evt->GetFunctionKey(KeyEvent::CAPS_LOCK_FUNCTION_KEY))+
        ",SCROLLFunctionKey:"+std::to_string(evt->GetFunctionKey(KeyEvent::SCROLL_LOCK_FUNCTION_KEY));
    MMI_HILOGI("$%{public}d__LINE__ %{public}s",__LINE__, logLkt.c_str());
}

3.3.ArkUI接收和转换时的输入事件

典型接收输入事件客户端是ArkUI,以鼠标事件为例。它在处理来自多模输入(MMI)的鼠标事件,首先转换成自己的事件类型如MouseEvent,然后进行后续ACE-ENGING的回调处理。需要在事件转换后和回调处理前进行日志记录,调用PrintState_MouseEvent(event)。

void AceViewOhos::ProcessMouseEvent(const std::shared_ptr<MMI::PointerEvent>& pointerEvent)
{
    MouseEvent event;
    if (pointerEvent) {
        auto container = Platform::AceContainer::GetContainer(instanceId_);
        CHECK_NULL_VOID(container);
        ConvertMouseEvent(pointerEvent, event, container->IsScenceBoardWindow());
    }

    CHECK_NULL_VOID(mouseEventCallback_);
    PrintState_MouseEvent(event);// sstate3-ARK
    mouseEventCallback_(event, markProcess);
}

转换时的日志实现

#include "magic_enum.h" // sstate3-ARK

void PrintState_MouseEvent(const MouseEvent& evt)
{
    MMI::PointerEvent::PointerItem item;
    evt.pointerEvent->GetPointerItem(evt.id, item);
    std::set<int32_t> pressedSet = evt.pointerEvent->GetPressedButtons();
    std::string logPkt;
    logPkt = "sstate3-ARK,[MouseEvent] mmi_Id:"+std::to_string(evt.pointerEvent->GetId())+
    //",time:"+std::to_string(evt.time.time_since_epoch().count()/1000)+
    ",deviceId:"+std::to_string(evt.deviceId)+
    ",action(mmi-ark):["+std::string(evt.pointerEvent->DumpPointerAction())+std::to_string(evt.pointerEvent->GetPointerAction())+
    "-"+magic_enum::enum_name(evt.action).data()+std::to_string(static_cast<int32_t>(evt.action))+"]"+
    //",pullAction:"+std::to_string(static_cast<int32_t>(evt.pullAction))+
    ",buttonId(mmi-ark):["+std::to_string(evt.pointerEvent->GetButtonId())+
    "-"+magic_enum::enum_name(evt.button).data()+std::to_string(static_cast<int32_t>(evt.button))+"]"+
    ",sourceType(mmi-ark):["+std::string(evt.pointerEvent->DumpSourceType())+std::to_string(evt.pointerEvent->GetSourceType())+
    "-"+magic_enum::enum_name(evt.sourceType).data()+std::to_string(static_cast<int32_t>(evt.sourceType))+"]"+
    ",pressedBtns_size(mmi):"+std::to_string(pressedSet.size());
    if (pressedSet.size() > 0) {
        logPkt += ",btnId(mmi-ark):[";
        for (int32_t btnId : pressedSet) {
            logPkt += " "+std::to_string(btnId);
        }
        logPkt += "-l1r2m4&"+std::to_string(evt.pressedButtons)+"]";
    }
    logPkt += ",pointerId(id):"+std::to_string(evt.id)+
    ",windowX(x):"+std::to_string(static_cast<int32_t>(evt.x))+
    ",windowY(y):"+std::to_string(static_cast<int32_t>(evt.y))+
    ",z(ark):"+std::to_string(static_cast<int32_t>(evt.z))+
    ",displayX(screenX):"+std::to_string(static_cast<int32_t>(evt.screenX))+
    ",displayY(screenY):"+std::to_string(static_cast<int32_t>(evt.screenY))+
    ",deltaX:"+std::to_string(static_cast<int32_t>(evt.deltaX))+
    ",deltaY:"+std::to_string(static_cast<int32_t>(evt.deltaY))+
    ",deltaZ:"+std::to_string(static_cast<int32_t>(evt.deltaZ))+
    ",scrollX:"+std::to_string(static_cast<int32_t>(evt.scrollX))+
    ",scrollY:"+std::to_string(static_cast<int32_t>(evt.scrollY))+
    ",scrollZ:"+std::to_string(static_cast<int32_t>(evt.scrollZ))+
    ",targetDisplayId:"+std::to_string(evt.targetDisplayId);
    for (size_t i=0; i<evt.enhanceData.size(); i++) {
        logPkt += ",enhanceData:"+std::to_string(evt.enhanceData[i]);
    }
    LOGI("$%{public}d__LINE__ %{public}s",__LINE__, logPkt.c_str());
}

3.4.参考日志

08-07 18:17:02.487   372   892 I C02800/EventNormalizeHandler: in PrintState_LibinputEvent, $743__LINE__ sstate1-COL,[EVENT_TOUCH]502 time:3245578489,x:271,y:715,toolx:0,tooly:0,longAxis:0,shortAxis:0,pressure:0,toolRectWidth:0,toolRectHeight:0,seatSlot:0,toolType:-1
08-07 18:17:02.489   372   892 I C02800/PACKAGE: in PrintState_PointerEvent, $573__LINE__ state2-PKG,[InputEvent] mmi_Id:99,ActionTime:3245578489,EventType:pointer131072,Action:0,DeviceId:8,TargetDisplayId:0,TargetWindowId:1,AgentWindowId:1,Flag:0,[PointerEvent] ActionTime:3245578489,Action:move3,PointerId:0,SourceType:touch-screen2,ButtonId:-1,FingerCount:0,Axes:0,pressedBtns_size:0,pointerIds.size:1,item-pointerId:0,IsPressed:1,DisplayX:305,DisplayY:715,Width:0,Height:0,TiltX:0,TiltY:0,ToolDisplayX:0,ToolDisplayY:0,ToolWindowX:0,ToolWindowY:0,ToolWidth:0,ToolHeight:0,Pressure:0,ToolType:0,LongAxis:0,ShortAxis:0,DeviceId:8,RawDx:0,RawDy:0,pressedKeys.size:0,buffer.size:0
08-07 18:17:02.490  1090  1090 I C03900/Ace: [mmi_event_convertor.cpp(PrintState_TouchEvent)-(0)] $113__LINE__ sstate4-ARK,[TouchEvent] mmi_Id:98,deviceId:8,action(mmi-ark):[move3-2],sourceType(mmi-ark):[touch-screen2-2],pointerIds.size:1,size:0,mmi_pointerId(id):0,mmi_windowX(x):305,mmi_windowY(y):763,mmi_displayX(screenX):305,mmi_displayY(screenY):763,IsPressed:1,mmi_pressure(force):0,tiltX:0,tiltY:0,toolType(mmi-ark):[0-1],targetDisplayId:0

4.枚举值到枚举名称的转换方法

现代C++的枚举静态反射(到字符串、从字符串、迭代),适用于任何枚举类型,无需任何宏或模板代码

4.1.原理简述

枚举值转换成字符串这一步,是是利用了函数模板和 PRETTY_FUNCTION 组合使用获得,也就是对 PRETTY_FUNCTION 进行截取得到的字符串。

__PRETTY_FUNCTION__ 在预编译阶段会替换成带有参数的函数名,比如 constexpr auto magic_enum::detail::n() [with E = WeekDay; E e = WD_MONDAY] 从中截取出 WD_MONDAY 就可以了。

4.2.局限性

为了实现从字符串到枚举值的转换,这个库的内部定义了一个整数范围,默认从-128到128,用于遍历查找字符串对应的枚举值是多少,并且在代码中加了 static_assert 来判断范围,如果超过了这个范围就会报编译错误

4.3.参考链接

  • 推荐一个C++枚举转字符串的开源项目https://blog.csdn.net/albertsh/article/details/125955230
  • magic_enum:C++ enum静态反射库https://zhuanlan.zhihu.com/p/464758750
  • magic_enum 开源库链接 https://github.com/neargye/magic_enum

5.缩短日志长度的方法

把焦点放在变化的参数上,删除重复参数的py逻辑实现,缺失的函数需要自己补齐。

def read_log_file(path):
    """
    # 读日志文件到字符串列表
    """
    file = open(path, 'r', encoding='utf-8',errors='ignore')
    file_line = file.readlines()
    return file_line

def abbreviating_log():
    # 读取日志
    log_lines = read_log_file('logfile.txt')
    if len(log_lines) == 0:
        print('Error: Log file is empty!')
        return
    # 抽取包含 '__LINE__' 关键字的日志,并且删除'/'前的日志前缀
    log_lines = preprocess_log_file(log_lines)
    file_line = log_lines.copy()
    i = 0
    # 删除相邻且行号相同日志中不变的参数
    while i <len(file_line):
        left_subcurrent_string = get_left_string(log_lines[i], '__LINE__')
        right_subcurrent_string = get_right_string(log_lines[i], '__LINE__')
        if right_subcurrent_string == '':
            i += 1
            continue
        for index in range(i-1, -1, -1):
            left_sub_pre_string = get_left_string(log_lines[index], '__LINE__')
            right_sub_pre_string = get_right_string(log_lines[index], '__LINE__')
            if left_sub_pre_string == left_subcurrent_string:
                current_items = re.split(r'[,]', right_subcurrent_string)
                pre_items = re.split(r'[,]', right_sub_pre_string)
                for k in range(len(current_items)-1, -1, -1):
                    if current_items[k] in pre_items:
                        current_items.pop(k)
                
                if len(current_items) > 0:
                    file_line[i] = left_subcurrent_string + '__LINE__'
                    for item in current_items:
                        file_line[i] += ',' + item
                    file_line[i] += '\n'
                else:
                    file_line[i] = ''
                break
        i += 1

    # 保存只有变化参数的日志
    save_file(file_line, 'log_abbreviating.txt')

6.附件

6.1.打桩代码

dumpInputEvent.rar

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

精彩评论1

诚迈_雨哥

沙发 发表于 2023-12-8 16:56:22
压缩包为打桩代码以及三方文件magic_enum

dumpInputEvent.rar

19.82 KB, 下载次数: 2

打桩代码

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部