From 80dd536e6307228397eaff4753404906be6cced2 Mon Sep 17 00:00:00 2001 From: Calin Crisan <ccrisan@gmail.com> Date: Sun, 6 Oct 2013 12:45:05 +0300 Subject: [PATCH] the motion remote control is now working --- doc/todo.txt | 38 ++++---- src/config.py | 1 - src/handlers.py | 214 +++++++++++++++++++++++++++++++--------------- src/remote.py | 47 ++++++++++ static/js/main.js | 97 ++++++++------------- 5 files changed, 250 insertions(+), 147 deletions(-) diff --git a/doc/todo.txt b/doc/todo.txt index fd41383..5a092c5 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -1,29 +1,33 @@ --> move/copy list available resolutions code to get_config --> bug: adding a remote device does not provide the available resolutions - --> bug: if updating a remote camera config, local motion will get restarted --> bug: when renaming the camera, the left/right texts set "Camera" should also change --> make camera frames positions configurable +-> add a motion running status indicator (and maybe a start/stop button) +-> add a timeout checker to check the running status of motion + +-> better ajax error handling +-> add a messaging mechanism + +-> authentication + -> hide horrible 404 image on cameras --> prevent Request closed errors by stopping mjpg clients before stopping motion -> camera not available background and icon design + +-> prevent Request closed errors by stopping mjpg clients before stopping motion +-> make all the server http requests async -> remove current snapshot GET logs --> add a motion running status indicator (and maybe a start/stop button) --> add a timeout checker to check the running status of motion --> group @config rules to top --> browser compatibility test --> requirements test + -> style scroll bars -> hint text next to section titles -> clickable hints --> authentication --> better ajax error handling --> proxy for slave motioneyes + +-> make camera frames positions configurable -> add a view log functionality -> click to zoom on cameras -> add a previewer for movies -> add a previewer for snapshots -> add a motioneye.svg icon + +-> add other options applicable only to special devices (rpi): wifi settings, notifications +-> group @config rules to top + +-> browser compatibility test +-> requirements test + -> other todos --> add a messaging mechanism --> add other options applicable only to special devices (rpi): wifi settings, notifications \ No newline at end of file diff --git a/src/config.py b/src/config.py index 3dae27d..7c7306a 100644 --- a/src/config.py +++ b/src/config.py @@ -207,7 +207,6 @@ def set_camera(camera_id, data): main_config['thread'] = threads - del data['@enabled'] if '@id' in data: del data['@id'] diff --git a/src/handlers.py b/src/handlers.py index b81e84d..8cccbc9 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -104,8 +104,7 @@ class ConfigHandler(BaseHandler): local_data = camera_config camera_config = self._camera_ui_to_dict(remote_ui_config) - camera_config['@proto'] = local_data['@proto'] - camera_config['@enabled'] = local_data['@enabled'] + camera_config.update(local_data) ui_config = self._camera_dict_to_ui(camera_config) @@ -125,16 +124,29 @@ class ConfigHandler(BaseHandler): ui_config = self._main_dict_to_ui(config.get_main()) self.finish_json(ui_config) - def set_config(self, camera_id): - try: - ui_config = json.loads(self.request.body) - - except Exception as e: - logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)}) - - raise + def set_config(self, camera_id, ui_config=None): + if ui_config is None: + try: + ui_config = json.loads(self.request.body) + + except Exception as e: + logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)}) + + raise - if camera_id: + if camera_id is not None: + if camera_id == 0: + logging.debug('setting multiple configs') + + for key, cfg in ui_config.items(): + if key == 'main': + self.set_config(None, cfg) + + else: + self.set_config(int(key), cfg) + + return + logging.debug('setting config for camera %(id)s' % {'id': camera_id}) camera_ids = config.get_camera_ids() @@ -143,18 +155,34 @@ class ConfigHandler(BaseHandler): camera_config = config.get_camera(camera_id) if camera_config['@proto'] == 'v4l2': + ui_config.setdefault('device', camera_config.get('videodevice', '')) + ui_config.setdefault('proto', camera_config['@proto']) + ui_config.setdefault('enabled', camera_config['@enabled']) + camera_config = self._camera_ui_to_dict(ui_config) - camera_config['@proto'] = 'v4l2' config.set_camera(camera_id, camera_config) - else: - remote.set_config( - camera_config.get('@host'), - camera_config.get('@port'), - camera_config.get('@username'), - camera_config.get('@password'), - camera_config.get('@remote_camera_id'), - ui_config) + else: # remote camera + # update the camera locally + camera_config['@enabled'] = ui_config['enabled'] + config.set_camera(camera_id, camera_config) + + # remove the fields that should not get to the remote side + del ui_config['device'] + del ui_config['proto'] + del ui_config['enabled'] + + try: + remote.set_config( + camera_config.get('@host'), + camera_config.get('@port'), + camera_config.get('@username'), + camera_config.get('@password'), + camera_config.get('@remote_camera_id'), + ui_config) + + except Exception as e: + return self.finish_json({'error': unicode(e)}) else: logging.debug('setting main config') @@ -162,8 +190,7 @@ class ConfigHandler(BaseHandler): main_config = self._main_ui_to_dict(ui_config) config.set_main(main_config) - if not ui_config.get('norestart'): - motionctl.restart() + motionctl.restart() def set_preview(self, camera_id): try: @@ -175,31 +202,45 @@ class ConfigHandler(BaseHandler): raise camera_config = config.get_camera(camera_id) - device = camera_config['videodevice'] - - if 'brightness' in controls: - value = int(controls['brightness']) - logging.debug('setting brightness to %(value)s...' % {'value': value}) - - v4l2ctl.set_brightness(device, value) - - if 'contrast' in controls: - value = int(controls['contrast']) - logging.debug('setting contrast to %(value)s...' % {'value': value}) - - v4l2ctl.set_contrast(device, value) - - if 'saturation' in controls: - value = int(controls['saturation']) - logging.debug('setting saturation to %(value)s...' % {'value': value}) - - v4l2ctl.set_saturation(device, value) - - if 'hue' in controls: - value = int(controls['hue']) - logging.debug('setting hue to %(value)s...' % {'value': value}) + if camera_config['@proto'] == 'v4l2': + device = camera_config['videodevice'] + + if 'brightness' in controls: + value = int(controls['brightness']) + logging.debug('setting brightness to %(value)s...' % {'value': value}) + + v4l2ctl.set_brightness(device, value) + + if 'contrast' in controls: + value = int(controls['contrast']) + logging.debug('setting contrast to %(value)s...' % {'value': value}) + + v4l2ctl.set_contrast(device, value) + + if 'saturation' in controls: + value = int(controls['saturation']) + logging.debug('setting saturation to %(value)s...' % {'value': value}) + + v4l2ctl.set_saturation(device, value) + + if 'hue' in controls: + value = int(controls['hue']) + logging.debug('setting hue to %(value)s...' % {'value': value}) + + v4l2ctl.set_hue(device, value) - v4l2ctl.set_hue(device, value) + else: + try: + remote.set_preview( + camera_config['@host'], + camera_config['@port'], + camera_config['@username'], + camera_config['@password'], + camera_config['@remote_camera_id'], + controls) + + except Exception as e: + self.finish_json({'error': unicode(e)}) def list_cameras(self): logging.debug('listing cameras') @@ -209,7 +250,7 @@ class ConfigHandler(BaseHandler): username = self.get_argument('username', None) password = self.get_argument('password', None) - if host: # remote listing + if host: # remote listing try: cameras = remote.list_cameras(host, port, username, password) @@ -221,11 +262,11 @@ class ConfigHandler(BaseHandler): for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) if camera_config['@proto'] == 'v4l2': - name = camera_config['@name'] - - else: # remote camera + ui_config = self._camera_dict_to_ui(camera_config) + + else: # remote camera try: - remote_camera_config = remote.get_config( + remote_ui_config = remote.get_config( camera_config.get('@host'), camera_config.get('@port'), camera_config.get('@username'), @@ -235,9 +276,11 @@ class ConfigHandler(BaseHandler): except: continue - name = remote_camera_config['name'] + ui_config = remote_ui_config + ui_config['id'] = camera_id + ui_config['enabled'] = camera_config['@enabled'] # override the enabled status - cameras.append({'name': name, 'id': camera_id}) + cameras.append(ui_config) self.finish_json({'cameras': cameras}) @@ -247,8 +290,9 @@ class ConfigHandler(BaseHandler): configured_devices = {} for camera_id in config.get_camera_ids(): data = config.get_camera(camera_id) - configured_devices[data['videodevice']] = True - + if data['@proto'] == 'v4l2': + configured_devices[data['videodevice']] = True + devices = [{'device': d[0], 'name': d[1], 'configured': d[0] in configured_devices} for d in v4l2ctl.list_devices()] @@ -293,8 +337,7 @@ class ConfigHandler(BaseHandler): local_data = camera_config camera_config = self._camera_ui_to_dict(remote_ui_config) - camera_config['@enabled'] = local_data['@enabled'] - camera_config['@proto'] = local_data['@proto'] + camera_config.update(local_data) camera_config['@id'] = camera_id @@ -340,14 +383,16 @@ class ConfigHandler(BaseHandler): } def _camera_ui_to_dict(self, ui): - if not ui.get('resolution'): # avoid errors for empty resolution setting + if not ui.get('resolution'): # avoid errors for empty resolution setting ui['resolution'] = '352x288' data = { # device '@name': ui.get('name', ''), '@enabled': ui.get('enabled', False), - 'lightswitch': int(ui.get('light_switch_detect', False) * 5), + '@proto': ui.get('proto', 'v4l2'), + 'videodevice': ui.get('device', ''), + 'lightswitch': int(ui.get('light_switch_detect', False)) * 5, 'auto_brightness': ui.get('auto_brightness', False), 'brightness': max(1, int(round(int(ui.get('brightness', 0)) * 2.55))), 'contrast': max(1, int(round(int(ui.get('contrast', 0)) * 2.55))), @@ -457,23 +502,35 @@ class ConfigHandler(BaseHandler): if ui.get('working_schedule', False): data['@working_schedule'] = ( - ui.get('monday_from', '') + '-' + ui.get('monday_to') + '|' + - ui.get('tuesday_from', '') + '-' + ui.get('tuesday_to') + '|' + - ui.get('wednesday_from', '') + '-' + ui.get('wednesday_to') + '|' + - ui.get('thursday_from', '') + '-' + ui.get('thursday_to') + '|' + - ui.get('friday_from', '') + '-' + ui.get('friday_to') + '|' + - ui.get('saturday_from', '') + '-' + ui.get('saturday_to') + '|' + + ui.get('monday_from', '') + '-' + ui.get('monday_to') + '|' + + ui.get('tuesday_from', '') + '-' + ui.get('tuesday_to') + '|' + + ui.get('wednesday_from', '') + '-' + ui.get('wednesday_to') + '|' + + ui.get('thursday_from', '') + '-' + ui.get('thursday_to') + '|' + + ui.get('friday_from', '') + '-' + ui.get('friday_to') + '|' + + ui.get('saturday_from', '') + '-' + ui.get('saturday_to') + '|' + ui.get('sunday_from', '') + '-' + ui.get('sunday_to')) return data def _camera_dict_to_ui(self, data): + if data['@proto'] == 'v4l2': + device_uri = data['videodevice'] + + else: + device_uri = '%(host)s:%(port)s/config/%(camera_id)s' % { + 'username': data['@username'], + 'password': '***', + 'host': data['@host'], + 'port': data['@port'], + 'camera_id': data['@remote_camera_id']} + ui = { # device 'name': data['@name'], 'enabled': data['@enabled'], 'id': data.get('@id'), 'proto': data['@proto'], + 'device': device_uri, 'light_switch_detect': data.get('lightswitch') > 0, 'auto_brightness': data.get('auto_brightness'), 'brightness': int(round(int(data.get('brightness')) / 2.55)), @@ -630,12 +687,29 @@ class SnapshotHandler(BaseHandler): raise HTTPError(400, 'unknown operation') def current(self, camera_id): - jpg = mjpgclient.get_jpg(camera_id) - if jpg is None: - return self.finish() + camera_config = config.get_camera(camera_id) + if camera_config['@proto'] == 'v4l2': + jpg = mjpgclient.get_jpg(camera_id) + if jpg is None: + return self.finish() - self.set_header('Content-Type', 'image/jpeg') - self.finish(jpg) + self.set_header('Content-Type', 'image/jpeg') + self.finish(jpg) + + else: + try: + jpg = remote.current_snapshot( + camera_config['@host'], + camera_config['@port'], + camera_config['@username'], + camera_config['@password'], + camera_config['@remote_camera_id']) + + except: + return self.finish() + + self.set_header('Content-Type', 'image/jpeg') + self.finish(jpg) def list(self, camera_id): logging.debug('listing snapshots for camera %(id)s' % {'id': camera_id}) diff --git a/src/remote.py b/src/remote.py index a3ec679..a753bc0 100644 --- a/src/remote.py +++ b/src/remote.py @@ -108,3 +108,50 @@ def set_config(host, port, username, password, camera_id, data): raise + +def set_preview(host, port, username, password, camera_id, controls): + logging.debug('setting preview for remote camera %(id)s on %(host)s:%(port)s' % { + 'id': camera_id, + 'host': host, + 'port': port}) + + controls = json.dumps(controls) + + url = _compose_url(host, port, username, password, '/config/%(id)s/set_preview/' % {'id': camera_id}) + request = urllib2.Request(url, data=controls) + + try: + urllib2.urlopen(request) + + except Exception as e: + logging.error('failed to set preview for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + 'id': camera_id, + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + + +def current_snapshot(host, port, username, password, camera_id): + logging.debug('getting current snapshot for remote camera %(id)s on %(host)s:%(port)s' % { + 'id': camera_id, + 'host': host, + 'port': port}) + + url = _compose_url(host, port, username, password, '/snapshot/%(id)s/current/' % {'id': camera_id}) + request = urllib2.Request(url) + + try: + response = urllib2.urlopen(request) + + except Exception as e: + logging.error('failed to get current snapshot for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + 'id': camera_id, + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + + return response.read() diff --git a/static/js/main.js b/static/js/main.js index b4856f7..647db28 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -255,27 +255,7 @@ function initUI() { $(window).resize(updateModalDialogPosition); /* remove camera button */ - $('div.button.rem-camera-button').click(function () { - var cameraId = $('#videoDeviceSelect').val(); - if (cameraId == null || cameraId === 'add') { - runAlertDialog('No camera to remove!'); - return; - } - - var deviceName = $('#videoDeviceSelect').find('option[value=' + cameraId + ']').text(); - - runConfirmDialog('Remove device ' + deviceName + '?', function () { - showProgress(); - ajax('POST', '/config/' + cameraId + '/rem/', null, function (data) { - if (data == null || data.error) { - return; // TODO handle error - } - - hideApply(); - fetchCurrentConfig(); - }); - }); - }); + $('div.button.rem-camera-button').click(doRemCamera); } @@ -472,7 +452,8 @@ function cameraUi2Dict() { /* video device */ 'enabled': $('#videoDeviceSwitch')[0].checked, 'name': $('#deviceNameEntry').val(), - 'device': $('#deviceEntry').val(), + 'proto': $('#deviceEntry').val().split('://')[0], + 'device': $('#deviceEntry').val().split('://')[1], 'light_switch_detect': $('#lightSwitchDetectSwitch')[0].checked, 'auto_brightness': $('#autoBrightnessSwitch')[0].checked, 'brightness': $('#brightnessSlider').val(), @@ -555,7 +536,7 @@ function dict2CameraUi(dict) { /* video device */ $('#videoDeviceSwitch')[0].checked = dict['enabled']; $('#deviceNameEntry').val(dict['name']); - $('#deviceEntry').val(dict['device']); + $('#deviceEntry').val(dict['proto'] + '://' + dict['device']); $('#lightSwitchDetectSwitch')[0].checked = dict['light_switch_detect']; $('#autoBrightnessSwitch')[0].checked = dict['auto_brightness']; $('#brightnessSlider').val(dict['brightness']); @@ -715,26 +696,6 @@ function isApplyVisible() { } function doApply() { - var finishedCount = 0; - var configs = []; - - function testReady() { - if (finishedCount >= configs.length) { - endProgress(); - recreateCameraFrames(); - } - } - - for (var key in pushConfigs) { - if (pushConfigs.hasOwnProperty(key)) { - configs.push({key: key, config: pushConfigs[key]}); - } - } - - if (configs.length === 0) { - return; - } - if (!configUiValid()) { runAlertDialog('Make sure all the configuration options are valid!'); @@ -743,27 +704,46 @@ function doApply() { showProgress(); - for (var i = 0; i < configs.length; i++) { - var config = configs[i]; - if (i === configs.length - 1) { - config.config['last'] = true; // TODO not used, to be replaced by norestart + ajax('POST', '/config/0/set/', pushConfigs, function (data) { + if (data == null || data.error) { + return; // TODO handle error } - ajax('POST', '/config/' + config.key + '/set/', config.config, function (data) { - if (data == null || data.error) { - return; // TODO handle error + + /* update the camera name in the device select */ + Object.keys(pushConfigs).forEach(function (key) { + var config = pushConfigs[key]; + if (config.key !== 'main') { + $('#videoDeviceSelect').find('option[value=' + key + ']').html(config.name); } - - finishedCount++; - testReady(); }); + + pushConfigs = {}; + endProgress(); - /* update the camera name in the device select */ - if (config.key !== 'main') { - $('#videoDeviceSelect').find('option[value=' + config.key + ']').html(config.config.name); - } + recreateCameraFrames(); + }); +} + +function doRemCamera() { + var cameraId = $('#videoDeviceSelect').val(); + if (cameraId == null || cameraId === 'add') { + runAlertDialog('No camera to remove!'); + return; } + + var deviceName = $('#videoDeviceSelect').find('option[value=' + cameraId + ']').text(); - pushConfigs = {}; + runConfirmDialog('Remove device ' + deviceName + '?', function () { + showProgress(); + ajax('POST', '/config/' + cameraId + '/rem/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + + hideApply(); + fetchCurrentConfig(); + }); + }); } @@ -1218,7 +1198,6 @@ function doCloseCamera(cameraId) { } data['enabled'] = false; - data['last'] = true;// TODO not used, to be replaced by norestart ajax('POST', '/config/' + cameraId + '/set/', data, function (data) { if (data == null || data.error) { return; // TODO handle error -- 2.39.5