| var util = require('util') |
| |
| var Promise = require('bluebird') |
| var syrup = require('stf-syrup') |
| var split = require('split') |
| var EventEmitter = require('eventemitter3').EventEmitter |
| var adbkit = require('adbkit') |
| var Parser = require('adbkit/lib/adb/parser') |
| |
| var wire = require('../../../../wire') |
| var logger = require('../../../../util/logger') |
| var lifecycle = require('../../../../util/lifecycle') |
| var SeqQueue = require('../../../../wire/seqqueue') |
| var StateQueue = require('../../../../util/statequeue') |
| var RiskyStream = require('../../../../util/riskystream') |
| var FailCounter = require('../../../../util/failcounter') |
| |
| module.exports = syrup.serial() |
| .dependency(require('../../support/adb')) |
| .dependency(require('../../support/router')) |
| .dependency(require('../../resources/minitouch')) |
| .dependency(require('../util/flags')) |
| .define(function(options, adb, router, minitouch, flags) { |
| var log = logger.createLogger('device:plugins:touch') |
| |
| function TouchConsumer(config) { |
| EventEmitter.call(this) |
| this.actionQueue = [] |
| this.runningState = TouchConsumer.STATE_STOPPED |
| this.desiredState = new StateQueue() |
| this.output = null |
| this.socket = null |
| this.banner = null |
| this.touchConfig = config |
| this.starter = Promise.resolve(true) |
| this.failCounter = new FailCounter(3, 10000) |
| this.failCounter.on('exceedLimit', this._failLimitExceeded.bind(this)) |
| this.failed = false |
| this.readableListener = this._readableListener.bind(this) |
| this.writeQueue = [] |
| } |
| |
| util.inherits(TouchConsumer, EventEmitter) |
| |
| TouchConsumer.STATE_STOPPED = 1 |
| TouchConsumer.STATE_STARTING = 2 |
| TouchConsumer.STATE_STARTED = 3 |
| TouchConsumer.STATE_STOPPING = 4 |
| |
| TouchConsumer.prototype._queueWrite = function(writer) { |
| switch (this.runningState) { |
| case TouchConsumer.STATE_STARTED: |
| writer.call(this) |
| break |
| default: |
| this.writeQueue.push(writer) |
| break |
| } |
| } |
| |
| TouchConsumer.prototype.touchDown = function(point) { |
| this._queueWrite(function() { |
| return this._write(util.format( |
| 'd %s %s %s %s\n' |
| , point.contact |
| , Math.floor(this.touchConfig.origin.x(point) * this.banner.maxX) |
| , Math.floor(this.touchConfig.origin.y(point) * this.banner.maxY) |
| , Math.floor((point.pressure || 0.5) * this.banner.maxPressure) |
| )) |
| }) |
| } |
| |
| TouchConsumer.prototype.touchMove = function(point) { |
| this._queueWrite(function() { |
| return this._write(util.format( |
| 'm %s %s %s %s\n' |
| , point.contact |
| , Math.floor(this.touchConfig.origin.x(point) * this.banner.maxX) |
| , Math.floor(this.touchConfig.origin.y(point) * this.banner.maxY) |
| , Math.floor((point.pressure || 0.5) * this.banner.maxPressure) |
| )) |
| }) |
| } |
| |
| TouchConsumer.prototype.touchUp = function(point) { |
| this._queueWrite(function() { |
| return this._write(util.format( |
| 'u %s\n' |
| , point.contact |
| )) |
| }) |
| } |
| |
| TouchConsumer.prototype.touchCommit = function() { |
| this._queueWrite(function() { |
| return this._write('c\n') |
| }) |
| } |
| |
| TouchConsumer.prototype.touchReset = function() { |
| this._queueWrite(function() { |
| return this._write('r\n') |
| }) |
| } |
| |
| TouchConsumer.prototype.tap = function(point) { |
| this.touchDown(point) |
| this.touchCommit() |
| this.touchUp(point) |
| this.touchCommit() |
| } |
| |
| TouchConsumer.prototype._ensureState = function() { |
| if (this.desiredState.empty()) { |
| return |
| } |
| |
| if (this.failed) { |
| log.warn('Will not apply desired state due to too many failures') |
| return |
| } |
| |
| switch (this.runningState) { |
| case TouchConsumer.STATE_STARTING: |
| case TouchConsumer.STATE_STOPPING: |
| // Just wait. |
| break |
| case TouchConsumer.STATE_STOPPED: |
| if (this.desiredState.next() === TouchConsumer.STATE_STARTED) { |
| this.runningState = TouchConsumer.STATE_STARTING |
| this.starter = this._startService().bind(this) |
| .then(function(out) { |
| this.output = new RiskyStream(out) |
| .on('unexpectedEnd', this._outputEnded.bind(this)) |
| return this._readOutput(this.output.stream) |
| }) |
| .then(function() { |
| return this._connectService() |
| }) |
| .then(function(socket) { |
| this.socket = new RiskyStream(socket) |
| .on('unexpectedEnd', this._socketEnded.bind(this)) |
| return this._readBanner(this.socket.stream) |
| }) |
| .then(function(banner) { |
| this.banner = banner |
| return this._readUnexpected(this.socket.stream) |
| }) |
| .then(function() { |
| this._processWriteQueue() |
| }) |
| .then(function() { |
| this.runningState = TouchConsumer.STATE_STARTED |
| this.emit('start') |
| }) |
| .catch(Promise.CancellationError, function() { |
| return this._stop() |
| }) |
| .catch(function(err) { |
| return this._stop().finally(function() { |
| this.failCounter.inc() |
| this.emit('error', err) |
| }) |
| }) |
| .finally(function() { |
| this._ensureState() |
| }) |
| } |
| else { |
| setImmediate(this._ensureState.bind(this)) |
| } |
| break |
| case TouchConsumer.STATE_STARTED: |
| if (this.desiredState.next() === TouchConsumer.STATE_STOPPED) { |
| this.runningState = TouchConsumer.STATE_STOPPING |
| this._stop().finally(function() { |
| this._ensureState() |
| }) |
| } |
| else { |
| setImmediate(this._ensureState.bind(this)) |
| } |
| break |
| } |
| } |
| |
| TouchConsumer.prototype.start = function() { |
| log.info('Requesting touch consumer to start') |
| this.desiredState.push(TouchConsumer.STATE_STARTED) |
| this._ensureState() |
| } |
| |
| TouchConsumer.prototype.stop = function() { |
| log.info('Requesting touch consumer to stop') |
| this.desiredState.push(TouchConsumer.STATE_STOPPED) |
| this._ensureState() |
| } |
| |
| TouchConsumer.prototype.restart = function() { |
| switch (this.runningState) { |
| case TouchConsumer.STATE_STARTED: |
| case TouchConsumer.STATE_STARTING: |
| this.starter.cancel() |
| this.desiredState.push(TouchConsumer.STATE_STOPPED) |
| this.desiredState.push(TouchConsumer.STATE_STARTED) |
| this._ensureState() |
| break |
| } |
| } |
| |
| TouchConsumer.prototype._configChanged = function() { |
| this.restart() |
| } |
| |
| TouchConsumer.prototype._socketEnded = function() { |
| log.warn('Connection to minitouch ended unexpectedly') |
| this.failCounter.inc() |
| this.restart() |
| } |
| |
| TouchConsumer.prototype._outputEnded = function() { |
| log.warn('Shell keeping minitouch running ended unexpectedly') |
| this.failCounter.inc() |
| this.restart() |
| } |
| |
| TouchConsumer.prototype._failLimitExceeded = function(limit, time) { |
| this._stop() |
| this.failed = true |
| this.emit('error', new Error(util.format( |
| 'Failed more than %d times in %dms' |
| , limit |
| , time |
| ))) |
| } |
| |
| TouchConsumer.prototype._startService = function() { |
| log.info('Launching screen service') |
| return minitouch.run() |
| .timeout(10000) |
| } |
| |
| TouchConsumer.prototype._readOutput = function(out) { |
| out.pipe(split()).on('data', function(line) { |
| var trimmed = line.toString().trim() |
| |
| if (trimmed === '') { |
| return |
| } |
| |
| if (/ERROR/.test(line)) { |
| log.fatal('minitouch error: "%s"', line) |
| return lifecycle.fatal() |
| } |
| |
| log.info('minitouch says: "%s"', line) |
| }) |
| } |
| |
| TouchConsumer.prototype._connectService = function() { |
| function tryConnect(times, delay) { |
| return adb.openLocal(options.serial, 'localabstract:minitouch') |
| .timeout(10000) |
| .then(function(out) { |
| return out |
| }) |
| .catch(function(err) { |
| if (/closed/.test(err.message) && times > 1) { |
| return Promise.delay(delay) |
| .then(function() { |
| return tryConnect(times - 1, delay * 2) |
| }) |
| } |
| return Promise.reject(err) |
| }) |
| } |
| log.info('Connecting to minitouch service') |
| // SH-03G can be very slow to start sometimes. Make sure we try long |
| // enough. |
| return tryConnect(7, 100) |
| } |
| |
| TouchConsumer.prototype._stop = function() { |
| return this._disconnectService(this.socket).bind(this) |
| .timeout(2000) |
| .then(function() { |
| return this._stopService(this.output).timeout(10000) |
| }) |
| .then(function() { |
| this.runningState = TouchConsumer.STATE_STOPPED |
| this.emit('stop') |
| }) |
| .catch(function(err) { |
| // In practice we _should_ never get here due to _stopService() |
| // being quite aggressive. But if we do, well... assume it |
| // stopped anyway for now. |
| this.runningState = TouchConsumer.STATE_STOPPED |
| this.emit('error', err) |
| this.emit('stop') |
| }) |
| .finally(function() { |
| this.output = null |
| this.socket = null |
| this.banner = null |
| }) |
| } |
| |
| TouchConsumer.prototype._disconnectService = function(socket) { |
| log.info('Disconnecting from minitouch service') |
| |
| if (!socket || socket.ended) { |
| return Promise.resolve(true) |
| } |
| |
| socket.stream.removeListener('readable', this.readableListener) |
| |
| var endListener |
| return new Promise(function(resolve) { |
| socket.on('end', endListener = function() { |
| resolve(true) |
| }) |
| |
| socket.stream.resume() |
| socket.end() |
| }) |
| .finally(function() { |
| socket.removeListener('end', endListener) |
| }) |
| } |
| |
| TouchConsumer.prototype._stopService = function(output) { |
| log.info('Stopping minitouch service') |
| |
| if (!output || output.ended) { |
| return Promise.resolve(true) |
| } |
| |
| var pid = this.banner ? this.banner.pid : -1 |
| |
| function kill(signal) { |
| if (pid <= 0) { |
| return Promise.reject(new Error('Minitouch service pid is unknown')) |
| } |
| |
| var signum = { |
| SIGTERM: -15 |
| , SIGKILL: -9 |
| }[signal] |
| |
| log.info('Sending %s to minitouch', signal) |
| return Promise.all([ |
| output.waitForEnd() |
| , adb.shell(options.serial, ['kill', signum, pid]) |
| .then(adbkit.util.readAll) |
| .return(true) |
| ]) |
| .timeout(2000) |
| } |
| |
| function kindKill() { |
| return kill('SIGTERM') |
| } |
| |
| function forceKill() { |
| return kill('SIGKILL') |
| } |
| |
| function forceEnd() { |
| log.info('Ending minitouch I/O as a last resort') |
| output.end() |
| return Promise.resolve(true) |
| } |
| |
| return kindKill() |
| .catch(Promise.TimeoutError, forceKill) |
| .catch(forceEnd) |
| } |
| |
| TouchConsumer.prototype._readBanner = function(socket) { |
| log.info('Reading minitouch banner') |
| |
| var parser = new Parser(socket) |
| var banner = { |
| pid: -1 // @todo |
| , version: 0 |
| , maxContacts: 0 |
| , maxX: 0 |
| , maxY: 0 |
| , maxPressure: 0 |
| } |
| |
| function readVersion() { |
| return parser.readLine() |
| .then(function(chunk) { |
| var args = chunk.toString().split(/ /g) |
| switch (args[0]) { |
| case 'v': |
| banner.version = Number(args[1]) |
| break |
| default: |
| throw new Error(util.format( |
| 'Unexpected output "%s", expecting version line' |
| , chunk |
| )) |
| } |
| }) |
| } |
| |
| function readLimits() { |
| return parser.readLine() |
| .then(function(chunk) { |
| var args = chunk.toString().split(/ /g) |
| switch (args[0]) { |
| case '^': |
| banner.maxContacts = args[1] |
| banner.maxX = args[2] |
| banner.maxY = args[3] |
| banner.maxPressure = args[4] |
| break |
| default: |
| throw new Error(util.format( |
| 'Unknown output "%s", expecting limits line' |
| , chunk |
| )) |
| } |
| }) |
| } |
| |
| function readPid() { |
| return parser.readLine() |
| .then(function(chunk) { |
| var args = chunk.toString().split(/ /g) |
| switch (args[0]) { |
| case '$': |
| banner.pid = Number(args[1]) |
| break |
| default: |
| throw new Error(util.format( |
| 'Unexpected output "%s", expecting pid line' |
| , chunk |
| )) |
| } |
| }) |
| } |
| |
| return readVersion() |
| .then(readLimits) |
| .then(readPid) |
| .return(banner) |
| .timeout(2000) |
| } |
| |
| TouchConsumer.prototype._readUnexpected = function(socket) { |
| socket.on('readable', this.readableListener) |
| |
| // We may already have data pending. |
| this.readableListener() |
| } |
| |
| TouchConsumer.prototype._readableListener = function() { |
| var chunk |
| |
| while ((chunk = this.socket.stream.read())) { |
| log.warn('Unexpected output from minitouch socket', chunk) |
| } |
| } |
| |
| TouchConsumer.prototype._processWriteQueue = function() { |
| for (var i = 0, l = this.writeQueue.length; i < l; ++i) { |
| this.writeQueue[i].call(this) |
| } |
| |
| this.writeQueue = [] |
| } |
| |
| TouchConsumer.prototype._write = function(chunk) { |
| this.socket.stream.write(chunk) |
| } |
| |
| function startConsumer() { |
| var touchConsumer = new TouchConsumer({ |
| // Usually the touch origin is the same as the display's origin, |
| // but sometimes it might not be. |
| origin: (function(origin) { |
| log.info('Touch origin is %s', origin) |
| return { |
| 'top left': { |
| x: function(point) { |
| return point.x |
| } |
| , y: function(point) { |
| return point.y |
| } |
| } |
| // So far the only device we've seen exhibiting this behavior |
| // is Yoga Tablet 8. |
| , 'bottom left': { |
| x: function(point) { |
| return 1 - point.y |
| } |
| , y: function(point) { |
| return point.x |
| } |
| } |
| }[origin] |
| })(flags.get('forceTouchOrigin', 'top left')) |
| }) |
| |
| var startListener, errorListener |
| |
| return new Promise(function(resolve, reject) { |
| touchConsumer.on('start', startListener = function() { |
| resolve(touchConsumer) |
| }) |
| |
| touchConsumer.on('error', errorListener = reject) |
| |
| touchConsumer.start() |
| }) |
| .finally(function() { |
| touchConsumer.removeListener('start', startListener) |
| touchConsumer.removeListener('error', errorListener) |
| }) |
| } |
| |
| return startConsumer() |
| .then(function(touchConsumer) { |
| var queue = new SeqQueue(100, 4) |
| |
| touchConsumer.on('error', function(err) { |
| log.fatal('Touch consumer had an error', err.stack) |
| lifecycle.fatal() |
| }) |
| |
| router |
| .on(wire.GestureStartMessage, function(channel, message) { |
| queue.start(message.seq) |
| }) |
| .on(wire.GestureStopMessage, function(channel, message) { |
| queue.push(message.seq, function() { |
| queue.stop() |
| }) |
| }) |
| .on(wire.TouchDownMessage, function(channel, message) { |
| queue.push(message.seq, function() { |
| touchConsumer.touchDown(message) |
| }) |
| }) |
| .on(wire.TouchMoveMessage, function(channel, message) { |
| queue.push(message.seq, function() { |
| touchConsumer.touchMove(message) |
| }) |
| }) |
| .on(wire.TouchUpMessage, function(channel, message) { |
| queue.push(message.seq, function() { |
| touchConsumer.touchUp(message) |
| }) |
| }) |
| .on(wire.TouchCommitMessage, function(channel, message) { |
| queue.push(message.seq, function() { |
| touchConsumer.touchCommit() |
| }) |
| }) |
| .on(wire.TouchResetMessage, function(channel, message) { |
| queue.push(message.seq, function() { |
| touchConsumer.touchReset() |
| }) |
| }) |
| |
| return touchConsumer |
| }) |
| }) |