const serial = chrome.serial; var showDebugLog = false; var SerialConnection = function() { this.connectionId = -1; this.boundOnReceive = this.onReceive.bind(this); this.boundOnReceiveError = this.onReceiveError.bind(this); this.onConnect = new MyEvent(); this.onReceive = new MyEvent(); this.onError = new MyEvent(); this.recvBuffer = new Uint8Array(8*1024); this.recvView = new DataView(this.recvBuffer.buffer); this.recvCursor = 0; this.bitrate = 57600; }; SerialConnection.prototype.onConnectComplete = function(connectionInfo) { if (!connectionInfo) { if (chrome.runtime.lastError != undefined) { logln('chrome.serial.connect error: ' + chrome.runtime.lastError.message); } return; } this.connectionId = connectionInfo.connectionId; chrome.serial.onReceive.addListener(this.boundOnReceive); chrome.serial.onReceiveError.addListener(this.boundOnReceiveError); this.onConnect.dispatch(); }; SerialConnection.prototype.onReceive = function(receiveInfo) { if (receiveInfo.connectionId !== this.connectionId) { return; } this.recvBuffer.set(new Uint8Array(receiveInfo.data), this.recvCursor); this.recvCursor += receiveInfo.data.byteLength; //console.log(buf2hex(receiveInfo.data)); if (this.recvCursor < 6) { return; } this._dispathReceiveData(); }; SerialConnection.prototype.clearRecvData = function() { this.recvCursor = 0; this.recvBuffer.fill(0); } SerialConnection.prototype._dispathReceiveData = function() { var dLen = this.recvView.getUint16(7); if (this.recvCursor < dLen + 9) { return; } var dataBuffer = new Uint8Array(dLen + 9); dataBuffer.set(this.recvBuffer.subarray(0, dLen + 9)); if (showDebugLog) { logln("recv: " + buf2hex(dataBuffer.buffer)); } var realCrc = calcCRC(dataBuffer, 6, dLen + 7); var crc = dataBuffer[dLen+7] * 256 + dataBuffer[dLen+8]; if (crc != realCrc) { logln("invalid crc " + crc + " ,real= " + realCrc); }else { var packet = new Packet().fromDataBuffer(dataBuffer); this.onReceive.dispatch(packet); } if (this.recvCursor > dLen + 9) { dataBuffer = new Uint8Array(this.recvCursor - dLen - 6); dataBuffer.set(this.recvBuffer.subarray(dLen + 9, this.recvCursor)); this.recvBuffer.fill(0); this.recvBuffer.set(dataBuffer); this.recvCursor -= dLen + 9; this._dispathReceiveData(); }else { this.recvCursor = 0; this.recvBuffer.fill(0); } } SerialConnection.prototype.onReceiveError = function(errorInfo) { if (errorInfo.connectionId === this.connectionId) { this.onError.dispatch(errorInfo.error); } }; SerialConnection.prototype.update = function(conf, cb) { if (!this.connectionId) {return;} serial.update(this.connectionId, conf, cb); } SerialConnection.prototype.connect = function(path, bitrate=57600) { this.clearRecvData(); this.bitrate = bitrate; serial.connect(path, { bitrate: this.bitrate },this.onConnectComplete.bind(this)) }; SerialConnection.prototype.send = function(packet) { if (this.connectionId < 0) { throw 'Invalid connection'; } var data = packet.getDataBuffer(); if (showDebugLog) { logln("send: " + buf2hex(data)); } serial.send(this.connectionId, data, function() {}); }; SerialConnection.prototype.disconnect = function() { if (this.connectionId < 0) { throw 'Invalid connection'; } serial.disconnect(this.connectionId, function() {}); }; //////////////////////////////////////////////////////// //////////////////////////////////////////////////////// function logln(msg) { var buffer = document.querySelector('#buffer'); buffer.innerHTML += msg + '
'; var msgEnd = document.querySelector('#msg_end'); msgEnd.scrollIntoView(); } function log(msg) { var buffer = document.querySelector('#buffer'); buffer.innerHTML += msg; var msgEnd = document.querySelector('#msg_end'); msgEnd.scrollIntoView(); } function buf2hex(buffer) { return '0x' + Array.prototype.map.call(new Uint8Array(buffer), x => ('0x00' + x.toString(16)).slice(-2)).join(' 0x'); } function calcCRC(buffer, start, end) { var crc = 0; for (var i = start; i < end; i++) { crc += buffer[i] & 0xff; } return crc & 0xffff; } const PacketTypeCmd = 0x01; const PacketTypeData = 0x02; const PacketTypeDataEnd = 0x08; const PacketTypeCmdResp = 0x07; const PacketTypeDataResp = 0x09; var Packet = function(data=new Uint8Array([0x35]), dataLen=1, type=PacketTypeCmd) { this.dataBuffer = new Uint8Array(512); this.dataBuffer.set([0xEF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF]); this.type = type; this.dataLen = dataLen; this.data = data; this.result = 0; } Packet.prototype.setCmd = function(cmd) { this.type = PacketTypeCmd; this.data = Uint8Array.of(cmd); this.dataLen = 1; } Packet.prototype.getDataBuffer = function () { var dataView = new DataView(this.dataBuffer.buffer); dataView.setUint8(6, this.type); var len = this.dataLen + 2; dataView.setUint16(7, len); this.dataBuffer.set(this.data, 9); var crc = calcCRC(this.dataBuffer, 6, this.dataLen + 9); dataView.setUint16(9 + this.dataLen, crc); return new Uint8Array(this.dataBuffer.buffer, 0, this.dataLen + 11); } Packet.prototype.fromDataBuffer = function(buffer) { this.dataBuffer.set(buffer); var dataView = new DataView(this.dataBuffer.buffer); this.type = dataView.getUint8(6); var len = dataView.getUint16(7); this.dataLen = len - 2; this.data = new Uint8Array(buffer.buffer, 9, this.dataLen); if (this.type == PacketTypeCmdResp) { this.result = this.data[0]; } return this; } //////////////////////////////////////////////////////// //////////////////////////////////////////////////////// var curDevice = null; function tryHandshake(device) { return new Promise((resolve, reject) =>{ logln("try handshake with " + device.path); var connection = new SerialConnection(); var isValidDevice = 0; device.bitrate = 57600; connection.onConnect.addListener(function() { logln("device " + device.path + " connected"); connection.send(new Packet()); }); connection.onReceive.addListener(function(packet) { if (packet.type == PacketTypeCmdResp && packet.result == 0) { isValidDevice = 1; connection.disconnect(); resolve(1) } }); connection.connect(device.path, device.bitrate); setTimeout(() => { if (isValidDevice) { }else { device.bitrate = 115200; connection.update({ bitrate: device.bitrate }, (result) => { if (result) { connection.send(new Packet()); setTimeout(() => { connection.disconnect(); if (!isValidDevice) { resolve(0); } }, 500); }else { connection.disconnect(); resolve(0); } }) } }, 500); }) } async function checkDevices(devices) { for (var device of devices) { logln("found device, path = " + device.path); var res = await tryHandshake(device); if (res) { curDevice = device; logln("found valid device " + device.path); break; } } } //////////////////////////////////////////////////////// var EnrollSchedule = function(device, enrollCount=6, callback=((err=null, step=0, state=0, data=null)=>{}), timeout = 60 * 1000) { this.step = 0; this.enrollCount = enrollCount; this.callback = callback; this.device = device; this.connection = null; this.timeout = timeout; this.timeoutFlag = 0; this.responseTime = 1000; this.responseTimeout = 0; this.responseCallback = null; this.canceled = false; } const STATE_WAIT_FINGER_DOWN = 1; const STATE_WAIT_FINGER_LEAVE = 2; const STATE_FINGER_DOWN = 3; const STATE_FINGER_LEAVE = 4; const STATE_EXTRACT_TEMPLATE = 5; const STATE_DOWNLOAD_TEMPLATE = 6; const STATE_EXTRACT_TEMPLATE_FAIL = 100; const STATE_EXTRACT_TEMPLATE_DIRTY = 101; const STATE_EXTRACT_TEMPLATE_POINTS_FEW = 102; const STATE_EXTRACT_TEMPLATE_MERGE_FAIL = 103; const ErrorReceiveDataErr = 1; const ErrorEnrollTimeout = 2; const ErrorDeviceNotFound = 3; const ErrorEmptyFail = 4; function printError(err) { logln( "enroll err with code: " + err); } EnrollSchedule.prototype.start = async function() { this.step = 0; this.canceled = false; try { await this._connect(); if(this.canceled) { return this._disConnect(); } var ret = await this._sendAndWaitRecvCmd(0x0d); if (ret == 0x11) { throw ErrorEmptyFail; } while(this.step < this.enrollCount) { this.step += 1; ret = await this._enrollOne(this.step); if (ret) { let stateErr = STATE_EXTRACT_TEMPLATE_FAIL; if(ret == 0x06) { stateErr = STATE_EXTRACT_TEMPLATE_DIRTY; }else if (ret == 0x07) { stateErr = STATE_EXTRACT_TEMPLATE_POINTS_FEW; }else if (ret == 0x0a) { stateErr = STATE_EXTRACT_TEMPLATE_MERGE_FAIL; } this.callback(null, this.step, stateErr, null); this.step -= 1; } if(this.canceled) { return this._disConnect(); } } logln("receive tempalte"); this.callback(null, this.step, STATE_DOWNLOAD_TEMPLATE, null); var ret = await this._mergeTemplate(); if(this.canceled) { return this._disConnect(); } var recvBuffer = await this._receiveTemplate(); if(this.canceled) { return this._disConnect(); } this.callback(null, this.step, 0, recvBuffer); }catch(e) { this.callback(e, this.step, 0, null); } this._disConnect(); } EnrollSchedule.prototype._mergeTemplate = function() { return this._sendAndWaitRecvCmd(0x05); } EnrollSchedule.prototype._receiveTemplate = function() { return new Promise((resolve, reject) => { var that = this; var data = Uint8Array.of(0x08, 0x01); var packet = new Packet(data, 2); var recvDataLen = 4 * 1024 * 1024; var recvData = new Uint8Array(recvDataLen); var recvDataCursor = 0; var resetTimeoutCheck = function() { if (that.responseTimeout) { clearTimeout(that.responseTimeout); } that.responseTimeout = setTimeout(() => { var dummy = new Packet(); that.connection.send(dummy); that.responseTimeout = setTimeout(() => { reject(ErrorReceiveDataErr); }, that.responseTime); }, that.responseTime); } this.responseCallback = (packet) => { resetTimeoutCheck(); if (packet.type == PacketTypeCmdResp) { }else if (packet.type == PacketTypeData || packet.type == PacketTypeDataEnd) { if (recvDataCursor + packet.dataLen < recvDataLen) { recvData.set(packet.data, recvDataCursor); recvDataCursor += packet.dataLen; }else { if (that.responseTimeout) { clearTimeout(that.responseTimeout); } reject("recv buffer full"); } } if (packet.type == PacketTypeDataEnd) { if (that.responseTimeout) { clearTimeout(that.responseTimeout); } resolve(recvData.slice(0, recvDataCursor)); } } this.connection.send(packet); resetTimeoutCheck(); }); } EnrollSchedule.prototype._sendAndWaitRecvCmd = function(cmd) { return new Promise((resolve, reject) =>{ var packet = new Packet(); packet.setCmd(cmd); this._sendAndWaitRecv(packet).then(packet => { if (packet.type == PacketTypeCmdResp) { resolve(packet.result); } }).catch(err => { reject(err); }); }); } EnrollSchedule.prototype._enrollOne = async function(step) { var down = false; var leave = false; var isTimeout = false; this.timeoutFlag = setTimeout(() => { if (!down) { isTimeout = true; } }, this.timeout); //wait finger leave logln("wait leave") this.callback(null, this.step, STATE_WAIT_FINGER_LEAVE, null); while (leave == false && !isTimeout) { if(this.canceled) { return; } leave = !(await this.captureOne()); } this.callback(null, this.step, STATE_FINGER_LEAVE, null); if (isTimeout) { throw ErrorEnrollTimeout; } this.callback(null, this.step, STATE_WAIT_FINGER_DOWN, null); //wait finger down logln("wait down") while (!down && !isTimeout) { if(this.canceled) { return; } down = await this.captureOne(); } if (isTimeout) { throw ErrorEnrollTimeout; } this.callback(null, this.step, STATE_FINGER_DOWN, null); logln("finger down step = " + step); if(this.canceled) { return; } this.callback(null, this.step, STATE_EXTRACT_TEMPLATE, null); var ret = await this.extractOne(step); if (ret != 0) { logln("extract tempate err " + ret); return ret; } return 0; } EnrollSchedule.prototype.extractOne = function(step) { return new Promise((resolve, reject) =>{ var data = Uint8Array.of(0x02, step); var packet = new Packet(data, 2); this._sendAndWaitRecv(packet).then(packet => { if (packet.type == PacketTypeCmdResp) { resolve(packet.result); } }).catch(err => { reject(err); }); }); } EnrollSchedule.prototype.captureOne = function() { return new Promise((resolve, reject) => { var packet = new Packet(); packet.setCmd(0x01); this._sendAndWaitRecv(packet).then(packet => { if (packet.type == PacketTypeCmdResp) { resolve(packet.result == 0); } }).catch(err => { reject(err); }); }); } EnrollSchedule.prototype._sendAndWaitRecv = function(packet) { return new Promise((resolve, reject) => { var that = this; this.responseCallback = (packet) => { if (packet.type == PacketTypeCmdResp) { if (that.responseTimeout) { clearTimeout(that.responseTimeout); that.responseTimeout = 0; } if (packet.result == 0x01) { reject(ErrorReceiveDataErr); }else { resolve(packet); } } } this.connection.send(packet); this.responseTimeout = setTimeout(() => { that.connection.send(packet); this.responseTimeout = setTimeout(() => { reject(ErrorReceiveDataErr); }, this.responseTime); }, this.responseTime); }); } EnrollSchedule.prototype._resolvePacket = function(packet) { if (this.responseCallback) { this.responseCallback(packet); } } EnrollSchedule.prototype._connect = function() { return new Promise((resolve, reject) => { var that = this; this.connection = new SerialConnection(); this.connection.onConnect.addListener(function() { logln("device " + that.device.path + " connected"); resolve(); }); this.connection.onReceive.addListener(function(packet) { that._resolvePacket(packet); }); this.connection.connect(this.device.path, this.device.bitrate); setTimeout(() => { reject("connect timeout"); }, 500); }); } EnrollSchedule.prototype._disConnect = function() { if (this.connection) { this.connection.disconnect(); this.connection = null; } if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = 0; } if (this.timeoutFlag) { clearTimeout(this.timeoutFlag); this.timeoutFlag = 0; } } EnrollSchedule.prototype.stop = function() { this.canceled = true; } var enrollSchedule = null; var tempData = null; function startEnroll(enrollCount=6, timeout=10*1000, cb) { if (!curDevice) { logln("device not found"); cb.call(null, { err: ErrorDeviceNotFound }, 0, null); return; } enrollSchedule = new EnrollSchedule(curDevice, enrollCount, (err, step, state, data) => { if (err) { printError(status.err); } if(cb) { cb.call(null, { err: err, state: state, step: step, data: data }); } if (data) { tempData = data; } }, timeout); logln("start enroll"); enrollSchedule.start(); } function stopEnroll() { if (enrollSchedule) { enrollSchedule.stop(); enrollSchedule = null; } } //////////////////////////////////////////////////////// //test EnrollSchedule.prototype.sleep = function(time) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, time); }); } EnrollSchedule.prototype.downloadTemplate = async function(data) { var cmdData = Uint8Array.of(0x09, 0x02); var packet = new Packet(cmdData, 2); this.connection.send(packet); await this.sleep(1000); var dataLength = data.length; var packetLen = 256; var sendCursor = 0; var sendLen = 0; var sendType = PacketTypeData; logln("downloading"); while (sendCursor < dataLength) { sendLen = (dataLength-sendCursor > packetLen) ? packetLen : (dataLength-sendCursor); sendType = (dataLength-sendCursor > packetLen) ? PacketTypeData : PacketTypeDataEnd; var sendData = new Uint8Array(data.buffer, sendCursor, sendLen); var dataPack = new Packet(sendData, sendLen, sendType); this.connection.send(dataPack); console.log("send "+ sendLen) log('.'); sendCursor += sendLen; await this.sleep(100); } cmdData = Uint8Array.of(0x06, 0x02, 0x00, 0x03); packet = new Packet(cmdData, 4); this.connection.send(packet); logln('.'); await this.sleep(500); } async function downloadTemplate() { if (!curDevice) { var err = "device not found"; logln(err); return; } if (!tempData) { var err = "please enroll"; logln(err); return; } var enroll = new EnrollSchedule(curDevice); try { await enroll._connect(); await enroll.downloadTemplate(tempData); logln("download complete"); }catch(err) { logln("download err: " + err); } await enroll._disConnect(); } async function matchTemplate() { if (!curDevice) { var err = "device not found"; logln(err); return; } var enroll = new EnrollSchedule(curDevice); try { await enroll._connect(); await enroll._enrollOne(1); var packData = Uint8Array.of(0x04, 0x01, 0x00, 0x03, 0x00, 0x03); var packet = new Packet(packData, 6); await new Promise((resolve, reject) =>{ enroll._sendAndWaitRecv(packet).then(packet => { if (packet.type == PacketTypeCmdResp) { if(packet.result) { logln("match fail"); }else { logln("match success score " + (packet.data[3] * 256 + packet.data[4])); } resolve(); } }).catch(err => { reject("match err: " + err); }); }) }catch(err) { logln("download err: " + err); } await enroll._disConnect(); } //////////////////////////////////////////////////////// //////////////////////////////////////////////////////// function disableAll() { document.querySelector("#enroll").disabled = true; document.querySelector("#down").disabled = true; document.querySelector("#match").disabled = true; document.querySelector("#refresh").disabled = true; document.querySelector("#export").disabled = true; document.querySelector("#import").disabled = true; } function enableAll() { document.querySelector("#enroll").disabled = false; document.querySelector("#down").disabled = false; document.querySelector("#match").disabled = false; document.querySelector("#refresh").disabled = false; document.querySelector("#export").disabled = false; document.querySelector("#import").disabled = false; } var server = null; onload = function() { document.querySelector('#show-log').checked = showDebugLog; document.querySelector('#show-log').onchange = function(e) { showDebugLog = e.target.checked; } document.querySelector('#enroll').onclick = function(e) { if (e.currentTarget.innerText == "Enroll") { e.currentTarget.innerText = "Stop"; startEnroll(6, 10* 1000, (status) => { console.log(status.step); if (status.err) { console.log(status.err); } if (status.err || status.data) { document.querySelector('#enroll').innerText = "Enroll"; } if (status.data) { // console.log(data); // console.log(buf2hex(data.buffer)); } }); }else { e.currentTarget.innerText = "Enroll"; stopEnroll(); } } document.querySelector('.msg').style="height:" + (window.innerHeight - 70) + "px"; chrome.serial.getDevices((devices) => { checkDevices(devices); }); document.querySelector("#down").onclick = async function(e) { disableAll(); try { await downloadTemplate(); }catch(err) { logln("download err: " + err); } enableAll(); } document.querySelector("#match").onclick = async function(e) { disableAll(); try { await matchTemplate(); }catch(err) { logln("match err: " + err); } enableAll(); } document.querySelector("#refresh").onclick = function(e) { disableAll(); chrome.serial.getDevices(async function(devices) { try { await checkDevices(devices); }catch (e) { console.log(e) } enableAll(); }); } document.querySelector("#export").onclick = function(e) { if (!tempData) { logln("please enroll"); return; } let reader = new FileReader(); reader.onload = function(eve) { if (eve.target.readyState == FileReader.DONE) { let a = document.createElement('a'); a.download = "mafp_template.bin"; a.href = this.result; a.click(); } } reader.readAsDataURL(new Blob([tempData])); } document.querySelector("#import").onclick = function(e) { let fileInput = document.querySelector("#import-file"); fileInput.value = ""; fileInput.click(); } document.querySelector("#import-file").onchange = function(e) { let files = e.target.files; if (files && files.length) { let reader = new FileReader(); reader.onload = async function(ev) { if (ev.total == 4096) { tempData = new Uint8Array(this.result); disableAll(); try { await downloadTemplate(); }catch(err) { logln("download err: " + err); } enableAll(); }else { logln("invalid file length " + ev.total); } } reader.readAsArrayBuffer(files[0]); } } const port = 9897; if (http.Server && http.WebSocketServer) { server = new http.Server(); var wsServer = new http.WebSocketServer(server); server.listen(port); logln("ws socket server listen at " + port); var connectedSocket = null; wsServer.addEventListener('request', function(req) { if (connectedSocket) { req.reject(); return; } console.log('Client connected'); var socket = req.accept(); connectedSocket = socket; socket.addEventListener('message', function(e) { var reqData = JSON.parse(e.data); if (reqData.cmd == "enrollStart") { startEnroll(reqData.config.enrollCount, reqData.config.enrollTimeout, (status) =>{ var resp = { err: status.err ? status.err : "", state: status.state ? status.state : 0, step: status.step } if (status.data) { resp.data = Array.from(status.data); } connectedSocket.send(JSON.stringify(resp)); }); }else if (reqData.cmd == "enrollCancel") { stopEnroll(); } }); socket.addEventListener('close', function() { connectedSocket = null; stopEnroll(); console.log('Client disconnected'); if (chrome.runtime.lastError != undefined) { console.log(chrome.runtime.lastError.message); } }); return true; }); } }; onresize = function(e) { document.querySelector('.msg').style="height:" + (e.currentTarget.innerHeight - 70) + "px"; document.querySelector('#msg_end').scrollIntoView(); }