自动草稿

特殊代码说明

一、获取商米设备标识

商米建议通过获取到以下内容来判断是否商米设备:

1.设备的品牌名 brand(如:SUNMI)

    商米的品牌名统一为 SUNMI

2.设备的系统型号 model(如:V1-B18)

    系统型号组成为 产品型号+硬件特性+‘-’+软件特性

    其中以V、M、P、L开头为手持设备,以T、D、S开头为横屏设备(截至2017年12月) 

3.设备的ROM版本号(如:1.1.0)。

4.设备的ROM顺序号(如:128)。

可以下载Demo,仿照Demo在自己项目src下面新建android.os包(固定写法),将SystemProperties.java放入该包下,按以下方法获取指定的值:

获取brand的代码为:

    String brand = SystemProperties.get(“ro.product.brand”);

获取model的方法为:

    String model = SystemProperties.get(“ro.product.model”);

获取ROM版本号的代码为:

    String versionname = SystemProperties.get(“ro.version.sunmi_versionname”);

获取ROM顺序号的方法为:

    String versioncode = SystemProperties.get(“ro.version.sunmi_versioncode”);

 二、获取设备的SN号

1.在AndroidManifest.xml中添加如下权限。

2.在需要的地方用以下代码获取商米SN号。

public static String getSN() {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            return Build.getSerial();

        } else

            return Build.SERIAL;

    }

三、隐藏及恢复底部导航栏

(注:商米新开发了Kiosk霸屏模式,APP不用做任何修改,仅通过云端设置即可实现隐藏状态栏、导航栏并无法通过手势唤出。   该功能在T2、K1设备上已实现,其他设备上线进展请联系技术支持400-666-6509(00:00~24:00,7*24小时服务)。   建议合作伙伴使用商米Kiosk霸屏模式,已获得更好的体验)

Android系统默认提供了隐藏系统的导航栏的方法,但对于Dialog的支持较差,导致全屏对话框打开时先弹出导航栏再隐藏(闪屏),SunmiOS针对此问题进行了修复(V1系统固件版本252后支持,T1系统固件版本132后支持)

1.Activity的全屏化

——安卓默认支持

public class MainActivity extends AppCompatActivity {

    @Override

    public void onWindowFocusChanged(boolean hasFocus) {

        super.onWindowFocusChanged(hasFocus);

        SystemUIUtils.setStickFullScreen(getWindow().getDecorView());

    }

}

public class SystemUIUtils {

    public static void setStickFullScreen(View view) {

        int systemUiVisibility = view.getSystemUiVisibility();

        int flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE

                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar

                | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar

                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;

        systemUiVisibility |= flags;

        view.setSystemUiVisibility(systemUiVisibility);

    }

}

2.Dialog的全屏化

——原生系统下 AOSP 的Bug会导致全屏对话框打开时先弹出导航栏然后再隐藏导航栏(闪屏)。

public AlertDialog create(boolean fullscreen) {

    LayoutInflater inflater = LayoutInflater.from(context);

    final AlertDialog dialog = new AlertDialog(context,

            R.style.DialogStyle);

    if(fullscreen){

        SystemUIUtils.setStickFullScreen(dialog.getWindow().getDecorView());

    }

}

3.设置隐藏后显示导航栏

3.1.全局底部上划

——上划后底部导航栏显示4s,4s后底部导航栏隐藏

3.2.切换应用至其他APP(如APP内跳转至第三方应用、第三方APP弹窗等)

——切换至其他APP是否显示底部导航栏以第三方应用要求为准,切换至自己APP时底部导航栏消失

四、避免重复申请外设权限

当APP需要通过USB关联外设来实现业务时(比如连接USB打印机打印小票),安卓要求用户手动确认设备使用权限,来保障用户信息安全,防止木马非法入侵USB设备。

1.如何避免USB设备重新插拔同一个外设时,APP反复申请该外设权限

需要用户手动确认时,勾选“默认情况下用于该USB设备”,无法通过代码绕过该安全机制

验证DEMO

2.如何避免设备重启后,已勾选“默认情况下用于该USB设备”的APP仍反复申请同一个外设的使用权限

2.1.在APP的AndroidManifest中指定某个Activity部分中,添加如下代码

                

2.2.在该目录下创建xml文档

    

其中,class的值需要和要连接的外设类型一致,外设class参照表如下

    /**

     * USB class indicating that the class is determined on a per-interface basis.

     */

    public static final int USB_CLASS_PER_INTERFACE = 0;

    /**

     * USB class for audio devices.

     */

    public static final int USB_CLASS_AUDIO = 1;

    /**

     * USB class for communication devices.

     */

    public static final int USB_CLASS_COMM = 2;

    /**

     * USB class for human interface devices (for example, mice and keyboards).

     */

    public static final int USB_CLASS_HID = 3;

    /**

     * USB class for physical devices.

     */

    public static final int USB_CLASS_PHYSICA = 5;

    /**

     * USB class for still image devices (digital cameras).

     */

    public static final int USB_CLASS_STILL_IMAGE = 6;

    /**

     * USB class for printers.

     */

    public static final int USB_CLASS_PRINTER = 7;

    /**

     * USB class for mass storage devices.

     */

    public static final int USB_CLASS_MASS_STORAGE = 8;

    /**

     * USB class for USB hubs.

     */

    public static final int USB_CLASS_HUB = 9;

    /**

     * USB class for CDC devices (communications device class).

     */

    public static final int USB_CLASS_CDC_DATA = 0x0a;

    /**

     * USB class for content smart card devices.

     */

    public static final int USB_CLASS_CSCID = 0x0b;

    /**

     * USB class for content security devices.

     */

    public static final int USB_CLASS_CONTENT_SEC = 0x0d;

    /**

     * USB class for video devices.

     */

    public static final int USB_CLASS_VIDEO = 0x0e;

    /**

     * USB class for wireless controller devices.

     */

    public static final int USB_CLASS_WIRELESS_CONTROLLER = 0xe0;

    /**

     * USB class for wireless miscellaneous devices.

     */

    public static final int USB_CLASS_MISC = 0xef;

    /**

     * Application specific USB class.

     */

    public static final int USB_CLASS_APP_SPEC = 0xfe;

    /**

     * Vendor specific USB class.

     */

    public static final int USB_CLASS_VENDOR_SPEC = 0xff;

3.如何避免前两步后设置后,业务页面会因USB设备插入而刷新

安卓原生逻辑导致用户选择“默认情况下用于该USB设备”会导致USB设备插入时打开某个指定activity。如果要避免页面刷新导致业务中断,可以增加如下代码防止页面刷新。

 五、如何避免自己的应用数据被清除

应用数据默认是可以通过系统设置删除的,删除后应用将恢复刚安装的状态。但是可以通过配置APP来精细管理应用数据(比如按照业务分类或时间删除数据),也可以避免重要的应用数据被删除。

在程序的manifest文件的application中加上manageSpaceActivity属性,并且指定一个Activity,这个Activity就是点击管理空间之后会跳转的那个Activity了。

 

    

 

PS 如果要避免数据被删除,可以创建一个自动关闭的Activity。

public class ManageSpaceActivity extends Activity {  

    @Override  

    protected void onCreate(Bundle savedInstanceState) {  

        super.onCreate(savedInstanceState);  

        finish();  

    }// onCreate  

}  

六、如何避免插入usb外设导致app界面闪烁

在APP的AndroidManifest中,添加如下代码

android:configChanges=”navigation|keyboardHidden|keyboard”

七、如何避免扫描枪扫出内容与实际不一致

一般情况下,文本框输入的所有内容会提交给输入法进行转换(如拼音、联想等),有的输入法经过转换后会转义部分字词导致输入内容不一致。

除了更换系统输入法为合适的输入法外,还可以利用输入法通常不对密码框进行处理的规则,调整文本框类型为“可见密码”(inputType),并且约定好可输入规范(digits)也可以避免该问题

调试设备说明

关于商米设备调试

ADB权限申请流程:在FT1mini 在设置->开发者选项->开发者指南->输入渠道绑定的手机号->获取验证码->填入收到的验证码->授权后就可以adb调试了

(本教程仅适用于非金融设备,如需要开启Sunmi P系列设备的调试权限,请联系销售人员)

默认商米的设备插上USB线就可以调试,商米也提供了设备调试权限的控制功能,合作伙伴在后台开启调试权限控制后(如下图所示),则需要通过邮箱或手机号获取调试权限才能调试该设备。如果不能直接调试,请查看合作伙伴后台是否开启了该权限。

注:调试权限的控制只针对绑定了合作伙伴(渠道)的设备生效。

启用调试权限控制后如何调试设备

如果合作伙伴启用了’调试权限控制’,则开发者需要在旗下设备上通过邮箱和手机号获取调试权限才能调试设备,合作伙伴可以在商米合作伙伴平台添加调试员(开发者)的手机号或邮箱。以下为获取设备调试权限的歩骤:

1.添加调试员。

开发者调试设备之前请了解设备所属的渠道,可以找自己公司相关的管理人员询问。管理人员需要在商米的合作伙伴平台后台添加调试员的手机号或邮箱。

2

2.插入USB(V1s、V2、M2、L2建议暂时通过关闭云端调试保护开关来调试设备)

确保自己有调试权限的手机号或邮箱后,将设备连接电脑,建议开发者在windows下调试,如果手机能被电脑正确识别,通常会出现如下的弹窗提示:

3

如果设备没有被电脑识别,请确认是否因以下原因引起。

  • 接触不良,请多次插拔扭动USB接口确认。
  • 数据线故障,换条数据线看下能不能识别。
  • 电脑没有安装移动设备驱动,可以使用第三方工具软件安装。

3.获取验证码。

点击上面的“我要调试”项,将进入手机号或邮箱验证权限步骤,同时设备的USB调试模式将自定开启(这里是指基础的调试模式,不是权限);点击“知道了”将退出弹窗,不会开启USB调试模式。

4

4.验证权限

输入之前添加的手机号或邮箱后点击’获取验证码’,商米将向手机号或邮箱发送验证码,填入验证码点击’授权并开启调试’。

5

5.打开权限。

打开权限后,可以查看logcat中有无输出判断是否可以调试设备。

6

自测平台使用手册

我们希望软件商可以通过自测平台可以自助完成软件授权开户对接,自测平台提供以下功能:
1.自动生成开发者Key;
2.自助提交Test环境配置;
3.自助完成接口调试(自测用例);
4.自助完成正式环境配置提交。

使用说明

1.登录软件商自测平台

软件商确认对接后,商米工作人员会发送自测平台地址。

如果没有账号,请注册开发者账号。

2.申请开发者key,提交测试环境配置

1)自动生成开发者KEY,开发者KEY生成后不可修改。
2)需要填写应用名称,选择是否支持修改省市区,以及接口地址配置。
3)填写并确认无误后,点击导航【接口调试】进入调试流程。

3.接口调试

1)点击请求接口下拉列表,选择对应的接口进行调试。
2)可以通过手动填写或json参数填写业务参数。
3)根据接口测试用例进行测试。
4)目前接口调试只支v1.9版本新版接口,老接口通过【旧版接口调试】进行联调。

4.提交正式环境配置

1)联调完成后,可以提交正式环境配置。
2)正式环境的配置地址可以跟测试环境不一样,需要重新提交。
3)提交配置后,运营人员会进行购买测试(在商米UAT环境和软件商正式环境中进行购买测试)
4)购买测试通过后,商米运营人员会进行正式环境上线。

商米AI识客SDK代码示例

  1. SDK 初始化
  2. 获取摄像头预览信息
  3. 获取摄像头拍照数据
  4. 获取特征值
  5. 人脸特征值比较
  6. 删除特征记录
  7. 会员库添加
  8. 会员注册
  9. IPC 回调接口

1. SDK初始化

public boolean initFaceSdkInstance(Context context, String licencePath) {
         //初始化配置文件
        int ret = SunmiFaceSDK.init(confPath);
        if (licencePath == null) {
            Log.d(TAG, "Please input licence");
            return false;
        }
        File f = new File(licencePath);
        if (!f.exists()) {
            Log.d(TAG, "licence is not exist");
            return false;
        }
        String licence = readToString(licencePath);
        //验证Licence
        ret = SunmiFaceSDK.verifyLicence(context, licence);
        if (ret != 0) {
            Log.d(TAG, "Licence is not OK ErrorCode " + ret);
            return false;
        }
        //设置配置参数
        SunmiFaceConfigParam param = new SunmiFaceConfigParam();
        SunmiFaceSDK.getConfig(param);
        param.setDistance_threshold_(1.3f);
        param.setYaw_threshold_(50.0f);
        param.setPitch_threshold_(50.0f);
        param.setRoll_threshold_(50.0f);
        param.setMin_face_size(60);
        param.setImage_quality_threshold_(10);
        param.setMin_luminance_(10);
        param.setMax_luminance_(180);
        param.setLiveness_on_(false);
        param.setThread_num_(1);
        //设置配置参数
        ret = SunmiFaceSDK.setConfig(param);

        if (ret != 0) {
            Log.d(TAG, "param is not init");
            return false;
        }
        return true;
    }

public void init(Context context) {
      //设置licence_valid.txt为sd卡路径,也可以换成app运行工作目录。
      String licencePath = Environment.getExternalStorageDirectory() + File.separator + "licence_valid.txt";
      initFaceSdkInstance(context, licence_path);
     // 调用IPCameraManager静态类getInstance初始化IPCManger对象
     mIPCManager = IPCameraManager.getInstance(context);
}

2. 获取摄像头预览信息

//摄像头数据回调接口	
CameraDataCallback cameraDataCallback = new CameraDataCallback() {
    @Override
    public void onImageDataArrival(Bitmap bitmap, int width, int height) {
        if (mBackgroundHandler != null) {
            if (mClickAction == 1 && !faceDetectState.get()) {
                mBackgroundHandler.post(new MotionDetector(bitmap, width, height));
            } else {
                bitmap.recycle();
            }
        }

    }
};
//开启摄像头预览
FaceCameraManager.getInstance().startPreview(getApplicationContext(), this, FaceCameraManager.getInstance().getCurCamera(), cameraView, PREVIEW_WIDTH, PREVIEW_HEIGHT, cameraDataCallback);

3. 获取摄像头拍照数据

{
//根据摄像头是USB还是系统摄像头,调用不同的方法,获取bitmap数据
if (FaceCameraManager.getInstance().getCurCamera() == FaceCameraManager.CAMERA_USB)
    bitmap = mUVCCameraView.captureStillImage();
else
    bitmap = mTextureView.getBitmap();

}

4. 获取特征值

说明:通过bitmap获取特征值

    public ArrayList‹SunmiFaceFeature› getFeatures(Bitmap bitmap, int maxFace) {

        int ret = 0;
        //bitmap的RGB数据转化成BGR数据
        byte[] srcData = ImageUtils.getPixelsBGR(bitmap);
        //通过BGR数据构造SunmifaceImage对象
        SunmiFaceImage image = new SunmiFaceImage(srcData, bitmap.getHeight(), bitmap.getWidth(), maxFace);
        SunmiFaceImageFeatures features = new SunmiFaceImageFeatures();
        //从SunmifaceImage数据中提取人脸特征
        ret = SunmiFaceSDK.getImageFeatures(image, features);
        //返回人脸特征数组
        SunmiFaceFeature feature_ary = features.getFeatures_();
        ArrayList‹SunmiFaceFeature› arrayList = new ArrayList‹›();
        if (features.getFeatures_count_() == 0) {
            SunmiFaceSDK.releaseImageFeatures(features);
            return arrayList;
        }

        for (int i = 0; i ‹ features.getFeatures_count_(); i++) {
            SunmiFaceFeature sunmiFaceFeature = SunmiFaceLib.SunmiFaceFeatureArray_getitem(feature_ary, i);
            arrayList.add(sunmiFaceFeature);
        }
        SunmiFaceSDK.releaseImageFeatures(features);
        return arrayList;
        // return getFeature(srcData, bitmap.getWidth(),bitmap.getHeight(), 1);
    }

5. 人脸特征值比较

    public SunmiFaceCompareResult compareFeature1V1(float similar, float[] feature1, float[] feature2) {
        //构造SunmiFaceFeature对象
        SunmiFaceFeature feat1 = new SunmiFaceFeature();
        SunmiFaceFeature feat2 = new SunmiFaceFeature();
        feat1.setFeature_(feature1);
        feat2.setFeature_(feature2);
        SunmiFaceCompareResult result = new SunmiFaceCompareResult();
        //调用compare1v1进行比较
        int ret = SunmiFaceSDK.compare1v1(feat1, feat2, result);
        return result;
    }

6. 会员删除

说明: 删除会员信息时,需要把删除的人脸会员信息同步到IPC。向IPC传递用户的唯一识别ID

{
//删除人脸库记录
int result = SunmiFaceSDK.deleteDBRecord(user.getImageName());
//同步删除IPC人脸记录
if (mIPCManager != null) {
    mIPCManager.deleteFaceRecord("GVIP", userId, new RPCCallback‹RPCResponse‹RPCResponse.FaceDeleteSubResult››() {
         //IPC人脸删除记录完成后回调接口
        @Override
        public void onComplete(RPCResponse‹RPCResponse.FaceDeleteSubResult› result) {
            Log.i(TAG, "deleteFaceRecord, code:" + result.code());
        }
    });
}
}

7. 人脸会员库添加

说明: 添加人脸时,同时要向IPC注册人脸信息。向IPC传递信息包括:人脸会员库(GVIP),人脸照片、人脸会员唯一识别ID。

{
        //使用faceFeature构建record
        SunmiFaceDBRecord record =     SunmiFaceSDK.faceFeature2FaceDBRecord(faceFeature);
        record.setId_(user.getUserId());
        record.setName_(user.getUserName());
        //把record添加到人脸库
        int ret = SunmiFaceSDK.addDBRecord(record);
        //把人脸照片传递给IPC,向IPC添加人脸记录   
         if (mIPCManager != null) {
                    mIPCManager.addFaceRecord("GVIP", picPath, uid, new RPCCallback‹RPCResponse›() {
                        //IPC人脸添加记录完成后回调接口
                        @Override
                        public void onComplete(RPCResponse result) {
                            Log.i(TAG, "addFaceRecord, code:" + result.code());
                        }
                        //IPC人脸添加记录失败时回调接口
                        @Override
                        public void onAbort(int httpStatus) {
                            Log.i(TAG, "addFaceRecord, httpStatus:" + httpStatus);
                        }
                        //IPC人脸添加记录异常时回调接口
                        @Override
                        public void onException(Throwable t) {
                            Log.i(TAG, "addFaceRecord, Exception:" + t.getMessage());
                        }
                    });
                }

}

8 会员注册

/*
 *向系统注册会员信息
 *@groupname, 会员组
 *@userNanem, 会员名
 *@picPath,   会员照片存放路劲
 *@userInfo,  会员详情
 *@faceFeature, 会员照片提取的特征值
 */
public boolean registerUserIntoDBmanager(String groupName, String userName, String picPath, String userInfo, SunmiFaceFeature faceFeature) {
        boolean isSuccess = false;
        Group group = new Group();
        group.setGroupId(groupName);
        //创建会员
        User user = new User();
        user.setGroupId(groupName);
        //final String uid = UUID.randomUUID().toString();
        String uid = String.valueOf(System.currentTimeMillis());
        user.setUserId(uid);
        user.setUserName(userName);
        user.setFeature(faceFeature.getFeature_());
        //创建人脸库记录信息
        SunmiFaceDBRecord record = SunmiFaceSDK.faceFeature2FaceDBRecord(faceFeature);
        record.setId_(user.getUserId());
        record.setName_(user.getUserName());
        //像人脸库添加记录
        int ret = SunmiFaceSDK.addDBRecord(record);
        if (ret != 0) {
            Log.d(TAG, "addDBRecord failed " + SunmiFaceSDK.getErrorString(ret));
            return false;
        }
        //在会员数据库中记录,特征值对应的图片在人脸数据中的ID
        user.setImageName(record.getImg_id_());
        if (userInfo != null) {
            user.setUserInfo(userInfo);
        }
        // 添加用户信息到数据库
        boolean importUserSuccess = FaceManager.getInstance().userAdd(user);
        if (importUserSuccess) {
            // 如果添加到数据库成功,则添加用户组信息到数据库
            // 如果当前图片组名和上一张图片组名相同,则不添加数据库到组表
            if (FaceManager.getInstance().groupAdd(group)) {
                isSuccess = true;
                if (mIPCManager != null) {
                    Log.i(TAG, "picPath: " + picPath);
                    Log.i(TAG, "uid: " + uid);
                    //把会员照片同步发送到IPC,同步会员信息到IPC
                    mIPCManager.addFaceRecord("GVIP", picPath, uid, new RPCCallback‹RPCResponse›() {
                        @Override
                        public void onComplete(RPCResponse result) {
                            Log.i(TAG, "addFaceRecord, code:" + result.code());
                        }

                        @Override
                        public void onAbort(int httpStatus) {
                            Log.i(TAG, "addFaceRecord, httpStatus:" + httpStatus);
                        }

                        @Override
                        public void onException(Throwable t) {
                            Log.i(TAG, "addFaceRecord, Exception:" + t.getMessage());
                        }
                    });
                }
            } else {
                isSuccess = false;
            }

        } else {
            isSuccess = false;
        }
        return isSuccess;
    }

9. IPC 回调接口

说明:用户获取IPC的状态、返回信息等。

{
                    //获取IPC信息
                    IPCameraManager manager = IPCameraManager.getInstance(getApplicationContext());
                    //IPC初始化
                    manager.init("test", "123456", "123456");
                    //注册IPC回调
                    manager.registerListener(new IPCameraListener() {
                        //IPC设备在线时回调接口
                        @Override
                        public void onDeviceOnline(IPCameraInfo device) {
                            showToast("[ " + device.getDeviceid() + " ]上线");
                        }
                        //IPC设备离线时回调接口
                        @Override
                        public void onDeviceOffline(IPCameraInfo device) {
                            showToast("[ " + device.getDeviceid() + " ]离线");
                        }
                        //IPC设备获取到进店信息后回调接口
                        @Override
                        public void onFaceDetect(String userId) {
                            String userName = null;
                            List‹User› users = DBManager.getInstance().queryUserByUserId(userId);
                            if (users != null && users.size() › 0) {
                                userName = users.get(0).getUserName();
                            }
                            if (userName == null) {
                                showToast("未注册的用户进店");
                            } else {
                                User user = users.get(0);
                                UserEvent userEvent = new UserEvent();
                                userEvent.setUserId(user.getUserId());
                                userEvent.setEnterTime(System.currentTimeMillis());
                                userEvent.setDesc("Enter Store");
                                DBManager.getInstance().addUserEvent(userEvent);
                                showToast("用户[ " + userName + " ]进店");
                            }
                        }
             }
}

10、IPC扫描

// 获取IPC扫描的AP热点
private void getWifiList() {
    BasicConfig.getInstance(context).getApListWithoutAuth(sunmiDevice.getDeviceid(),
            new RPCCallback‹RPCResponse‹IpcApBean››() {
                @Override
                public void onComplete(RPCResponse‹IpcApBean› result) {
                    if (result.code() == RPCErrorCode.SUCCESS) {
                        wifiListGetSuccess(result.data());
                    } else {
                        Log.i(TAG, "getApListWithoutAuth failed, errcode: " + result.code());
                    }
                }
        });

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            if (wifiList.size() == 0) {
                setNoWifiVisible(View.VISIBLE);
            }
        }
    }, TIMEOUT_GET_WIFI);
}

11、IPC连接AP

private void setIpcWifi(String ssid, String psw) {
    showLoadingDialog();
    BasicConfig.getInstance(context).setWifiConfWithoutAuth(sunmiDevice.getDeviceid(),
            ssid, psw, new RPCCallback‹RPCResponse›() {
                @Override
                public void onComplete(RPCResponse result) {
                    if (result.code() == RPCErrorCode.SUCCESS) {
                        hideLoadingDialog();
                        shortTip("配置成功,请等待设备联网,联网后指示灯会变成蓝色");
                        createWaitDialog();
                    } else {
                        hideLoadingDialog();
                        shortTip("配置失败");
                    }
                }

                @Override
                public void onError(Throwable t) {
                    hideLoadingDialog();
                    shortTip("配置失败");
                }
            });
}

12、激活IPC

//激活ipc设备,并设置回调接口
DeviceManage.getInstance(context).activate(ipcList.get(postion).getDeviceid(),
    new RPCCallback‹RPCResponse›() {
        //IPC返回相关错误码。
        @Override
        public void onComplete(RPCResponse result) {
            if (result.code() == RPCErrorCode.SUCCESS || result.code() == RPCErrorCode.DEVICE_ACTIVATED) {
                Log.i(TAG, "activate ipc success");
            } else {
                Log.i(TAG, "activate ipc failed");
            }
        }
        //网络问题导致无法通信,会调用onError接口
        @Override
        public void onError(Throwable t) {
            Log.i(TAG, "activate ipc failed");
        }
    });

13、RTSP播放

    private void openMediaPlayer() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //获取播放流
                VideoStream.getInstance(context).getLiveStream(mDevice.getDeviceid(),
                        new RPCCallback‹RPCResponse‹RPCResponse.LiveAddressBean››() {
                            @Override
                            public void onComplete(RPCResponse‹RPCResponse.LiveAddressBean› result) {
                                if (result.code() == RPCErrorCode.SUCCESS) {
                                    Log.i(TAG, "live url: " + result.data().fhd_live_url);
                                    if (mPlayer == null) {
                                       //自定义的rtsp播放器
                                        mPlayer = new SunmiPlayer(context);
                                        mPlayer.setListener(new SunmiPlayerListener() {
                                            @Override
                                            public void onPrepared(IMediaPlayer iMediaPlayer) {
                                                iMediaPlayer.start();
                                            }
                                        });
                                        mPlayer.setSurface(mVideoView.getHolder().getSurface());
                                        //获取到ipc返回的播放流rtsp地址
                                        String liveUrl = result.data().fhd_live_url.replaceFirst("^rtsp://", "rtsp://admin:admin@");
                                        //自定义播放器开始播放rtsp视频流
                                        mPlayer.setUp(liveUrl);
                                    }
                                }
                            }
                });
            }
        }).start();
    }

14、IPC对焦、调焦

rivate BasicConfig mBasicConfig;

public void init() {
    ...
    mBasicConfig = BasicConfig.getInstance(context);
    ...
}

// 调焦
private void func1(){
    ...
    mBasicConfig.setZoom(mDevice.getDeviceid(), zfBean.zoom, new RPCCallback‹RPCResponse›() {
        @Override
        public void onComplete(RPCResponse result) {
            Log.i(TAG, "setZoom, code:" + result.code());
        }
    });
    ...
}

// 自动对焦
private void func2() {
    ...
    mBasicConfig.autoFocus(mDevice.getDeviceid(), xRelative, yRelative, new RPCCallback‹RPCResponse›() {
        @Override
        public void  onComplete(RPCResponse result) {
            Log.d(TAG, "autoFocus, code:" + result.code());
            if (result.code() == RPCErrorCode.SUCCESS) {
                mBasicConfig.getZoomFocusConf(mDevice.getDeviceid(), new RPCCallback‹RPCResponse‹RPCResponse.ZoomFocusBean››() {
                    @Override
                    public void onComplete(RPCResponse‹RPCResponse.ZoomFocusBean› result) {
                        if (result.code() == RPCErrorCode.SUCCESS) {
                            zfBean = result.data();
                            mSbZoom.setProgress(zfBean.zoom);
                        }
                    }
                });
            }
        }
    });
    ...
}

// 自动对焦如果不够清晰,可以手动对焦进行微调
private void func3() {
    ...
    mBasicConfig.manualFocus(mDevice.getDeviceid(), focus, new RPCCallback‹RPCResponse›() {
        @Override
        public void onComplete(RPCResponse result) {
            if (result.code() == RPCErrorCode.SUCCESS) {
                zfBean.focus = focus;
            }
        }
    });
    ....
}

15、设置IPC门线

private void func() {
    ...
    PeopleFlowStats.getInstance(context).setDoorLine(mDevice.getDeviceid(), 0, lineStart[0],
            lineStart[1], lineEnd[0], lineEnd[1], new RPCCallback‹RPCResponse›() {
                @Override
                public void onComplete(RPCResponse result) {
                    if (result.code() == RPCErrorCode.SUCCESS) {
                        stopPlay();
                        finish();
                        startActivity(new Intent(context, MainActivity.class));
                    }
                }
            });
    ...
}

16、IPC监听人脸消息

IPCameraManager.getInstance(context).setFaceDetectListener(new FaceDetectListener() {
    @Override
    public void onFaceDetect(String userId) {
        String userName = null;
        if (userName == null) {
            showToast(getApplicationContext(), "未注册的用户进店");
        } else {
            showToast(getApplicationContext(), "用户[ " + userName + " ]进店");
        }
    }
});

商米AI识客SDK开发指南

  1. 准备工作

1.1 申请SDK所需的材料

步骤一:联系商米售前技术团队,申请SDK需要向售前技术支持团队提供如下软件开发商信息

客户提供的信息说明
客户名称软件商公司名称

步骤二:售前技术团队根据客户的需求信息,返回软件商以下内容

返回给客户的信息说明
SDK开发包用于人脸识别以及AI识客设备开发的SDK(目前只支持安卓版本)

SDK开发包的内容如下:

序号文件/文件名说明
1FaceDemoDemo工程源码
2face-release.aar商米人脸识别SDK
3libipcsdk-release.aarIPC 联动版sdk
4asset商米人脸识别资源文件
5FaceDemo.apkFaceDemo程序

2. 申请Licence

拿到SDK后,联系商米售前人员,提供以下信息申请Licence

提供的信息说明
收银设备硬件指纹1、通过Demo APP查看硬件指纹信息。安装运行FaceDemo.apk,授权FaceDemo存储器权限,会在sdcard目录生成一个硬件指纹信息文件device_fingerprint.txt。
2、或者通过SDK API查看硬件指纹信息。

返回的信息:

信息说明
Licence 激活人脸识别的SDK的Licence
appid和secret_key 激活设备IPC设备

SaaS开发者获取到以上信息后,可以使用人脸识别SDK,并激活IPC设备。

3. 添加SDK到工程

3.1 导入SDK

右键点击app,选择New->Module->Import .JAR/.AAR Package

点击Finish即导入aar模块

3.2 权限申明

在AndroidManifest.xml文件里添加如下权限:

	‹uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/›
	‹uses-permission android:name="android.permission.INTERNET" /›
	‹uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /›
	‹uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /›
	‹uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /›
	‹uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /›
	‹uses-permission android:name="android.permission.CAMERA" /›

3.3 导入asset资源

导入提供的assets资源

说明:资源文件需要APP启动的时候复制到APP的运行目录,以方便SDK后续初始化。

3.4 导入licence文件

获取到licence_valid.txt文件,可以放到assets资源目录,也可以放到sdcard目录。

1)放到assets资源目录,则需要把licence_valid.txt文件从资源文件复制到app的工作目录。

2)如果放到sdcard目录,则需要app申请sdcard读取权限。

无论哪一种情况,最后使用的时候,需要传递licence_valid.txt文件的路径,以激活人脸识别SDK。详细见SDK初始化代码

4. DEMO 工程目录结构

4.1 app Module

app Module是DEMO的入口。其中包含了各个模块的入口函数调用。

4.2 commonlib

commonlib Module包含了整个DEMO通用的接口。其中包含摄像头管理,工具类。如下图:

4.2.1 FaceCameraManager

FaceCameraManager统一管理设备自带摄像头和USB摄像头。设备自带摄像头的流程是使用Android系统提供的摄像头通用方法。USB摄像头则使用UVC库提供的操作摄像头的方法。FaceCameraManager对上层应用提供统一的方法:startPreview和 stopPreview。

4.2.2 NormalCameraManager

NormalCameraManager使用Android系统提供的摄像头方法,实现startPreview和stopPreview。并在内部实现回调接口用于返回每一帧摄像头获得的数据。

4.2.3 UVCCameraManager

UVCCameraManager使用第三方库UVClib提供的方法用于操作USB摄像头,实现startPreview和stopPreview。 并在内部实现回调接口用于返回每一帧摄像头获得的数据 。

4.2.4 ByteUtils

ByteUtils提供Byte操作的一些通用方法。

4.2.5 DBHelper

DBHelper提供创建数据的方法。

4.2.6 ImageUtils

ImageUtils提供图片操作的一些方法。

4.3 libuvccamera

libuvccamera Module采用第三方库开源库libuvccamera

4.4 facedemo

facedemo Module是DEMO实现功能的主要模块。其包含:配置IPC、会员注册、会员管理、会员查询等功能。

5. 配置IPC

IPC设备使用时,如果未激活,需要先激活IPC设备。激活步骤如下:

5.1 SDK初始化

APP_ID:激活与API调用校验使用的账号。

SECRET_KEY:API调用所需的签名密钥。

LICENSE:激活API所需的激活码。

IPCameraManager mIPCameraManager = IPCameraManager.getInstance(context);
mIPCameraManager.init(APP_ID, SECRET_KEY, LICENSE);

5.2 配网

IPC设备激活时需要连接到互联网。IPC连接互联网时,可以通过无线(wifi)或有线(以太网口)连接到路由器。无线连接时需要做配网设置。

有线接入:

有线接入只需要通过网线把IPC设备和集成SDK的Android设备接入到同一局域网即可,不需要其它设置。

无线接入:

无线配网方式相对要复杂点,步骤如下:

  1. 使用手机/PC的无线网卡扫描IPC的AP热点,一般AP热点的名称为SUNMI_XXXX,其中XXXX为MAC地址最后2个字节的16进制数字,MAC地址可以通过设备机身后背的标贴或者包装盒的标贴查到,AP热点本身是无加密的。
  2. 使用手机/PC的无线网卡连接IPC的AP热点,此时手机/PC就会获取到IPC分配的IP地址(按照设备发现描述的方法即可获取到),一般会是192.168.200.XXX,手机/PC的网关地址就是IPC的地址,一般会是192.168.200.1。
  3. 调用无线配置 API(见获取无线扫描AP列表 (无需签名校验)的描述)获取IPC扫描到的AP热点。
  4. 调用无线配置 API(见设置无线参数(无需签名校验)的描述)设置IPC要连接的无线网络(例如无线路由器的SSID和密码),使得IPC能够从网关处获取到IP地址。
  5. 如果网络是可以正常上网的话,IPC取到IP地址后很快就会亮蓝灯,此时表明IPC可以正常连接Internet了。

IPC扫描AP代码见 商米AI识客SDK示例代码->IPC扫描

IPC连接Wifi代码详细见 商米AI识客SDK示例代码->IPC连接AP

5.3 激活

通过 5.2配网 后,IPC通过无线连接到路由或者直接有线连接路由。

由于使用IPC的其它API时需要签名校验,所以在使用API之前需要激活IPC。激活只需要在第一次使用IPC时进行,后续都不再需要激活。

调用active接口既可以激活IPC设备。详细见 商米AI识客SDK示例代码-> 激活IPC

5.4 画面调整

由于IPC的人脸识别对于人脸图像质量有一定要求,因此在使用IPC前需要调整IPC的画面,以达到最好的体验效果。画面调整包括镜头的调焦、对焦。步骤如下:

5.4.1 RTSP 播放

首先需要通过预览视频查看IPC设备镜头是对焦,画面是否清晰。IPC设备是通过RTSP协议传递预览视频,所以需要自定义一款RTSP播放器才能看到视频流。

自定义的RTSP播放器调用VideoStream.getInstance(context).getLiveStream接口可以获取到RTSP播放流的地址。详细代码见 商米AI识客SDK示例代码-> RTSP播放

5.4.2 调焦、对焦

为了更好的人脸识别效果,需要对IPC设备进行调焦对焦。

调用接口BasicConfig.getInstance(context).setZoom调焦; BasicConfig.getInstance(context) .autoFoucus自动进行对焦调焦。

详细代码见 商米AI识客SDK示例代码-> IPC对焦、调焦

6. 会员注册

完成会员的注册功能。输入会员的ID、分组、备注信息,并采集用户的人脸,注册人脸特征信息。注册信息的同时,会通过摄像头采集人脸照片传递给IPC设备。

6.1 获取摄像头数据

详细代码见 商米AI识客SDK示例代码->获取摄像头信息

6.2 注册会员信息和人脸信息

详细代码见 商米AI识客SDK示例代码-> 会员注册

7. 会员管理

包括会员列表、会员删除、会员查看、会员注册。

会员删除详细代码见 会员删除

会员注册详细代码见  会员注册

8. 会员实时查询

主要功能实现了实时的获取摄像头预览数据,并且实时对获取的摄像头数据进行特征提取,根据人脸特征信息查询会员。做到通过人脸查询会员的目的。主要使用到的功能有:实时获取摄像头预览数据以及人脸特征提取两个功能

详细代码见 商米AI识客SDK示例代码->获取摄像头预览信息

详细代码见 商米AI识客SDK示例代码->人脸特征提取

商米AI识客SDK接口说明

1. 商米人脸识别接口

人脸识别详细API详见链接 商米人脸识别

2. IPC管理接口

详细见 设备端SDK

发布应用说明

发布应用

关于商米应用市场

每个商米合作伙伴都可以在商米官网 注册商米合作伙伴帐号,有一个自己的操作后台,原则上每一台商米的设备在卖出去的时候都会和一个合作伙伴账号绑定,商米会以合作伙伴为粒度提供部分功能和权限的控制服务。

SUNMIUI内部有一个应用市场,合作伙伴可以通过应用市场将自己的应用大规模分发到商米的设备上。默认旗下用户只能通过商米应用市场为设备安装应用。

应用市场规则

商米为部分合作伙伴分配了不同权限的应用市场,默认合作伙伴上传的应用不用商米审核就可以出现在自己的应用市场中。商米可以审核该应用,通过审核后该应用会出现在其他合作伙伴的应用市场中,当然部分合作伙伴可以选择自己的应用不出现在其他人的应用市场,也可以选择自己应用市场不出现其他人的应用,关于权限的授予可以咨询商米客服400-902-1168(每天9:00~21:00,节日除外)

应用分发流程

应用开发完成后,上传到应用市场,用户可以在设备上的应用市场搜索到应用,如果是渠道合作伙伴可以设置应用在旗下的设备自动安装。以下讲解应用首次上传分发的流程。

1.上传应用。

在 合作伙伴后台,进入”我的应用”选项,点击创建应用

2

2.填写相关内容

点击’上传安装包’,从文件夹中选择您要上传的apk,等待上传完成后,商米后台将自动分析安装包,下图绿色箭头指示部分将会自动填写好,开发者手动填写应用介绍,应用适配的设备,应用类型等信息。

  • 上传APK安装包时如果提示包名重复,则表示此APK已经在商米应用市场上架,如果此APK是贵公司所有,请联系商米客服400-902-1168(每天9:00~21:00,节日除外),进入认领。
3

3.提交应用。

合作伙伴将剩余部分内容补充完整后,下图的’提交’按钮将由灰色不可点击变成绿色可点击状态,点击’提交’将会完成应用的上传,

4

关于灰度部署

上图中’提交’按钮上方有一个’是否灰度部署’选项,勾选后提交按钮将变成’灰度部署’项,点击后将进入灰度部署配置项中。

灰度部署是商米针对合作伙伴的实际需求提供的一项功能,每台设备在使用的时候会有一个地理理位置信息,合作伙伴在开发好自己的应用适配商米的设备后,可以根据地理位置或者SN号在部分设备上部署,只有灰度部署范围内的设备的应用市场中才会出现该应用。在小范围的灰度部署后,开发者可以将应用部署到全范围设备上。

5

设置自动安装。

如果希望上传的应用自动在旗下所有设备上安装,可以在’我的应用市场’项中设置该应用为自动安装应用(如下图箭头所示),旗下设备在收到推送通知后会自动下载安装该应用,设置自动安装后还可以设置该应用是否作为默认启动应用(如下图箭头所示),设备开机后将自动启动该应用。

6

更新应用

在应用通过商米审核后,在我的应用中列表项中会有’更新’按钮,开发者可以发布该应用的升级包。点击后进入更新应用页,更新的流程和发布流程类似,请参照上面的发布流程。

  • 上传更新包的时候如果提示签名异常,请检查APK签名是否与原来的安装包不一致。如确认是需要变更签名,则请删除应用后再创建应用
  • 上传更新包的时候如果提示包名不一致,请检查APK包名是否有变动或有没有上传错误。如确认是需要变更包名,则请删除应用后再创建应用
7

常见问题和帮助

开发常见问题

阅读指引

为了更快地在机具上完成收银应用开发,以及加快刷脸应用和iot sdk的集成联调开发,机具端侧开发过程中的问题汇总如下。

FT1 MINI硬件调试开发

Q:ISV开发测试建议

A:由于团餐场景的特点,就餐速度快,排队人数多,付费过程快等因素。建议ISV根据实际场景和环境对设备和软件进行相关的压测,实际场景会有很多刷脸的状况和意外情况,所以请ISV务必进行压测,尽量将问题暴露在压测阶段。做过压测后无问题再投入市场使用。

Q:设备如何进行adb调试连接?

A:  推出FT1 MINI设备键盘,拆开维保码下面的防水盖,插入micro USB口连接FT1 MINI和PC机。 不支持debug调试,需要打印日志方式调试。在adb调试过程中,确保RJ45和键盘USB没有插入。

Q:如何排查深度摄像头硬件是否工作正常?

A:在FT1 MINI应用列表界面,点击”MarioSdk“图标进入摄像头工厂模式界面,再点击”D2C测试“按钮,查看摄像头预览界面是否正常。如果有类似马赛克的界面则初步判断正常,参考画面如下:

Q:如何排查键盘不响应?

A:第一步:确认是否延长线断裂不通,可以将键盘不用延长线插入机器尝试确认。

第二步:不用延长线直接插入键盘,可以进入CIT模式做单体测试(在FT1 MINI应用列表界面点击CIT图标),确认键盘是否不良。

第三步:更换键盘再确定机器USB是否通路,用U盘或鼠标测试确认。如不通,报修或更换整机。

Q:如何排查NFC不能正常工作?

A:进入设置界面,看NFC 的设置开关是否可以正常打开、关闭。要确认NFC设置为“打开”状态。合作方对NFC的监听应用需要独立于机具的收银应用工作,NFC的开发采用标准的android NFC开发原理。

Q:机器长时间运行状况,是否连续数天处于不关机状态?

A:如无人为关机行为,建议客户设置定时开关机,连续数天不关机可能造成机器长时间老化导致卡顿,延迟,寿命缩短等问题。

Q:机器开机默认霸屏模式,如何退出霸屏模式

A:同时按下键盘上 “功能”+“+”+“设置”即可退出霸屏模式。

Q:键盘外置的情况下,安装建议?

A:键盘外置时,需要连接延长线,建议在接头位置包裹绝缘胶带以起到放松防水作用。

Q:键盘套使用

A: 我们的键盘具有IP55级别防尘放水,并使用的防油污材料。但是由于食堂环境油污严重,环境复杂,后期我们将随设备配套键盘保护套,请在安装时同步装上键盘保护套。防止油污进入键盘,影响键盘使用。

Q:频繁刷脸运行后,设备卡顿,设备反应慢,频繁发生。

A:有以下两种可能原因,需要客户自查。

1,可能原因:线程操作有问题,频繁调用smile 导致smile 发生ANR,客户的APP 一直在等待,导致界面卡死,出现白屏、黑屏。建议客户不要在主线程有过多的耗时操作,后续客户修改逻辑正常。

2、可能原因2 :APP 也是频繁操作,导致资源没有来得及释放,出现内存泄漏导致机器卡顿,重启APP正常,建议客户:适当优化释放时间,后正常。

Q:副屏显示设置建议。

A:建议副屏显示设置的文字越大越好,对比度越大越好,利于食堂阿姨能够更清晰的看到消费金额。

Q:排插选型

A:由于我们使用的是插墙式的适配器,适配器本体比较宽,所以建议客户选宽间距的排插。窄间距的排插利用率较低。适配器宽度尺寸为51*51mm。

Q:键盘不响应。

A:偶然出现,建议重新插拔键盘或者重启设备。

   出现概率比较高的情况,如何排查键盘不响应

1、确认是否延长线断裂不通,可以将键盘不用延长线插入机器尝试确认。

2、不用延长线直接插入键盘,可以进入CIT模式做单体测试(在海马应用列表界面点击CIT图标),确认键盘是否不良。

3、更换键盘再确定机器USB是否通路,用U盘或鼠标测试确认。如不通,报修或更换整机。

Q:设备用有线网络的话,对网线种类有要求吗?用无线网路的话,对无线路由器有要求吗?

A:设备需要稳定的网络,要求采用国标超五类及以上的网线或者企业级无线路由器。

Q:设备的安装方式

A:设备配有标准滑入式挂架,可以吊装,左装,右装,贴合玻璃,配合桌面支架安装(桌面支架需要单独购买)。安装方式详情请参考开发指南的安装指引。

Q:设备安装高度是多少?

A:根据设备安装点位最大程度贴近学校平均身高即可。

   参照平均身高选择安装高度(摄像头距地面的距离)。

高度角度身高范围(距离60cm) 
1400125(最低)175(最高)
1500135(最低)185(最高)
1600145(最低)195(最高)

假设安装高度160cm的话, 覆盖145~195cm 身高人群。如果是上下垂直调整的话, 就直接相减即可。比如安装高度下降30cm至 130cm的位置, 则覆盖身高变为115~165cm。如果离得更近,那身高范围会缩小一些。以上是理论计算, 实际测试会比这个范围大。

Q:摄像头无法识别人脸。

A:在FT1 MINI应用列表界面,点击”MarioSdk“图标进入摄像头工厂模式界面,再点击”D2C测试“按钮,查看摄像头预览界面是否正常。如果有类似马赛克的界面则初步判断正常(参考图如下),如显示正常,请在联网状态下打开smilepay软件,选项—正式环境—刷脸,如刷脸正常,即可判定摄像头为正常。需要从软件方向寻找问题原因。

如以上检查无法正常刷脸或者深度读取不正常,可判定摄像头不良,需走售后流程安排维修或者换机。

Q:如何进行设备单项功能测试

A:设备应用界面—点 CIT—选择  单项测试—进行判断可能问题的单项测试。

接口使用说明

如何找到接线口:

  第一步:在机器背面,如红色箭头方向所示,从左向右推出接线盖。

  第二步:从左向右推出键盘。

   第三步:看到两个防水密封盖A和B。

  第四步:用十字螺丝刀拆除3颗螺丝后,打开防水盖。

  第五步:打开后出现3个外设I/O口,分别是RJ45、USB2.0*2。

  说明:如果网线无法插入现有机器网口,请使用随机网线转接线接入网络。