- demo https://developer.sunmi.com/docs/preview/zh-CN/rqfeghjk535
- IoT能力接入 https://developer.sunmi.com/docs/zh-CN/xeghjk491/rareghjk568
- 开发者账号
- appId、appKey
- 版本
- jdk 1.8
- gradle 7.3.3(7.2+)
- Android Gradle Plugin 4.2.2
- Android compile sdk 32
- jks或keystore的md5值
- IDE工具
- Android Studio
- HbuilderX
- 自助结账机可通过数据线连接至电脑,进行真机调试
- 自助结账机需要在网络环境下通过商米鉴权,获取商米的license,放入后续的请求头中进行交互
Android作为载体,并负责与硬件交互,页面交给uniapp开发,打包为h5嵌入webview中,通过JSBridge与Android交互
硬件能力 <=商米IoT=> Android <=调用硬件=> uniapp <=数据流转=> 服务端
- 刷手牌 ✔
- 扫预订单 ✔
- 扫会员码 ✔
- 刷会员卡 ✔
- 支付扫码 ✔
- 打印 ✔
- 账单 ✔
- 增删会员卡 ✔
- 增删预订单 ✔
- 支付 ✔
基于官方demo项目结构,使用java进行开发
-
机器左下角 扫码、读卡器均可以通过监听此硬件的广播获取数据
参考文档
-
小票打印
- 创建webview
WebView webView = (WebView) findViewById(R.id.web_view);
// 内容页面的访问地址
String url = "http://172.16.9.79:8080/#/";
WebSettings webSettings = webView.getSettings();
// 加载网络图片
webSettings.setBlockNetworkImage(false);
// 开启JavaScript
webSettings.setJavaScriptEnabled(true);
// 不加载缓存内容
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
// 向js暴露调用的方法
webView.addJavascriptInterface(new WebAppInterface(this),"K2");
webView.loadUrl(url);
webView.setWebViewClient(new MyWebViewClient(){
@Override
@SuppressWarnings("deprecation")
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();// 接受所有网站的证书
super.onReceivedSslError(view, handler, error);
}
});
- webview对外暴露的方法
public class WebAppInterface {
Context mContext;
private int ret;
WebAppInterface(Context c) {
mContext = c;
}
// 打印小票方法
@JavascriptInterface
public void doPrint(String b64) {
print(PrintUtil.getPicRequest(b64));
}
// 提示信息方法
@JavascriptInterface
public void doToast(String msg) {
LogUtil.e(TAG, "webview send-----:" + msg);
Toast.makeText(app, "webview打印:" + msg, Toast.LENGTH_SHORT).show();
}
}
onCreate中建立监听
private IntentFilter filter;
private SMSReceiver sms;
// 在官方文档中获取的广播id
private static final String ACTION_DATA_CODE_RECEIVED = "com.sunmi.scanner.ACTION_DATA_CODE_RECEIVED";
filter = new IntentFilter(ACTION_DATA_CODE_RECEIVED);
sms = new SMSReceiver();
registerReceiver(sms, filter);
监听类
class SMSReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String code = intent.getStringExtra(DATA);
String arr = Arrays.toString(intent.getByteArrayExtra(SOURCE));
Log.d("TTTT", "读手牌:" + code + "---" + arr);
WebView webView = (WebView) findViewById(R.id.web_view);
// 通知webview
runOnUiThread(() -> {
webView.loadUrl("javascript:recK2Msg('" + code + "')");
});
}
}
在application中获取硬件服务
private void getService() {
int initResult = ThingSDK.getInstance().init(getApplicationContext(), license, new ThingSDK.StatusListener() {
@Override
public void connected() {
try {
printerList = ThingSDK.getInstance().getService("printer");
LogUtil.e(TAG+" print: ", printerList + " ------------- " + printerList.size());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void disconnected() {
LogUtil.e(TAG, "disconnected");
}
});
LogUtil.i(TAG, "ThingSDK init result: " + initResult);
}
打印方法1
考虑到排版美观和可控,采用图片打印,即前端页面=>图片=>base64 如果使用aidl可能会使此方法失效
private synchronized void print(CommandRequest request) {
if (Application.printerList == null || Application.printerList.size() == 0) {
Toast.makeText(this, "获取打印机失败", Toast.LENGTH_SHORT).show();
return;
} else {
LogUtil.e(TAG, "sass send------:" + PrintUtil.gson.toJson(Application.printerList.get(0)));
}
String callUuid = null;
ThingService service = Application.printerList.get(0);
final long time = SystemClock.elapsedRealtime();
LogUtil.e(TAG, "sass send:" + SystemClock.elapsedRealtime());
try {
callUuid = ThingSDK.getInstance().execute(service, ThingSDK.ACTION_TYPE_COMMAND, ThingSDK.ACTION_EXECUTE, PrintUtil.gson.toJson(request), new IResponseCallback.Stub() {
@Override
public void response(String uuid, Map data) throws RemoteException {
LogUtil.e(TAG, "sass receive:" + SystemClock.elapsedRealtime());
LogUtil.e(TAG, uuid + ";" + time);
long t1 = SystemClock.elapsedRealtime();
LogUtil.e(TAG, time + ";" + t1 + " ; " + (t1 - time));
if (data != null) {
StringBuilder builder = new StringBuilder("print-->");
for (Object s : data.keySet()) {
builder.append(s + " : " + data.get(s) + ";");
}
LogUtil.e(TAG, "res:" + builder.toString() + " ;" + t1);
}
}
});
} catch (RemoteException e) {
e.printStackTrace();
}
LogUtil.e(TAG, " print call :" + callUuid + ";" + PrintUtil.gson.toJson(request));
}
打印方法2
使用aidl中的打印图片方法,考虑到跨域以及排版,前端页面=>图片=>base64=>安卓转Bitmap
@JavascriptInterface
public void doPrint(String b64) {
byte[] bytes = android.util.Base64.decode(b64, android.util.Base64.DEFAULT);
AidlUtil.getInstance().printBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length));
// 切纸
AidlUtil.getInstance().cutPaper();
}
切纸
官方的IOT标准能力sdk以及提供的demo,包括新版的商米内置打印机sdk中,并没有发现有触发切纸的操作,结束打印后需要人工撕小票,所以需要参考旧版SDK文档,通过AIDL的方式连接打印服务
- 首先定义AIDL工具类,提供打印服务初始化和切纸操作方法
import com.sunmi.extprinterservice.ExtPrinterService;
public class AidlUtil {
private static final String SERVICE_PACKAGE = "com.sunmi.extprinterservice";
private static final String SERVICE_ACTION = "com.sunmi.extprinterservice.PrinterService";
private ExtPrinterService printerService;
private static AidlUtil mAidlUtil = new AidlUtil();
private Context context;
private AidlUtil() {
}
public static AidlUtil getInstance() {
return mAidlUtil;
}
public void connectPrinterService(Context context) {
this.context = context.getApplicationContext();
Intent intent = new Intent();
intent.setPackage(SERVICE_PACKAGE);
intent.setAction(SERVICE_ACTION);
context.getApplicationContext().startService(intent);
context.getApplicationContext().bindService(intent, connService, Context.BIND_AUTO_CREATE);
}
public void disconnectPrinterService(Context context) {
if (printerService != null) {
context.getApplicationContext().unbindService(connService);
printerService = null;
}
}
public boolean isConnect() {
return printerService != null;
}
private ServiceConnection connService = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i("tryhard","aidl service is disconnected");
printerService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i("tryhard","aidl service is connected");
printerService = ExtPrinterService.Stub.asInterface(service);
}
};
public void initPrinter() {
if (printerService == null) {
Toast.makeText(context,"Init",Toast.LENGTH_LONG).show();
return;
}
try {
printerService.printerInit();
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void cutPaper() {
if (printerService == null) {
Toast.makeText(context,"Cut",Toast.LENGTH_LONG).show();
return;
}
try {
printerService.cutPaper(0, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
- 在应用的onCreate阶段执行初始化
import com.self.demo01.utils.AidlUtil;
import com.sunmi.thingservice.sdk.IResponseCallback;
import com.sunmi.thingservice.sdk.IServiceEventListener;
import com.sunmi.thingservice.sdk.ThingSDK;
import com.sunmi.thingservice.sdk.ThingService;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = this;
AidlUtil.getInstance().connectPrinterService(this);
...
}
- 在打印事件的回调中调用切纸方法
private synchronized void print(CommandRequest request) {
if (Application.printerList == null || Application.printerList.size() == 0) {
Toast.makeText(this, "获取打印机失败", Toast.LENGTH_SHORT).show();
return;
} else {
LogUtil.e(TAG, "sass send------:" + PrintUtil.gson.toJson(Application.printerList.get(0)));
}
String callUuid = null;
ThingService service = Application.printerList.get(0);
try {
callUuid = ThingSDK.getInstance().execute(service, ThingSDK.ACTION_TYPE_COMMAND, ThingSDK.ACTION_EXECUTE, PrintUtil.gson.toJson(request), new IResponseCallback.Stub() {
@Override
public void response(String uuid, Map data) throws RemoteException {
...
// 切纸
AidlUtil.getInstance().cutPaper();
}
});
} catch (RemoteException e) {
e.printStackTrace();
}
}
通过window对象获取安卓webview中对外暴露的对象K2,K2会主动通知扫码与读卡事件,因为是同一广播,只能凭借经验对code进行判断区分,前端通过eventBus的机制进行全局监听和响应。
app.vue
const cxt = getCurrentInstance()
const bus = cxt.appContext.config.globalProperties.$bus
const K2 = window.K2;
if(!K2){
showFailToast("硬件初始化失败")
}
window.recK2Msg = function(code){
console.log('收到K2消息:', code)
// 是否只有数字
const reg = /^[0-9]*$/
if (reg.test(code)) {
if(code.length > 10){
if(code.length === 12){
// 预订单券码
bus.emit('recResrOrderCode', code)
}else{
// 支付码
K2.doToast("支付码")
bus.emit('recPayCode', code)
}
}else{
// 手牌号或会员卡号
bus.emit('recCardUid', code)
}
}else{
K2.doToast("无法识别,请重试")
}
}