From 766d7132b553666aac52cfb8eecd4d7c99bc4684 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 18 Feb 2017 21:32:40 +0200 Subject: [PATCH] merged with thingOS --- board/common/overlay/etc/fstab | 2 + board/common/overlay/etc/init.d/S61proftpd | 45 + board/common/overlay/etc/init.d/S62smb | 59 + board/common/overlay/etc/init.d/S85motioneye | 149 +++ .../overlay/etc/modprobe.d/modprobe.conf | 4 + board/common/overlay/etc/motioneye.conf | 24 + board/common/overlay/etc/os.conf | 2 +- board/common/overlay/etc/proftpd.conf | 25 + board/common/overlay/etc/samba/smb.conf | 31 + board/common/overlay/etc/version | 2 +- board/common/skeleton/home/ftp/.empty | 0 board/common/skeleton/home/ftp/sdcard/.empty | 0 board/common/skeleton/home/ftp/storage/.empty | 0 board/odroidc1/overlay/etc/os.conf | 2 +- board/odroidc2/overlay/etc/os.conf | 2 +- .../raspberrypi/motioneye-modules/boardctl.py | 310 +++++ .../motioneye-modules/streameyectl.py | 1172 +++++++++++++++++ .../overlay/etc/init.d/S84streameye | 51 + board/raspberrypi/overlay/etc/modules | 3 + .../raspberrypi/overlay/usr/bin/streameye.sh | 93 ++ .../motioneye-modules/boardctl.py | 290 ++++ .../motioneye-modules/streameyectl.py | 1172 +++++++++++++++++ .../overlay/etc/init.d/S84streameye | 51 + board/raspberrypi2/overlay/etc/modules | 3 + .../raspberrypi2/overlay/usr/bin/streameye.sh | 93 ++ .../motioneye-modules/boardctl.py | 238 ++++ .../motioneye-modules/streameyectl.py | 1172 +++++++++++++++++ .../overlay/etc/init.d/S84streameye | 51 + board/raspberrypi3/overlay/etc/modules | 3 + .../raspberrypi3/overlay/usr/bin/streameye.sh | 93 ++ configs/bananapi_defconfig | 25 + configs/odroidc1_defconfig | 24 + configs/odroidc2_defconfig | 25 + configs/odroidxu4_defconfig | 24 + configs/raspberrypi2_defconfig | 29 + configs/raspberrypi3_defconfig | 29 + configs/raspberrypi_defconfig | 30 + package/Config.in | 4 + package/libwebcam/Config.in | 3 + package/libwebcam/libwebcam.mk | 18 + package/motion/0001-version.patch | 16 + package/motion/Config.in | 3 + package/motion/motion.mk | 18 + package/motioneye/Config.in | 2 + package/motioneye/dropbox.keys | 2 + package/motioneye/extractl.py | 638 +++++++++ package/motioneye/ipctl.py | 248 ++++ package/motioneye/motioneye.mk | 71 + package/motioneye/servicectl.py | 314 +++++ package/motioneye/update.py | 263 ++++ package/motioneye/watchctl.py | 282 ++++ package/streameye/Config.in | 3 + package/streameye/streameye.mk | 21 + 53 files changed, 7230 insertions(+), 4 deletions(-) create mode 100755 board/common/overlay/etc/init.d/S61proftpd create mode 100755 board/common/overlay/etc/init.d/S62smb create mode 100755 board/common/overlay/etc/init.d/S85motioneye create mode 100644 board/common/overlay/etc/motioneye.conf create mode 100644 board/common/overlay/etc/proftpd.conf create mode 100644 board/common/overlay/etc/samba/smb.conf create mode 100644 board/common/skeleton/home/ftp/.empty create mode 100644 board/common/skeleton/home/ftp/sdcard/.empty create mode 100644 board/common/skeleton/home/ftp/storage/.empty create mode 100644 board/raspberrypi/motioneye-modules/boardctl.py create mode 100644 board/raspberrypi/motioneye-modules/streameyectl.py create mode 100755 board/raspberrypi/overlay/etc/init.d/S84streameye create mode 100644 board/raspberrypi/overlay/etc/modules create mode 100755 board/raspberrypi/overlay/usr/bin/streameye.sh create mode 100644 board/raspberrypi2/motioneye-modules/boardctl.py create mode 100644 board/raspberrypi2/motioneye-modules/streameyectl.py create mode 100755 board/raspberrypi2/overlay/etc/init.d/S84streameye create mode 100644 board/raspberrypi2/overlay/etc/modules create mode 100755 board/raspberrypi2/overlay/usr/bin/streameye.sh create mode 100644 board/raspberrypi3/motioneye-modules/boardctl.py create mode 100644 board/raspberrypi3/motioneye-modules/streameyectl.py create mode 100755 board/raspberrypi3/overlay/etc/init.d/S84streameye create mode 100644 board/raspberrypi3/overlay/etc/modules create mode 100755 board/raspberrypi3/overlay/usr/bin/streameye.sh create mode 100644 package/libwebcam/Config.in create mode 100644 package/libwebcam/libwebcam.mk create mode 100644 package/motion/0001-version.patch create mode 100644 package/motion/Config.in create mode 100644 package/motion/motion.mk create mode 100644 package/motioneye/Config.in create mode 100644 package/motioneye/dropbox.keys create mode 100644 package/motioneye/extractl.py create mode 100644 package/motioneye/ipctl.py create mode 100644 package/motioneye/motioneye.mk create mode 100644 package/motioneye/servicectl.py create mode 100644 package/motioneye/update.py create mode 100644 package/motioneye/watchctl.py create mode 100644 package/streameye/Config.in create mode 100644 package/streameye/streameye.mk 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)) +