有赞零售智能硬件体系搭建历程

前言

有赞零售 App 上线至今,为了降低商家硬件迁移成本,同时提高商家硬件采购的选择多样性,陆陆续续对接了市面上 Top 20+ 的智能硬件,包括打印机、电子秤、扫码枪、摄像头、一体机等, 在硬件对接过程中团队投入了大量的人力进行支持,受限于硬件架构不成体系、硬件类目划分不清晰、通信协议多样性、多端重复适配造轮子等因素,导致硬件线上问题较多,且投入的开发成本很高,也影响了商家的正常经营。为了彻底解决这些问题,提高新设备对接效率,并确保硬件交互质量,有赞零售移动团队对硬件体系做了几次重构演进,目前一款新硬件的对接与适配成本已经控制在一到两个工作日内,相较2019年人力投入降低了50%。同时通过不断完善硬件 FAQ 文档,协助商家与硬件支持同学快速定位解决问题,硬件开发同学直接处理的线上问题数量相较2019下半年环比下降55%,技术支持同学对接的硬件问题也环比下降了33%,提效比较明显。

智能硬件矩阵

设备使用场景简介

硬件类型 使用场景 对接设备
一体机 线下门店都会在收银台配置一款收银机,方便商家与收银员进行门店经营开单操作 商米、天波、联迪、中科英泰等
打印机 订单正向与逆向环节需要打印小票,比如购车小票、退货小票等 365 、映美云、佳博、思普瑞特、易联云等
副屏 开单支付与会员结算流程中,订单信息对顾客足够透明,可通过副屏将购物车、会员、支付相关信息投影到副屏上 商米 T2、T1、D2、联迪等
客显 除了副屏可以投影订单数据之外,还提供客显这种低成本的外接设备进行数据投影 中崎等
人脸识别 通过摄像头采集顾客人脸信息,支持会员快速识别、快捷支付等 青蛙 Pro、蜻蜓、三方摄像头等
NFC & Rfid 通过对接磁条、nfc、Rfid 等外接设备,满足实体卡支付、Rfid 条码商品(一般应用在服装品类)等加购场景 cas、灵天智能等
电子秤 生鲜果蔬商家涉及到称重环节,通过适配电子秤满足称重的经营场景 凯士、大华、欧陆达、S2 等
POS机 部分商家不采购收银机,只需要使用 POS 进行订单结算,且需要支持刷卡功能 WANGPOS、SUNMI P 系列等

硬件矩阵图

体系搭建介绍

有赞零售对接的设备种类繁多,由于篇幅内容有限,接下来会着重讲解打印机、 POS 、电子秤、副屏相关技术的设计细节。

一、硬件库拆解重构

零售设备库 sdk 早期设计类似于全家桶,聚合了打印机、电子秤、POS 机等所有设备,扩展性比较差,随着新机器的适配接入,造成 sdk 频繁升级,稳定性无法保证。前期只接入几款设备勉强还能应付过来,随着业务迭代发展,设备接入种类与数量越来越多,当前的设备库架构设计显得非常臃肿,维护与适配成本比较高,开发对接效率也非常低。

为了彻底解决这些问题,组内经过多次讨论与论证,全家桶的方式需要被彻底推翻改造,首先要做的就是对设备进行分类,将通用的设备放到单独的 module 中进行维护,打成 aar 给业务方灵活调用,且需要下沉抽象出一些通用能力,降低新设备的接入成本,通过这次的架构设计迭代,新设备适配人力成本减少 2 倍以上,且硬件上线质量也得到了有效保证。

架构图

新设备库框架部分参考 Android 系统架构模型,分为 OEM、Core 、 Base 、Library 四层,OEM 为业务 Manager 层,业务方只需要感知 Manager 提供的 Api ,底层能力通过 Core、Base 支撑,同时 Library 层将硬件之间一些通用的三方sdk(比方说 WANGPOS SDK 既提供刷卡能力,又提供打印能力,聚合多个 OEM 的功能,可以共享)共享出来,供 OEM 层调用。

1.1 设备库架构介绍

1.1.1 OEM 层

提供 PrinterManager 、 PosManager 、 WeightManager 、 XXXManager 等设备管理类,供业务方调用,且每个设备单独打成 aar ,供业务方灵活依赖。

例如:

零售工程通过模块化进行开发管理,所有硬件能力通过 module_device 向外提供能力,业务 module 通过调用模块间定义的向外暴露接口(有赞零售模块间通信方式详细设计请参考这篇文章 Android -模块化-面向接口编程)来访问 module_device 提供的能力,同时 module_device 会依赖设备能力各自的 aar ,其他业务模块只需要向设备模块要能力,不需要关心设备模块具体的实现,模块之间责任划分清晰。

调用示例图:

1.1.2 Core 层

提供设备通用能力,包括设备模型、连接能力、缓存能力、设备状态心跳检测、异常处理、线程管理、读写能力等。

1.1.2.1 设备模型

零售对接了如此多的的设备,设备模型的抽象尤为重要,包括设备连接类型、设备 id 、设备型号、设备状态、设备标签、是否需要缓存等,分类设备又可以基于设备模型进行接口扩展,比如 IPrinter 抽象出打印能力,IPos 抽象出刷卡、退款等能力,IWeight 抽象出称重、置零、去皮等能力,设备实体各自实现 IPrinter 、IPos 、 IWeight 接口,实现接口提供的相应方法,通过面向接口编程,业务划分与代码管理清晰很多。

UML :

1.1.2.2 设备状态心跳检测

有赞零售收银台右上角“收银中心”聚合了很多收银通用能力,其中就包括了外接设备的状态管理,该功能可以实时监测设备状态,在快速定位线上问题过程中发挥了非常重要的作用,且也能协助商家对设备进行健康自检。

  • 注册心跳,开启心跳检测
/**
 * 注册监听
 */
fun registerCheckState(listener: IDeviceStateListener) {  
    if (!deviceStateListeners.contains(listener)){
        deviceStateListeners.add(listener)
    }
    checkHeart()
}


/**
 * 检查心跳
 */
private fun checkHeart() {  
    if (cacheDevices.isNullOrEmpty()) {
        return
    }
    if (!isCheckingHeart) {
        isCheckingHeart = true
        DeviceThreadManager.threadPoolProxy.getHeartExecutor().execute(HeartTask())
    }
}
  • 心跳机制

单线程内开启 while 循环,每次心跳间隔 2 秒

inner class HeartTask : Runnable {  
        override fun run() {
            while (true) {
                if (cacheDevices.isNullOrEmpty()) {
                    continue
                }
                for (entity in cacheDevices) {
                    val newState = entity.device.getState()
                    var countChange = false
                    if (deviceCount != cacheDevices.size){
                        countChange = true
                        deviceCount = cacheDevices.size
                    }
                    val shouldNotify = (entity.getState() != newState) || countChange
                    entity.setState(newState)
                    for (listener in deviceStateListeners){
                        if (listener is IDeviceStateAlwaysListener){
                            listener.onDeviceState(entity)
                        } else {
                            // 会与上一次心跳状态进行比较,状态不一样时,才会回调
                            if (shouldNotify){
                                listener.onDeviceState(entity)
                            }
                        }
                    }

                }

                try {
                    Thread.sleep(STATE_UPDATE_SUSPEND)
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }
    }
}
1.1.2.3 读写能力

打印小票的前提是将 ESC / POS 协议字节数据输入到打印机驱动中,这里涉及到写的场景。而在生鲜果蔬行业涉及到称重场景中要用到电子秤,商品重量需要实时传输到收银机,这个又涉及到读的场景,底层抽象读写接口,业务方自己实现,这块底层做的比较轻。

 /**
 * 读接口
 */
interface IRead<T> {  
    fun read(): T?
}

/**
 * 写接口
 */
interface IWrite<T> {  
    fun write(content: T?)
}

// 大华电子秤读取商品重量,业务方自己实现
class DahuaWeight: IWeight, IRead<String?>{


    override fun read(): String? {
        return DahuaWeightSdk.getWeight()
    }
}
1.1.2.4 缓存能力

有赞零售 app 为了满足设备连接多样性,支持同时连接多款设备,且针对每款设备提供手动断开、连接能力(比方说餐饮行业,前台与后厨都连接了打印机,退款小票只需要在前台打印机打印的话,后厨的打印机可以手动点击断开),且我们需要确保商家退出 app 、app 覆盖升级等场景,设备的状态可以恢复,基于这种场景必须要支持本地缓存能力,下次 app 进入读取本地缓存,绘制 UI 即可。

/**
 * 设备缓存管理
 * 缓存到本地文件
 */
class DeviceCacheManager {  
    //添加设备
    fun addDevice(deviceInfo: DeviceInfo?) {
        if (addInner(deviceInfo)){//添加到内存
            memoryToCache()//刷到缓存
        }
    }
    //删除设备
    fun removeDevice(deviceInfo: DeviceInfo?) {
        if (removeInner(deviceInfo)){//从内存中删除
            memoryToCache()//刷到缓存
        }
    }
    //获取设备列表
    fun getCacheDevices(tag: String): List<IDevice>? {
        if (cacheDevices.isNullOrEmpty()){
            cacheToMemory()
        }

        return getDevicesByTag(tag)
    }
}
1.1.2.5 线程管理

设备的状态监测、IO 读写、耗时逻辑处理都涉及到线程切换,目前底层提供配置线程池统一管理,避免线程随意创建,抢占系统资源,拖累收银机的性能(零售对接了很多低端设备,线程控制非常严格,且部分机型可能出现 p-thread 问题,线程创建数量超出一定数量后, app 将 crash )。

... ...
private val diskIOExecutor  = Executors.newSingleThreadExecutor(DeviceThreadFactory("diskIO"))  
private val heartExecutor  = Executors.newSingleThreadExecutor(DeviceThreadFactory("heart"))  
private val networkExecutor = Executors.newFixedThreadPool(3, DeviceThreadFactory("network"))  
private val scheduleExecutor = ScheduledThreadPoolExecutor(5, DeviceThreadFactory("schedule"), ThreadPoolExecutor.AbortPolicy())  
... ...
1.1.2.6 异常模型

硬件的异常管理在实际开发与交互提示流程中非常重要,比方说打印机是否缺纸了、电子秤是否断开了等场景,通过交互提示能协助快速定位排查问题。

{
    // 设备名称
    "deviceName":"sunmi",
    // 额外信息
    "extra":"",
    // 当前设备的连接状态
    "state":0,
    "error":{
        // 打印机异常状态码
        "code":1,
        // 打印机异常信息详情
        "message":"打印机缺纸/打印机离线/打印机断开"
    }
}
1.1.3 Library 层

部分设备连接需要依赖硬件厂商提供的 sdk , 且不同分类的设备可能共享该 sdk , 这类的sdk可以放到 Library 进行管理,避免设备重复依赖。

库简介:

library名称 功能介绍
woyou.aidlservice 商米打印与称重 aidl 接口
sprtprintersdk 思普瑞特打印能力
paymentService 商米 P1 刷卡 sdk
cloudpossdk、wangpossdk WANGPOS 刷卡打印能力
1.1.4 Base 层

提供最基础的能力,包括网络请求、log 埋点等。

1.2 硬件库实现细节

1.2.1 打印机

零售对接的打印设备非常多,包括蓝牙、usb 、http 等,原有的设计中打印机与 pos 、电子秤功能聚合在一起,功能耦合严重,不同的硬件开发人员都会改动设备库的代码,导致 sdk 频繁发版,违背开闭原则,设备库稳定性也无法得到保证。需要将通用能力抽出来,包括连接能力、打印能力、协议封装能力等,确保新的设备能够快速接入。

解决方案

UML :

技术细节描述:

PrinterManager 暴露相应的 api 给业务方调用,DeviceCoreManager 提供 Core 通用能力(包含缓存能力、连接能力、线程切换能力等)并作为 PrinterManager 的成员变量,所有的打印机实体继承 AbsPrinter 基类(实现一些基本信息,以及相关方法做了默认实现),AbsPrinter 又实现 IPrinter 接口,IPrinter 继而又继承 IDevice 接口,同时部分打印机又可以打开钱箱,需要实现 IMoneyBox 接口。

IPrinter :

interface IPrinter : IDevice {  
    ... ...
    /**
     * 设备纸张类型
     *
     * @return
     */
    fun getPagerType(): PagerType

    /**
     * 获取设备协议
     *
     * @return
     */
    fun getProtocol(): Protocol


    /**
     * 打印内容
     *
     */
    fun print(content: ByteArray): PrinterResponse

    /**
     * 打印内容,附加一些信息
     *
     */
    fun print(content: ByteArray, extraInfo: String?): PrinterResponse

    /**
     * js DeviceName
     * @return
     */
    fun jsDeviceName(): String

    fun isSupportJSPrinter(): Boolean
    ... ...
}
1.2.2 POS 机

零售开发早期,开发了独立的 POS 收银台,直接访问第三方支付公司(通联等)提供的刷卡接口,且针对 8583 协议(8583协议介绍)进行自定义封装,代码复杂度与维护成本很高,在线上运行一段时间后,发现接口不太稳定,商家经常出现刷卡不成功问题。后期与 POS 厂商沟通后,直接对接了 POS 厂商提供的刷卡 sdk, 刷卡稳定性得到了提升,但是从设备库设计来说还是要兼容自建收银台功能,目前还有部分商家在使用老的刷卡方式能力,不能贸然迁移。

零售 POS 对接现状:

交易模块、订单模块、储值模块、支付模块都有使用过刷卡能力,但是各自调用的 sdk 不尽相同,包括 ecosy、zanpay、pos_pay_sdk 等,开发与维护成本很高

解决方案

UML :

技术细节描述:

PosManager 暴露相应的 api 给业务方调用,DeviceCoreManager 提供 Core 通用能力(连接能力、线程切换能力等)并作为 PosManager 的成员变量,所有的 POS 机实体继承 AbsCashier 基类(实现一些基本信息,以及相关方法做了默认实现),AbsCashier 又实现 IPos 接口,同时 IPos 继承 IDevice 接口。AbsPrinter 会维护 PosChainTaskList 队列,分别对应 POS 中签到、收单、支付、上报流程。这些 Task 业务方需要注入并做接口实现,底层只会维护调用链路,不关心业务 Task 的执行内容。

IPos :

interface IPos : IDevice {

    /**
     * 刷卡支付
     *
     */
    fun payByPos(entity: PhoinexPosPayResult): Observable<PhoinexPosPayResult>

    /**
     * 取消支付
     *
     * @param orderNo
     * @param voucherNo
     */
    fun revoke(orderNo: String, voucherNo: String): Observable<Any>

    /**
     * 退款
     *
     * @param orderNo
     */
    fun refund(orderNo: String): Observable<Any>
}
1.2.3 电子秤

电子秤提供的能力比较简单,IWeight 提供去皮、置零等能力,电子秤的读取通过 CallableData (类似参考LiveData实现)进行 postValue 分发,同时 WeightManager 提供了设备基本的增删改查能力。

解决方案

UML :

技术细节描述:

WeightManager 暴露相应的 api 给业务方调用,Weight 相对比较简单,所有的电子秤都实现 IWeight 接口,IWeight 集成 IDevice 接口,同时 DeviceCoreManager 为电子秤提供底层能力(读写能力、连接能力、缓存能力等)支持,电子秤部分是串口通信,需要实现 UsbReceiver 广播监听 usb 线的插拔状态。

IWeight :

interface IWeight: IDevice {

    // 去皮
    fun doTare(): Pair<Boolean, String>

    // 置零
    fun fun doZero(): Pair<Boolean, String>

}

1.3 灰度上线方案

硬件重构相当于推倒重来,如此大的改动上线必须要稳,故此采用 AB Test 进行灰度,一部分商家继续使用老 sdk ,一部分商家使用新 sdk ,新 sdk 进行数据异常埋点,当检测到新的设备库出现问题后,配置中心操作,使用新 sdk 的商家收银机会立即回滚到老设备库。

方案图

灰度工具: AB-Test

二、打印机协议统一

移动团队配合硬件支持同学根据商家需求适配对接了十几款市面上口碑与稳定性较高的打印机设备,包括 365 、佳博、映美云、思普瑞特、飞蛾等品牌,且技术上适配了 usb 、蓝牙、 wifi 等多种连接方式,为商家硬件选配提供了多样性选择。团队对接打印机的过程中投入了大量的人力支持,也踩了不少坑,同时新设备的对接效率始终比较低,且稳定性不够,商家经常反馈一些连接与打印问题,开发人员的自我成就不高,且对商家的经营场景造成了影响。在技术侧特别是打印机协议适配涉及到多端参与( Android 、iOS 、前端等),重复造轮子的同时,也很难保证协议解析的稳定性与统一性,为了降低多端打印协议适配成本,痛定思痛,技术上利用 js 作为桥接层对打印协议进行统一解析预处理,业务方只需要根据一定格式(类似于 html )输入打印内容,js 层会针对打印内容映射为打印协议,且该方案支持跨平台与动态化,目前零售所有的打印业务都是通过这种方式进行适配,稳定性得到了保障,且维护成本也被极大的降低,详细技术方案请看这两篇文章。(有赞零售小票打印跨平台解决方案有赞零售跨平台打印库方案

架构图

PC、Android、iOS 将打印内容输入到 JsCore , JsCore 解析匹配打印数据,适配成特定的打印协议( ESC / POS 等),端获取到打印协议后,将打印协议输入给打印机,打印机读取到协议数据后进行打印,且 JsCore 可通过后端配置中心进行动态下发,实时修复问题,无需重发版。

2.1 举例:打印电子发票

2.1.1 小票模板编辑

每个小票都可在后台配置小票模板,对小票的基本信息、商品信息、支付信息、买家信息、其他信息进行编辑,且编辑后之后可以实时预览,小票模板编辑完成后,有赞零售 app 启动后会拉取小票模板数据,存在本地,当下次触发小票打印任务时,会将本地模板数据与打印数据进行结合,传入到 JsCore 中,输出打印协议,传输到打印机中进行打印。

  • 小票模板配置样式

  • 小票模板预览样式

  • 小票模板配置源代码
<html><head></head>  
<body>  
<p style="font-size:24px;text-align:center;">{{shopName}}</p>  
<p style="font-size:24px;text-align:center;">电子发票自助开票</p><br>  
<p>订单号:{{orderNo}}</p>  
<p>订单时间:{{createTime}}</p>  
<p>店铺名称:{{shopName}}</p><br>  
<p>电子发票开票日期同申请电子发票的日期</p><br>  
<p>建议您在消费后{{timeScopeStr}}扫码开具发票,超过建议时间后无法开票请联系商家,服务电话:{{shopPhone}}</p>  
<qrcode style="text-align:center;">{{invoiceUrl}}</qrcode>  
<p style="text-align:center;">微信扫码开具电子发票</p>  
</body>  
</html>  
2.1.2 小票打印内容:

小票进行打印时,实时从后端拉取打印内容

{"shopName":"有赞的店", "orderNo":"12345xxx", "createTime":"2020/07/01-11:00", "timeScopeStr":"12345xxx", "invoiceUrl":"http://xxxxxx"}
2.1.3 JsCore执行流程:

将小票打印内容与打印模板数据传入到 JsCore 中,js 会将模板进行填充(打印模板中{{ key }}与打印内容的 value 映射匹配起来),jsCore 解析 html 样式,翻译成相应的打印协议( ESC / POS 、三方打印机自定义打印协议等)

2.1.4 JsCore封装打印协议优势:
  1. 多端打印协议解析逻辑统一,节省人员投入成本
  2. js 可动态下发,动态修复线上问题,无需发版
  3. jsCore 单端维护,开发与维护成本非常低

三、副屏布局插件化改造

商家在使用有赞零售进行收银过程中每天都会进行开单操作,开单完成后大部分顾客都想实时感知自己买了哪些商品、结算了多少钱、享受了多少优惠,为了保证交易透明,零售开发了副屏功能,支持将购物车商品列表、会员信息、支付信息、营销结算等信息实时投影到副屏上,同时支持闲时、忙时动态配置切换,商家可在pc后台编辑广告图片与视频资源,投放到有赞零售app副屏上,起到广告宣传作用。

副屏内容编辑后台

副屏开发过程中也磕磕绊绊,踩过不少坑,比如副屏的连接稳定性、View 的布局绘制性能、图片内存占用被打爆问题,且副屏的架构设计也经历了几次迭代,现在功能趋于稳定,业务方可以灵活定制自己的插件,注册到副屏模块中,模块底层识别插件,按照一定的规则进行渲染展示。在保证高扩展性的同时,也降低了接入成本。

UML

SubMainManager 作为副屏初始化入口,在 App Application 初始化的时候被调用,目前实现了 Sunmi7Manager(商米 7 寸,AIDL 通信)、Sunmi14Manager (商米 14 寸, AIDL 通信)、SunmiT2AclasManager (商米 T2 等设备,presentation 通信)等设备,
通过 SubEntity 对象与副屏进行通信,业务方可自定义 Plugin( Plugin 内自定义业务需要显示的 View ),发送到相应设备的副屏 Manager ,副屏 Manager 最终会调用 SubTemplateManger 对 SubEntity 进行解析,将业务 Plugin (反射构造 Plugin 实体)提供的 LayoutId 解析成相应的 View ,添加到副屏上进行渲染投屏。

3.1 SubEntity

主机通过 SubEntity 与副屏进行通信,可定义具体的 action 、 jsonData (业务方自定义数据)、 leftPlugin (副屏左边屏幕显示的插件,内容为 plugin 实体的 className )、 rightPlugin (副屏右边屏幕显示的插件,内容为 plugin 实体的 className )等。

@Keep
public class SubEntity {  
    ... ...
    @SerializedName("action")
    public int action;

    @SerializedName("title")
    public String title;

    @SerializedName("sub_setting")
    public String subSetting;

    @SerializedName("templateName")
    public String templateName;

    /**
     * 统一数据
     * (如果为数据结构,建议请自行通过json解析与反解析,
     *  原则上只通过此字符串交流,如果要处理数据,请自行序列化,并且自行解析。
     *  本质上仅为字符串,由使用方充分使用即可)
     */
    @SerializedName("jsonData")
    @Nullable public String jsonData;

    @SerializedName("leftPlugin")
    public String leftPlugin;

    @SerializedName("rightPlugin")
    public String rightPlugin;
    ... ...

}

3.2 SubPlugin

每个业务实现自己的业务插件,插件中包含 LayoutId, SubTemplateManager 会解析 Plugin 数据,将插件提供的 LayoutId Inflate 成 View 渲染在副屏上

副屏样式实例:

public abstract class SubPlugin {

    private Context context;

    public SubPlugin(Context mContext) {
        context = mContext;
    }

    public abstract int getLayout();

    public abstract void createView(View view, Bundle bundle);

    public abstract void updateView(Bundle bundle);

    public abstract View getView();

    @Nullable
    public abstract View getView(LayoutInflater inflater, @Nullable ViewGroup container);
}

3.3 SubTemplateManager 解析 Plugin 过程

3.3.1 通过 Plugin ClassName 反射构造 Plugin 实体
@Nullable
    public static SubPlugin getPlugin(Context context, String className){
        if(TextUtils.isEmpty(className)){
            return null;
        }

        SubPlugin plugin = null;
        try {
            plugin = (SubPlugin) Class.forName(className)
                .getConstructor(Context.class).newInstance(context);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return plugin;
    }
3.3.2 解析 Plugin layoutId 字段 inflate 成 View 布局,并将 View 渲染到副屏上
... ...
if (null != leftPlugin) {  
    View left = LayoutInflater.from(mContext).inflate(
        leftPlugin.getLayout(), leftView, false);
    leftPlugin.createView(left, bundle);
    leftView.removeAllViews();
    leftView.addView(leftPlugin.getView());
}

if (null != rightPlugin) {  
    View right = LayoutInflater.from(mContext).inflate(
        rightPlugin.getLayout(), rightView, false);
    rightPlugin.createView(right, bundle);
    rightView.removeAllViews();
    rightView.addView(rightPlugin.getView());
}
... ...

3.4 踩过的坑

3.4.1 副屏存储空间有限,容易被充满,导致副屏功能不可用

开启 Timer 4 小时检查一次副屏,当副屏可用存储空间小于总空间的 30 %,主动清空副屏磁盘

 mConsumer = new Consumer<Long>() {
            @Override
            public void accept(Long aLong) throws Exception {
                checkCache();
                checkMainStorage(mContext);
            }
        };

// 默认是4小时检查一次缓存
int CHECK_CACHE_TIMER = 4;  
mFlowable = Flowable.interval(CHECK_CACHE_TIMER, TimeUnit.HOURS)  
        .subscribeOn(Schedulers.io())
        .doOnError(new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {
                throwable.printStackTrace();
            }
        });
3.4.2 商米 T1 副屏调试困难,前期开发过程中采用打 log 方式调试,效率非常低

由于商米 T1 主副屏通过 usb 进行连接,当 pc 电脑插上 usb 后,pc 电脑将被认为是主设备,而收银机则成为从设备,收银机主副屏的连接将断开。可以通过 adb connect 方式进行调试,adb connect 192.168.xx:5555 连接主屏, adb connect 192.168.xx:5554 连接副屏, ip地址为收银机的 ip 地址。

原理图:

IoT

硬件问题排查过程非常痛苦,商家的网络环境、设备连接状况、外接设备类型这些关键信息总是无法及时收集,且商家反馈内容经过服务同学、技术支持等一层层上来后,信息容易失真,从而造成问题排查成本非常高。零售提供 IoT 解决方案,商家所有外接设备全部上云,当商家设备出现问题,可以通过后台数据及时拉取商家实时的设备状态,协助快速排查问题。

  • 后台:

后台可以采集设备的类型、名称、型号、连接状态等信息。

  • 客户端对接IoT流程:

设备 sdk 检测到设备状态变更后将设备状态及时同步到 IoT 后台,同时后台可以对设备进行远程解绑、删除等操作。

提效数据统计

新硬件开发成本降低50%

随着设备不断重构优化迭代,新设备接入时间成本减少了50%,团队开发提效不少

2019年新设备接入日常排期:

2020年新设备接入日常排期:

硬件线上问题数量降低33-55%

2019年下半年与2020年上半年开发同学处理问题总数环比下降55%,技术支持同学处理问题总数环比下降33%。

总结

硬件在零售业务发展中起到非常重要的作用,每天支撑商家数以万计的小票打印、刷卡支付、人脸采集、称重、副屏展示等各个流程,始终为商家门店经营保驾护航。然而开发硬件的历程也经历坎坷、备受挫折,需要足够的延迟满足感。团队一直秉承追求卓越,守护信任的原则,一次次的优化重构,为每次硬件的完美交互做出了最大努力,且后续还会加油持续做的更好。

未来展望

  1. 打造与完善 IoT 平台,将硬件解决方案推广到全公司,供其他业务方灵活接入。
  2. 提供硬件对接开放接口,供第三方接入,比方说很多商家有自己的设备,零售没有覆盖到,商家可以对接开放接口完成设备的接入流程。
  3. 开发硬件自检助手,帮助商家自己解决问题,节省开发排查问题的成本。
  4. 设备同一种分类内再做粒度细分(例如可单独选择几款打印机进行依赖),提高业务对接灵活性。

有赞零售移动团队 Slogan :

打造极致好用各方面业界最 NB 的移动端产品,对于每一行代码,我们都追求卓越、不随意、不凑合。

目前团队 Android 与 iOS 岗位还有空缺,欢迎优秀的人才加入我们的团队,一起搞事情。

内推邮箱地址: hongentao@youzan.com

参考文档:

有赞零售跨平台打印库方案

有赞零售小票打印跨平台解决方案

Android -模块化-面向接口编程

商米开发者文档

WANGPOS 开发者文档

8583 协议

串口通信开源库

ESC / POS 协议

欢迎关注我们的公众号