diff --git a/lib/roles/app.js b/lib/roles/app.js index 2ce54a93..f4cbede5 100644 --- a/lib/roles/app.js +++ b/lib/roles/app.js @@ -79,6 +79,7 @@ module.exports = function(options) { var whitelist = { 'devices/index': true , 'devices/control': true + , 'devices/screen': true } if (whitelist.hasOwnProperty(req.params[0])) { diff --git a/lib/roles/device.js b/lib/roles/device.js index dce7afdd..d94d1fa7 100644 --- a/lib/roles/device.js +++ b/lib/roles/device.js @@ -355,27 +355,51 @@ module.exports = function(options) { }) .on(wire.TouchDownMessage, function(channel, message) { services.input.touchDownAsync(message.x, message.y) + .catch(function(err) { + log.error('touchDown failed', err.stack) + }) }) .on(wire.TouchMoveMessage, function(channel, message) { services.input.touchMoveAsync(message.x, message.y) + .catch(function(err) { + log.error('touchMove failed', err.stack) + }) }) .on(wire.TouchUpMessage, function(channel, message) { services.input.touchUpAsync(message.x, message.y) + .catch(function(err) { + log.error('touchUp failed', err.stack) + }) }) .on(wire.TapMessage, function(channel, message) { services.input.tapAsync(message.x, message.y) + .catch(function(err) { + log.error('tap failed', err.stack) + }) }) .on(wire.TypeMessage, function(channel, message) { services.monkey.typeAsync(message.text) + .catch(function(err) { + log.error('type failed', err.stack) + }) }) .on(wire.KeyDownMessage, function(channel, message) { services.monkey.keyDownAsync(message.key) + .catch(function(err) { + log.error('keyDown failed', err.stack) + }) }) .on(wire.KeyUpMessage, function(channel, message) { services.monkey.keyUpAsync(message.key) + .catch(function(err) { + log.error('keyUp failed', err.stack) + }) }) .on(wire.KeyPressMessage, function(channel, message) { services.monkey.pressAsync(message.key) + .catch(function(err) { + log.error('keyPress failed', err.stack) + }) }) .on(wire.ShellCommandMessage, function(channel, message) { log.info('Running shell command "%s"', message.command.join(' ')) diff --git a/res/app/scripts/controllers/DeviceControlCtrl.js b/res/app/scripts/controllers/DeviceControlCtrl.js index 22f6f679..0f8df94e 100644 --- a/res/app/scripts/controllers/DeviceControlCtrl.js +++ b/res/app/scripts/controllers/DeviceControlCtrl.js @@ -3,10 +3,11 @@ define(['./_module'], function(app) { $scope.device = null $scope.control = null - deviceService.get($routeParams.serial) + $scope.promiseOfDevice = deviceService.get($routeParams.serial) .then(function(device) { $scope.device = device $scope.control = controlService.forChannel(device.channel) + return device }) } diff --git a/res/app/scripts/controllers/DeviceScreenCtrl.js b/res/app/scripts/controllers/DeviceScreenCtrl.js new file mode 100644 index 00000000..4ee43991 --- /dev/null +++ b/res/app/scripts/controllers/DeviceScreenCtrl.js @@ -0,0 +1,140 @@ +define(['./_module'], function(app) { + function DeviceScreenCtrl($scope, scalingService) { + $scope.ready = false + $scope.displayError = false + $scope.scalingService = scalingService + + $scope.promiseOfDevice.then(function() { + $scope.ready = true + }) + } + + function DeviceScreenDirective($document, scalingService) { + return { + restrict: 'E' + , templateUrl: 'partials/devices/screen' + , link: function($scope, element, attrs) { + $scope.promiseOfDevice.then(function(device) { + var loader = new Image() + , canvas = element.find('canvas')[0] + , g = canvas.getContext('2d') + , displayWidth = 0 + , displayHeight = 0 + , scaler = scalingService.coordinator( + device.display.width + , device.display.height + ) + + function updateDisplaySize() { + displayWidth = element[0].offsetWidth + displayHeight = element[0].offsetHeight + + // Developer error, let's try to reduce debug time + if (!displayWidth || !displayHeight) { + throw new Error( + 'Unable to update display size; container must have dimensions' + ) + } + } + + function loadScreen() { + loader.src = device.display.url + + '?width=' + displayWidth + + '&height=' + displayHeight + + '&time=' + Date.now() + } + + loader.onload = function() { + var size = scaler.projectedSize(displayWidth, displayHeight) + + // Make sure we're rendering pixels 1 to 1 + canvas.width = this.width + canvas.height = this.height + + // Perhaps we have a massive screen but not enough pixels. Let's + // scale up + canvas.style.width = size.width + 'px' + canvas.style.height = size.height + 'px' + + // Draw the image + g.drawImage(this, 0, 0) + + // Reset error, if any + if ($scope.displayError) { + $scope.$apply(function() { + $scope.displayError = false + }) + } + + // Next please + loadScreen() + } + + loader.onerror = function() { + $scope.$apply(function() { + $scope.displayError = true + }) + } + + function sendTouch(type, e) { + var scaled = scaler.coords( + displayWidth + , displayHeight + , e.offsetX + , e.offsetY + ) + + $scope.control[type]( + scaled.xP * device.display.width + , scaled.yP * device.display.height + ) + } + + function downListener(e) { + e.preventDefault() + sendTouch('touchDown', e) + element.bind('mousemove', moveListener) + $document.bind('mouseup', upListener) + $document.bind('mouseleave', upListener) + } + + function moveListener(e) { + sendTouch('touchMove', e) + } + + function upListener(e) { + sendTouch('touchUp', e) + stop() + } + + function stop() { + element.unbind('mousemove', moveListener) + $document.unbind('mouseup', upListener) + $document.unbind('mouseleave', upListener) + } + + $scope.$on('$destroy', function() { + loader.onload = loader.onerror = null + $document.unbind('mouseup', upListener) + $document.unbind('mouseleave', upListener) + }) + + element.bind('mousedown', downListener) + updateDisplaySize() + loadScreen() + }) + } + } + } + + app.controller('DeviceScreenCtrl' + , [ '$scope' + , 'ScalingService' + , DeviceScreenCtrl + ]) + .directive('deviceScreen' + , [ '$document' + , 'ScalingService' + , DeviceScreenDirective + ]) +}) diff --git a/res/app/scripts/controllers/_index.js b/res/app/scripts/controllers/_index.js index 0b7420f5..f541f004 100644 --- a/res/app/scripts/controllers/_index.js +++ b/res/app/scripts/controllers/_index.js @@ -1,6 +1,7 @@ define([ './DeviceListCtrl' , './DeviceControlCtrl' + , './DeviceScreenCtrl' ] , function() { } diff --git a/res/app/scripts/services/ScalingService.js b/res/app/scripts/services/ScalingService.js new file mode 100644 index 00000000..639d19c8 --- /dev/null +++ b/res/app/scripts/services/ScalingService.js @@ -0,0 +1,128 @@ +define(['./_module'], function(app) { + function ScalingServiceFactory() { + var scalingService = { + } + + scalingService.coordinator = function(realWidth, realHeight) { + var realRatio = realWidth / realHeight + + return { + coords: function(width, height, x, y) { + var ratio = width / height + , scaledValue + + if (realRatio > ratio) { + // covers the area horizontally + scaledValue = width / realRatio; + + // adjust y to start from the scaled top edge + y -= (height - scaledValue) / 2 + + // not touching the screen, but we want to trigger certain events + // (like touchup) anyway, so let's do it on the edges. + if (y < 0) { + y = 0 + } + else if (y > scaledValue) { + y = scaledValue + } + + // make sure x is within bounds too + if (x < 0) { + x = 0 + } + else if (x > width) { + x = width + } + + height = scaledValue + } + else { + // covers the area vertically + scaledValue = height * realRatio + + // adjust x to start from the scaled left edge + x -= (width - scaledValue) / 2 + + // not touching the screen, but we want to trigger certain events + // (like touchup) anyway, so let's do it on the edges. + if (x < 0) { + x = 0 + } + else if (x > scaledValue) { + x = scaledValue + } + + // make sure y is within bounds too + if (y < 0) { + y = 0 + } + else if (y > height) { + y = height + } + + width = scaledValue + } + + return { + xP: x / width + , yP: y / height + } + } + , size: function(width, height) { + var ratio = width / height + + if (realRatio > ratio) { + // covers the area horizontally + + if (width >= realWidth) { + // don't go over max size + width = realWidth + height = realHeight + } + else { + height = Math.floor(width / realRatio) + } + } + else { + // covers the area vertically + + if (height >= realHeight) { + // don't go over max size + height = realHeight + width = realWidth + } + else { + width = Math.floor(height * realRatio) + } + } + + return { + width: width + , height: height + } + } + , projectedSize: function(width, height) { + var ratio = width / height + + if (realRatio > ratio) { + // covers the area horizontally + height = Math.floor(width / realRatio) + } + else { + width = Math.floor(height * realRatio) + } + + return { + width: width + , height: height + } + } + } + } + + return scalingService + } + + app.factory('ScalingService', [ScalingServiceFactory]) +}) diff --git a/res/app/scripts/services/_index.js b/res/app/scripts/services/_index.js index c96d8997..c786de45 100644 --- a/res/app/scripts/services/_index.js +++ b/res/app/scripts/services/_index.js @@ -4,6 +4,7 @@ define([ , './GroupService' , './UserService' , './ControlService' + , './ScalingService' ] , function() { } diff --git a/res/app/views/partials/devices/control.jade b/res/app/views/partials/devices/control.jade index 1f2c4322..8f403bcb 100644 --- a/res/app/views/partials/devices/control.jade +++ b/res/app/views/partials/devices/control.jade @@ -1,5 +1,23 @@ h1 {{ device.serial }} +style. + device-screen { + position: relative; + display: block; + } + device-screen canvas { + position: absolute; + margin: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; /* MUST HAVE */ + } + +div(ng-controller='DeviceScreenCtrl') + device-screen(style='width: 400px; height: 600px; background: gray') + button(ng-click='control.menu()') Menu button(ng-click='control.home()') Home button(ng-click='control.back()') Back diff --git a/res/app/views/partials/devices/screen.jade b/res/app/views/partials/devices/screen.jade new file mode 100644 index 00000000..ba2b16b6 --- /dev/null +++ b/res/app/views/partials/devices/screen.jade @@ -0,0 +1,2 @@ +canvas(ng-show='ready') +div(ng-if='displayError') Screen error