diff --git a/board/common/overlay/etc/fstab b/board/common/overlay/etc/fstab
index 0d7dc25122..a138aac079 100644
--- a/board/common/overlay/etc/fstab
+++ b/board/common/overlay/etc/fstab
@@ -6,4 +6,6 @@ tmpfs /tmp tmpfs mode=1777 0 0
sysfs /sys sysfs defaults 0 0
/dev/mmcblk0p1 /boot vfat ro,defaults 0 0
/dev/mmcblk0p3 /data ext4 defaults,noatime 0 0
+/data/output /home/ftp/sdcard rbind rbind 0 0
+/data/media /home/ftp/storage rbind rbind 0 0
diff --git a/board/common/overlay/etc/init.d/S61proftpd b/board/common/overlay/etc/init.d/S61proftpd
new file mode 100755
index 0000000000..92ad723780
--- /dev/null
+++ b/board/common/overlay/etc/init.d/S61proftpd
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+test -f /etc/proftpd.conf || exit 0
+
+test -n "$os_version" || source /etc/init.d/base
+test -n "$os_debug" || source /etc/init.d/conf
+
+test "$os_networkless" == "true" && exit 0
+
+start() {
+ msg_begin "Starting proftpd"
+ mkdir -p /var/run/proftpd
+ touch /var/log/wtmp
+ /usr/sbin/proftpd &>/dev/null
+ test $? == 0 && msg_done || msg_fail
+}
+
+stop() {
+ msg_begin "Stopping proftpd"
+ killall proftpd &>/dev/null
+ test $? == 0 && msg_done || msg_fail
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+ ;;
+esac
+
+exit 0
+
diff --git a/board/common/overlay/etc/init.d/S62smb b/board/common/overlay/etc/init.d/S62smb
new file mode 100755
index 0000000000..f7d3ebd102
--- /dev/null
+++ b/board/common/overlay/etc/init.d/S62smb
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+test -f /etc/samba/smb.conf || exit 0
+
+test -n "$os_version" || source /etc/init.d/base
+test -n "$os_debug" || source /etc/init.d/conf
+
+test "$os_networkless" == "true" && exit 0
+
+start() {
+ msg_begin "Setting smb admin password"
+
+ mkdir -p /var/log/samba
+ mkdir -p /var/lib/samba/private
+
+ password=$(/etc/init.d/adminpw)
+ echo -e "$password\n$password\n" | /usr/bin/smbpasswd -a admin -s > /dev/null
+ test $? == 0 && msg_done || msg_fail
+
+ msg_begin "Starting smbd"
+ smbd -D
+ test $? == 0 && msg_done || msg_fail
+
+ msg_begin "Starting nmbd"
+ nmbd -D
+ test $? == 0 && msg_done || msg_fail
+}
+
+stop() {
+ msg_begin "Stopping smbd"
+ killall smbd &>/dev/null
+ test $? == 0 && msg_done || msg_fail
+
+ msg_begin "Stopping nmbd"
+ killall nmbd &>/dev/null
+ test $? == 0 && msg_done || msg_fail
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $?
+
diff --git a/board/common/overlay/etc/init.d/S85motioneye b/board/common/overlay/etc/init.d/S85motioneye
new file mode 100755
index 0000000000..34f758e294
--- /dev/null
+++ b/board/common/overlay/etc/init.d/S85motioneye
@@ -0,0 +1,149 @@
+#!/bin/bash
+
+sys_conf="/etc/motioneye.conf"
+boot_conf="/boot/motioneye.conf"
+conf="/data/etc/motioneye.conf"
+motion_conf="/data/etc/motion.conf"
+watch_conf="/data/etc/watch.conf"
+
+meyewatch_timeout=120
+meyewatch_disable="false"
+dev_v4l_by_id="/dev/v4l/by-id/"
+media_dir="/data/output"
+
+if ! [ -f $conf ]; then
+ if [ -f $boot_conf ]; then
+ cp $boor_conf $conf
+ elif [ -f $sys_conf ]; then
+ cp $sys_conf $conf
+ fi
+fi
+
+test -f "$conf" || exit 0
+
+test -r $watch_conf && source $watch_conf
+
+test -n "$os_version" || source /etc/init.d/base
+
+opts=$(cat "$conf" | while read line; do echo "--$line"; done)
+port=$(echo "$opts" | grep -oE 'port [[:digit:]]+' | cut -d ' ' -f 2)
+
+responsive() {
+ wget --method=HEAD -q -t 1 -T 2 -O - http://127.0.0.1:$port &>/dev/null && return 0 || return 1
+}
+
+watch() {
+ count=0
+ while true; do
+ sleep 5
+ if responsive; then
+ count=0
+ else
+ if [ $count -lt $meyewatch_timeout ]; then
+ count=$(($count + 5))
+ else
+ logger -t motioneye -s "not responding for $meyewatch_timeout seconds, rebooting"
+ reboot
+ fi
+ fi
+ done
+}
+
+find_persistent_device() {
+ device=$1
+
+ if ! [ -d $dev_v4l_by_id ]; then
+ echo $device
+ return
+ fi
+
+ for p in $dev_v4l_by_id/*; do
+ if [ $(realpath "$p") == $device ]; then
+ echo $p | sed 's#//*#/#g'
+ return
+ fi
+ done
+
+ echo $device
+}
+
+start() {
+ msg_begin "Starting motioneye"
+
+ mkdir -p $media_dir
+ meyectl startserver -b -c $conf -l
+
+ count=0
+ while true; do
+ sleep 1
+
+ if responsive; then
+ break
+ fi
+
+ if [ $count -gt $meyewatch_timeout ]; then
+ msg_fail
+ test "$meyewatch_disable" == "false" && reboot
+ return 1
+ fi
+
+ count=$(($count + 1))
+ done
+
+ # add connected camera(s) with default settings
+ if responsive && ! [ -f $motion_conf ]; then
+ count=$(ls /dev/video* 2>/dev/null | wc -l)
+ index=1
+ for device in $(ls /dev/video* 2>/dev/null); do
+ if [ "$count" -gt "1" ]; then
+ output_dir="/data/output/camera$index/"
+ else
+ output_dir="/data/output/"
+ fi
+ loc="/config/add/?_username=admin"
+ device=$(find_persistent_device $device)
+ body="{\"path\": \"$device\", \"proto\": \"v4l2\"}"
+ signature=$(echo -n "POST:$loc:$body:" | sha1sum | cut -d ' ' -f 1)
+
+ curl -s -m 60 --data "$body" "http://127.0.0.1:$port$loc&_signature=$signature" > /dev/null
+ index=$(($index + 1))
+ done
+
+ sync
+ fi
+
+ if [ "$meyewatch_disable" == "false" ]; then
+ watch &
+ fi
+
+ msg_done
+}
+
+stop() {
+ msg_begin "Stopping motioneye"
+ meyectl stopserver -c $conf &>/dev/null
+ test $? == 0 && msg_done || msg_fail
+ ps | grep motioneye | grep -v $$ | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1 | xargs -r kill
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $?
+
diff --git a/board/common/overlay/etc/modprobe.d/modprobe.conf b/board/common/overlay/etc/modprobe.d/modprobe.conf
index fd739a3b6e..ad96e42ffa 100644
--- a/board/common/overlay/etc/modprobe.d/modprobe.conf
+++ b/board/common/overlay/etc/modprobe.d/modprobe.conf
@@ -5,3 +5,7 @@ options rndis_wlan power_save=0
options b43 hwpctl=0
options ath6kl_core suspend_mode=1
options brcmfmac roamoff=1
+
+# media
+options stk1160 keep_buffers=1
+
diff --git a/board/common/overlay/etc/motioneye.conf b/board/common/overlay/etc/motioneye.conf
new file mode 100644
index 0000000000..774921b3c1
--- /dev/null
+++ b/board/common/overlay/etc/motioneye.conf
@@ -0,0 +1,24 @@
+conf_path /data/etc
+run_path /tmp
+log_path /var/log
+media_path /data/output
+motion_binary /usr/bin/motion
+log_level info
+listen 0.0.0.0
+port 80
+mount_check_interval 300
+motion_check_interval 10
+cleanup_interval 43200
+remote_request_timeout 10
+mjpg_client_timeout 10
+mjpg_client_idle_timeout 10
+smb_shares true
+smb_mount_root /data/media
+wpa_supplicant_conf /data/etc/wpa_supplicant.conf
+local_time_file /data/etc/localtime
+enable_reboot true
+enable_update true
+smtp_timeout 60
+zip_timeout 500
+add_remove_cameras true
+
diff --git a/board/common/overlay/etc/os.conf b/board/common/overlay/etc/os.conf
index 96fff5eb4a..56f584fbd9 100644
--- a/board/common/overlay/etc/os.conf
+++ b/board/common/overlay/etc/os.conf
@@ -7,6 +7,6 @@ os_ppp="ppp0"
os_networkless="false"
os_country="GB"
os_firmware_method="github"
-os_firmware_repo="ccrisan/thingos"
+os_firmware_repo="ccrisan/motioneyeos"
os_firmware_username=""
os_firmware_password=""
diff --git a/board/common/overlay/etc/proftpd.conf b/board/common/overlay/etc/proftpd.conf
new file mode 100644
index 0000000000..054e36c9e1
--- /dev/null
+++ b/board/common/overlay/etc/proftpd.conf
@@ -0,0 +1,25 @@
+ServerIdent off
+ServerType standalone
+DefaultServer on
+Port 21
+UseIPv6 off
+Umask 022
+MaxInstances 4
+User ftp
+Group nogroup
+DefaultRoot /home/ftp
+AllowOverwrite on
+RequireValidShell off
+UseFtpUsers off
+RootLogin on
+
+
+ DenyAll
+
+
+
+ DenyAll
+
+
+Include /data/etc/proftpd*.conf
+
diff --git a/board/common/overlay/etc/samba/smb.conf b/board/common/overlay/etc/samba/smb.conf
new file mode 100644
index 0000000000..652b03952e
--- /dev/null
+++ b/board/common/overlay/etc/samba/smb.conf
@@ -0,0 +1,31 @@
+[global]
+ workgroup = MOTIONEYE
+ server string = motionEye
+ security = user
+ map to guest = bad user
+ encrypt passwords = yes
+ private dir = /var/lib/samba/private
+ public = no
+ writable = no
+ include = /data/etc/smb.conf
+ load printers = no
+ printing = bsd
+ printcap name = /dev/null
+ disable spoolss = yes
+ bind interfaces only = yes
+ interfaces = eth0 wlan0
+ log level = 0
+ syslog = 0
+ preferred master = no
+ domain master = no
+ local master = no
+ os level = 0
+
+[sdcard]
+ comment = SD Card Output Directory
+ path = /data/output
+
+[storage]
+ comment = Attached Storage Devices
+ path = /data/media
+
diff --git a/board/common/overlay/etc/version b/board/common/overlay/etc/version
index d7b4c0174e..3bb1c3982b 100644
--- a/board/common/overlay/etc/version
+++ b/board/common/overlay/etc/version
@@ -1,4 +1,4 @@
os_name="motionEyeOS"
os_short_name="motioneyeos"
os_prefix="meye"
-os_version="20170212"
+os_version="20170219"
diff --git a/board/common/skeleton/home/ftp/.empty b/board/common/skeleton/home/ftp/.empty
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/board/common/skeleton/home/ftp/sdcard/.empty b/board/common/skeleton/home/ftp/sdcard/.empty
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/board/common/skeleton/home/ftp/storage/.empty b/board/common/skeleton/home/ftp/storage/.empty
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/board/odroidc1/overlay/etc/os.conf b/board/odroidc1/overlay/etc/os.conf
index e12b10ec7d..d3ce9dfd6c 100644
--- a/board/odroidc1/overlay/etc/os.conf
+++ b/board/odroidc1/overlay/etc/os.conf
@@ -6,6 +6,6 @@ os_wlan="wlan0"
os_ppp="ppp0"
os_networkless="false"
os_firmware_method="github"
-os_firmware_repo="ccrisan/thingos"
+os_firmware_repo="ccrisan/motioneyeos"
os_firmware_username=""
os_firmware_password=""
diff --git a/board/odroidc2/overlay/etc/os.conf b/board/odroidc2/overlay/etc/os.conf
index e12b10ec7d..d3ce9dfd6c 100644
--- a/board/odroidc2/overlay/etc/os.conf
+++ b/board/odroidc2/overlay/etc/os.conf
@@ -6,6 +6,6 @@ os_wlan="wlan0"
os_ppp="ppp0"
os_networkless="false"
os_firmware_method="github"
-os_firmware_repo="ccrisan/thingos"
+os_firmware_repo="ccrisan/motioneyeos"
os_firmware_username=""
os_firmware_password=""
diff --git a/board/raspberrypi/motioneye-modules/boardctl.py b/board/raspberrypi/motioneye-modules/boardctl.py
new file mode 100644
index 0000000000..289deb93a4
--- /dev/null
+++ b/board/raspberrypi/motioneye-modules/boardctl.py
@@ -0,0 +1,310 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+import subprocess
+
+from config import additional_config
+
+import streameyectl
+
+
+CONFIG_TXT = '/boot/config.txt'
+MONITOR = '/data/etc/monitor_1'
+
+MONITOR_SCRIPT = '''#!/bin/bash
+
+net_tmp=/tmp/netspeed.tmp
+temp=$(($(cat /sys/devices/virtual/thermal/thermal_zone0/temp) / 1000))
+load=$(cat /proc/loadavg | cut -d ' ' -f 2)
+recv=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 3 | tail -n +3 | awk '{s+=$1} END {print s}')
+send=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 11 | tail -n +3 | awk '{s+=$1} END {print s}')
+total=$(($recv + $send))
+
+if [ -e $net_tmp ]; then
+ prev_total=$(cat $net_tmp)
+ speed=$(($total - $prev_total))
+else
+ speed=0
+fi
+
+echo $total > $net_tmp
+speed=$(($speed / 1024))
+
+echo -n "$temp°|$load|${speed}kB/s"
+'''
+
+OVERCLOCK = {
+ 700: '700|250|400|0',
+ 800: '800|250|400|0',
+ 900: '900|250|450|0',
+ 950: '950|250|450|0',
+ 1000: '1000|500|600|6',
+ 1001: '1001|300|450|6'
+}
+
+def _is_pi_zero():
+ try:
+ subprocess.check_call('grep -q "^Revision\s*:\s*[ 123][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]09[0-9a-fA-F]$" /proc/cpuinfo', shell=True)
+ return True
+
+ except:
+ return False
+
+
+def _get_board_settings():
+ gpu_mem = 128
+ camera_led = True
+ if _is_pi_zero():
+ arm_freq = 1001
+
+ else:
+ arm_freq = 700
+
+ if os.path.exists(CONFIG_TXT):
+ logging.debug('reading board settings from %s' % CONFIG_TXT)
+
+ with open(CONFIG_TXT) as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+
+ parts = line.split('=', 1)
+ if len(parts) != 2:
+ continue
+
+ name, value = parts
+ name = name.strip()
+ value = value.strip()
+
+ if name.startswith('gpu_mem'):
+ gpu_mem = int(value)
+
+ elif name == 'arm_freq':
+ arm_freq = int(value)
+
+ elif name == 'disable_camera_led':
+ camera_led = value == '0'
+
+ overclock = OVERCLOCK.get(arm_freq, '700|250|400|0')
+
+ s = {
+ 'gpuMem': gpu_mem,
+ 'overclock': overclock,
+ 'cameraLed': camera_led
+ }
+
+ logging.debug('board settings: gpu_mem=%(gpuMem)s, overclock=%(overclock)s, camera_led=%(cameraLed)s' % s)
+
+ return s
+
+
+def _set_board_settings(s):
+ s.setdefault('gpuMem', 128)
+ s.setdefault('overclock', '700|250|400|0')
+ s.setdefault('cameraLed', True)
+
+ old_settings = _get_board_settings()
+ if s == old_settings:
+ return # nothing has changed
+
+ seen = set()
+
+ logging.debug('writing board settings to %s: ' % CONFIG_TXT +
+ 'gpu_mem=%(gpuMem)s, overclock=%(overclock)s, camera_led=%(cameraLed)s' % s)
+
+ arm_freq, gpu_freq, sdram_freq, over_voltage = s['overclock'].split('|')
+
+ lines = []
+ if os.path.exists(CONFIG_TXT):
+ with open(CONFIG_TXT) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ line = line.strip('#')
+
+ try:
+ name, _ = line.split('=', 1)
+ name = name.strip()
+
+ except:
+ continue
+
+ seen.add(name)
+
+ if name.startswith('gpu_mem'):
+ lines[i] = '%s=%s' % (name, s['gpuMem'])
+
+ elif name == 'arm_freq':
+ lines[i] = 'arm_freq=%s' % arm_freq
+
+ elif name in ['gpu_freq', 'core_freq']:
+ lines[i] = '%s=%s' % (name, gpu_freq)
+
+ elif name == 'sdram_freq':
+ lines[i] = 'sdram_freq=%s' % sdram_freq
+
+ elif name == 'over_voltage':
+ lines[i] = 'over_voltage=%s' % over_voltage
+
+ elif name == 'disable_camera_led':
+ lines[i] = 'disable_camera_led=%s' % ['1', '0'][s['cameraLed']]
+
+ if 'gpu_mem' not in seen:
+ lines.append('gpu_mem=%s' % s['gpuMem'])
+
+ if 'gpu_mem_256' not in seen:
+ lines.append('gpu_mem_256=%s' % s['gpuMem'])
+
+ if 'gpu_mem_512' not in seen:
+ lines.append('gpu_mem_512=%s' % s['gpuMem'])
+
+ if 'arm_freq' not in seen:
+ lines.append('arm_freq=%s' % arm_freq)
+
+ if 'gpu_freq' not in seen:
+ lines.append('gpu_freq=%s' % gpu_freq)
+
+ if 'sdram_freq' not in seen:
+ lines.append('sdram_freq=%s' % sdram_freq)
+
+ if 'over_voltage' not in seen:
+ lines.append('over_voltage=%s' % over_voltage)
+
+ if 'disable_camera_led' not in seen:
+ lines.append('disable_camera_led=%s' % ['1', '0'][s['cameraLed']])
+
+ logging.debug('remounting /boot read-write')
+ if os.system('mount -o remount,rw /boot'):
+ logging.error('failed to remount /boot read-write')
+
+ with open(CONFIG_TXT, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_sys_mon():
+ return os.access(MONITOR, os.X_OK)
+
+
+def _set_sys_mon(sys_mon):
+ if sys_mon:
+ if os.access(MONITOR, os.X_OK):
+ pass
+
+ else:
+ if os.path.exists(MONITOR):
+ os.system('chmod +x %s' % MONITOR)
+
+ else:
+ with open(MONITOR, 'w') as f:
+ f.write(MONITOR_SCRIPT)
+
+ os.system('chmod +x %s' % MONITOR)
+
+ else:
+ if os.access(MONITOR, os.X_OK):
+ os.system('chmod -x %s' % MONITOR)
+
+
+@additional_config
+def boardSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def gpuMem():
+ return {
+ 'label': 'GPU Memory',
+ 'description': 'set the amount of memory reserved for the GPU (choose at least 96MB if you use the CSI camera board)',
+ 'type': 'number',
+ 'min': '16',
+ 'max': '448',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def cameraLed():
+ return {
+ 'label': 'Enable CSI Camera Led',
+ 'description': 'control the led on the CSI camera board',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def overclock():
+ return {
+ 'label': 'Overclocking',
+ 'description': 'choose an overclocking preset for your Raspberry PI',
+ 'type': 'choices',
+ 'choices': [
+ ('700|250|400|0', 'none (700/250/400/0)'),
+ ('800|250|400|0', 'modest (800/250/400/0)'),
+ ('900|250|450|0', 'medium (900/250/450/0)'),
+ ('950|250|450|0', 'high (950/250/450/0)'),
+ ('1000|500|600|6', 'turbo (1000/500/600/6)'),
+ ('1001|300|450|6', 'PiZero (1000/300/450/6)'),
+ ],
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sysMon():
+ return {
+ 'label': 'Enable System Monitoring',
+ 'description': 'when this is enabled, system monitoring info will be overlaid on top of the first camera frame',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': False,
+ 'get': _get_sys_mon,
+ 'set': _set_sys_mon
+ }
+
diff --git a/board/raspberrypi/motioneye-modules/streameyectl.py b/board/raspberrypi/motioneye-modules/streameyectl.py
new file mode 100644
index 0000000000..5f751332f4
--- /dev/null
+++ b/board/raspberrypi/motioneye-modules/streameyectl.py
@@ -0,0 +1,1172 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import config
+import hashlib
+import logging
+import os.path
+import re
+
+from tornado.ioloop import IOLoop
+
+import settings
+
+from config import additional_config
+
+
+MOTIONEYE_CONF = '/data/etc/motioneye.conf'
+RASPIMJPEG_CONF = '/data/etc/raspimjpeg.conf'
+STREAMEYE_CONF = '/data/etc/streameye.conf'
+
+EXPOSURE_CHOICES = [
+ ('off', 'Off'),
+ ('auto', 'Auto'),
+ ('night', 'Night'),
+ ('nightpreview', 'Night Preview'),
+ ('backlight', 'Backlight'),
+ ('spotlight', 'Spotlight'),
+ ('sports', 'Sports'),
+ ('snow', 'Snow'),
+ ('beach', 'Beach'),
+ ('verylong', 'Very Long'),
+ ('fixedfps', 'Fixed FPS'),
+ ('antishake', 'Antishake'),
+ ('fireworks', 'Fireworks')
+]
+
+AWB_CHOICES = [
+ ('off', 'Off'),
+ ('auto', 'Auto'),
+ ('sunlight', 'Sunlight'),
+ ('cloudy', 'Cloudy'),
+ ('shade', 'Shade'),
+ ('tungsten', 'Tungsten'),
+ ('fluorescent', 'Fluorescent'),
+ ('incandescent', 'Incandescent'),
+ ('flash', 'Flash'),
+ ('horizon', 'Horizon')
+]
+
+METERING_CHOICES = [
+ ('average', 'Average'),
+ ('spot', 'Spot'),
+ ('backlit', 'Backlit'),
+ ('matrix', 'Matrix')
+]
+
+DRC_CHOICES = [
+ ('off', 'Off'),
+ ('low', 'Low'),
+ ('medium', 'Medium'),
+ ('high', 'High')
+]
+
+IMXFX_CHOICES = [
+ ('none', 'None'),
+ ('negative', 'Negative'),
+ ('solarize', 'Solarize'),
+ ('sketch', 'Sketch'),
+ ('denoise', 'Denoise'),
+ ('emboss', 'Emboss'),
+ ('oilpaint', 'Oilpaint'),
+ ('hatch', 'Hatch'),
+ ('gpen', 'G Pen'),
+ ('pastel', 'Pastel'),
+ ('watercolor', 'Water Color'),
+ ('film', 'Film'),
+ ('blur', 'Blur'),
+ ('saturation', 'Saturation'),
+ ('colorswap', 'Color Swap'),
+ ('washedout', 'Washed Out'),
+ ('posterise', 'Posterize'),
+ ('colorpoint', 'Color Point'),
+ ('colorbalance', 'Color Balance'),
+ ('cartoon', 'Cartoon'),
+ ('deinterlace1', 'Deinterlace 1'),
+ ('deinterlace2', 'Deinterlace 2')
+]
+
+RESOLUTION_CHOICES = [
+ ('320x200', '320x200'),
+ ('320x240', '320x240'),
+ ('640x480', '640x480'),
+ ('800x480', '800x480'),
+ ('800x600', '800x600'),
+ ('1024x576', '1024x576'),
+ ('1024x768', '1024x768'),
+ ('1280x720', '1280x720'),
+ ('1280x800', '1280x800'),
+ ('1280x960', '1280x960'),
+ ('1280x1024', '1280x1024'),
+ ('1296x972', '1296x972'),
+ ('1440x960', '1440x960'),
+ ('1440x1024', '1440x1024'),
+ ('1600x1200', '1600x1200'),
+ ('1640x922', '1640x922'),
+ ('1640x1232', '1640x1232'),
+ ('1920x1080', '1920x1080'),
+ ('2592x1944', '2592x1944'),
+ ('3280x2464', '3280x2464')
+]
+
+ROTATION_CHOICES = [
+ ('0', '0°'),
+ ('90', '90°'),
+ ('180', '180°'),
+ ('270', '270°')
+]
+
+AUTH_CHOICES = [
+ ('disabled', 'Disabled'),
+ ('basic', 'Basic'),
+]
+
+_streameye_enabled = None
+
+
+def _get_streameye_enabled():
+ global _streameye_enabled
+
+ if _streameye_enabled is not None:
+ return _streameye_enabled
+
+ camera_ids = config.get_camera_ids(filter_valid=False) # filter_valid prevents infinte recursion
+ if len(camera_ids) != 1:
+ _streameye_enabled = False
+ return False
+
+ camera_config = config.get_camera(camera_ids[0], as_lines=True) # as_lines prevents infinte recursion
+ camera_config = config._conf_to_dict(camera_config)
+ if camera_config.get('@proto') != 'mjpeg':
+ _streameye_enabled = False
+ return False
+ if '127.0.0.1:' not in camera_config.get('@url', ''):
+ _streameye_enabled = False
+ return False
+
+ _streameye_enabled = True
+ return True
+
+
+def _set_streameye_enabled_deferred(enabled):
+ was_enabled = _get_streameye_enabled()
+ if enabled and not was_enabled:
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_set_streameye_enabled, True)
+
+ elif not enabled and was_enabled:
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_set_streameye_enabled, False)
+
+ if enabled:
+ # this will force updating streameye settings whenever the surveillance credentials are changed
+ streameye_settings = _get_streameye_settings(1)
+ _set_streameye_settings(1, streameye_settings)
+
+
+def _set_streameye_enabled(enabled):
+ global _streameye_enabled
+
+ if enabled:
+ logging.debug('removing all cameras from cache')
+ config._camera_config_cache = {}
+ config._camera_ids_cache = []
+
+ logging.debug('disabling all cameras in motion.conf')
+ cmd = 'sed -r -i "s/^thread (.*)/#thread \\1/" /data/etc/motion.conf &>/dev/null'
+ if os.system(cmd):
+ logging.error('failed to disable cameras in motion.conf')
+
+ logging.debug('renaming thread files')
+ for name in os.listdir(settings.CONF_PATH):
+ if re.match('^thread-\d+.conf$', name):
+ os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name + '.bak'))
+
+ logging.debug('adding simple mjpeg camera')
+
+ streameye_settings = _get_streameye_settings(1)
+ main_config = config.get_main()
+
+ device_details = {
+ 'proto': 'mjpeg',
+ 'host': '127.0.0.1',
+ 'port': streameye_settings['sePort'],
+ 'username': '',
+ 'password': '',
+ 'scheme': 'http',
+ 'path': '/'
+ }
+
+ if streameye_settings['seAuthMode'] == 'basic':
+ device_details['username'] = main_config['@normal_username']
+ device_details['password'] = main_config['@normal_password']
+
+ _streameye_enabled = True
+ config._additional_structure_cache = {}
+ camera_config = config.add_camera(device_details)
+
+ # call set_camera again so that the streamEye-related defaults are saved
+ config.set_camera(camera_config['@id'], camera_config)
+
+ _set_motioneye_add_remove_cameras(False)
+
+ else: # disabled
+ logging.debug('removing simple mjpeg camera')
+ for camera_id in config.get_camera_ids():
+ camera_config = config.get_camera(camera_id)
+ if camera_config.get('@proto') == 'mjpeg':
+ config.rem_camera(camera_id)
+
+ logging.debug('renaming thread files')
+ for name in os.listdir(settings.CONF_PATH):
+ if re.match('^thread-\d+.conf.bak$', name):
+ os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name[:-4]))
+
+ _streameye_enabled = False
+ config.invalidate()
+
+ logging.debug('enabling all cameras')
+ for camera_id in config.get_camera_ids():
+ camera_config = config.get_camera(camera_id)
+ camera_config['@enabled'] = True
+ config.set_camera(camera_id, camera_config)
+
+ _set_motioneye_add_remove_cameras(True)
+
+
+def _set_motioneye_add_remove_cameras(enabled):
+ logging.debug('%s motionEye add/remove cameras' % ['disabling', 'enabling'][enabled])
+
+ lines = []
+ found = False
+ if os.path.exists(MOTIONEYE_CONF):
+ with open(MOTIONEYE_CONF) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, _ = line.split(' ', 2)
+
+ except:
+ continue
+
+ name = name.replace('_', '-')
+
+ if name == 'add-remove-cameras':
+ lines[i] = 'add-remove-cameras %s' % str(enabled).lower()
+ found = True
+
+ if not found:
+ lines.append('add-remove-cameras %s' % str(enabled).lower())
+
+ with open(MOTIONEYE_CONF, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_raspimjpeg_settings(camera_id):
+ s = {
+ 'preview': False,
+ 'brightness': 50,
+ 'contrast': 0,
+ 'saturation': 0,
+ 'sharpness': 0,
+ 'iso': 400,
+ 'ev': 0,
+ 'shutter': 0,
+ 'exposure': 'auto',
+ 'awb': 'auto',
+ 'metering': 'average',
+ 'drc': 'off',
+ 'vstab': False,
+ 'denoise': False,
+ 'imxfx': 'none',
+ 'width': 640,
+ 'height': 480,
+ 'rotation': 0,
+ 'vflip': False,
+ 'hflip': False,
+ 'framerate': 15,
+ 'quality': 25,
+ 'zoomx': 0,
+ 'zoomy': 0,
+ 'zoomw': 100,
+ 'zoomh': 100
+ }
+
+ if os.path.exists(RASPIMJPEG_CONF):
+ logging.debug('reading raspimjpeg settings from %s' % RASPIMJPEG_CONF)
+
+ with open(RASPIMJPEG_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, value = line.split(' ', 1)
+
+ except:
+ continue
+
+ name = name.replace('_', '-')
+
+ try:
+ value = int(value)
+
+ except:
+ pass
+
+ if value == 'false':
+ value = False
+
+ elif value == 'true':
+ value = True
+
+ if name == 'zoom':
+ try:
+ parts = value.split(',')
+ s['zoomx'] = int(float(parts[0]) * 100)
+ s['zoomy'] = int(float(parts[1]) * 100)
+ s['zoomw'] = int(float(parts[2]) * 100)
+ s['zoomh'] = int(float(parts[3]) * 100)
+
+ except:
+ logging.error('failed to parse zoom setting "%s"' % value)
+
+ continue
+
+ s[name] = value
+
+ s['contrast'] = (s['contrast'] + 100) / 2
+ s['saturation'] = (s['saturation'] + 100) / 2
+ s['sharpness'] = (s['sharpness'] + 100) / 2
+
+ s['resolution'] = '%sx%s' % (s.pop('width'), s.pop('height'))
+
+ s = dict(('se' + n[0].upper() + n[1:], v) for (n, v) in s.items())
+
+ return s
+
+
+def _set_raspimjpeg_settings(camera_id, s):
+ s = dict((n[2].lower() + n[3:], v) for (n, v) in s.items())
+
+ s['width'] = int(s['resolution'].split('x')[0])
+ s['height'] = int(s.pop('resolution').split('x')[1])
+
+ s['zoom'] = '%.2f,%.2f,%.2f,%.2f' % (
+ s.pop('zoomx') / 100.0, s.pop('zoomy') / 100.0,
+ s.pop('zoomw') / 100.0, s.pop('zoomh') / 100.0)
+
+ s['contrast'] = s['contrast'] * 2 - 100
+ s['saturation'] = s['saturation'] * 2 - 100
+ s['sharpness'] = s['sharpness'] * 2 - 100
+
+ logging.debug('writing raspimjpeg settings to %s' % RASPIMJPEG_CONF)
+
+ lines = []
+ for name, value in sorted(s.items(), key=lambda i: i[0]):
+ if isinstance(value, bool):
+ value = str(value).lower()
+
+ line = '%s %s\n' % (name, value)
+ lines.append(line)
+
+ with open(RASPIMJPEG_CONF, 'w') as f:
+ for line in lines:
+ f.write(line)
+
+
+def _get_streameye_settings(camera_id):
+ s = {
+ 'seAuthMode': 'disabled',
+ 'sePort': 8081,
+ }
+
+ if os.path.exists(STREAMEYE_CONF):
+ logging.debug('reading streameye settings from %s' % STREAMEYE_CONF)
+
+ with open(STREAMEYE_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ m = re.findall('PORT="?(\d+)"?', line)
+ if m:
+ s['sePort'] = int(m[0])
+ continue
+
+ m = re.findall('AUTH="?(\w+)"?', line)
+ if m:
+ s['seAuthMode'] = m[0]
+
+ return s
+
+
+def _set_streameye_settings(camera_id, s):
+ s = dict(s)
+ s.setdefault('sePort', 8081)
+ s.setdefault('seAuthMode', 'disabled')
+
+ main_config = config.get_main()
+ username = main_config['@normal_username']
+ password = main_config['@normal_password']
+ realm = 'motionEyeOS'
+
+ logging.debug('writing streameye settings to %s' % STREAMEYE_CONF)
+
+ lines = [
+ 'PORT="%s"' % s['sePort'],
+ 'AUTH="%s"' % s['seAuthMode'],
+ 'CREDENTIALS="%s:%s:%s"' % (username, password, realm)
+ ]
+
+ with open(STREAMEYE_CONF, 'w') as f:
+ for line in lines:
+ f.write(line + '\n')
+
+ if 1 in config.get_camera_ids():
+ # a workaround to update the camera username and password
+ # since we cannot call set_camera() from here
+ if s['seAuthMode'] == 'basic':
+ url = 'http://%s:%s@127.0.0.1:%s/' % (username, password, s['sePort'])
+
+ else:
+ url = 'http://127.0.0.1:%s/' % s['sePort']
+
+ if 1 in config._camera_config_cache:
+ logging.debug('updating streaming authentication in config cache')
+ config._camera_config_cache[1]['@url'] = url
+
+ lines = config.get_camera(1, as_lines=True)
+ for i, line in enumerate(lines):
+ if line.startswith('# @url'):
+ lines[i] = '# @url %s' % url
+
+ config_file = os.path.join(settings.CONF_PATH, config._CAMERA_CONFIG_FILE_NAME % {'id': 1})
+ logging.debug('updating streaming authentication in camera config file %s' % config_file)
+ with open(config_file, 'w') as f:
+ for line in lines:
+ f.write(line + '\n')
+
+ logging.debug('restarting streameye')
+ if os.system('streameye.sh restart'):
+ logging.error('streameye restart failed')
+
+
+# make streameye-related log files downloadable
+
+if _get_streameye_enabled():
+ def _add_log_handlers():
+ import handlers
+ handlers.LogHandler.LOGS['streameye'] = (os.path.join(settings.LOG_PATH, 'streameye.log'), 'streameye.log')
+ handlers.LogHandler.LOGS['raspimjpeg'] = (os.path.join(settings.LOG_PATH, 'raspimjpeg.log'), 'raspimjpeg.log')
+
+ # handlers.LogHandler is not yet available
+ # at the time streameyectl is imported
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_add_log_handlers)
+
+ @additional_config
+ def streamEyeLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': lambda: 'streameye.log',
+ }
+
+ @additional_config
+ def raspiMjpegLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': lambda: 'raspimjpeg.log',
+ }
+
+
+@additional_config
+def streamEyeMainSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def streamEye():
+ return {
+ 'label': 'Fast Network Camera',
+ 'description': 'Enabling this option will turn your Raspberry PI into a simple and fast MJPEG network camera, ' +
+ 'disabling motion detection, media files and all other advanced features (works only with the CSI camera)',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_streameye_enabled,
+ 'set': _set_streameye_enabled_deferred,
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator1():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seBrightness():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Brightness',
+ 'description': 'sets a desired brightness level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seContrast():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Contrast',
+ 'description': 'sets a desired contrast level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seSaturation():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Saturation',
+ 'description': 'sets a desired saturation level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seSharpness():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Sharpness',
+ 'description': 'sets a desired sharpness level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+
+@additional_config
+def streamEyeCameraSeparator2():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seResolution():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Resolution',
+ 'description': 'the video resolution (larger values produce better quality but require more CPU power, larger storage space and bandwidth)',
+ 'type': 'choices',
+ 'choices': RESOLUTION_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seRotation():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Rotation',
+ 'description': 'use this to rotate the captured image, if your camera is not positioned correctly',
+ 'type': 'choices',
+ 'choices': ROTATION_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seVflip():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Flip Vertically',
+ 'description': 'enable this to flip the captured image vertically',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seHflip():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Flip Horizontally',
+ 'description': 'enable this to flip the captured image horizontally',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seFramerate():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Frame Rate',
+ 'description': 'sets the number of frames captured by the camera every second (higher values produce smoother videos but require more CPU power, larger storage space and bandwidth)',
+ 'type': 'range',
+ 'min': 1,
+ 'max': 30,
+ 'snap': 0,
+ 'ticks': "1|5|10|15|20|25|30",
+ 'decimals': 0,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seQuality():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Image Quality',
+ 'description': 'sets the JPEG image quality (higher values produce a better image quality but require more storage space and bandwidth)',
+ 'type': 'range',
+ 'min': 1,
+ 'max': 100,
+ 'snap': 2,
+ 'ticks': '1|25|50|75|100',
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomx():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom X',
+ 'description': 'sets the horizontal zoom offset',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 80,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomy():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Y',
+ 'description': 'sets the vertical zoom offset',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 80,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomw():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Width',
+ 'description': 'sets the zoom width',
+ 'type': 'range',
+ 'min': 20,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomh():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Height',
+ 'description': 'sets the zoom height',
+ 'type': 'range',
+ 'min': 20,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sePreview():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'HDMI Preview',
+ 'description': 'enable this if you want to see the preview on an HDMI-connected monitor',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator3():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seIso():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'ISO',
+ 'description': 'sets a desired ISO level for this camera',
+ 'type': 'range',
+ 'min': 100,
+ 'max': 800,
+ 'snap': 1,
+ 'ticksnum': 8,
+ 'decimals': 0,
+ 'unit': '',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seShutter():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Shutter Speed',
+ 'description': 'sets a desired shutter speed for this camera',
+ 'type': 'number',
+ 'min': 0,
+ 'max': 6000000,
+ 'unit': 'microseconds',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator4():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seExposure():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Exposure Mode',
+ 'description': 'sets a desired exposure mode for this camera',
+ 'type': 'choices',
+ 'choices': EXPOSURE_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seEv():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Exposure Compensation',
+ 'description': 'sets a desired exposure compensation for this camera',
+ 'type': 'range',
+ 'min': -25,
+ 'max': 25,
+ 'snap': 1,
+ 'ticksnum': 11,
+ 'decimals': 0,
+ 'unit': '',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seAwb():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Automatic White Balance',
+ 'description': 'sets a desired automatic white balance mode for this camera',
+ 'type': 'choices',
+ 'choices': AWB_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seMetering():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Metering Mode',
+ 'description': 'sets a desired metering mode for this camera',
+ 'type': 'choices',
+ 'choices': METERING_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seDrc():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Dynamic Range Compensation',
+ 'description': 'sets a desired dynamic range compensation level for this camera',
+ 'type': 'choices',
+ 'choices': DRC_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seVstab():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Stabilization',
+ 'description': 'enables or disables video stabilization for this camera',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seDenoise():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Denoise',
+ 'description': 'enables image denoising',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seImxfx():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Image Effect',
+ 'description': 'sets a desired image effect for this camera',
+ 'type': 'choices',
+ 'choices': IMXFX_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sePort():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Streaming Port',
+ 'description': 'sets the TCP port on which the webcam streaming server listens',
+ 'type': 'number',
+ 'min': 1024,
+ 'max': 65535,
+ 'section': 'streaming',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_streameye_settings,
+ 'set': _set_streameye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seAuthMode():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Authentication Mode',
+ 'description': 'the authentication mode to use when accessing the stream (use Basic instead of Digest if you encounter issues with third party apps)',
+ 'type': 'choices',
+ 'choices': AUTH_CHOICES,
+ 'section': 'streaming',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_streameye_settings,
+ 'set': _set_streameye_settings,
+ 'get_set_dict': True
+ }
+
diff --git a/board/raspberrypi/overlay/etc/init.d/S84streameye b/board/raspberrypi/overlay/etc/init.d/S84streameye
new file mode 100755
index 0000000000..81e2e2c896
--- /dev/null
+++ b/board/raspberrypi/overlay/etc/init.d/S84streameye
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+motioneye_conf_dir="/data/etc/"
+
+test -n "$os_version" || source /etc/init.d/base
+
+enabled() {
+ test $(ls -1 $motioneye_conf_dir/thread-*.conf 2>/dev/null| wc -l) == 1 || return 1
+
+ grep '# @proto mjpeg' $motioneye_conf_dir/thread-1.conf &>/dev/null || return 1
+
+ grep -E '# @url http://(.*)127.0.0.1:' $motioneye_conf_dir/thread-1.conf &>/dev/null || return 1
+
+ return 0
+}
+
+enabled || exit 0
+
+start() {
+ msg_begin "Starting streameye"
+ streameye.sh start
+ test $? == 0 && msg_done || msg_fail
+}
+
+stop() {
+ msg_begin "Stopping streameye"
+ streameye.sh stop
+ test $? == 0 && msg_done || msg_fail
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $?
+
diff --git a/board/raspberrypi/overlay/etc/modules b/board/raspberrypi/overlay/etc/modules
new file mode 100644
index 0000000000..2990e0b8e0
--- /dev/null
+++ b/board/raspberrypi/overlay/etc/modules
@@ -0,0 +1,3 @@
+bcm2835-v4l2 max_video_width=2592 max_video_height=1944
+bcm2835-wdt
+
diff --git a/board/raspberrypi/overlay/usr/bin/streameye.sh b/board/raspberrypi/overlay/usr/bin/streameye.sh
new file mode 100755
index 0000000000..29e5b79d8b
--- /dev/null
+++ b/board/raspberrypi/overlay/usr/bin/streameye.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+RASPIMJPEG_CONF=/data/etc/raspimjpeg.conf
+RASPIMJPEG_LOG=/var/log/raspimjpeg.log
+MOTIONEYE_CONF=/data/etc/motioneye.conf
+STREAMEYE_CONF=/data/etc/streameye.conf
+STREAMEYE_LOG=/var/log/streameye.log
+
+test -r $RASPIMJPEG_CONF || exit 1
+test -r $STREAMEYE_CONF || exit 1
+
+watch() {
+ count=0
+ while true; do
+ sleep 5
+ if ! ps aux | grep raspimjpeg.py | grep -v grep &>/dev/null; then
+ logger -t streameye -s "not running, respawning"
+ start
+ fi
+ done
+}
+
+function start() {
+ pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1)
+ if [ -n "$pid" ]; then
+ return
+ fi
+
+ raspimjpeg_opts=""
+ while read line; do
+ if echo "$line" | grep false &>/dev/null; then
+ continue
+ fi
+ if echo "$line" | grep true &>/dev/null; then
+ line=$(echo $line | cut -d ' ' -f 1)
+ fi
+ raspimjpeg_opts="$raspimjpeg_opts --$line"
+ done < $RASPIMJPEG_CONF
+
+ source $STREAMEYE_CONF
+ streameye_opts="-p $PORT"
+ if [ -n "$CREDENTIALS" ] && [ "$AUTH" = "basic" ]; then
+ streameye_opts="$streameye_opts -a basic -c $CREDENTIALS"
+ fi
+
+ if [ -r $MOTIONEYE_CONF ] && grep 'log-level debug' $MOTIONEYE_CONF >/dev/null; then
+ raspimjpeg_opts="$raspimjpeg_opts -d"
+ streameye_opts="$streameye_opts -d"
+ fi
+
+ raspimjpeg.py $raspimjpeg_opts 2>$RASPIMJPEG_LOG | streameye $streameye_opts &>$STREAMEYE_LOG &
+}
+
+function stop() {
+ # stop the streameye background watch process
+ ps | grep streameye.sh | grep -v $$ | grep -v S94streameye| grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1 | xargs -r kill
+
+ # stop the raspimjpeg process
+ pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1)
+ if [ -z "$pid" ]; then
+ return
+ fi
+
+ kill -HUP "$pid" &>/dev/null
+ count=0
+ while kill -0 "$pid" &>/dev/null && [ $count -lt 5 ]; do
+ sleep 1
+ count=$(($count + 1))
+ done
+ kill -KILL "$pid" &>/dev/null || true
+}
+
+case "$1" in
+ start)
+ start
+ watch &
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ watch &
+ ;;
+
+ *)
+ echo $"Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
diff --git a/board/raspberrypi2/motioneye-modules/boardctl.py b/board/raspberrypi2/motioneye-modules/boardctl.py
new file mode 100644
index 0000000000..c162502ced
--- /dev/null
+++ b/board/raspberrypi2/motioneye-modules/boardctl.py
@@ -0,0 +1,290 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+
+from config import additional_config
+
+import streameyectl
+
+
+CONFIG_TXT = '/boot/config.txt'
+MONITOR = '/data/etc/monitor_1'
+
+MONITOR_SCRIPT = '''#!/bin/bash
+
+net_tmp=/tmp/netspeed.tmp
+temp=$(($(cat /sys/devices/virtual/thermal/thermal_zone0/temp) / 1000))
+load=$(cat /proc/loadavg | cut -d ' ' -f 2)
+recv=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 3 | tail -n +3 | awk '{s+=$1} END {print s}')
+send=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 11 | tail -n +3 | awk '{s+=$1} END {print s}')
+total=$(($recv + $send))
+
+if [ -e $net_tmp ]; then
+ prev_total=$(cat $net_tmp)
+ speed=$(($total - $prev_total))
+else
+ speed=0
+fi
+
+echo $total > $net_tmp
+speed=$(($speed / 1024))
+
+echo -n "$temp°|$load|${speed}kB/s"
+'''
+
+OVERCLOCK = {
+ 700: '700|250|400|0',
+ 800: '800|250|400|0',
+ 900: '900|250|450|0',
+ 950: '950|250|450|0',
+ 1000: '1000|500|600|6',
+ 1001: '1001|500|500|2'
+}
+
+def _get_board_settings():
+ gpu_mem = 128
+ camera_led = True
+ arm_freq = 1001
+
+ if os.path.exists(CONFIG_TXT):
+ logging.debug('reading board settings from %s' % CONFIG_TXT)
+
+ with open(CONFIG_TXT) as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+
+ parts = line.split('=', 1)
+ if len(parts) != 2:
+ continue
+
+ name, value = parts
+ name = name.strip()
+ value = value.strip()
+
+ if name == 'gpu_mem':
+ gpu_mem = int(value)
+
+ elif name == 'arm_freq':
+ arm_freq = int(value)
+
+ elif name == 'disable_camera_led':
+ camera_led = value == '0'
+
+ overclock = OVERCLOCK.get(arm_freq, '700|250|400|0')
+
+ s = {
+ 'gpuMem': gpu_mem,
+ 'overclock': overclock,
+ 'cameraLed': camera_led
+ }
+
+ logging.debug('board settings: gpu_mem=%(gpuMem)s, overclock=%(overclock)s, camera_led=%(cameraLed)s' % s)
+
+ return s
+
+
+def _set_board_settings(s):
+ s.setdefault('gpuMem', 128)
+ s.setdefault('overclock', '700|250|400|0')
+ s.setdefault('cameraLed', True)
+
+ old_settings = _get_board_settings()
+ if s == old_settings:
+ return # nothing has changed
+
+ seen = set()
+
+ logging.debug('writing board settings to %s: ' % CONFIG_TXT +
+ 'gpu_mem=%(gpuMem)s, overclock=%(overclock)s, camera_led=%(cameraLed)s' % s)
+
+ arm_freq, gpu_freq, sdram_freq, over_voltage = s['overclock'].split('|')
+
+ lines = []
+ if os.path.exists(CONFIG_TXT):
+ with open(CONFIG_TXT) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ line = line.strip('#')
+
+ try:
+ name, _ = line.split('=', 1)
+ name = name.strip()
+
+ except:
+ continue
+
+ seen.add(name)
+
+ if name == 'gpu_mem':
+ lines[i] = '%s=%s' % (name, s['gpuMem'])
+
+ elif name == 'arm_freq':
+ lines[i] = 'arm_freq=%s' % arm_freq
+
+ elif name in ['gpu_freq', 'core_freq']:
+ lines[i] = '%s=%s' % (name, gpu_freq)
+
+ elif name == 'sdram_freq':
+ lines[i] = 'sdram_freq=%s' % sdram_freq
+
+ elif name == 'over_voltage':
+ lines[i] = 'over_voltage=%s' % over_voltage
+
+ elif name == 'disable_camera_led':
+ lines[i] = 'disable_camera_led=%s' % ['1', '0'][s['cameraLed']]
+
+ if 'gpu_mem' not in seen:
+ lines.append('gpu_mem=%s' % s['gpuMem'])
+
+ if 'arm_freq' not in seen:
+ lines.append('arm_freq=%s' % arm_freq)
+
+ if 'gpu_freq' not in seen:
+ lines.append('gpu_freq=%s' % gpu_freq)
+
+ if 'sdram_freq' not in seen:
+ lines.append('sdram_freq=%s' % sdram_freq)
+
+ if 'over_voltage' not in seen:
+ lines.append('over_voltage=%s' % over_voltage)
+
+ if 'disable_camera_led' not in seen:
+ lines.append('disable_camera_led=%s' % ['1', '0'][s['cameraLed']])
+
+ logging.debug('remounting /boot read-write')
+ if os.system('mount -o remount,rw /boot'):
+ logging.error('failed to remount /boot read-write')
+
+ with open(CONFIG_TXT, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_sys_mon():
+ return os.access(MONITOR, os.X_OK)
+
+
+def _set_sys_mon(sys_mon):
+ if sys_mon:
+ if os.access(MONITOR, os.X_OK):
+ pass
+
+ else:
+ if os.path.exists(MONITOR):
+ os.system('chmod +x %s' % MONITOR)
+
+ else:
+ with open(MONITOR, 'w') as f:
+ f.write(MONITOR_SCRIPT)
+
+ os.system('chmod +x %s' % MONITOR)
+
+ else:
+ if os.access(MONITOR, os.X_OK):
+ os.system('chmod -x %s' % MONITOR)
+
+
+@additional_config
+def boardSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def gpuMem():
+ return {
+ 'label': 'GPU Memory',
+ 'description': 'set the amount of memory reserved for the GPU (choose at least 96MB if you use the CSI camera board)',
+ 'type': 'number',
+ 'min': '16',
+ 'max': '944',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def cameraLed():
+ return {
+ 'label': 'Enable CSI Camera Led',
+ 'description': 'control the led on the CSI camera board',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def overclock():
+ return {
+ 'label': 'Overclocking',
+ 'description': 'choose an overclocking preset for your Raspberry PI',
+ 'type': 'choices',
+ 'choices': [
+ ('700|250|400|0', 'none (700/250/400/0)'),
+ ('800|250|400|0', 'modest (800/250/400/0)'),
+ ('900|250|450|0', 'medium (900/250/450/0)'),
+ ('950|250|450|0', 'high (950/250/450/0)'),
+ ('1000|500|600|6', 'turbo (1000/500/600/6)'),
+ ('1001|500|500|2', 'Pi2 (1000/500/500/2)')
+ ],
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sysMon():
+ return {
+ 'label': 'Enable System Monitoring',
+ 'description': 'when this is enabled, system monitoring info will be overlaid on top of the first camera frame',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': False,
+ 'get': _get_sys_mon,
+ 'set': _set_sys_mon
+ }
+
diff --git a/board/raspberrypi2/motioneye-modules/streameyectl.py b/board/raspberrypi2/motioneye-modules/streameyectl.py
new file mode 100644
index 0000000000..5f751332f4
--- /dev/null
+++ b/board/raspberrypi2/motioneye-modules/streameyectl.py
@@ -0,0 +1,1172 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import config
+import hashlib
+import logging
+import os.path
+import re
+
+from tornado.ioloop import IOLoop
+
+import settings
+
+from config import additional_config
+
+
+MOTIONEYE_CONF = '/data/etc/motioneye.conf'
+RASPIMJPEG_CONF = '/data/etc/raspimjpeg.conf'
+STREAMEYE_CONF = '/data/etc/streameye.conf'
+
+EXPOSURE_CHOICES = [
+ ('off', 'Off'),
+ ('auto', 'Auto'),
+ ('night', 'Night'),
+ ('nightpreview', 'Night Preview'),
+ ('backlight', 'Backlight'),
+ ('spotlight', 'Spotlight'),
+ ('sports', 'Sports'),
+ ('snow', 'Snow'),
+ ('beach', 'Beach'),
+ ('verylong', 'Very Long'),
+ ('fixedfps', 'Fixed FPS'),
+ ('antishake', 'Antishake'),
+ ('fireworks', 'Fireworks')
+]
+
+AWB_CHOICES = [
+ ('off', 'Off'),
+ ('auto', 'Auto'),
+ ('sunlight', 'Sunlight'),
+ ('cloudy', 'Cloudy'),
+ ('shade', 'Shade'),
+ ('tungsten', 'Tungsten'),
+ ('fluorescent', 'Fluorescent'),
+ ('incandescent', 'Incandescent'),
+ ('flash', 'Flash'),
+ ('horizon', 'Horizon')
+]
+
+METERING_CHOICES = [
+ ('average', 'Average'),
+ ('spot', 'Spot'),
+ ('backlit', 'Backlit'),
+ ('matrix', 'Matrix')
+]
+
+DRC_CHOICES = [
+ ('off', 'Off'),
+ ('low', 'Low'),
+ ('medium', 'Medium'),
+ ('high', 'High')
+]
+
+IMXFX_CHOICES = [
+ ('none', 'None'),
+ ('negative', 'Negative'),
+ ('solarize', 'Solarize'),
+ ('sketch', 'Sketch'),
+ ('denoise', 'Denoise'),
+ ('emboss', 'Emboss'),
+ ('oilpaint', 'Oilpaint'),
+ ('hatch', 'Hatch'),
+ ('gpen', 'G Pen'),
+ ('pastel', 'Pastel'),
+ ('watercolor', 'Water Color'),
+ ('film', 'Film'),
+ ('blur', 'Blur'),
+ ('saturation', 'Saturation'),
+ ('colorswap', 'Color Swap'),
+ ('washedout', 'Washed Out'),
+ ('posterise', 'Posterize'),
+ ('colorpoint', 'Color Point'),
+ ('colorbalance', 'Color Balance'),
+ ('cartoon', 'Cartoon'),
+ ('deinterlace1', 'Deinterlace 1'),
+ ('deinterlace2', 'Deinterlace 2')
+]
+
+RESOLUTION_CHOICES = [
+ ('320x200', '320x200'),
+ ('320x240', '320x240'),
+ ('640x480', '640x480'),
+ ('800x480', '800x480'),
+ ('800x600', '800x600'),
+ ('1024x576', '1024x576'),
+ ('1024x768', '1024x768'),
+ ('1280x720', '1280x720'),
+ ('1280x800', '1280x800'),
+ ('1280x960', '1280x960'),
+ ('1280x1024', '1280x1024'),
+ ('1296x972', '1296x972'),
+ ('1440x960', '1440x960'),
+ ('1440x1024', '1440x1024'),
+ ('1600x1200', '1600x1200'),
+ ('1640x922', '1640x922'),
+ ('1640x1232', '1640x1232'),
+ ('1920x1080', '1920x1080'),
+ ('2592x1944', '2592x1944'),
+ ('3280x2464', '3280x2464')
+]
+
+ROTATION_CHOICES = [
+ ('0', '0°'),
+ ('90', '90°'),
+ ('180', '180°'),
+ ('270', '270°')
+]
+
+AUTH_CHOICES = [
+ ('disabled', 'Disabled'),
+ ('basic', 'Basic'),
+]
+
+_streameye_enabled = None
+
+
+def _get_streameye_enabled():
+ global _streameye_enabled
+
+ if _streameye_enabled is not None:
+ return _streameye_enabled
+
+ camera_ids = config.get_camera_ids(filter_valid=False) # filter_valid prevents infinte recursion
+ if len(camera_ids) != 1:
+ _streameye_enabled = False
+ return False
+
+ camera_config = config.get_camera(camera_ids[0], as_lines=True) # as_lines prevents infinte recursion
+ camera_config = config._conf_to_dict(camera_config)
+ if camera_config.get('@proto') != 'mjpeg':
+ _streameye_enabled = False
+ return False
+ if '127.0.0.1:' not in camera_config.get('@url', ''):
+ _streameye_enabled = False
+ return False
+
+ _streameye_enabled = True
+ return True
+
+
+def _set_streameye_enabled_deferred(enabled):
+ was_enabled = _get_streameye_enabled()
+ if enabled and not was_enabled:
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_set_streameye_enabled, True)
+
+ elif not enabled and was_enabled:
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_set_streameye_enabled, False)
+
+ if enabled:
+ # this will force updating streameye settings whenever the surveillance credentials are changed
+ streameye_settings = _get_streameye_settings(1)
+ _set_streameye_settings(1, streameye_settings)
+
+
+def _set_streameye_enabled(enabled):
+ global _streameye_enabled
+
+ if enabled:
+ logging.debug('removing all cameras from cache')
+ config._camera_config_cache = {}
+ config._camera_ids_cache = []
+
+ logging.debug('disabling all cameras in motion.conf')
+ cmd = 'sed -r -i "s/^thread (.*)/#thread \\1/" /data/etc/motion.conf &>/dev/null'
+ if os.system(cmd):
+ logging.error('failed to disable cameras in motion.conf')
+
+ logging.debug('renaming thread files')
+ for name in os.listdir(settings.CONF_PATH):
+ if re.match('^thread-\d+.conf$', name):
+ os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name + '.bak'))
+
+ logging.debug('adding simple mjpeg camera')
+
+ streameye_settings = _get_streameye_settings(1)
+ main_config = config.get_main()
+
+ device_details = {
+ 'proto': 'mjpeg',
+ 'host': '127.0.0.1',
+ 'port': streameye_settings['sePort'],
+ 'username': '',
+ 'password': '',
+ 'scheme': 'http',
+ 'path': '/'
+ }
+
+ if streameye_settings['seAuthMode'] == 'basic':
+ device_details['username'] = main_config['@normal_username']
+ device_details['password'] = main_config['@normal_password']
+
+ _streameye_enabled = True
+ config._additional_structure_cache = {}
+ camera_config = config.add_camera(device_details)
+
+ # call set_camera again so that the streamEye-related defaults are saved
+ config.set_camera(camera_config['@id'], camera_config)
+
+ _set_motioneye_add_remove_cameras(False)
+
+ else: # disabled
+ logging.debug('removing simple mjpeg camera')
+ for camera_id in config.get_camera_ids():
+ camera_config = config.get_camera(camera_id)
+ if camera_config.get('@proto') == 'mjpeg':
+ config.rem_camera(camera_id)
+
+ logging.debug('renaming thread files')
+ for name in os.listdir(settings.CONF_PATH):
+ if re.match('^thread-\d+.conf.bak$', name):
+ os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name[:-4]))
+
+ _streameye_enabled = False
+ config.invalidate()
+
+ logging.debug('enabling all cameras')
+ for camera_id in config.get_camera_ids():
+ camera_config = config.get_camera(camera_id)
+ camera_config['@enabled'] = True
+ config.set_camera(camera_id, camera_config)
+
+ _set_motioneye_add_remove_cameras(True)
+
+
+def _set_motioneye_add_remove_cameras(enabled):
+ logging.debug('%s motionEye add/remove cameras' % ['disabling', 'enabling'][enabled])
+
+ lines = []
+ found = False
+ if os.path.exists(MOTIONEYE_CONF):
+ with open(MOTIONEYE_CONF) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, _ = line.split(' ', 2)
+
+ except:
+ continue
+
+ name = name.replace('_', '-')
+
+ if name == 'add-remove-cameras':
+ lines[i] = 'add-remove-cameras %s' % str(enabled).lower()
+ found = True
+
+ if not found:
+ lines.append('add-remove-cameras %s' % str(enabled).lower())
+
+ with open(MOTIONEYE_CONF, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_raspimjpeg_settings(camera_id):
+ s = {
+ 'preview': False,
+ 'brightness': 50,
+ 'contrast': 0,
+ 'saturation': 0,
+ 'sharpness': 0,
+ 'iso': 400,
+ 'ev': 0,
+ 'shutter': 0,
+ 'exposure': 'auto',
+ 'awb': 'auto',
+ 'metering': 'average',
+ 'drc': 'off',
+ 'vstab': False,
+ 'denoise': False,
+ 'imxfx': 'none',
+ 'width': 640,
+ 'height': 480,
+ 'rotation': 0,
+ 'vflip': False,
+ 'hflip': False,
+ 'framerate': 15,
+ 'quality': 25,
+ 'zoomx': 0,
+ 'zoomy': 0,
+ 'zoomw': 100,
+ 'zoomh': 100
+ }
+
+ if os.path.exists(RASPIMJPEG_CONF):
+ logging.debug('reading raspimjpeg settings from %s' % RASPIMJPEG_CONF)
+
+ with open(RASPIMJPEG_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, value = line.split(' ', 1)
+
+ except:
+ continue
+
+ name = name.replace('_', '-')
+
+ try:
+ value = int(value)
+
+ except:
+ pass
+
+ if value == 'false':
+ value = False
+
+ elif value == 'true':
+ value = True
+
+ if name == 'zoom':
+ try:
+ parts = value.split(',')
+ s['zoomx'] = int(float(parts[0]) * 100)
+ s['zoomy'] = int(float(parts[1]) * 100)
+ s['zoomw'] = int(float(parts[2]) * 100)
+ s['zoomh'] = int(float(parts[3]) * 100)
+
+ except:
+ logging.error('failed to parse zoom setting "%s"' % value)
+
+ continue
+
+ s[name] = value
+
+ s['contrast'] = (s['contrast'] + 100) / 2
+ s['saturation'] = (s['saturation'] + 100) / 2
+ s['sharpness'] = (s['sharpness'] + 100) / 2
+
+ s['resolution'] = '%sx%s' % (s.pop('width'), s.pop('height'))
+
+ s = dict(('se' + n[0].upper() + n[1:], v) for (n, v) in s.items())
+
+ return s
+
+
+def _set_raspimjpeg_settings(camera_id, s):
+ s = dict((n[2].lower() + n[3:], v) for (n, v) in s.items())
+
+ s['width'] = int(s['resolution'].split('x')[0])
+ s['height'] = int(s.pop('resolution').split('x')[1])
+
+ s['zoom'] = '%.2f,%.2f,%.2f,%.2f' % (
+ s.pop('zoomx') / 100.0, s.pop('zoomy') / 100.0,
+ s.pop('zoomw') / 100.0, s.pop('zoomh') / 100.0)
+
+ s['contrast'] = s['contrast'] * 2 - 100
+ s['saturation'] = s['saturation'] * 2 - 100
+ s['sharpness'] = s['sharpness'] * 2 - 100
+
+ logging.debug('writing raspimjpeg settings to %s' % RASPIMJPEG_CONF)
+
+ lines = []
+ for name, value in sorted(s.items(), key=lambda i: i[0]):
+ if isinstance(value, bool):
+ value = str(value).lower()
+
+ line = '%s %s\n' % (name, value)
+ lines.append(line)
+
+ with open(RASPIMJPEG_CONF, 'w') as f:
+ for line in lines:
+ f.write(line)
+
+
+def _get_streameye_settings(camera_id):
+ s = {
+ 'seAuthMode': 'disabled',
+ 'sePort': 8081,
+ }
+
+ if os.path.exists(STREAMEYE_CONF):
+ logging.debug('reading streameye settings from %s' % STREAMEYE_CONF)
+
+ with open(STREAMEYE_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ m = re.findall('PORT="?(\d+)"?', line)
+ if m:
+ s['sePort'] = int(m[0])
+ continue
+
+ m = re.findall('AUTH="?(\w+)"?', line)
+ if m:
+ s['seAuthMode'] = m[0]
+
+ return s
+
+
+def _set_streameye_settings(camera_id, s):
+ s = dict(s)
+ s.setdefault('sePort', 8081)
+ s.setdefault('seAuthMode', 'disabled')
+
+ main_config = config.get_main()
+ username = main_config['@normal_username']
+ password = main_config['@normal_password']
+ realm = 'motionEyeOS'
+
+ logging.debug('writing streameye settings to %s' % STREAMEYE_CONF)
+
+ lines = [
+ 'PORT="%s"' % s['sePort'],
+ 'AUTH="%s"' % s['seAuthMode'],
+ 'CREDENTIALS="%s:%s:%s"' % (username, password, realm)
+ ]
+
+ with open(STREAMEYE_CONF, 'w') as f:
+ for line in lines:
+ f.write(line + '\n')
+
+ if 1 in config.get_camera_ids():
+ # a workaround to update the camera username and password
+ # since we cannot call set_camera() from here
+ if s['seAuthMode'] == 'basic':
+ url = 'http://%s:%s@127.0.0.1:%s/' % (username, password, s['sePort'])
+
+ else:
+ url = 'http://127.0.0.1:%s/' % s['sePort']
+
+ if 1 in config._camera_config_cache:
+ logging.debug('updating streaming authentication in config cache')
+ config._camera_config_cache[1]['@url'] = url
+
+ lines = config.get_camera(1, as_lines=True)
+ for i, line in enumerate(lines):
+ if line.startswith('# @url'):
+ lines[i] = '# @url %s' % url
+
+ config_file = os.path.join(settings.CONF_PATH, config._CAMERA_CONFIG_FILE_NAME % {'id': 1})
+ logging.debug('updating streaming authentication in camera config file %s' % config_file)
+ with open(config_file, 'w') as f:
+ for line in lines:
+ f.write(line + '\n')
+
+ logging.debug('restarting streameye')
+ if os.system('streameye.sh restart'):
+ logging.error('streameye restart failed')
+
+
+# make streameye-related log files downloadable
+
+if _get_streameye_enabled():
+ def _add_log_handlers():
+ import handlers
+ handlers.LogHandler.LOGS['streameye'] = (os.path.join(settings.LOG_PATH, 'streameye.log'), 'streameye.log')
+ handlers.LogHandler.LOGS['raspimjpeg'] = (os.path.join(settings.LOG_PATH, 'raspimjpeg.log'), 'raspimjpeg.log')
+
+ # handlers.LogHandler is not yet available
+ # at the time streameyectl is imported
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_add_log_handlers)
+
+ @additional_config
+ def streamEyeLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': lambda: 'streameye.log',
+ }
+
+ @additional_config
+ def raspiMjpegLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': lambda: 'raspimjpeg.log',
+ }
+
+
+@additional_config
+def streamEyeMainSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def streamEye():
+ return {
+ 'label': 'Fast Network Camera',
+ 'description': 'Enabling this option will turn your Raspberry PI into a simple and fast MJPEG network camera, ' +
+ 'disabling motion detection, media files and all other advanced features (works only with the CSI camera)',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_streameye_enabled,
+ 'set': _set_streameye_enabled_deferred,
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator1():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seBrightness():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Brightness',
+ 'description': 'sets a desired brightness level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seContrast():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Contrast',
+ 'description': 'sets a desired contrast level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seSaturation():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Saturation',
+ 'description': 'sets a desired saturation level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seSharpness():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Sharpness',
+ 'description': 'sets a desired sharpness level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+
+@additional_config
+def streamEyeCameraSeparator2():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seResolution():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Resolution',
+ 'description': 'the video resolution (larger values produce better quality but require more CPU power, larger storage space and bandwidth)',
+ 'type': 'choices',
+ 'choices': RESOLUTION_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seRotation():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Rotation',
+ 'description': 'use this to rotate the captured image, if your camera is not positioned correctly',
+ 'type': 'choices',
+ 'choices': ROTATION_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seVflip():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Flip Vertically',
+ 'description': 'enable this to flip the captured image vertically',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seHflip():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Flip Horizontally',
+ 'description': 'enable this to flip the captured image horizontally',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seFramerate():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Frame Rate',
+ 'description': 'sets the number of frames captured by the camera every second (higher values produce smoother videos but require more CPU power, larger storage space and bandwidth)',
+ 'type': 'range',
+ 'min': 1,
+ 'max': 30,
+ 'snap': 0,
+ 'ticks': "1|5|10|15|20|25|30",
+ 'decimals': 0,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seQuality():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Image Quality',
+ 'description': 'sets the JPEG image quality (higher values produce a better image quality but require more storage space and bandwidth)',
+ 'type': 'range',
+ 'min': 1,
+ 'max': 100,
+ 'snap': 2,
+ 'ticks': '1|25|50|75|100',
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomx():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom X',
+ 'description': 'sets the horizontal zoom offset',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 80,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomy():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Y',
+ 'description': 'sets the vertical zoom offset',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 80,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomw():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Width',
+ 'description': 'sets the zoom width',
+ 'type': 'range',
+ 'min': 20,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomh():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Height',
+ 'description': 'sets the zoom height',
+ 'type': 'range',
+ 'min': 20,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sePreview():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'HDMI Preview',
+ 'description': 'enable this if you want to see the preview on an HDMI-connected monitor',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator3():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seIso():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'ISO',
+ 'description': 'sets a desired ISO level for this camera',
+ 'type': 'range',
+ 'min': 100,
+ 'max': 800,
+ 'snap': 1,
+ 'ticksnum': 8,
+ 'decimals': 0,
+ 'unit': '',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seShutter():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Shutter Speed',
+ 'description': 'sets a desired shutter speed for this camera',
+ 'type': 'number',
+ 'min': 0,
+ 'max': 6000000,
+ 'unit': 'microseconds',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator4():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seExposure():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Exposure Mode',
+ 'description': 'sets a desired exposure mode for this camera',
+ 'type': 'choices',
+ 'choices': EXPOSURE_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seEv():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Exposure Compensation',
+ 'description': 'sets a desired exposure compensation for this camera',
+ 'type': 'range',
+ 'min': -25,
+ 'max': 25,
+ 'snap': 1,
+ 'ticksnum': 11,
+ 'decimals': 0,
+ 'unit': '',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seAwb():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Automatic White Balance',
+ 'description': 'sets a desired automatic white balance mode for this camera',
+ 'type': 'choices',
+ 'choices': AWB_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seMetering():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Metering Mode',
+ 'description': 'sets a desired metering mode for this camera',
+ 'type': 'choices',
+ 'choices': METERING_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seDrc():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Dynamic Range Compensation',
+ 'description': 'sets a desired dynamic range compensation level for this camera',
+ 'type': 'choices',
+ 'choices': DRC_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seVstab():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Stabilization',
+ 'description': 'enables or disables video stabilization for this camera',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seDenoise():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Denoise',
+ 'description': 'enables image denoising',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seImxfx():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Image Effect',
+ 'description': 'sets a desired image effect for this camera',
+ 'type': 'choices',
+ 'choices': IMXFX_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sePort():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Streaming Port',
+ 'description': 'sets the TCP port on which the webcam streaming server listens',
+ 'type': 'number',
+ 'min': 1024,
+ 'max': 65535,
+ 'section': 'streaming',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_streameye_settings,
+ 'set': _set_streameye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seAuthMode():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Authentication Mode',
+ 'description': 'the authentication mode to use when accessing the stream (use Basic instead of Digest if you encounter issues with third party apps)',
+ 'type': 'choices',
+ 'choices': AUTH_CHOICES,
+ 'section': 'streaming',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_streameye_settings,
+ 'set': _set_streameye_settings,
+ 'get_set_dict': True
+ }
+
diff --git a/board/raspberrypi2/overlay/etc/init.d/S84streameye b/board/raspberrypi2/overlay/etc/init.d/S84streameye
new file mode 100755
index 0000000000..81e2e2c896
--- /dev/null
+++ b/board/raspberrypi2/overlay/etc/init.d/S84streameye
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+motioneye_conf_dir="/data/etc/"
+
+test -n "$os_version" || source /etc/init.d/base
+
+enabled() {
+ test $(ls -1 $motioneye_conf_dir/thread-*.conf 2>/dev/null| wc -l) == 1 || return 1
+
+ grep '# @proto mjpeg' $motioneye_conf_dir/thread-1.conf &>/dev/null || return 1
+
+ grep -E '# @url http://(.*)127.0.0.1:' $motioneye_conf_dir/thread-1.conf &>/dev/null || return 1
+
+ return 0
+}
+
+enabled || exit 0
+
+start() {
+ msg_begin "Starting streameye"
+ streameye.sh start
+ test $? == 0 && msg_done || msg_fail
+}
+
+stop() {
+ msg_begin "Stopping streameye"
+ streameye.sh stop
+ test $? == 0 && msg_done || msg_fail
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $?
+
diff --git a/board/raspberrypi2/overlay/etc/modules b/board/raspberrypi2/overlay/etc/modules
new file mode 100644
index 0000000000..2990e0b8e0
--- /dev/null
+++ b/board/raspberrypi2/overlay/etc/modules
@@ -0,0 +1,3 @@
+bcm2835-v4l2 max_video_width=2592 max_video_height=1944
+bcm2835-wdt
+
diff --git a/board/raspberrypi2/overlay/usr/bin/streameye.sh b/board/raspberrypi2/overlay/usr/bin/streameye.sh
new file mode 100755
index 0000000000..8ab060182b
--- /dev/null
+++ b/board/raspberrypi2/overlay/usr/bin/streameye.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+RASPIMJPEG_CONF=/data/etc/raspimjpeg.conf
+RASPIMJPEG_LOG=/var/log/raspimjpeg.log
+MOTIONEYE_CONF=/data/etc/motioneye.conf
+STREAMEYE_CONF=/data/etc/streameye.conf
+STREAMEYE_LOG=/var/log/streameye.log
+
+test -r $RASPIMJPEG_CONF || exit 1
+test -r $STREAMEYE_CONF || exit 1
+
+watch() {
+ count=0
+ while true; do
+ sleep 5
+ if ! ps aux | grep raspimjpeg.py | grep -v grep &>/dev/null; then
+ logger -t streameye -s "not running, respawning"
+ start
+ fi
+ done
+}
+
+function start() {
+ pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1)
+ if [ -n "$pid" ]; then
+ return
+ fi
+
+ raspimjpeg_opts=""
+ while read line; do
+ if echo "$line" | grep false &>/dev/null; then
+ continue
+ fi
+ if echo "$line" | grep true &>/dev/null; then
+ line=$(echo $line | cut -d ' ' -f 1)
+ fi
+ raspimjpeg_opts="$raspimjpeg_opts --$line"
+ done < $RASPIMJPEG_CONF
+
+ source $STREAMEYE_CONF
+ streameye_opts="-p $PORT"
+ if [ -n "$CREDENTIALS" ] && [ "$AUTH" = "basic" ]; then
+ streameye_opts="$streameye_opts -a basic -c $CREDENTIALS"
+ fi
+
+ if [ -r $MOTIONEYE_CONF ] && grep 'log-level debug' $MOTIONEYE_CONF >/dev/null; then
+ raspimjpeg_opts="$raspimjpeg_opts -d"
+ streameye_opts="$streameye_opts -d"
+ fi
+
+ raspimjpeg.py $raspimjpeg_opts 2>$RASPIMJPEG_LOG | streameye $streameye_opts &>$STREAMEYE_LOG &
+}
+
+function stop() {
+ # stop the streameye background watch process
+ ps | grep streameye.sh | grep -v $$ | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1 | xargs -r kill
+
+ # stop the raspimjpeg process
+ pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1)
+ if [ -z "$pid" ]; then
+ return
+ fi
+
+ kill -HUP "$pid" &>/dev/null
+ count=0
+ while kill -0 "$pid" &>/dev/null && [ $count -lt 5 ]; do
+ sleep 1
+ count=$(($count + 1))
+ done
+ kill -KILL "$pid" &>/dev/null || true
+}
+
+case "$1" in
+ start)
+ start
+ watch &
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ watch &
+ ;;
+
+ *)
+ echo $"Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
diff --git a/board/raspberrypi3/motioneye-modules/boardctl.py b/board/raspberrypi3/motioneye-modules/boardctl.py
new file mode 100644
index 0000000000..9fdfa06e54
--- /dev/null
+++ b/board/raspberrypi3/motioneye-modules/boardctl.py
@@ -0,0 +1,238 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+
+from config import additional_config
+
+import streameyectl
+
+
+CONFIG_TXT = '/boot/config.txt'
+MONITOR = '/data/etc/monitor_1'
+
+MONITOR_SCRIPT = '''#!/bin/bash
+
+net_tmp=/tmp/netspeed.tmp
+temp=$(($(cat /sys/devices/virtual/thermal/thermal_zone0/temp) / 1000))
+load=$(cat /proc/loadavg | cut -d ' ' -f 2)
+recv=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 3 | tail -n +3 | awk '{s+=$1} END {print s}')
+send=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 11 | tail -n +3 | awk '{s+=$1} END {print s}')
+total=$(($recv + $send))
+
+if [ -e $net_tmp ]; then
+ prev_total=$(cat $net_tmp)
+ speed=$(($total - $prev_total))
+else
+ speed=0
+fi
+
+echo $total > $net_tmp
+speed=$(($speed / 1024))
+
+echo -n "$temp°|$load|${speed}kB/s"
+'''
+
+OVERCLOCK = {
+ 700: '700|250|400|0',
+ 800: '800|250|400|0',
+ 900: '900|250|450|0',
+ 950: '950|250|450|0',
+ 1000: '1000|500|600|6',
+ 1001: '1001|500|500|2'
+}
+
+def _get_board_settings():
+ gpu_mem = 128
+ camera_led = True
+ arm_freq = 700
+
+ if os.path.exists(CONFIG_TXT):
+ logging.debug('reading board settings from %s' % CONFIG_TXT)
+
+ with open(CONFIG_TXT) as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+
+ parts = line.split('=', 1)
+ if len(parts) != 2:
+ continue
+
+ name, value = parts
+ name = name.strip()
+ value = value.strip()
+
+ if name == 'gpu_mem':
+ gpu_mem = int(value)
+
+ elif name == 'arm_freq':
+ arm_freq = int(value)
+
+ elif name == 'disable_camera_led':
+ camera_led = value == '0'
+
+
+ s = {
+ 'gpuMem': gpu_mem,
+ 'cameraLed': camera_led
+ }
+
+ logging.debug('board settings: gpu_mem=%(gpuMem)s, camera_led=%(cameraLed)s' % s)
+
+ return s
+
+
+def _set_board_settings(s):
+ s.setdefault('gpuMem', 128)
+ s.setdefault('cameraLed', True)
+
+ old_settings = _get_board_settings()
+ if s == old_settings:
+ return # nothing has changed
+
+ seen = set()
+
+ logging.debug('writing board settings to %s: ' % CONFIG_TXT +
+ 'gpu_mem=%(gpuMem)s, camera_led=%(cameraLed)s' % s)
+
+ lines = []
+ if os.path.exists(CONFIG_TXT):
+ with open(CONFIG_TXT) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ line = line.strip('#')
+
+ try:
+ name, _ = line.split('=', 1)
+ name = name.strip()
+
+ except:
+ continue
+
+ seen.add(name)
+
+ if name == 'gpu_mem':
+ lines[i] = '%s=%s' % (name, s['gpuMem'])
+
+ elif name == 'disable_camera_led':
+ lines[i] = 'disable_camera_led=%s' % ['1', '0'][s['cameraLed']]
+
+ if 'gpu_mem' not in seen:
+ lines.append('gpu_mem=%s' % s['gpuMem'])
+
+ if 'disable_camera_led' not in seen:
+ lines.append('disable_camera_led=%s' % ['1', '0'][s['cameraLed']])
+
+ logging.debug('remounting /boot read-write')
+ if os.system('mount -o remount,rw /boot'):
+ logging.error('failed to remount /boot read-write')
+
+ with open(CONFIG_TXT, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_sys_mon():
+ return os.access(MONITOR, os.X_OK)
+
+
+def _set_sys_mon(sys_mon):
+ if sys_mon:
+ if os.access(MONITOR, os.X_OK):
+ pass
+
+ else:
+ if os.path.exists(MONITOR):
+ os.system('chmod +x %s' % MONITOR)
+
+ else:
+ with open(MONITOR, 'w') as f:
+ f.write(MONITOR_SCRIPT)
+
+ os.system('chmod +x %s' % MONITOR)
+
+ else:
+ if os.access(MONITOR, os.X_OK):
+ os.system('chmod -x %s' % MONITOR)
+
+
+@additional_config
+def boardSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def gpuMem():
+ return {
+ 'label': 'GPU Memory',
+ 'description': 'set the amount of memory reserved for the GPU (choose at least 96MB if you use the CSI camera board)',
+ 'type': 'number',
+ 'min': '16',
+ 'max': '944',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def cameraLed():
+ return {
+ 'label': 'Enable CSI Camera Led',
+ 'description': 'control the led on the CSI camera board',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_board_settings,
+ 'set': _set_board_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sysMon():
+ return {
+ 'label': 'Enable System Monitoring',
+ 'description': 'when this is enabled, system monitoring info will be overlaid on top of the first camera frame',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': False,
+ 'get': _get_sys_mon,
+ 'set': _set_sys_mon
+ }
+
diff --git a/board/raspberrypi3/motioneye-modules/streameyectl.py b/board/raspberrypi3/motioneye-modules/streameyectl.py
new file mode 100644
index 0000000000..5f751332f4
--- /dev/null
+++ b/board/raspberrypi3/motioneye-modules/streameyectl.py
@@ -0,0 +1,1172 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import config
+import hashlib
+import logging
+import os.path
+import re
+
+from tornado.ioloop import IOLoop
+
+import settings
+
+from config import additional_config
+
+
+MOTIONEYE_CONF = '/data/etc/motioneye.conf'
+RASPIMJPEG_CONF = '/data/etc/raspimjpeg.conf'
+STREAMEYE_CONF = '/data/etc/streameye.conf'
+
+EXPOSURE_CHOICES = [
+ ('off', 'Off'),
+ ('auto', 'Auto'),
+ ('night', 'Night'),
+ ('nightpreview', 'Night Preview'),
+ ('backlight', 'Backlight'),
+ ('spotlight', 'Spotlight'),
+ ('sports', 'Sports'),
+ ('snow', 'Snow'),
+ ('beach', 'Beach'),
+ ('verylong', 'Very Long'),
+ ('fixedfps', 'Fixed FPS'),
+ ('antishake', 'Antishake'),
+ ('fireworks', 'Fireworks')
+]
+
+AWB_CHOICES = [
+ ('off', 'Off'),
+ ('auto', 'Auto'),
+ ('sunlight', 'Sunlight'),
+ ('cloudy', 'Cloudy'),
+ ('shade', 'Shade'),
+ ('tungsten', 'Tungsten'),
+ ('fluorescent', 'Fluorescent'),
+ ('incandescent', 'Incandescent'),
+ ('flash', 'Flash'),
+ ('horizon', 'Horizon')
+]
+
+METERING_CHOICES = [
+ ('average', 'Average'),
+ ('spot', 'Spot'),
+ ('backlit', 'Backlit'),
+ ('matrix', 'Matrix')
+]
+
+DRC_CHOICES = [
+ ('off', 'Off'),
+ ('low', 'Low'),
+ ('medium', 'Medium'),
+ ('high', 'High')
+]
+
+IMXFX_CHOICES = [
+ ('none', 'None'),
+ ('negative', 'Negative'),
+ ('solarize', 'Solarize'),
+ ('sketch', 'Sketch'),
+ ('denoise', 'Denoise'),
+ ('emboss', 'Emboss'),
+ ('oilpaint', 'Oilpaint'),
+ ('hatch', 'Hatch'),
+ ('gpen', 'G Pen'),
+ ('pastel', 'Pastel'),
+ ('watercolor', 'Water Color'),
+ ('film', 'Film'),
+ ('blur', 'Blur'),
+ ('saturation', 'Saturation'),
+ ('colorswap', 'Color Swap'),
+ ('washedout', 'Washed Out'),
+ ('posterise', 'Posterize'),
+ ('colorpoint', 'Color Point'),
+ ('colorbalance', 'Color Balance'),
+ ('cartoon', 'Cartoon'),
+ ('deinterlace1', 'Deinterlace 1'),
+ ('deinterlace2', 'Deinterlace 2')
+]
+
+RESOLUTION_CHOICES = [
+ ('320x200', '320x200'),
+ ('320x240', '320x240'),
+ ('640x480', '640x480'),
+ ('800x480', '800x480'),
+ ('800x600', '800x600'),
+ ('1024x576', '1024x576'),
+ ('1024x768', '1024x768'),
+ ('1280x720', '1280x720'),
+ ('1280x800', '1280x800'),
+ ('1280x960', '1280x960'),
+ ('1280x1024', '1280x1024'),
+ ('1296x972', '1296x972'),
+ ('1440x960', '1440x960'),
+ ('1440x1024', '1440x1024'),
+ ('1600x1200', '1600x1200'),
+ ('1640x922', '1640x922'),
+ ('1640x1232', '1640x1232'),
+ ('1920x1080', '1920x1080'),
+ ('2592x1944', '2592x1944'),
+ ('3280x2464', '3280x2464')
+]
+
+ROTATION_CHOICES = [
+ ('0', '0°'),
+ ('90', '90°'),
+ ('180', '180°'),
+ ('270', '270°')
+]
+
+AUTH_CHOICES = [
+ ('disabled', 'Disabled'),
+ ('basic', 'Basic'),
+]
+
+_streameye_enabled = None
+
+
+def _get_streameye_enabled():
+ global _streameye_enabled
+
+ if _streameye_enabled is not None:
+ return _streameye_enabled
+
+ camera_ids = config.get_camera_ids(filter_valid=False) # filter_valid prevents infinte recursion
+ if len(camera_ids) != 1:
+ _streameye_enabled = False
+ return False
+
+ camera_config = config.get_camera(camera_ids[0], as_lines=True) # as_lines prevents infinte recursion
+ camera_config = config._conf_to_dict(camera_config)
+ if camera_config.get('@proto') != 'mjpeg':
+ _streameye_enabled = False
+ return False
+ if '127.0.0.1:' not in camera_config.get('@url', ''):
+ _streameye_enabled = False
+ return False
+
+ _streameye_enabled = True
+ return True
+
+
+def _set_streameye_enabled_deferred(enabled):
+ was_enabled = _get_streameye_enabled()
+ if enabled and not was_enabled:
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_set_streameye_enabled, True)
+
+ elif not enabled and was_enabled:
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_set_streameye_enabled, False)
+
+ if enabled:
+ # this will force updating streameye settings whenever the surveillance credentials are changed
+ streameye_settings = _get_streameye_settings(1)
+ _set_streameye_settings(1, streameye_settings)
+
+
+def _set_streameye_enabled(enabled):
+ global _streameye_enabled
+
+ if enabled:
+ logging.debug('removing all cameras from cache')
+ config._camera_config_cache = {}
+ config._camera_ids_cache = []
+
+ logging.debug('disabling all cameras in motion.conf')
+ cmd = 'sed -r -i "s/^thread (.*)/#thread \\1/" /data/etc/motion.conf &>/dev/null'
+ if os.system(cmd):
+ logging.error('failed to disable cameras in motion.conf')
+
+ logging.debug('renaming thread files')
+ for name in os.listdir(settings.CONF_PATH):
+ if re.match('^thread-\d+.conf$', name):
+ os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name + '.bak'))
+
+ logging.debug('adding simple mjpeg camera')
+
+ streameye_settings = _get_streameye_settings(1)
+ main_config = config.get_main()
+
+ device_details = {
+ 'proto': 'mjpeg',
+ 'host': '127.0.0.1',
+ 'port': streameye_settings['sePort'],
+ 'username': '',
+ 'password': '',
+ 'scheme': 'http',
+ 'path': '/'
+ }
+
+ if streameye_settings['seAuthMode'] == 'basic':
+ device_details['username'] = main_config['@normal_username']
+ device_details['password'] = main_config['@normal_password']
+
+ _streameye_enabled = True
+ config._additional_structure_cache = {}
+ camera_config = config.add_camera(device_details)
+
+ # call set_camera again so that the streamEye-related defaults are saved
+ config.set_camera(camera_config['@id'], camera_config)
+
+ _set_motioneye_add_remove_cameras(False)
+
+ else: # disabled
+ logging.debug('removing simple mjpeg camera')
+ for camera_id in config.get_camera_ids():
+ camera_config = config.get_camera(camera_id)
+ if camera_config.get('@proto') == 'mjpeg':
+ config.rem_camera(camera_id)
+
+ logging.debug('renaming thread files')
+ for name in os.listdir(settings.CONF_PATH):
+ if re.match('^thread-\d+.conf.bak$', name):
+ os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name[:-4]))
+
+ _streameye_enabled = False
+ config.invalidate()
+
+ logging.debug('enabling all cameras')
+ for camera_id in config.get_camera_ids():
+ camera_config = config.get_camera(camera_id)
+ camera_config['@enabled'] = True
+ config.set_camera(camera_id, camera_config)
+
+ _set_motioneye_add_remove_cameras(True)
+
+
+def _set_motioneye_add_remove_cameras(enabled):
+ logging.debug('%s motionEye add/remove cameras' % ['disabling', 'enabling'][enabled])
+
+ lines = []
+ found = False
+ if os.path.exists(MOTIONEYE_CONF):
+ with open(MOTIONEYE_CONF) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, _ = line.split(' ', 2)
+
+ except:
+ continue
+
+ name = name.replace('_', '-')
+
+ if name == 'add-remove-cameras':
+ lines[i] = 'add-remove-cameras %s' % str(enabled).lower()
+ found = True
+
+ if not found:
+ lines.append('add-remove-cameras %s' % str(enabled).lower())
+
+ with open(MOTIONEYE_CONF, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_raspimjpeg_settings(camera_id):
+ s = {
+ 'preview': False,
+ 'brightness': 50,
+ 'contrast': 0,
+ 'saturation': 0,
+ 'sharpness': 0,
+ 'iso': 400,
+ 'ev': 0,
+ 'shutter': 0,
+ 'exposure': 'auto',
+ 'awb': 'auto',
+ 'metering': 'average',
+ 'drc': 'off',
+ 'vstab': False,
+ 'denoise': False,
+ 'imxfx': 'none',
+ 'width': 640,
+ 'height': 480,
+ 'rotation': 0,
+ 'vflip': False,
+ 'hflip': False,
+ 'framerate': 15,
+ 'quality': 25,
+ 'zoomx': 0,
+ 'zoomy': 0,
+ 'zoomw': 100,
+ 'zoomh': 100
+ }
+
+ if os.path.exists(RASPIMJPEG_CONF):
+ logging.debug('reading raspimjpeg settings from %s' % RASPIMJPEG_CONF)
+
+ with open(RASPIMJPEG_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, value = line.split(' ', 1)
+
+ except:
+ continue
+
+ name = name.replace('_', '-')
+
+ try:
+ value = int(value)
+
+ except:
+ pass
+
+ if value == 'false':
+ value = False
+
+ elif value == 'true':
+ value = True
+
+ if name == 'zoom':
+ try:
+ parts = value.split(',')
+ s['zoomx'] = int(float(parts[0]) * 100)
+ s['zoomy'] = int(float(parts[1]) * 100)
+ s['zoomw'] = int(float(parts[2]) * 100)
+ s['zoomh'] = int(float(parts[3]) * 100)
+
+ except:
+ logging.error('failed to parse zoom setting "%s"' % value)
+
+ continue
+
+ s[name] = value
+
+ s['contrast'] = (s['contrast'] + 100) / 2
+ s['saturation'] = (s['saturation'] + 100) / 2
+ s['sharpness'] = (s['sharpness'] + 100) / 2
+
+ s['resolution'] = '%sx%s' % (s.pop('width'), s.pop('height'))
+
+ s = dict(('se' + n[0].upper() + n[1:], v) for (n, v) in s.items())
+
+ return s
+
+
+def _set_raspimjpeg_settings(camera_id, s):
+ s = dict((n[2].lower() + n[3:], v) for (n, v) in s.items())
+
+ s['width'] = int(s['resolution'].split('x')[0])
+ s['height'] = int(s.pop('resolution').split('x')[1])
+
+ s['zoom'] = '%.2f,%.2f,%.2f,%.2f' % (
+ s.pop('zoomx') / 100.0, s.pop('zoomy') / 100.0,
+ s.pop('zoomw') / 100.0, s.pop('zoomh') / 100.0)
+
+ s['contrast'] = s['contrast'] * 2 - 100
+ s['saturation'] = s['saturation'] * 2 - 100
+ s['sharpness'] = s['sharpness'] * 2 - 100
+
+ logging.debug('writing raspimjpeg settings to %s' % RASPIMJPEG_CONF)
+
+ lines = []
+ for name, value in sorted(s.items(), key=lambda i: i[0]):
+ if isinstance(value, bool):
+ value = str(value).lower()
+
+ line = '%s %s\n' % (name, value)
+ lines.append(line)
+
+ with open(RASPIMJPEG_CONF, 'w') as f:
+ for line in lines:
+ f.write(line)
+
+
+def _get_streameye_settings(camera_id):
+ s = {
+ 'seAuthMode': 'disabled',
+ 'sePort': 8081,
+ }
+
+ if os.path.exists(STREAMEYE_CONF):
+ logging.debug('reading streameye settings from %s' % STREAMEYE_CONF)
+
+ with open(STREAMEYE_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ m = re.findall('PORT="?(\d+)"?', line)
+ if m:
+ s['sePort'] = int(m[0])
+ continue
+
+ m = re.findall('AUTH="?(\w+)"?', line)
+ if m:
+ s['seAuthMode'] = m[0]
+
+ return s
+
+
+def _set_streameye_settings(camera_id, s):
+ s = dict(s)
+ s.setdefault('sePort', 8081)
+ s.setdefault('seAuthMode', 'disabled')
+
+ main_config = config.get_main()
+ username = main_config['@normal_username']
+ password = main_config['@normal_password']
+ realm = 'motionEyeOS'
+
+ logging.debug('writing streameye settings to %s' % STREAMEYE_CONF)
+
+ lines = [
+ 'PORT="%s"' % s['sePort'],
+ 'AUTH="%s"' % s['seAuthMode'],
+ 'CREDENTIALS="%s:%s:%s"' % (username, password, realm)
+ ]
+
+ with open(STREAMEYE_CONF, 'w') as f:
+ for line in lines:
+ f.write(line + '\n')
+
+ if 1 in config.get_camera_ids():
+ # a workaround to update the camera username and password
+ # since we cannot call set_camera() from here
+ if s['seAuthMode'] == 'basic':
+ url = 'http://%s:%s@127.0.0.1:%s/' % (username, password, s['sePort'])
+
+ else:
+ url = 'http://127.0.0.1:%s/' % s['sePort']
+
+ if 1 in config._camera_config_cache:
+ logging.debug('updating streaming authentication in config cache')
+ config._camera_config_cache[1]['@url'] = url
+
+ lines = config.get_camera(1, as_lines=True)
+ for i, line in enumerate(lines):
+ if line.startswith('# @url'):
+ lines[i] = '# @url %s' % url
+
+ config_file = os.path.join(settings.CONF_PATH, config._CAMERA_CONFIG_FILE_NAME % {'id': 1})
+ logging.debug('updating streaming authentication in camera config file %s' % config_file)
+ with open(config_file, 'w') as f:
+ for line in lines:
+ f.write(line + '\n')
+
+ logging.debug('restarting streameye')
+ if os.system('streameye.sh restart'):
+ logging.error('streameye restart failed')
+
+
+# make streameye-related log files downloadable
+
+if _get_streameye_enabled():
+ def _add_log_handlers():
+ import handlers
+ handlers.LogHandler.LOGS['streameye'] = (os.path.join(settings.LOG_PATH, 'streameye.log'), 'streameye.log')
+ handlers.LogHandler.LOGS['raspimjpeg'] = (os.path.join(settings.LOG_PATH, 'raspimjpeg.log'), 'raspimjpeg.log')
+
+ # handlers.LogHandler is not yet available
+ # at the time streameyectl is imported
+ io_loop = IOLoop.instance()
+ io_loop.add_callback(_add_log_handlers)
+
+ @additional_config
+ def streamEyeLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': lambda: 'streameye.log',
+ }
+
+ @additional_config
+ def raspiMjpegLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': lambda: 'raspimjpeg.log',
+ }
+
+
+@additional_config
+def streamEyeMainSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def streamEye():
+ return {
+ 'label': 'Fast Network Camera',
+ 'description': 'Enabling this option will turn your Raspberry PI into a simple and fast MJPEG network camera, ' +
+ 'disabling motion detection, media files and all other advanced features (works only with the CSI camera)',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_streameye_enabled,
+ 'set': _set_streameye_enabled_deferred,
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator1():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seBrightness():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Brightness',
+ 'description': 'sets a desired brightness level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seContrast():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Contrast',
+ 'description': 'sets a desired contrast level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seSaturation():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Saturation',
+ 'description': 'sets a desired saturation level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seSharpness():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Sharpness',
+ 'description': 'sets a desired sharpness level for this camera',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+
+@additional_config
+def streamEyeCameraSeparator2():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seResolution():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Resolution',
+ 'description': 'the video resolution (larger values produce better quality but require more CPU power, larger storage space and bandwidth)',
+ 'type': 'choices',
+ 'choices': RESOLUTION_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seRotation():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Rotation',
+ 'description': 'use this to rotate the captured image, if your camera is not positioned correctly',
+ 'type': 'choices',
+ 'choices': ROTATION_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seVflip():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Flip Vertically',
+ 'description': 'enable this to flip the captured image vertically',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seHflip():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Flip Horizontally',
+ 'description': 'enable this to flip the captured image horizontally',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seFramerate():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Frame Rate',
+ 'description': 'sets the number of frames captured by the camera every second (higher values produce smoother videos but require more CPU power, larger storage space and bandwidth)',
+ 'type': 'range',
+ 'min': 1,
+ 'max': 30,
+ 'snap': 0,
+ 'ticks': "1|5|10|15|20|25|30",
+ 'decimals': 0,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seQuality():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Image Quality',
+ 'description': 'sets the JPEG image quality (higher values produce a better image quality but require more storage space and bandwidth)',
+ 'type': 'range',
+ 'min': 1,
+ 'max': 100,
+ 'snap': 2,
+ 'ticks': '1|25|50|75|100',
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomx():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom X',
+ 'description': 'sets the horizontal zoom offset',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 80,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomy():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Y',
+ 'description': 'sets the vertical zoom offset',
+ 'type': 'range',
+ 'min': 0,
+ 'max': 80,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomw():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Width',
+ 'description': 'sets the zoom width',
+ 'type': 'range',
+ 'min': 20,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seZoomh():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Zoom Height',
+ 'description': 'sets the zoom height',
+ 'type': 'range',
+ 'min': 20,
+ 'max': 100,
+ 'snap': 2,
+ 'ticksnum': 5,
+ 'decimals': 0,
+ 'unit': '%',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sePreview():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'HDMI Preview',
+ 'description': 'enable this if you want to see the preview on an HDMI-connected monitor',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator3():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seIso():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'ISO',
+ 'description': 'sets a desired ISO level for this camera',
+ 'type': 'range',
+ 'min': 100,
+ 'max': 800,
+ 'snap': 1,
+ 'ticksnum': 8,
+ 'decimals': 0,
+ 'unit': '',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seShutter():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Shutter Speed',
+ 'description': 'sets a desired shutter speed for this camera',
+ 'type': 'number',
+ 'min': 0,
+ 'max': 6000000,
+ 'unit': 'microseconds',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def streamEyeCameraSeparator4():
+ return {
+ 'type': 'separator',
+ 'section': 'device',
+ 'camera': True,
+ 'advanced': True
+ }
+
+
+@additional_config
+def seExposure():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Exposure Mode',
+ 'description': 'sets a desired exposure mode for this camera',
+ 'type': 'choices',
+ 'choices': EXPOSURE_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seEv():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Exposure Compensation',
+ 'description': 'sets a desired exposure compensation for this camera',
+ 'type': 'range',
+ 'min': -25,
+ 'max': 25,
+ 'snap': 1,
+ 'ticksnum': 11,
+ 'decimals': 0,
+ 'unit': '',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seAwb():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Automatic White Balance',
+ 'description': 'sets a desired automatic white balance mode for this camera',
+ 'type': 'choices',
+ 'choices': AWB_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seMetering():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Metering Mode',
+ 'description': 'sets a desired metering mode for this camera',
+ 'type': 'choices',
+ 'choices': METERING_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seDrc():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Dynamic Range Compensation',
+ 'description': 'sets a desired dynamic range compensation level for this camera',
+ 'type': 'choices',
+ 'choices': DRC_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seVstab():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Video Stabilization',
+ 'description': 'enables or disables video stabilization for this camera',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seDenoise():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Denoise',
+ 'description': 'enables image denoising',
+ 'type': 'bool',
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seImxfx():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Image Effect',
+ 'description': 'sets a desired image effect for this camera',
+ 'type': 'choices',
+ 'choices': IMXFX_CHOICES,
+ 'section': 'device',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_raspimjpeg_settings,
+ 'set': _set_raspimjpeg_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def sePort():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Streaming Port',
+ 'description': 'sets the TCP port on which the webcam streaming server listens',
+ 'type': 'number',
+ 'min': 1024,
+ 'max': 65535,
+ 'section': 'streaming',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_streameye_settings,
+ 'set': _set_streameye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def seAuthMode():
+ if not _get_streameye_enabled():
+ return None
+
+ return {
+ 'label': 'Authentication Mode',
+ 'description': 'the authentication mode to use when accessing the stream (use Basic instead of Digest if you encounter issues with third party apps)',
+ 'type': 'choices',
+ 'choices': AUTH_CHOICES,
+ 'section': 'streaming',
+ 'advanced': True,
+ 'camera': True,
+ 'required': True,
+ 'get': _get_streameye_settings,
+ 'set': _set_streameye_settings,
+ 'get_set_dict': True
+ }
+
diff --git a/board/raspberrypi3/overlay/etc/init.d/S84streameye b/board/raspberrypi3/overlay/etc/init.d/S84streameye
new file mode 100755
index 0000000000..81e2e2c896
--- /dev/null
+++ b/board/raspberrypi3/overlay/etc/init.d/S84streameye
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+motioneye_conf_dir="/data/etc/"
+
+test -n "$os_version" || source /etc/init.d/base
+
+enabled() {
+ test $(ls -1 $motioneye_conf_dir/thread-*.conf 2>/dev/null| wc -l) == 1 || return 1
+
+ grep '# @proto mjpeg' $motioneye_conf_dir/thread-1.conf &>/dev/null || return 1
+
+ grep -E '# @url http://(.*)127.0.0.1:' $motioneye_conf_dir/thread-1.conf &>/dev/null || return 1
+
+ return 0
+}
+
+enabled || exit 0
+
+start() {
+ msg_begin "Starting streameye"
+ streameye.sh start
+ test $? == 0 && msg_done || msg_fail
+}
+
+stop() {
+ msg_begin "Stopping streameye"
+ streameye.sh stop
+ test $? == 0 && msg_done || msg_fail
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $?
+
diff --git a/board/raspberrypi3/overlay/etc/modules b/board/raspberrypi3/overlay/etc/modules
new file mode 100644
index 0000000000..2990e0b8e0
--- /dev/null
+++ b/board/raspberrypi3/overlay/etc/modules
@@ -0,0 +1,3 @@
+bcm2835-v4l2 max_video_width=2592 max_video_height=1944
+bcm2835-wdt
+
diff --git a/board/raspberrypi3/overlay/usr/bin/streameye.sh b/board/raspberrypi3/overlay/usr/bin/streameye.sh
new file mode 100755
index 0000000000..8ab060182b
--- /dev/null
+++ b/board/raspberrypi3/overlay/usr/bin/streameye.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+RASPIMJPEG_CONF=/data/etc/raspimjpeg.conf
+RASPIMJPEG_LOG=/var/log/raspimjpeg.log
+MOTIONEYE_CONF=/data/etc/motioneye.conf
+STREAMEYE_CONF=/data/etc/streameye.conf
+STREAMEYE_LOG=/var/log/streameye.log
+
+test -r $RASPIMJPEG_CONF || exit 1
+test -r $STREAMEYE_CONF || exit 1
+
+watch() {
+ count=0
+ while true; do
+ sleep 5
+ if ! ps aux | grep raspimjpeg.py | grep -v grep &>/dev/null; then
+ logger -t streameye -s "not running, respawning"
+ start
+ fi
+ done
+}
+
+function start() {
+ pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1)
+ if [ -n "$pid" ]; then
+ return
+ fi
+
+ raspimjpeg_opts=""
+ while read line; do
+ if echo "$line" | grep false &>/dev/null; then
+ continue
+ fi
+ if echo "$line" | grep true &>/dev/null; then
+ line=$(echo $line | cut -d ' ' -f 1)
+ fi
+ raspimjpeg_opts="$raspimjpeg_opts --$line"
+ done < $RASPIMJPEG_CONF
+
+ source $STREAMEYE_CONF
+ streameye_opts="-p $PORT"
+ if [ -n "$CREDENTIALS" ] && [ "$AUTH" = "basic" ]; then
+ streameye_opts="$streameye_opts -a basic -c $CREDENTIALS"
+ fi
+
+ if [ -r $MOTIONEYE_CONF ] && grep 'log-level debug' $MOTIONEYE_CONF >/dev/null; then
+ raspimjpeg_opts="$raspimjpeg_opts -d"
+ streameye_opts="$streameye_opts -d"
+ fi
+
+ raspimjpeg.py $raspimjpeg_opts 2>$RASPIMJPEG_LOG | streameye $streameye_opts &>$STREAMEYE_LOG &
+}
+
+function stop() {
+ # stop the streameye background watch process
+ ps | grep streameye.sh | grep -v $$ | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1 | xargs -r kill
+
+ # stop the raspimjpeg process
+ pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1)
+ if [ -z "$pid" ]; then
+ return
+ fi
+
+ kill -HUP "$pid" &>/dev/null
+ count=0
+ while kill -0 "$pid" &>/dev/null && [ $count -lt 5 ]; do
+ sleep 1
+ count=$(($count + 1))
+ done
+ kill -KILL "$pid" &>/dev/null || true
+}
+
+case "$1" in
+ start)
+ start
+ watch &
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart)
+ stop
+ start
+ watch &
+ ;;
+
+ *)
+ echo $"Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
diff --git a/configs/bananapi_defconfig b/configs/bananapi_defconfig
index c5d7fde6af..d38393080e 100644
--- a/configs/bananapi_defconfig
+++ b/configs/bananapi_defconfig
@@ -25,10 +25,18 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="sun7i-a20-bananapi"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_LIBWEBCAM=y
+BR2_PACKAGE_MOTION=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -62,9 +70,23 @@ BR2_PACKAGE_LINUX_FIRMWARE_RTL_88XX=y
BR2_PACKAGE_SUNXI_BOARDS=y
BR2_PACKAGE_SUNXI_BOARDS_FEX_FILE="a20/Bananapi.fex"
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -81,6 +103,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/configs/odroidc1_defconfig b/configs/odroidc1_defconfig
index 76c9d38c7d..7dd5b6a8e3 100644
--- a/configs/odroidc1_defconfig
+++ b/configs/odroidc1_defconfig
@@ -32,10 +32,17 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="meson8b_odroidc"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_MOTION=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -67,9 +74,23 @@ BR2_PACKAGE_LINUX_FIRMWARE_RTL_81XX=y
BR2_PACKAGE_LINUX_FIRMWARE_RTL_87XX=y
BR2_PACKAGE_LINUX_FIRMWARE_RTL_88XX=y
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -86,6 +107,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/configs/odroidc2_defconfig b/configs/odroidc2_defconfig
index d329491e9a..b751c6f2af 100644
--- a/configs/odroidc2_defconfig
+++ b/configs/odroidc2_defconfig
@@ -21,10 +21,17 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="meson64_odroidc2"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_MOTION=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -56,9 +63,24 @@ BR2_PACKAGE_LINUX_FIRMWARE_RTL_81XX=y
BR2_PACKAGE_LINUX_FIRMWARE_RTL_87XX=y
BR2_PACKAGE_LINUX_FIRMWARE_RTL_88XX=y
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_JPEG_TURBO=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -75,6 +97,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/configs/odroidxu4_defconfig b/configs/odroidxu4_defconfig
index e478f8d505..8335fdb31e 100644
--- a/configs/odroidxu4_defconfig
+++ b/configs/odroidxu4_defconfig
@@ -23,10 +23,17 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="exynos5422-odroidxu3"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_MOTION=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -58,9 +65,23 @@ BR2_PACKAGE_LINUX_FIRMWARE_RTL_81XX=y
BR2_PACKAGE_LINUX_FIRMWARE_RTL_87XX=y
BR2_PACKAGE_LINUX_FIRMWARE_RTL_88XX=y
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -77,6 +98,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/configs/raspberrypi2_defconfig b/configs/raspberrypi2_defconfig
index 53d286b279..b7522d297e 100644
--- a/configs/raspberrypi2_defconfig
+++ b/configs/raspberrypi2_defconfig
@@ -23,10 +23,20 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="bcm2709-rpi-2-b"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_LIBWEBCAM=y
+BR2_PACKAGE_MOTION=y
+BR2_PACKAGE_MOTION_MMAL=y
+BR2_PACKAGE_STREAMEYE=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -61,9 +71,25 @@ BR2_PACKAGE_RPI_FIRMWARE=y
BR2_PACKAGE_RPI_FIRMWARE_X=y
BR2_PACKAGE_RPI_USERLAND=y
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PICAMERA=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_RPI_GPIO=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -80,6 +106,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/configs/raspberrypi3_defconfig b/configs/raspberrypi3_defconfig
index da79d13f44..d886b29c85 100644
--- a/configs/raspberrypi3_defconfig
+++ b/configs/raspberrypi3_defconfig
@@ -23,10 +23,20 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="bcm2710-rpi-3-b"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_LIBWEBCAM=y
+BR2_PACKAGE_MOTION=y
+BR2_PACKAGE_MOTION_MMAL=y
+BR2_PACKAGE_STREAMEYE=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -61,9 +71,25 @@ BR2_PACKAGE_RPI_FIRMWARE=y
BR2_PACKAGE_RPI_FIRMWARE_X=y
BR2_PACKAGE_RPI_USERLAND=y
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PICAMERA=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_RPI_GPIO=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -80,6 +106,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/configs/raspberrypi_defconfig b/configs/raspberrypi_defconfig
index 22e1a5e103..212c77156b 100644
--- a/configs/raspberrypi_defconfig
+++ b/configs/raspberrypi_defconfig
@@ -27,10 +27,20 @@ BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="bcm2708-rpi-b bcm2708-rpi-b-plus bcm2708-rpi-cm"
BR2_PACKAGE_BUSYBOX_CONFIG="board/common/busybox.config"
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
+BR2_PACKAGE_MOTIONEYE=y
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
+BR2_PACKAGE_FFMPEG=y
+BR2_PACKAGE_FFMPEG_GPL=y
+BR2_PACKAGE_FFMPEG_NONFREE=y
+BR2_PACKAGE_FFMPEG_SWSCALE=y
+BR2_PACKAGE_LIBWEBCAM=y
+BR2_PACKAGE_MOTION=y
+BR2_PACKAGE_MOTION_MMAL=y
+BR2_PACKAGE_STREAMEYE=y
BR2_PACKAGE_GZIP=y
BR2_PACKAGE_JQ=y
+BR2_PACKAGE_CIFS_UTILS=y
BR2_PACKAGE_E2FSPROGS=y
# BR2_PACKAGE_E2FSPROGS_BADBLOCKS is not set
# BR2_PACKAGE_E2FSPROGS_CHATTR is not set
@@ -66,9 +76,26 @@ BR2_PACKAGE_RPI_FIRMWARE_X=y
BR2_PACKAGE_RPI_ARMMEM=y
BR2_PACKAGE_RPI_USERLAND=y
BR2_PACKAGE_USB_MODESWITCH_DATA=y
+BR2_PACKAGE_PYTHON_SSL=y
+BR2_PACKAGE_PYTHON_HASHLIB=y
+BR2_PACKAGE_PYTHON_JINJA2=y
+BR2_PACKAGE_PYTHON_PICAMERA=y
+BR2_PACKAGE_PYTHON_PILLOW=y
+BR2_PACKAGE_PYTHON_PYCURL=y
+BR2_PACKAGE_PYTHON_PYTZ=y
+BR2_PACKAGE_PYTHON_RPI_GPIO=y
+BR2_PACKAGE_PYTHON_TORNADO=y
+BR2_PACKAGE_PYTHON_VERSIONTOOLS=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_NETTLE=y
BR2_PACKAGE_LIBFUSE=y
+BR2_PACKAGE_JPEG_TURBO=y
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
+BR2_PACKAGE_LIBXML2=y
+BR2_PACKAGE_LIBTHEORA=y
+BR2_PACKAGE_X264=y
+BR2_PACKAGE_X265=y
BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_CURL=y
BR2_PACKAGE_LIBCAP=y
@@ -85,6 +112,9 @@ BR2_PACKAGE_NTP=y
BR2_PACKAGE_NTP_NTPDATE=y
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_PPPD=y
+BR2_PACKAGE_PROFTPD=y
+BR2_PACKAGE_RSYNC=y
+BR2_PACKAGE_SAMBA4=y
BR2_PACKAGE_WGET=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
diff --git a/package/Config.in b/package/Config.in
index 177a67cf0c..aeb33ced8e 100644
--- a/package/Config.in
+++ b/package/Config.in
@@ -2,6 +2,7 @@ menu "Target packages"
source "package/busybox/Config.in"
source "package/skeleton/Config.in"
+ source "package/motioneye/Config.in"
menu "Audio and video applications"
source "package/alsa-utils/Config.in"
@@ -21,9 +22,11 @@ menu "Audio and video applications"
source "package/jack2/Config.in"
source "package/kodi/Config.in"
source "package/lame/Config.in"
+ source "package/libwebcam/Config.in"
source "package/madplay/Config.in"
source "package/miraclecast/Config.in"
source "package/mjpegtools/Config.in"
+ source "package/motion/Config.in"
source "package/modplugtools/Config.in"
source "package/mpd/Config.in"
source "package/mpd-mpc/Config.in"
@@ -37,6 +40,7 @@ menu "Audio and video applications"
source "package/opus-tools/Config.in"
source "package/pulseaudio/Config.in"
source "package/sox/Config.in"
+ source "package/streameye/Config.in"
source "package/squeezelite/Config.in"
source "package/tidsp-binaries/Config.in"
source "package/tovid/Config.in"
diff --git a/package/libwebcam/Config.in b/package/libwebcam/Config.in
new file mode 100644
index 0000000000..be202b0c41
--- /dev/null
+++ b/package/libwebcam/Config.in
@@ -0,0 +1,3 @@
+config BR2_PACKAGE_LIBWEBCAM
+ bool "libwebcam"
+
diff --git a/package/libwebcam/libwebcam.mk b/package/libwebcam/libwebcam.mk
new file mode 100644
index 0000000000..defdc8638c
--- /dev/null
+++ b/package/libwebcam/libwebcam.mk
@@ -0,0 +1,18 @@
+################################################################################
+#
+# libwebcam
+#
+################################################################################
+
+LIBWEBCAM_VERSION = 0.2.5
+LIBWEBCAM_SOURCE = libwebcam-src-$(LIBWEBCAM_VERSION).tar.gz
+LIBWEBCAM_SITE = http://freefr.dl.sourceforge.net/project/libwebcam/source
+LIBWEBCAM_DEPENDENCIES = libxml2
+
+define LIBWEBCAM_INSTALL_TARGET_CMDS
+ rm $(@D)/uvcdynctrl/uvcdynctrl-*.gz
+ cp $(@D)/uvcdynctrl/uvcdynctrl $(TARGET_DIR)/usr/bin/uvcdynctrl
+ cp -d $(@D)/libwebcam/libwebcam.so* $(TARGET_DIR)/usr/lib
+endef
+
+$(eval $(cmake-package))
diff --git a/package/motion/0001-version.patch b/package/motion/0001-version.patch
new file mode 100644
index 0000000000..610a7c7e2b
--- /dev/null
+++ b/package/motion/0001-version.patch
@@ -0,0 +1,16 @@
+diff -uNr motion/version.sh motion.new/version.sh
+--- motion/version.sh 2016-11-13 12:45:14.143548855 +0200
++++ motion.new/version.sh 2016-11-13 12:45:31.683635991 +0200
+@@ -1,8 +1,9 @@
+ #!/bin/sh
+ BASE_VERSION="4.0.1"
+ if [ -d .git ]; then
+- GIT_COMMIT=`git show -s --format=%h`
+- printf "$BASE_VERSION+git$GIT_COMMIT"
++ GIT_COMMIT=`git show -s --format=%h`
++ printf "$BASE_VERSION+git$GIT_COMMIT"
+ else
+- printf "$BASE_VERSION+gitUNKNOWN"
++ printf "$BASE_VERSION+git$(basename $(pwd) | cut -d '-' -f 2)"
+ fi
++
diff --git a/package/motion/Config.in b/package/motion/Config.in
new file mode 100644
index 0000000000..6bd458ed34
--- /dev/null
+++ b/package/motion/Config.in
@@ -0,0 +1,3 @@
+config BR2_PACKAGE_MOTION
+ bool "motion"
+ help
diff --git a/package/motion/motion.mk b/package/motion/motion.mk
new file mode 100644
index 0000000000..f3815e37a4
--- /dev/null
+++ b/package/motion/motion.mk
@@ -0,0 +1,18 @@
+################################################################################
+#
+# motion
+#
+################################################################################
+
+MOTION_VERSION = 37b3595
+MOTION_SITE = $(call github,motion-project,motion,$(MOTION_VERSION))
+MOTION_AUTORECONF = YES
+MOTION_CONF_OPTS = --without-pgsql --without-sdl --without-sqlite3 --without-mysql --with-ffmpeg=$(STAGING_DIR)/usr/lib \
+ --with-ffmpeg-headers=$(STAGING_DIR)/usr/include
+
+define MOTION_INSTALL_TARGET_CMDS
+ cp $(@D)/motion $(TARGET_DIR)/usr/bin/motion
+endef
+
+$(eval $(autotools-package))
+
diff --git a/package/motioneye/Config.in b/package/motioneye/Config.in
new file mode 100644
index 0000000000..0a2a6fd0fe
--- /dev/null
+++ b/package/motioneye/Config.in
@@ -0,0 +1,2 @@
+config BR2_PACKAGE_MOTIONEYE
+ bool "motioneye"
diff --git a/package/motioneye/dropbox.keys b/package/motioneye/dropbox.keys
new file mode 100644
index 0000000000..ac0d962753
--- /dev/null
+++ b/package/motioneye/dropbox.keys
@@ -0,0 +1,2 @@
+CLIENT_ID="dwiw710jz6r60pq"
+CLIENT_SECRET="8jz75qo405ritd5"
diff --git a/package/motioneye/extractl.py b/package/motioneye/extractl.py
new file mode 100644
index 0000000000..d8eb0a3c35
--- /dev/null
+++ b/package/motioneye/extractl.py
@@ -0,0 +1,638 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+import subprocess
+
+from config import additional_config
+
+
+MOTIONEYE_CONF = '/data/etc/motioneye.conf'
+OS_CONF = '/data/etc/os.conf'
+HOSTNAME_CONF = '/data/etc/hostname'
+DATE_CONF = '/data/etc/date.conf'
+
+
+def _get_hostname():
+ try:
+ with open(HOSTNAME_CONF) as f:
+ hostname = f.read().strip()
+ logging.debug('hostname %s read from %s' % (hostname, HOSTNAME_CONF))
+ return hostname
+
+ except:
+ return ''
+
+
+def _set_hostname(hostname):
+ if hostname:
+ with open(HOSTNAME_CONF, 'w') as f:
+ f.write(hostname)
+
+ logging.debug('hostname %s written to %s' % (hostname, HOSTNAME_CONF))
+
+ else:
+ try:
+ os.remove(HOSTNAME_CONF)
+ logging.debug('hostname file %s removed' % HOSTNAME_CONF)
+
+ except:
+ pass
+
+def _get_date_settings():
+ date_method = 'http'
+ date_host = 'google.com'
+ date_ntp_server = ''
+ date_timeout = 10
+ date_interval = 900
+
+ if os.path.exists(DATE_CONF):
+ logging.debug('reading date settings from %s' % DATE_CONF)
+
+ with open(DATE_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ if line.startswith('#'):
+ continue
+
+ try:
+ name, value = line.split('=')
+ value = value.strip('"').strip("'")
+
+ except:
+ continue
+
+ if name == 'date_method':
+ date_method = value
+
+ elif name == 'date_host':
+ date_host = value
+
+ elif name == 'date_ntp_server':
+ date_ntp_server = value
+
+ elif name == 'date_timeout':
+ date_timeout = int(value)
+
+ elif name == 'date_interval':
+ date_interval = int(value)
+
+ s = {
+ 'dateMethod': date_method,
+ 'dateHost': date_host,
+ 'dateNtpServer': date_ntp_server,
+ 'dateTimeout': date_timeout,
+ 'dateInterval': date_interval
+ }
+
+ logging.debug('date settings: method=%(dateMethod)s, host=%(dateHost)s, ntp_server=%(dateNtpServer)s, timeout=%(dateTimeout)s, interval=%(dateInterval)s' % s)
+
+ return s
+
+
+def _set_date_settings(s):
+ s.setdefault('dateMethod', 'http')
+ s.setdefault('dateHost', 'google.com')
+ s.setdefault('dateNtpServer', '')
+ s.setdefault('dateTimeout', 10)
+ s.setdefault('dateInterval', 900)
+
+ logging.debug('writing date settings to %s: ' % DATE_CONF +
+ 'method=%(dateMethod)s, host=%(dateHost)s, ntp_server=%(dateNtpServer)s, timeout=%(dateTimeout)s, interval=%(dateInterval)s' % s)
+
+ with open(DATE_CONF, 'w') as f:
+ f.write('date_method=%s\n' % s['dateMethod'])
+ f.write('date_host=%s\n' % s['dateHost'])
+ f.write('date_ntp_server=%s\n' % s['dateNtpServer'])
+ f.write('date_timeout=%s\n' % s['dateTimeout'])
+ f.write('date_interval=%s\n' % s['dateInterval'])
+
+
+def _get_os_settings():
+ prereleases = False
+
+ if os.path.exists(OS_CONF):
+ logging.debug('reading OS settings from %s' % OS_CONF)
+
+ with open(OS_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ if line.startswith('#'):
+ continue
+
+ try:
+ name, value = line.split('=')
+ value = value.strip('"').strip("'")
+
+ except:
+ continue
+
+ if name == 'os_prereleases':
+ prereleases = value == 'true'
+
+ s = {
+ 'prereleases': prereleases
+ }
+
+ logging.debug('OS settings: prereleases=%(prereleases)s' % s)
+
+ return s
+
+
+def _set_os_settings(s):
+ s = dict(s)
+
+ s.setdefault('prereleases', False)
+
+ logging.debug('writing OS settings to %s: ' % OS_CONF +
+ 'prereleases=%(prereleases)s' % s)
+
+ lines = []
+ if os.path.exists(OS_CONF):
+ with open(OS_CONF) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, _ = line.split('=', 2)
+
+ except:
+ continue
+
+ if name == 'os_prereleases':
+ lines[i] = 'os_prereleases="%s"' % str(s.pop('prereleases')).lower()
+
+ if 'prereleases' in s:
+ lines.append('os_prereleases="%s"' % str(s.pop('prereleases')).lower())
+
+ with open(OS_CONF, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+
+def _get_motioneye_settings():
+ port = 80
+ base_path = ''
+ motion_binary = '/usr/bin/motion'
+ debug = False
+ motion_keep_alive = False
+
+ if os.path.exists(MOTIONEYE_CONF):
+ logging.debug('reading motioneye settings from %s' % MOTIONEYE_CONF)
+
+ with open(MOTIONEYE_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, value = line.split(' ', 1)
+
+ except:
+ continue
+
+ name = name.replace('-', '_')
+
+ if name == 'port':
+ port = int(value)
+
+ elif name == 'base_path':
+ base_path = value.strip()
+
+ elif name == 'motion_binary':
+ motion_binary = value
+
+ elif name == 'log_level':
+ debug = value == 'debug'
+
+ elif name == 'mjpg_client_idle_timeout':
+ motion_keep_alive = value == '0'
+
+ s = {
+ 'port': port,
+ 'basePath': base_path,
+ 'motionBinary': motion_binary,
+ 'motionKeepAlive': motion_keep_alive,
+ 'debug': debug
+ }
+
+ logging.debug(('motioneye settings: port=%(port)s, base_path=%(basePath)s, motion_binary=%(motionBinary)s, ' +
+ 'motion_keep_alive=%(motionKeepAlive)s, debug=%(debug)s') % s)
+
+ return s
+
+
+def _set_motioneye_settings(s):
+ s = dict(s)
+ s.setdefault('port', 80)
+ s.setdefault('basePath', '')
+ s.setdefault('motionBinary', '/usr/bin/motion')
+ debug = s.setdefault('debug', False) # value needed later
+ s.setdefault('motion_keep_alive', False)
+
+ logging.debug('writing motioneye settings to %s: ' % MOTIONEYE_CONF +
+ ('port=%(port)s, base_path=%(basePath)s, motion_binary=%(motionBinary)s, ' +
+ 'motion_keep_alive=%(motionKeepAlive)s, debug=%(debug)s') % s)
+
+ lines = []
+ if os.path.exists(MOTIONEYE_CONF):
+ with open(MOTIONEYE_CONF) as f:
+ lines = f.readlines()
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ name, _ = line.split(' ', 2)
+
+ except:
+ continue
+
+ name = name.replace('-', '_')
+
+ if name == 'port':
+ lines[i] = 'port %s' % s.pop('port')
+
+ elif name == 'base_path':
+ base_path = s.pop('basePath')
+ if base_path:
+ lines[i] = 'base_path %s' % base_path
+
+ else:
+ lines[i] = None
+
+ elif name == 'motion_binary':
+ lines[i] = 'motion_binary %s' % s.pop('motionBinary')
+
+ elif name == 'log_level':
+ lines[i] = 'log_level %s' % ['info', 'debug'][s.pop('debug')]
+
+ elif name == 'mjpg_client_idle_timeout':
+ lines[i] = 'mjpg_client_idle_timeout %s' % [10, 0][s.pop('motionKeepAlive')]
+
+ lines = [l for l in lines if l is not None]
+
+ if 'port' in s:
+ lines.append('port %s' % s.pop('port'))
+
+ if s.get('basePath'):
+ lines.append('base_path %s' % s.pop('basePath'))
+
+ if 'motionBinary' in s:
+ lines.append('motion_binary %s' % s.pop('motionBinary'))
+
+ if 'debug' in s:
+ lines.append('log_level %s' % ['info', 'debug'][s.pop('debug')])
+
+ if 'motionKeepAlive' in s:
+ lines.append('mjpg_client_idle_timeout %s' % [10, 0][s.pop('motionKeepAlive')])
+
+ with open(MOTIONEYE_CONF, 'w') as f:
+ for line in lines:
+ if not line.strip():
+ continue
+ if not line.endswith('\n'):
+ line += '\n'
+ f.write(line)
+
+ # also update debug in os.conf
+ if debug:
+ cmd = "sed -i -r 's/os_debug=\"?false\"?/os_debug=\"true\"/' %s" % OS_CONF
+
+ else:
+ cmd = "sed -i -r 's/os_debug=\"?true\"?/os_debug=\"false\"/' %s" % OS_CONF
+
+ if os.system(cmd):
+ logging.error('failed to set debug flag in os.conf')
+
+
+def _get_motion_log():
+ return 'motion.log'
+
+
+def _get_motion_eye_log():
+ return 'motioneye.log'
+
+
+def _get_messages_log():
+ return 'messages.log'
+
+
+def _get_boot_log():
+ return 'boot.log'
+
+
+def _get_dmesg_log():
+ return 'dmesg.log'
+
+
+@additional_config
+def hostname():
+ return {
+ 'label': 'Hostname',
+ 'description': 'sets a custom hostname for the device (leave blank for default)',
+ 'type': 'str',
+ 'section': 'general',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': False,
+ 'validate': '^[a-z0-9\-_.]{0,64}$',
+ 'get': _get_hostname,
+ 'set': _set_hostname
+ }
+
+
+@additional_config
+def extraDateSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def dateMethod():
+ return {
+ 'label': 'Date Method',
+ 'description': 'decides whether NTP or HTTP is used for setting and updating the system date',
+ 'type': 'choices',
+ 'choices': [('http', 'HTTP'), ('ntp', 'NTP')],
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'get': _get_date_settings,
+ 'set': _set_date_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def dateHost():
+ return {
+ 'label': 'Date HTTP Host',
+ 'description': 'sets the hostname or IP address to which the HTTP request will be made',
+ 'type': 'str',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['dateMethod==http'],
+ 'get': _get_date_settings,
+ 'set': _set_date_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def dateNtpServer():
+ return {
+ 'label': 'NTP Server',
+ 'description': 'sets a custom NTP server (leave blank to use the default server)',
+ 'type': 'str',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': False,
+ 'depends': ['dateMethod==ntp'],
+ 'get': _get_date_settings,
+ 'set': _set_date_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def dateTimeout():
+ return {
+ 'label': 'Date Updating Timeout',
+ 'description': 'sets the number of seconds to wait when requesting the date/time',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 3600,
+ 'unit': 's',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'get': _get_date_settings,
+ 'set': _set_date_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def dateInterval():
+ return {
+ 'label': 'Date Updating Interval',
+ 'description': 'sets the interval between system date updates',
+ 'type': 'number',
+ 'min': 10,
+ 'max': 86400,
+ 'unit': 's',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['dateMethod==http'],
+ 'get': _get_date_settings,
+ 'set': _set_date_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def extraMotionEyeSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def port():
+ return {
+ 'label': 'HTTP Port',
+ 'description': 'sets the port on which the motionEye HTTP server listens',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 65535,
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'get': _get_motioneye_settings,
+ 'set': _set_motioneye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def basePath():
+ return {
+ 'label': 'Base Path',
+ 'description': 'sets a base path of all the URIs used by motionEye (useful when running behind a reverse proxy exposing the motionEye UI at /cams, for example)',
+ 'type': 'str',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_motioneye_settings,
+ 'set': _set_motioneye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def motionBinary():
+ return {
+ 'label': 'Motion Binary',
+ 'description': 'sets the path to the motion binary',
+ 'type': 'str',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'get': _get_motioneye_settings,
+ 'set': _set_motioneye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def motionKeepAlive():
+ return {
+ 'label': 'Motion Keep-alive',
+ 'description': 'enables continuous motion daemon hang detection (at the expense of a slightly higher CPU usage)',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_motioneye_settings,
+ 'set': _set_motioneye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def debug():
+ return {
+ 'label': 'Enable Debugging',
+ 'description': 'turning debugging on will generate verbose log messages and will mount all the partitions in read-write mode',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_motioneye_settings,
+ 'set': _set_motioneye_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def prereleases():
+ return {
+ 'label': 'Enable Prereleases',
+ 'description': 'turning this option on will allow updating to prereleases (untested, possibly unstable versions)',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_os_settings,
+ 'set': _set_os_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def extraLogsSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'expertSettings',
+ 'advanced': True
+ }
+
+
+@additional_config
+def motionLog():
+ return {
+ 'label': 'Log Files',
+ 'description': 'download the log files and include them with any issue you want to report',
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': _get_motion_log,
+ }
+
+
+@additional_config
+def motionEyeLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': _get_motion_eye_log,
+ }
+
+
+@additional_config
+def messagesLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': _get_messages_log,
+ }
+
+
+@additional_config
+def bootLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': _get_boot_log,
+ }
+
+
+@additional_config
+def dmesgLog():
+ return {
+ 'type': 'html',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'get': _get_dmesg_log,
+ }
+
diff --git a/package/motioneye/ipctl.py b/package/motioneye/ipctl.py
new file mode 100644
index 0000000000..e1528cedbc
--- /dev/null
+++ b/package/motioneye/ipctl.py
@@ -0,0 +1,248 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+import re
+
+from collections import OrderedDict
+
+from config import additional_config
+
+
+STATIC_IP_CONF = '/data/etc/static_ip.conf'
+
+
+def _get_ip_settings():
+ ip = None
+ ip_comment = False
+ cidr = '24'
+ gw = '192.168.1.1'
+ gw_comment = False
+ dns = '8.8.8.8'
+ dns_comment = False
+
+ if os.path.exists(STATIC_IP_CONF):
+ logging.debug('reading ip settings from %s' % STATIC_IP_CONF)
+
+ with open(STATIC_IP_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith('#'):
+ comment = True
+ line = line.strip('#')
+
+ else:
+ comment = False
+
+ if not line:
+ continue
+
+ match = re.match('^static_ip="(.*)/(.*)"$', line)
+ if match:
+ ip, cidr = match.groups()
+ ip_comment = comment
+ continue
+
+ match = re.match('^static_gw="(.*)"$', line)
+ if match:
+ gw = match.group(1)
+ gw_comment = comment
+ continue
+
+ match = re.match('^static_dns="(.*)"$', line)
+ if match:
+ dns = match.group(1)
+ dns_comment = comment
+ continue
+
+ if ip is None or ip_comment:
+ type = 'dhcp'
+
+ else:
+ type = 'static'
+
+ if ip is None:
+ ip = '192.168.1.101'
+
+ bits = 0
+ for i in xrange(32 - int(cidr), 32):
+ bits |= (1 << i)
+
+ mask = '%d.%d.%d.%d' % ((bits & 0xff000000) >> 24, (bits & 0xff0000) >> 16, (bits & 0xff00) >> 8 , (bits & 0xff))
+
+ if gw_comment and type == 'static':
+ gw = None
+
+ if dns_comment and type == 'static':
+ dns = None
+
+ s = {
+ 'ipConfigType': type,
+ 'ipConfigStaticAddr': ip,
+ 'ipConfigStaticMask': mask,
+ 'ipConfigStaticGw': gw,
+ 'ipConfigStaticDns': dns
+ }
+
+ logging.debug(('ip settings: type=%(ipConfigType)s, addr=%(ipConfigStaticAddr)s, mask=%(ipConfigStaticMask)s, ' +
+ 'gw=%(ipConfigStaticGw)s, dns=%(ipConfigStaticDns)s') % s)
+
+ return s
+
+
+def _set_ip_settings(s):
+ type = s.get('ipConfigType', 'dhcp')
+ ip = s.get('ipConfigStaticAddr', '192.168.1.101')
+ mask = s.get('ipConfigStaticMask', '255.255.255.0')
+ gw = s.get('ipConfigStaticGw', '192.168.1.1')
+ dns = s.get('ipConfigStaticDns', '8.8.8.8')
+
+ logging.debug('writing ip settings to %s: ' % STATIC_IP_CONF +
+ ('type=%(ipConfigType)s, addr=%(ipConfigStaticAddr)s, mask=%(ipConfigStaticMask)s, ' +
+ 'gw=%(ipConfigStaticGw)s, dns=%(ipConfigStaticDns)s') % s)
+
+ cidr = '24'
+ if mask:
+ binary_str = ''
+ for octet in mask.split('.'):
+ binary_str += bin(int(octet))[2:].zfill(8)
+ cidr = str(len(binary_str.rstrip('0')))
+
+ current_settings = OrderedDict()
+ if os.path.exists(STATIC_IP_CONF):
+ with open(STATIC_IP_CONF, 'r') as f:
+ for line in f:
+ line = line.strip().split('=', 1)
+ if len(line) != 2:
+ continue
+ key, value = line
+ if key.startswith('#'):
+ current_settings[key.strip('#')] = (value, False)
+
+ else:
+ current_settings[key] = (value, True)
+
+ enabled = type != 'dhcp'
+ current_settings['static_ip'] = ('"%s/%s"' % (ip, cidr), enabled)
+ current_settings['static_gw'] = ('"%s"' % gw, enabled)
+ current_settings['static_dns'] = ('"%s"' % dns, enabled)
+
+ with open(STATIC_IP_CONF, 'w') as f:
+ for key, value in current_settings.items():
+ (value, enabled) = value
+ if not enabled:
+ key = '#' + key
+ f.write('%s=%s\n' % (key, value))
+
+
+@additional_config
+def ipSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'network',
+ 'advanced': True
+ }
+
+
+@additional_config
+def ipConfigType():
+ return {
+ 'label': 'IP Configuration',
+ 'description': 'select the way your IP address is configured',
+ 'type': 'choices',
+ 'choices': [('dhcp', 'Automatic (DHCP)'), ('static', 'Manual (Static IP)')],
+ 'section': 'network',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_ip_settings,
+ 'set': _set_ip_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def ipConfigStaticAddr():
+ return {
+ 'label': 'IP Address',
+ 'description': 'manually set your static IP address',
+ 'type': 'str',
+ 'validate': '^[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?$',
+ 'section': 'network',
+ 'advanced': True,
+ 'required': True,
+ 'depends': ['ipConfigType==static'],
+ 'reboot': True,
+ 'get': _get_ip_settings,
+ 'set': _set_ip_settings,
+ 'get_set_dict': True,
+ }
+
+
+@additional_config
+def ipConfigStaticMask():
+ return {
+ 'label': 'Network Mask',
+ 'description': 'manually set your network mask',
+ 'type': 'str',
+ 'validate': '^[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?$',
+ 'section': 'network',
+ 'advanced': True,
+ 'required': True,
+ 'depends': ['ipConfigType==static'],
+ 'reboot': True,
+ 'get': _get_ip_settings,
+ 'set': _set_ip_settings,
+ 'get_set_dict': True,
+ }
+
+
+@additional_config
+def ipConfigStaticGw():
+ return {
+ 'label': 'Default Gateway',
+ 'description': 'manually set your default gateway',
+ 'type': 'str',
+ 'validate': '^[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?$',
+ 'section': 'network',
+ 'advanced': True,
+ 'required': True,
+ 'depends': ['ipConfigType==static'],
+ 'reboot': True,
+ 'get': _get_ip_settings,
+ 'set': _set_ip_settings,
+ 'get_set_dict': True,
+ }
+
+
+@additional_config
+def ipConfigStaticDns():
+ return {
+ 'label': 'DNS Server',
+ 'description': 'manually set your DNS server',
+ 'type': 'str',
+ 'validate': '^[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?$',
+ 'section': 'network',
+ 'advanced': True,
+ 'required': True,
+ 'depends': ['ipConfigType==static'],
+ 'reboot': True,
+ 'get': _get_ip_settings,
+ 'set': _set_ip_settings,
+ 'get_set_dict': True,
+ }
+
diff --git a/package/motioneye/motioneye.mk b/package/motioneye/motioneye.mk
new file mode 100644
index 0000000000..71c97dc81b
--- /dev/null
+++ b/package/motioneye/motioneye.mk
@@ -0,0 +1,71 @@
+#############################################################
+#
+# motioneye
+#
+#############################################################
+
+MOTIONEYE_VERSION = 0f92e7bd26a79f1c0d55b4d0ce17c3d434496c10
+MOTIONEYE_SITE = $(call github,ccrisan,motioneye,$(MOTIONEYE_VERSION))
+MOTIONEYE_SOURCE = $(MOTIONEYE_VERSION).tar.gz
+MOTIONEYE_LICENSE = GPLv3
+MOTIONEYE_LICENSE_FILES = LICENCE
+MOTIONEYE_INSTALL_TARGET = YES
+MOTIONEYE_SETUP_TYPE = setuptools
+
+DST_DIR = $(TARGET_DIR)/usr/lib/python2.7/site-packages/motioneye
+SHARE_DIR = $(TARGET_DIR)/usr/share/motioneye
+BOARD = $(shell basename $(BASE_DIR))
+BOARD_DIR = $(BASE_DIR)/../../board/$(BOARD)
+COMMON_DIR = $(BASE_DIR)/../../board/common
+
+
+define MOTIONEYE_INSTALL_TARGET_CMDS
+ # setuptools install
+ (cd $($(PKG)_BUILDDIR)/; \
+ $($(PKG)_BASE_ENV) $($(PKG)_ENV) \
+ $($(PKG)_PYTHON_INTERPRETER) setup.py install \
+ $($(PKG)_BASE_INSTALL_TARGET_OPTS) \
+ $($(PKG)_INSTALL_TARGET_OPTS))
+
+ # additional config modules
+ cp package/motioneye/update.py $(DST_DIR)
+ cp package/motioneye/ipctl.py $(DST_DIR)
+ cp package/motioneye/servicectl.py $(DST_DIR)
+ cp package/motioneye/watchctl.py $(DST_DIR)
+ cp package/motioneye/extractl.py $(DST_DIR)
+ test -d $(BOARD_DIR)/motioneye-modules && cp $(BOARD_DIR)/motioneye-modules/*.py $(DST_DIR) || true
+ grep servicectl $(DST_DIR)/config.py &>/dev/null || echo -e '\nimport ipctl\nimport servicectl\nimport watchctl\nimport extractl\ntry:\n import boardctl\nexcept ImportError:\n pass' >> $(DST_DIR)/config.py
+
+ # log files
+ if ! grep 'motioneye.log' $(DST_DIR)/handlers.py &>/dev/null; then \
+ lineno=$$(grep -n -m1 LOGS $(DST_DIR)/handlers.py | cut -d ':' -f 1); \
+ head -n $$(($$lineno + 1)) $(DST_DIR)/handlers.py > /tmp/handlers.py.new; \
+ echo " 'motioneye': ('/var/log/motioneye.log', 'motioneye.log')," >> /tmp/handlers.py.new; \
+ echo " 'messages': ('/var/log/messages', 'messages.log')," >> /tmp/handlers.py.new; \
+ echo " 'boot': ('/var/log/boot.log', 'boot.log')," >> /tmp/handlers.py.new; \
+ echo " 'dmesg': ('/var/log/dmesg.log', 'dmesg.log')," >> /tmp/handlers.py.new; \
+ tail -n +$$(($$lineno + 2)) $(DST_DIR)/handlers.py >> /tmp/handlers.py.new; \
+ mv /tmp/handlers.py.new $(DST_DIR)/handlers.py; \
+ fi
+
+ # version & update
+ source $(COMMON_DIR)/overlay/etc/version; \
+ sed -r -i "s%VERSION = .*%VERSION = '$$os_version'%" $(DST_DIR)/__init__.py
+ sed -r -i "s%enable_update=False%enable_update=True%" $(DST_DIR)/handlers.py
+ source package/motioneye/dropbox.keys; \
+ sed -i "s/dropbox_client_id_placeholder/$$CLIENT_ID/" $(DST_DIR)/uploadservices.py; \
+ sed -i "s/dropbox_client_secret_placeholder/$$CLIENT_SECRET/" $(DST_DIR)/uploadservices.py
+
+ # (re)compile all python modules
+ $($(PKG)_PYTHON_INTERPRETER) -m compileall -d /usr/lib/python2.7/site-packages/motioneye -f $(DST_DIR)
+
+ # meyectl
+ echo -e '#!/bin/bash\n/usr/bin/python /usr/lib/python2.7/site-packages/motioneye/meyectl.pyc "$$@"' > $(TARGET_DIR)/usr/bin/meyectl
+ chmod +x $(TARGET_DIR)/usr/bin/meyectl
+
+ # cleanups
+ rm -rf $(SHARE_DIR)/extra
+endef
+
+$(eval $(python-package))
+
diff --git a/package/motioneye/servicectl.py b/package/motioneye/servicectl.py
new file mode 100644
index 0000000000..29291787e5
--- /dev/null
+++ b/package/motioneye/servicectl.py
@@ -0,0 +1,314 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+import re
+
+from config import additional_config, additional_section
+
+
+FTP_CONF = '/data/etc/proftpd.conf'
+FTP_DISABLE_FILE = '/data/etc/no_S61proftpd'
+SMB_CONF = '/data/etc/smb.conf'
+SMB_DISABLE_FILE = '/data/etc/no_S62smb'
+SSH_DISABLE_FILE = '/data/etc/no_S60sshd'
+
+
+def _get_service_settings():
+ ftp_enabled = True
+ ftp_auth = True
+ ftp_writable = False
+ smb_enabled = True
+ smb_auth = True
+ smb_writable = False
+ ssh_enabled = True
+
+ # FTP
+ if os.path.exists(FTP_DISABLE_FILE):
+ ftp_enabled = False
+
+ if os.path.exists(FTP_CONF):
+ logging.debug('reading ftp settings from %s' % FTP_CONF)
+
+ with open(FTP_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+
+ if line == '':
+ ftp_auth = False
+
+ elif line == 'AllowAll':
+ ftp_writable = True
+
+ # SMB
+ if os.path.exists(SMB_DISABLE_FILE):
+ smb_enabled = False
+
+ if os.path.exists(SMB_CONF):
+ logging.debug('reading smb settings from %s' % SMB_CONF)
+
+ with open(SMB_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if re.match('^\s*public\s*=\s*yes\s*$', line):
+ smb_auth = False
+
+ elif re.match('^\s*writable\s*=\s*yes\s*$', line):
+ smb_writable = True
+
+ # SSH
+ if os.path.exists(SSH_DISABLE_FILE):
+ ssh_enabled = False
+
+ s = {
+ 'ftpEnabled': ftp_enabled,
+ 'ftpAuth': ftp_auth,
+ 'ftpWritable': ftp_writable,
+ 'smbEnabled': smb_enabled,
+ 'smbAuth': smb_auth,
+ 'smbWritable': smb_writable,
+ 'sshEnabled': ssh_enabled
+ }
+
+ logging.debug(('service settings: ftp=%(ftpEnabled)s, ftp_auth=%(ftpAuth)s, ftp_writable=%(ftpWritable)s, ' +
+ 'smb=%(smbEnabled)s, smb_auth=%(smbAuth)s, smb_writable=%(smbWritable)s, ' +
+ 'ssh=%(sshEnabled)s') % s)
+
+ return s
+
+
+def _set_service_settings(s):
+ s.setdefault('ftpEnabled', True)
+ s.setdefault('ftpAuth', True)
+ s.setdefault('ftpWritable', False)
+ s.setdefault('smbEnabled', True)
+ s.setdefault('smbAuth', True)
+ s.setdefault('smbWritable', False)
+ s.setdefault('sshEnabled', True)
+
+ logging.debug(('saving service settings: ftp=%(ftpEnabled)s, ftp_auth=%(ftpAuth)s, ftp_writable=%(ftpWritable)s, ' +
+ 'smb=%(smbEnabled)s, smb_auth=%(smbAuth)s, smb_writable=%(smbWritable)s, ' +
+ 'ssh=%(sshEnabled)s') % s)
+
+ # FTP
+ ftp_mode = 'off' if not s['ftpEnabled'] else ('public' if not s['ftpAuth'] else ('auth' if not s['ftpWritable'] else 'writable'))
+ logging.debug('setting FTP mode: %s' % ftp_mode)
+ if s['ftpEnabled']:
+ try:
+ os.remove(FTP_DISABLE_FILE)
+
+ except:
+ pass
+
+ else:
+ with open(FTP_DISABLE_FILE, 'w'):
+ pass
+
+ with open(FTP_CONF, 'w') as f:
+ if s['ftpAuth']:
+ if s['ftpWritable']:
+ f.write('\n')
+ f.write(' AllowAll\n')
+ f.write('\n')
+
+ else:
+ f.write('\n')
+ f.write(' User ftp\n')
+ f.write(' Group nogroup\n')
+ f.write(' UserAlias anonymous ftp\n')
+ f.write(' MaxClients 4\n')
+ f.write(' \n')
+ f.write(' DenyAll\n')
+ f.write(' \n')
+ f.write('\n')
+
+ # SMB
+ smb_mode = 'off' if not s['smbEnabled'] else ('public' if not s['smbAuth'] else ('auth' if not s['smbWritable'] else 'writable'))
+ logging.debug('setting SMB mode: %s' % smb_mode)
+ if s['smbEnabled']:
+ try:
+ os.remove(SMB_DISABLE_FILE)
+
+ except:
+ pass
+
+ else:
+ with open(SMB_DISABLE_FILE, 'w'):
+ pass
+
+ with open(SMB_CONF, 'w') as f:
+ if s['smbAuth']:
+ if s['smbWritable']:
+ f.write('writable = yes\n')
+ f.write('public = no\n')
+
+ else:
+ f.write('writable = no\n')
+ f.write('public = yes\n')
+
+ # SSH
+ ssh_mode = 'off' if not s['sshEnabled'] else 'enabled'
+ logging.debug('setting SSH mode: %s' % ssh_mode)
+ if s['sshEnabled']:
+ try:
+ os.remove(SSH_DISABLE_FILE)
+
+ except:
+ pass
+
+ else:
+ with open(SSH_DISABLE_FILE, 'w'):
+ pass
+
+
+@additional_section
+def services():
+ return {
+ 'label': 'Services',
+ 'description': 'configure extra services (such as FTP or SSH)',
+ 'advanced': True
+ }
+
+
+@additional_config
+def ftpEnabled():
+ return {
+ 'label': 'Enable FTP Server',
+ 'description': 'enable this if you want to access the files on your motionEyeOS system using FTP',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def ftpAuth():
+ return {
+ 'label': 'Require FTP Authentication',
+ 'description': 'enable this if you want the FTP server to ask for credentials (i.e. to disable anonymous logins)',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'depends': ['ftpEnabled'],
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def ftpWritable():
+ return {
+ 'label': 'Enable FTP Write Support',
+ 'description': 'enable this if you want to allow creating, editing or removing files/directories through FTP (i.e. to disable read-only mode)',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'depends': ['ftpAuth', 'ftpEnabled'],
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def ftpSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'services',
+ 'advanced': True
+ }
+
+
+@additional_config
+def smbEnabled():
+ return {
+ 'label': 'Enable Samba Server',
+ 'description': 'enable this if you want files on your motionEyeOS system to be visible on the local network (using SMB protocol)',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def smbAuth():
+ return {
+ 'label': 'Require Samba Authentication',
+ 'description': 'enable this if you want the Samba server to ask for credentials (i.e. to disable guest access)',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'depends': ['smbEnabled'],
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def smbWritable():
+ return {
+ 'label': 'Enable Samba Write Support',
+ 'description': 'enable this if you want to allow creating, editing or removing files/directories on your motionEyeOS system from the local network',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'depends': ['smbAuth', 'smbEnabled'],
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def smbSeparator():
+ return {
+ 'type': 'separator',
+ 'section': 'services',
+ 'advanced': True
+ }
+
+
+@additional_config
+def sshEnabled():
+ return {
+ 'label': 'Enable SSH Server',
+ 'description': 'enable this if you want to login remotely on your motionEyeOS system using an SSH client (such as Putty)',
+ 'type': 'bool',
+ 'section': 'services',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_service_settings,
+ 'set': _set_service_settings,
+ 'get_set_dict': True
+ }
diff --git a/package/motioneye/update.py b/package/motioneye/update.py
new file mode 100644
index 0000000000..40135b1290
--- /dev/null
+++ b/package/motioneye/update.py
@@ -0,0 +1,263 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import json
+import logging
+import os.path
+import re
+import shutil
+import ssl
+import subprocess
+import time
+import urllib2
+
+import settings
+
+
+_BOARD = open('/etc/board').read().strip()
+_REPO = ('ccrisan', 'motioneyeos')
+_DOWNLOAD_URL = 'https://github.com/{owner}/{repo}/releases/download/%(version)s/motioneyeos-%(board)s-%(version)s.img.gz'.format(
+ owner=_REPO[0], repo=_REPO[1])
+_LIST_VERSIONS_URL = 'https://api.github.com/repos/{owner}/{repo}/releases'.format(
+ owner=_REPO[0], repo=_REPO[1])
+_DOWNLOAD_DIR = '/data/.firmware_update'
+_DOWNLOAD_FILE_NAME = os.path.join(_DOWNLOAD_DIR, 'firmware.gz')
+
+
+# versions
+
+def get_version():
+ import motioneye
+
+ return motioneye.VERSION
+
+
+def get_all_versions():
+ url = _LIST_VERSIONS_URL
+ url += '?_=' + str(int(time.time())) # prevents caching
+
+ want_prereleases = subprocess.check_output('source /data/etc/os.conf && echo $os_prereleases', shell=True, stderr=subprocess.STDOUT).strip() == 'true'
+
+ try:
+ logging.debug('board is %s' % _BOARD)
+ logging.debug('fetching %s...' % url)
+
+ context = ssl._create_unverified_context()
+
+ response = urllib2.urlopen(url, timeout=settings.REMOTE_REQUEST_TIMEOUT, context=context)
+ releases = json.load(response)
+
+ versions = []
+ for release in releases:
+ if release.get('prerelease') and not want_prereleases:
+ continue
+
+ for asset in release.get('assets', []):
+ if not re.match('^motioneyeos-%s-\d{8}\.img.gz$' % _BOARD, asset['name']):
+ continue
+
+ versions.append(release['name'])
+
+ logging.debug('available versions: %(versions)s' % {'versions': ', '.join(versions)})
+
+ return sorted(versions)
+
+ except Exception as e:
+ logging.error('could not get versions: %s' % e, exc_info=True)
+
+ return []
+
+
+def compare_versions(version1, version2):
+ version1 = re.sub('[^0-9.]', '', version1)
+ version2 = re.sub('[^0-9.]', '', version2)
+
+ def int_or_0(n):
+ try:
+ return int(n)
+
+ except:
+ return 0
+
+ version1 = [int_or_0(n) for n in version1.split('.')]
+ version2 = [int_or_0(n) for n in version2.split('.')]
+
+ len1 = len(version1)
+ len2 = len(version2)
+ length = min(len1, len2)
+ for i in xrange(length):
+ p1 = version1[i]
+ p2 = version2[i]
+
+ if p1 < p2:
+ return -1
+
+ elif p1 > p2:
+ return 1
+
+ if len1 < len2:
+ return -1
+
+ elif len1 > len2:
+ return 1
+
+ else:
+ return 0
+
+
+# updating
+
+def download(version):
+ url = _DOWNLOAD_URL % {'version': version, 'board': _BOARD}
+
+ try:
+ logging.info('downloading %s...' % url)
+
+ shutil.rmtree(_DOWNLOAD_DIR, ignore_errors=True)
+ os.makedirs(_DOWNLOAD_DIR)
+ subprocess.check_call(['/usr/bin/wget', url, '--no-check-certificate', '-O', _DOWNLOAD_FILE_NAME])
+
+ except Exception as e:
+ logging.error('could not download update: %s' % e)
+
+ raise
+
+ try:
+ logging.info('decompressing %s...' % _DOWNLOAD_FILE_NAME)
+
+ subprocess.check_call(['/bin/gunzip', _DOWNLOAD_FILE_NAME])
+
+ except Exception as e:
+ logging.error('could not decompress archive: %s' % e)
+
+ raise
+
+ extracted_file_name = _DOWNLOAD_FILE_NAME.replace('.gz', '')
+
+ try:
+ logging.info('reading partiton table...')
+
+ output = subprocess.check_output(['/sbin/fdisk', '-l', extracted_file_name])
+ lines = [l.strip().replace('*', ' ') for l in output.split('\n') if l.startswith(extracted_file_name)]
+ boot_info = lines[0].split()
+ root_info = lines[1].split()
+
+ boot_start, boot_end = int(boot_info[1]), int(boot_info[2])
+ root_start, root_end = int(root_info[1]), int(root_info[2])
+
+ except Exception as e:
+ logging.error('could not read partition table: %s' % e)
+
+ raise
+
+ try:
+ logging.info('extracting boot.img...')
+
+ subprocess.check_call(['/bin/dd', 'if=' + extracted_file_name, 'of=' + os.path.join(_DOWNLOAD_DIR, 'boot.img'),
+ 'bs=2048', 'skip=' + str(boot_start / 4), 'count=' + str((boot_end - boot_start + 1) / 4)])
+
+ except Exception as e:
+ logging.error('could not extract boot.img: %s' % e)
+
+ raise
+
+ try:
+ logging.info('extracting root.img...')
+
+ subprocess.check_call(['/bin/dd', 'if=' + extracted_file_name, 'of=' + os.path.join(_DOWNLOAD_DIR, 'root.img'),
+ 'bs=2048', 'skip=' + str(root_start / 4), 'count=' + str((root_end - root_start + 1) / 4)])
+
+ except Exception as e:
+ logging.error('could not extract root.img: %s' % e)
+
+ raise
+
+
+def perform_update(version):
+ logging.info('updating to version %(version)s...' % {'version': version})
+
+ logging.info('killing motioneye init script...')
+ os.system('kill $(pidof S85motioneye)')
+
+ logging.info('stopping netwatch init script...')
+ os.system('/etc/init.d/S41netwatch stop')
+
+ download(version)
+
+ logging.info('backing up /boot/config.txt')
+ if os.system('/bin/cp /boot/config.txt /tmp/config.txt'):
+ logging.error('failed to backup /boot/config.txt')
+
+ raise Exception('failed to backup /boot/config.txt')
+
+ logging.info('unmounting boot partition...')
+ if os.system('/bin/umount /boot'):
+ logging.error('failed to unmount boot partition')
+
+ raise Exception('failed to unmount boot partition')
+
+ try:
+ logging.info('installing boot image...')
+ boot_img = os.path.join(_DOWNLOAD_DIR, 'boot.img')
+
+ subprocess.check_call(['/bin/dd', 'if=' + boot_img, 'of=/dev/mmcblk0p1', 'bs=1M'])
+
+ except Exception as e:
+ logging.error('could not install boot image: %s' % e)
+
+ raise
+
+ logging.info('mounting boot partition read-write...')
+ if os.system('/bin/mount -o rw /dev/mmcblk0p1 /boot'):
+ logging.error('failed to mount boot partition')
+
+ raise Exception('failed to mount boot partition')
+
+ logging.info('restoring up /boot/config.txt')
+ if os.system('/bin/cp /tmp/config.txt /boot/config.txt'):
+ logging.error('failed to restore /boot/config.txt')
+
+ raise Exception('failed to restore /boot/config.txt')
+
+ logging.info('preparing to boot in fwupdate mode...')
+ try:
+ config_lines = [c.strip() for c in open('/boot/config.txt', 'r').readlines() if c.strip()]
+
+ except Exception as e:
+ logging.error('failed to read /boot/config.txt: %s' % e, exc_info=True)
+
+ raise
+
+ config_lines.append('initramfs fwupdater.gz')
+
+ try:
+ with open('/boot/config.txt', 'w') as f:
+ for line in config_lines:
+ f.write(line + '\n')
+
+ except Exception as e:
+ logging.error('failed to write /boot/config.txt: %s' % e, exc_info=True)
+
+ raise
+
+ logging.info('rebooting...')
+
+ if os.system('/sbin/reboot'):
+ logging.error('failed to reboot')
+ logging.info('hard rebooting...')
+ open('/proc/sysrq-trigger', 'w').write('b') # reboot
+
diff --git a/package/motioneye/watchctl.py b/package/motioneye/watchctl.py
new file mode 100644
index 0000000000..045a12fbcf
--- /dev/null
+++ b/package/motioneye/watchctl.py
@@ -0,0 +1,282 @@
+
+# Copyright (c) 2015 Calin Crisan
+# This file is part of motionEyeOS.
+#
+# motionEyeOS is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import logging
+import os.path
+
+from config import additional_config, additional_section
+
+
+WATCH_CONF = '/data/etc/watch.conf'
+
+
+def _get_watch_settings():
+ watch_link = False
+ watch_link_timeout = 20
+
+ watch_connect = False
+ watch_connect_host = 'www.google.com'
+ watch_connect_port = 80
+ watch_connect_retries = 3
+ watch_connect_timeout = 5
+ watch_connect_interval = 20
+
+ if os.path.exists(WATCH_CONF):
+ logging.debug('reading watch settings from %s' % WATCH_CONF)
+
+ with open(WATCH_CONF) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+
+ comment = False
+ if line.startswith('#'):
+ line = line.strip('#')
+ comment = True
+
+ try:
+ name, value = line.split('=')
+ value = value.strip('"').strip("'")
+
+ except:
+ continue
+
+ if name == 'link_watch':
+ watch_link = (value == 'true') and not comment
+
+ elif name == 'link_watch_timeout':
+ watch_link_timeout = int(value)
+
+ elif name == 'netwatch_host':
+ watch_connect = not comment
+ watch_connect_host = value
+
+ elif name == 'netwatch_port':
+ watch_connect_port = int(value)
+
+ elif name == 'netwatch_timeout':
+ watch_connect_timeout = int(value)
+
+ elif name == 'netwatch_retries':
+ watch_connect_retries = int(value)
+
+ elif name == 'netwatch_interval':
+ watch_connect_interval = int(value)
+
+ s = {
+ 'watchLink': watch_link,
+ 'watchLinkTimeout': watch_link_timeout,
+ 'watchConnect': watch_connect,
+ 'watchConnectHost': watch_connect_host,
+ 'watchConnectPort': watch_connect_port,
+ 'watchConnectRetries': watch_connect_retries,
+ 'watchConnectTimeout': watch_connect_timeout,
+ 'watchConnectInterval': watch_connect_interval
+ }
+
+ logging.debug(('watch settings: watch_link=%(watchLink)s, watch_link_timeout=%(watchLinkTimeout)s, ' +
+ 'watch_connect=%(watchConnect)s, watch_connect_host=%(watchConnectHost)s, ' +
+ 'watch_connect_port=%(watchConnectPort)s, watch_connect_retries=%(watchConnectRetries)s, ' +
+ 'watch_connect_timeout=%(watchConnectTimeout)s, watch_connect_interval=%(watchConnectInterval)s') % s)
+
+ return s
+
+
+def _set_watch_settings(s):
+ s.setdefault('watchLink', False)
+ s.setdefault('watchLinkTimeout', 20)
+ s.setdefault('watchConnect', False)
+ s.setdefault('watchConnectHost', 'www.google.com')
+ s.setdefault('watchConnectPort', 80)
+ s.setdefault('watchConnectRetries', 3)
+ s.setdefault('watchConnectTimeout', 5)
+ s.setdefault('watchConnectInterval', 20)
+
+ logging.debug('writing watch settings to %s: ' % WATCH_CONF +
+ ('watch_link=%(watchLink)s, watch_link_timeout=%(watchLinkTimeout)s, ' +
+ 'watch_connect=%(watchConnect)s, watch_connect_host=%(watchConnectHost)s, ' +
+ 'watch_connect_port=%(watchConnectPort)s, watch_connect_retries=%(watchConnectRetries)s, ' +
+ 'watch_connect_timeout=%(watchConnectTimeout)s, watch_connect_interval=%(watchConnectInterval)s') % s)
+
+
+ with open(WATCH_CONF, 'w') as f:
+ f.write('link_watch=%s\n' % ['"false"', '"true"'][s['watchLink']])
+ f.write('link_watch_timeout=%s\n' % s['watchLinkTimeout'])
+ f.write('\n')
+ f.write('ip_watch=%s\n' % ['"false"', '"true"'][s['watchLink']])
+ f.write('ip_watch_timeout=%s\n' % s['watchLinkTimeout'])
+ f.write('\n')
+ f.write('%snetwatch_host=%s\n' % (('#' if not s['watchConnect'] else ''), s['watchConnectHost']))
+ f.write('netwatch_port=%s\n' % s['watchConnectPort'])
+ f.write('netwatch_retries=%s\n' % s['watchConnectRetries'])
+ f.write('netwatch_timeout=%s\n' % s['watchConnectTimeout'])
+ f.write('netwatch_interval=%s\n' % s['watchConnectInterval'])
+
+
+@additional_section
+def expertSettings():
+ return {
+ 'label': 'Expert Settings',
+ 'description': 'system tweaks and board-specific options',
+ 'advanced': True
+ }
+
+
+@additional_config
+def watchLink():
+ return {
+ 'label': 'Network Link Watch',
+ 'description': 'enable this if you want the system to reboot upon detecting network link issues',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchLinkTimeout():
+ return {
+ 'label': 'Network Link Timeout',
+ 'description': 'sets the time after which the network link is considered down',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 3600,
+ 'unit': 's',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['watchLink'],
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchConnect():
+ return {
+ 'label': 'Connectivity Watch',
+ 'description': 'enable this if you want the system to constantly try to connect to a certain host and reboot upon failure',
+ 'type': 'bool',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchConnectHost():
+ return {
+ 'label': 'Connectivity Watch Host',
+ 'description': 'sets the hostname or IP address to which a TCP connection will be opened',
+ 'type': 'str',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['watchConnect'],
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchConnectPort():
+ return {
+ 'label': 'Connectivity Watch Port',
+ 'description': 'sets the TCP port TO which the TCP connection will be opened',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 65535,
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['watchConnect'],
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchConnectRetries():
+ return {
+ 'label': 'Connectivity Watch Retries',
+ 'description': 'sets the number of times to retry to connect',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 100,
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['watchConnect'],
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchConnectTimeout():
+ return {
+ 'label': 'Connectivity Watch Timeout',
+ 'description': 'sets the time to wait for the connection to succeed',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 3600,
+ 'unit': 's',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['watchConnect'],
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def watchConnectInterval():
+ return {
+ 'label': 'Connectivity Watch Interval',
+ 'description': 'sets the time to wait between connections',
+ 'type': 'number',
+ 'min': 1,
+ 'max': 3600,
+ 'unit': 's',
+ 'section': 'expertSettings',
+ 'advanced': True,
+ 'reboot': True,
+ 'required': True,
+ 'depends': ['watchConnect'],
+ 'get': _get_watch_settings,
+ 'set': _set_watch_settings,
+ 'get_set_dict': True
+ }
diff --git a/package/streameye/Config.in b/package/streameye/Config.in
new file mode 100644
index 0000000000..51bd52f25f
--- /dev/null
+++ b/package/streameye/Config.in
@@ -0,0 +1,3 @@
+config BR2_PACKAGE_STREAMEYE
+ bool "streameye"
+
diff --git a/package/streameye/streameye.mk b/package/streameye/streameye.mk
new file mode 100644
index 0000000000..3510e1506b
--- /dev/null
+++ b/package/streameye/streameye.mk
@@ -0,0 +1,21 @@
+################################################################################
+#
+# streameye
+#
+################################################################################
+
+STREAMEYE_VERSION = ffd6f28a094f70c70893f6e516610459faa15ea2
+STREAMEYE_SITE = $(call github,ccrisan,streameye,$(STREAMEYE_VERSION))
+STREAMEYE_LICENSE = GPLv3
+
+define STREAMEYE_BUILD_CMDS
+ make CC="$(TARGET_CC)" -C "$(@D)"
+endef
+
+define STREAMEYE_INSTALL_TARGET_CMDS
+ cp $(@D)/streameye $(TARGET_DIR)/usr/bin/
+ cp $(@D)/extras/raspimjpeg.py $(TARGET_DIR)/usr/bin/
+endef
+
+$(eval $(generic-package))
+