diff --git a/zh-cn/device-dev/apis/usb/usbd_client.h b/zh-cn/device-dev/apis/usb/usbd_client.h new file mode 100644 index 0000000000000000000000000000000000000000..a70be10399d71c66d53f4fcc4f1182345b8582b1 --- /dev/null +++ b/zh-cn/device-dev/apis/usb/usbd_client.h @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @addtogroup USB + * @{ + * + * @brief Declares USB-related APIs, including the custom data types and functions + * used to obtain descriptors, interface objects, and request objects, and to submit requests. + * + * @since 3.0 + * @version 1.0 + */ + +/** + * @file usbd_client.h + * + * @brief Defines the usbd Interface. + * + * @since 3.0 + * @version 1.0 + */ + +#ifndef USBD_CLIENT_H +#define USBD_CLIENT_H + +#include "usb_param.h" +#include "usbd_subscriber.h" + +namespace OHOS { +namespace USB { +class UsbdClient { +public: + /* * + * @brief 打开设备,建立连接 + * + * @param dev usb设备地址信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t OpenDevice(const UsbDev &dev); + + /* * + * @brief 关闭设备,释放与设备相关的所有系统资源 + * + * @param dev usb设备地址信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t CloseDevice(const UsbDev &dev); + + /* * + * @brief 获取设备描述符device + * + * @param dev usb设备地址信息 + * @param decriptor usb设备描述符信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t GetDeviceDescriptor(const UsbDev &dev, std::vector &decriptor); + + /* * + * @brief 根据String ID获取设备的字符串描述符string + * + * @param dev usb设备地址信息 + * @param descId usb的string ID + * @param decriptor 获取usb设备config信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t GetStringDescriptor(const UsbDev &dev, uint8_t descId, std::vector &decriptor); + + /* * + * @brief 根据config ID获取设备的配置描述符config + * + * @param dev usb设备地址信息 + * @param descId usb的config ID + * @param decriptor 获取usb设备config信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t GetConfigDescriptor(const UsbDev &dev, uint8_t descId, std::vector &decriptor); + + /* * + * @brief 获取原始描述符 + * + * @param dev usb设备地址信息 + * @param decriptor usb设备原始描述符 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t GetRawDescriptor(const UsbDev &dev, std::vector &decriptor); + + /* * + * @brief 设置当前的config信息 + * + * @param dev usb设备地址信息 + * @param configIndex usb设备config信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t SetConfig(const UsbDev &dev, uint8_t configIndex); + + /* * + * @brief 获取当前的config信息 + * + * @param dev usb设备地址信息 + * @param configIndex usb设备config信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t GetConfig(const UsbDev &dev, uint8_t &configIndex); + + /* * + * @brief 打开接口,并申明独占接口,必须在数据传输前执行 + * + * @param dev usb设备地址信息 + * @param interfaceid usb设备interface ID + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t ClaimInterface(const UsbDev &dev, uint8_t interfaceid); + + /* * + * @brief 关闭接口,释放接口的占用,在停止数据传输后执行 + * + * @param dev usb设备地址信息 + * @param interfaceid usb设备interface ID + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t ReleaseInterface(const UsbDev &dev, uint8_t interfaceid); + + /* * + * @brief 设置指定接口的备选设置,用于在具有相同ID但不同备用设置的两个接口之间进行选择 + * + * @param dev usb设备地址信息 + * @param interfaceid usb设备interface ID + * @param altIndex interface 的 AlternateSetting 信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t SetInterface(const UsbDev &dev, uint8_t interfaceid, uint8_t altIndex); + + /* * + * @brief 在给定端点上执行批量数据读取,返回读取的数据和长度,端点方向必须为数据读取可以设置超时时间 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param timeout 超时时间 + * @param data 获取写入的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t + BulkTransferRead(const UsbDev &dev, const UsbPipe &pipe, int32_t timeout, std::vector &data); + + /* * + * @brief 在给定端点上执行批量数据写入, 返回读取的数据和长度,端点方向必须为数据写入 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param timeout 超时时间 + * @param data 写入的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t + BulkTransferWrite(const UsbDev &dev, const UsbPipe &pipe, int32_t timeout, const std::vector &data); + + /* * + * @brief 对此设备执行端点零的控制事务,传输方向由请求类型决定。 如果requestType& + * USB_ENDPOINT_DIR_MASK是USB_DIR_OUT ,则传输是写入,如果是USB_DIR_IN ,则传输是读取。 + * + * @param dev usb设备地址信息 + * @param ctrl usb设备控制数据包结构 + * @param data 读取/写入 的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t ControlTransfer(const UsbDev &dev, const UsbCtrlTransfer &ctrl, std::vector &data); + + /* * + * @brief 在给定端点上执行中断数据读取, 返回读取的数据和长度,端点方向必须为数据读取 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param timeout 超时时间 + * @param data 读取的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t + InterruptTransferRead(const UsbDev &dev, const UsbPipe &pipe, int32_t timeout, std::vector &data); + + /* * + * @brief 在给定端点上执行中断数据写入, 返回读取的数据和长度,端点方向必须为数据写入 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param timeout 超时时间 + * @param data 读取的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t + InterruptTransferWrite(const UsbDev &dev, const UsbPipe &pipe, int32_t timeout, std::vector &data); + + /* * + * @brief 在给定端点上执行等时数据读取, 返回读取的数据和长度,端点方向必须为数据读取 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param timeout 超时时间 + * @param data 读取的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t IsoTransferRead(const UsbDev &dev, const UsbPipe &pipe, int32_t timeout, std::vector &data); + + /* * + * @brief 在给定端点上执行等时数据写入, 返回读取的数据和长度,端点方向必须为数据写入 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param timeout 超时时间 + * @param data 读取的数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t + IsoTransferWrite(const UsbDev &dev, const UsbPipe &pipe, int32_t timeout, std::vector &data); + + /* * + * @brief 将指定的端点进行异步数据发送或者接收请求,数据传输方向由端点方向决定 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * @param clientData 用户数据 + * @param buffer 传输数据 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t RequestQueue(const UsbDev &dev, + const UsbPipe &pipe, + const std::vector &clientData, + const std::vector &buffer); + + /* * + * @brief 等待RequestQueue异步请求的操作结果 + * + * @param dev usb设备地址信息 + * @param clientData 用户数据 + * @param buffer 传输数据 + * @param timeout 超时时间 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t + RequestWait(const UsbDev &dev, std::vector &clientData, std::vector &buffer, int32_t timeout); + + /* * + * @brief 取消待处理的数据请求 + * + * @param dev usb设备地址信息 + * @param pipe usb设备pipe信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t RequestCancel(const UsbDev &dev, const UsbPipe &pipe); + + /* * + * @brief 获取从设备支持的功能列表(按位域表示)(从设备) + * + * @param funcs 获取当前设备的function的值 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t GetCurrentFunctions(int32_t &funcs); + + /* * + * @brief 设置从设备支持的功能列表(按位域表示)(从设备) + * + * @param funcs 传入设备支持的function的值 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t SetCurrentFunctions(int32_t funcs); + + /* * + * @brief 关闭设备,释放与设备相关的所有系统资源 + * + * @param portId port接口 ID + * @param powerRole 电源角色的值 + * @param dataRole 数据角色的值 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t SetPortRole(int32_t portId, int32_t powerRole, int32_t dataRole); + + /* * + * @brief 查询port端口的当前设置 + * + * @param portId port接口 ID + * @param powerRole 电源角色的值 + * @param dataRole 数据角色的值 + * @param mode 模式的值 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static int32_t QueryPort(int32_t &portId, int32_t &powerRole, int32_t &dataRole, int32_t &mode); + + /* * + * @brief 绑定订阅者 + * + * @param subscriber 订阅者信息 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static ErrCode BindUsbdSubscriber(const sptr &subscriber); + + /* * + * @brief 解绑订阅者 + * + * @return 0 表示成功,其他返回值表示失败 + * @since 3.0 + */ + static ErrCode UnbindUsbdSubscriber(); + +private: + static void PrintBuffer(const char *title, const uint8_t *buffer, uint32_t length); + static int32_t SetDeviceMessage(MessageParcel &data, const UsbDev &dev); + static int32_t SetBufferMessage(MessageParcel &data, const std::vector &tdata); + static int32_t GetBufferMessage(MessageParcel &data, std::vector &tdata); + static sptr GetUsbdService(); + static ErrCode DoDispatch(uint32_t cmd, MessageParcel &data, MessageParcel &reply); +}; +} // namespace USB +} // namespace OHOS +#endif // USBD_CLIENT_H diff --git a/zh-cn/device-dev/subsystems/Readme-CN.md b/zh-cn/device-dev/subsystems/Readme-CN.md index 73a09eb14ca1e80dbfd79c1b91aea1dffb68b78f..6923a5a776a083f835d067f68f2adeaabaf7f346 100755 --- a/zh-cn/device-dev/subsystems/Readme-CN.md +++ b/zh-cn/device-dev/subsystems/Readme-CN.md @@ -43,6 +43,7 @@ - [Sensor服务子系概述](subsys-sensor-overview.md) - [Sensor服务子系使用指导](subsys-sensor-guide.md) - [Sensor服务子系使用实例](subsys-sensor-demo.md) +- [Usb子系统服务指南](subsys-usb-service.md) - [用户程序框架](subsys-application-framework.md) - [概述](subsys-application-framework-overview.md) - [搭建环境](subsys-application-framework-envbuild.md) diff --git "a/zh-cn/device-dev/subsystems/figure/Function\347\256\241\347\220\206.png" "b/zh-cn/device-dev/subsystems/figure/Function\347\256\241\347\220\206.png" new file mode 100644 index 0000000000000000000000000000000000000000..37361456afa94c7618445caa2fa9e3cce0f6a424 Binary files /dev/null and "b/zh-cn/device-dev/subsystems/figure/Function\347\256\241\347\220\206.png" differ diff --git "a/zh-cn/device-dev/subsystems/figure/Service\347\224\237\345\221\275\345\221\250\346\234\237.png" "b/zh-cn/device-dev/subsystems/figure/Service\347\224\237\345\221\275\345\221\250\346\234\237.png" new file mode 100644 index 0000000000000000000000000000000000000000..693c612861bc7ac85810526621fe55c6a4e300e7 Binary files /dev/null and "b/zh-cn/device-dev/subsystems/figure/Service\347\224\237\345\221\275\345\221\250\346\234\237.png" differ diff --git "a/zh-cn/device-dev/subsystems/figure/USB\346\234\215\345\212\241\346\236\266\346\236\204\345\233\276.png" "b/zh-cn/device-dev/subsystems/figure/USB\346\234\215\345\212\241\346\236\266\346\236\204\345\233\276.png" new file mode 100644 index 0000000000000000000000000000000000000000..8242995a475477530e35577ced60f6ec44da4007 Binary files /dev/null and "b/zh-cn/device-dev/subsystems/figure/USB\346\234\215\345\212\241\346\236\266\346\236\204\345\233\276.png" differ diff --git "a/zh-cn/device-dev/subsystems/figure/USB\350\256\276\345\244\207\345\210\227\350\241\250\347\256\241\347\220\206.png" "b/zh-cn/device-dev/subsystems/figure/USB\350\256\276\345\244\207\345\210\227\350\241\250\347\256\241\347\220\206.png" new file mode 100644 index 0000000000000000000000000000000000000000..cc9f164641da4f2a54ff49e2666413db6a9beea3 Binary files /dev/null and "b/zh-cn/device-dev/subsystems/figure/USB\350\256\276\345\244\207\345\210\227\350\241\250\347\256\241\347\220\206.png" differ diff --git "a/zh-cn/device-dev/subsystems/figure/USB\350\256\276\345\244\207\346\235\203\351\231\220\347\256\241\347\220\206.png" "b/zh-cn/device-dev/subsystems/figure/USB\350\256\276\345\244\207\346\235\203\351\231\220\347\256\241\347\220\206.png" new file mode 100644 index 0000000000000000000000000000000000000000..72f91d045057498d5717644dc720e616c19a48d4 Binary files /dev/null and "b/zh-cn/device-dev/subsystems/figure/USB\350\256\276\345\244\207\346\235\203\351\231\220\347\256\241\347\220\206.png" differ diff --git a/zh-cn/device-dev/subsystems/subsys-usb-service.md b/zh-cn/device-dev/subsystems/subsys-usb-service.md new file mode 100644 index 0000000000000000000000000000000000000000..789a3c3c0efa4f5ecf226fdf3037584b3192153d --- /dev/null +++ b/zh-cn/device-dev/subsystems/subsys-usb-service.md @@ -0,0 +1,320 @@ +# USB + +- [概述](#section175431838101617) + +- [USB服务架构](#section350923483241) + - [生命周期](#section894546131154) + +- [主要功能介绍](#section951321854211) + - [USB设备管理列表](#section951321855211) + - [Function管理](#section951321856212) + - [USB设备权限管理](#section951321857213) + +- [接口说明](#section83365421647) + - [Host部分](#section83365421658) + - [Device部分](#section83365421669) + - [Port部分](#section83365421670) + +- [开发实例](#section54626568156) + + +## 概述 + + USB设备分为Host设备(主机设备)和Device设备(从设备)。用户可通过Port Service来根据实际业务把运行OpenHarmony的设备切换为Host设备或者Device设备。目前在Host模式下,支持获取USB设备列表,USB设备权限管理,控制\批量的同异步数据传输等,在Device模式下,支持HDC(调试)、ACM(串口)、ECM(网口)等Function功能的切换。 + +## USB服务架构 + + USB服务系统分为USB FWK/API、USB Service、USB HDI、USB HAL四个部分。 + + USB服务架构如下图所示: + + ![](figure/USB服务架构图.png "USB服务架构图") + +- ### USB FWK/API + 基于USB Service服务,使用NAPI技术,向上提供JS接口。 + +- ### USB Service + 使用C++代码实现,包含Host、Device、Port三个模块。基于HDI的接口,主要实现USB设备的列表管理、Function 管理、Port管理、USB设备权限管理等功能。 + +- ### USB HDI + 基于HAL层,向上提供C++接口。 + +- ### USB HAL + 使用C代码实现,基于Host SDK和Device SDK,封装了对USB设备的基本操作,同时通过HDF框架接收内核上报的信息。 + +## 生命周期 + + USB Service作为系统服务随系统启动、停止。 +- ### OS INIT: + 操作系统初始化阶段 + +- ### USB Service Start: + USB Service的初始化阶段,进行一些初始化操作,初始化之后将USB Service挂起,等待事件触发和调用 + +- ### USB Service Active: + USB Service的执行和调用阶段,具体描述为USB的插入触发了USB通知,并进行USB设备连接,创建数据连接通道,进行数据传输等一系列操作 + +- ### USB Service Background: + 当所有的USB设备被拔出之后,USB Service不再处理USB业务,在后台挂起,等待下一次的USB插入之后触发USB Service Active,或者在操作系统关机的时候进行销毁处理 + +- ### USB Service Stop: + 操作系统关机的时候,销毁USB Service + +Service的生命周期如下图: + +![](figure/Service生命周期.png "Service生命周期") + +## 主要功能介绍 + +- ### USB设备列表管理 + + USB插拔事件通过订阅/发布的消息机制通知到Host Service,改变Device Map中的Device数量。应用层通过API接口调用从 Device Map中获取所有的Device设备列表。 + + USB设备管理如下图: + +![](figure/USB设备列表管理.png "USB设备列表管理") + +- ### Function管理 + + Function Manager中定义好支持的function 列表。通过调用接口设置functions 和 获取当前functions,且能实现string和number的转换。 + HDC是通过修改SystemParameter实现加载和卸载;ACM和ECM是通过给ACM服务和ECM服务(基于HDF框架)发消息来实现加载和卸载。 + + Function管理如下图: + +![](figure/Function管理.png "Function管理") + +- ### USB设备权限管理 + + 当APP调用USB设备操作接口时,Host Service 使用设备唯一标识,在管理的权限列表中查询该设备是否有访问权限,若有权限,会执行操作,否则将返回权限不足信息。 + 当APP无此设备的使用权限时,App可以调用权限申请接口(RequestRight),会在权限列表中加入App与设备对应的权限信息,后续进行USB操作时将正常执行。 + + USB设备权限管理如下图: + +![](figure/USB设备权限管理.png "USB设备权限管理") + +## 接口说明 + +- ### Host部分 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

头文件

+

接口名称

+

功能描述

+

usb_srv_client.h

+

+

+

int32_t OpenDevice(const UsbDevice &device, USBDevicePipe &pip);

+

打开USB设备,建立连接

+

int32_t HasRight(std::string deviceName);

+

判断是否有权访问设备

+

int32_t RequestRight(std::string deviceName);

+

请求给定软件包的临时权限以访问设备

+

int32_t GetDevices(std::vector &deviceList);

+

获取USB设备列表

+

int32_t ClaimInterface(USBDevicePipe &pip, const UsbInterface &interface, bool force);

+

打开接口,并申明独占接口,必须在数据传输前执行

+

int32_t ReleaseInterface(USBDevicePipe &pip, const UsbInterface &interface);

+

关闭接口,释放接口的占用,在停止数据传输后执行

+

int32_t BulkTransfer(USBDevicePipe &pip, const USBEndpoint &endpoint, std::vector &vdata, int32_t timeout);

+

在给定端点上执行批量数据传输, 返回读取或发送的数据长度,通过端点方向确定读取或发送数据

+

int32_t ControlTransfer(USBDevicePipe &pip, const UsbCtrlTransfer &ctrl, std::vector &vdata);

+

对此设备执行端点零的控制事务,传输方向由请求类型决定

+

int32_t SetConfiguration(USBDevicePipe &pip, const USBConfig &config);

+

设置设备当前使用的配置,通过配置值进行指定

+

int32_t SetInterface(USBDevicePipe &pipe, const UsbInterface &interface);

+

设置指定接口的备选设置,用于在具有相同ID但不同备用设置的两个接口之间进行选择

+

int32_t GetRawDescriptors(std::vector &vdata);

+

获取原始的USB描述符

+

int32_t GetFileDescriptor();

+

获取文件描述符

+

bool Close(const USBDevicePipe &pip);

+

关闭设备,释放与设备相关的所有系统资源

+

int32_t PipeRequestWait(USBDevicePipe &pip, int64_t timeout, UsbRequest &req);

+

获取异步传输结果

+

int32_t RequestInitialize(UsbRequest &request);

+

初始化异步数据传输request

+

int32_t RequestFree(UsbRequest &request);

+

释放异步数据传输request

+
+

int32_t RequestAbort(UsbRequest &request);

+

取消待处理的数据请求

+

int32_t RequestQueue(UsbRequest &request);

+

将指定的端点进行异步数据发送或者接收请求,数据传输方向由端点方向决定

+
+ +- ### Device部分 + + + + + + + + + + + + + + + + + + + + + +

头文件

+

接口名称

+

功能描述

+

usb_srv_client.h

+

+

+

int32_t GetCurrentFunctions(int32_t &funcs);

+

获取设备模式下的当前USB功能列表的数字组合掩码

+

int32_t SetCurrentFunctions(int32_t funcs);

+

在设备模式下设置当前的USB功能列表

+

int32_t UsbFunctionsFromString(std::string funcs);

+

将给定的功能列表描述字符串转换为功能列表的数字组合掩码

+

std::string UsbFunctionsToString(int32_t funcs);

+

将给定的功能列表的数字组合掩码转换为功能列表描述字符串

+
+ +- ### Port部分 + + + + + + + + + + + + + + + + + + +

头文件

+

接口名称

+

功能描述

+

usb_srv_client.h

+

+

+

int32_t GetSupportedModes(int32_t portId, int32_t &supportedModes);

+

获取指定的端口支持的模式列表的组合掩码

+

int32_t SetPortRole(int32_t portId, int32_t powerRole, int32_t dataRole);

+

设置指定的端口支持的角色模式,包含充电角色、数据传输角色

+

int32_t GetPorts(std::vector &usbPorts);

+

获取物理USB端口描述信息列表

+
+ +## 开发实例 + +1. 获取usb service实例 + + ``` + static UsbSrvClient &g_usbClient = UsbSrvClient::GetInstance(); + ``` + +2. 获取usb设备列表 + + ``` + std::vector deviceList; + int32_t ret = g_usbClient.GetDevices(deviceList); + ``` + +3. 打开设备 + + ``` + USBDevicePipe pip; + int32_t ret = g_usbClient.OpenDevice(dev, pip); + ``` + +4. 打开接口 + + ``` + g_usbClient.ClaimInterface(pip, interface, force); + interface为deviceList中device的interface。 + ``` + +5. 数据传输 + + ``` + g_usbClient.BulkTransfer(pipe, endpoint, vdata, timeout); + ``` + pipe为打开设备后的数据传输通道,endpoint为device中数据传输的端点,vdata是需要传输或读取的二进制数据块,timeout为传输超时时长 + +6. 关闭接口 + + 关闭设备释放资源 + + ``` + g_usbClient.ReleaseInterface(pipe, interface); + g_usbClient.Close(pipe); + ```