Skip to content

Commit

Permalink
Increasing some logging information, add option for reboot, motion th…
Browse files Browse the repository at this point in the history
…reshold.. (#46)

* misc

* trying to debug issue

* misc changes

* update help

* fix typo

* add config option for motion detection threshold

* removing some debug options

* misc changes for threshold

* workaround for unhandled exception

* queue fixes

* misc changes

* including log of motion thresholds

* increase threshold

* disable AWB

* bump version

* camera fixes

* more changes

* update todo
  • Loading branch information
FutureSharks authored Aug 12, 2019
1 parent e7f8281 commit 2ddb2ab
Show file tree
Hide file tree
Showing 11 changed files with 50 additions and 46 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Install open-cv and rpi-security:

```console
sudo pip3 install opencv-contrib-python opencv-contrib-python-headless
sudo pip3 install --no-binary :all: https://github.com/FutureSharks/rpi-security/archive/1.2.zip
sudo pip3 install --no-binary :all: https://github.com/FutureSharks/rpi-security/archive/1.3.zip
```

Reload systemd configuration and enable the service:
Expand Down Expand Up @@ -130,7 +130,7 @@ It runs as a service and logs to syslog. To see the logs check `/var/log/syslog`
You can start `rpi-security.py` manually with debug output. First add the monitor mode interface:

```console
root@raspberrypi:~# iw phy phy0 interface add mon0 type monitor
root@raspberrypi:~# iw phy phy1 interface add mon0 type monitor
root@raspberrypi:~# ifconfig mon0 up
```

Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# To do

- Automatically clean up old photos
- Sometimes motion trigger doesn't run arp_ping before sending images, it sends them immediately.
- Increase resolution of motion detection and image captured with bounding box as it's currently hard coded to 500 pixel
8 changes: 5 additions & 3 deletions bin/rpi-security.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ def setup_logging(debug_mode, log_to_stdout):

try:
rpis = rpisec.RpisSecurity(args.config_file, args.data_file)
camera = rpisec.RpisCamera(rpis.photo_size, rpis.gif_size, rpis.motion_size,
rpis.camera_vflip, rpis.camera_hflip, rpis.camera_capture_length,
rpis.camera_mode)
camera = rpisec.RpisCamera(photo_size=rpis.photo_size, gif_size=rpis.gif_size,
motion_size=rpis.motion_size, motion_detection_threshold=rpis.motion_detection_threshold,
camera_vflip=rpis.camera_vflip, camera_hflip=rpis.camera_hflip,
camera_capture_length=rpis.camera_capture_length,camera_mode=rpis.camera_mode
)
if rpis.debug_mode:
logger.handlers[0].setLevel(logging.DEBUG)
except Exception as e:
Expand Down
13 changes: 9 additions & 4 deletions etc/rpi-security.conf
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ debug_mode=false
packet_timeout=700

# How many times to ARP ping the MAC addresses
arp_ping_count=7
arp_ping_count=12

# camera_mode can be 'photo' or 'gif'
camera_mode=photo
Expand All @@ -36,11 +36,16 @@ camera_hflip=false
# Higher than 3 with gif mode will run out of memory on models with 128MB of RAM.
camera_capture_length=2

# Image size for photos
photo_size=2592x1944
# Image size for photos. Must be set to a valid resolution for your camera module
# Max resolution for V2 module: 3280x2464
# Max resolution for V1 module: 2592x1944
photo_size=3280x2464

# Size for GIF files. Anything higher than 800x600 can use too much memory
gif_size=800x600
gif_size=640x480

# Resolution for motion detection
motion_size=640x480

# Threshold for detection of motion. Lower is more sensitivity.
motion_detection_threshold=1000
40 changes: 15 additions & 25 deletions rpisec/rpis_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class RpisCamera(object):
captues photos and GIFs.
'''
def __init__(self, photo_size, gif_size, motion_size, camera_vflip,
camera_hflip, camera_capture_length,
camera_hflip, camera_capture_length, motion_detection_threshold,
camera_mode):
self.photo_size = photo_size
self.gif_size = gif_size
Expand All @@ -36,6 +36,7 @@ def __init__(self, photo_size, gif_size, motion_size, camera_vflip,
self.queue = Queue()
self.motion_framerate = 5
self.motion_size = motion_size
self.motion_detection_threshold = motion_detection_threshold
self.temp_directory = '/var/tmp'
self.camera_save_path = '/var/tmp'
self.camera_capture_length = camera_capture_length
Expand All @@ -45,6 +46,7 @@ def __init__(self, photo_size, gif_size, motion_size, camera_vflip,
self.camera = PiCamera()
self.camera.vflip = self.camera_vflip
self.camera.hflip = self.camera_hflip
self.camera.awb_mode = 'off'
except Exception as e:
exit_error('Camera module failed to intialise with error {0}'.format(repr(e)))

Expand All @@ -55,13 +57,15 @@ def take_photo(self, filename_extra_suffix=''):
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
photo = '{0}/rpi-security-{1}{2}.jpeg'.format(self.camera_save_path, timestamp, filename_extra_suffix)
try:
self.set_normal_settings()
self.camera.resolution = self.photo_size
with self.lock:
while self.camera.recording:
time.sleep(0.1)
time.sleep(2)
self.camera.resolution = self.photo_size
self.camera.capture(photo, use_video_port=False)
except PiCameraRuntimeError as e:
logger.error('Failed to take photo, camera error: {0}'.format(repr(e)))
return None
except Exception as e:
logger.error('Failed to take photo: {0}'.format(repr(e)))
return None
Expand All @@ -75,7 +79,7 @@ def take_gif(self):
temp_jpeg_path = '{0}/rpi-security-{1}-gif-part'.format(self.temp_directory, timestamp)
jpeg_files = ['{0}-{1}.jpg'.format(temp_jpeg_path, i) for i in range(self.camera_capture_length*3)]
try:
self.set_normal_settings()
self.camera.resolution = self.gif_size
for jpeg in jpeg_files:
with self.lock:
while self.camera.recording:
Expand Down Expand Up @@ -106,42 +110,27 @@ def trigger_camera(self):
else:
logger.error('Unsupported camera_mode: {0}'.format(self.camera_mode))

def set_normal_settings(self):
self.camera.awb_mode = 'auto'
self.camera.exposure_mode = 'auto'

def set_motion_settings(self):
self.camera.resolution = self.motion_size
self.camera.framerate = self.motion_framerate
exposure_speed = self.camera.exposure_speed
awb_gains = self.camera.awb_gains
self.camera.shutter_speed = exposure_speed
self.camera.awb_mode = 'off'
self.camera.awb_gains = awb_gains
self.camera.exposure_mode = 'off'

def start_motion_detection(self, rpis):
min_area = 500
past_frame = None
logger.debug("Starting motion detection")
self.camera.resolution = self.motion_size
while not self.lock.locked() and rpis.state.current == 'armed':
stream = io.BytesIO()
self.camera.resolution = self.motion_size
self.camera.capture(stream, format='jpeg', use_video_port=False)
data = np.fromstring(stream.getvalue(), dtype=np.uint8)
frame = cv2.imdecode(data, 1)

# if frame is initialized, we have not reach the end of the video
if frame is not None:
past_frame = self.handle_new_frame(frame, past_frame, min_area)
past_frame = self.handle_new_frame(frame, past_frame)
else:
logger.error("No more frame")
rpis.state.check()
time.sleep(0.3)
else:
self.stop_motion_detection()

def handle_new_frame(self, frame, past_frame, min_area):
def handle_new_frame(self, frame, past_frame):
(h, w) = frame.shape[:2]
r = 500 / float(w)
dim = (500, int(h * r))
Expand Down Expand Up @@ -175,11 +164,12 @@ def handle_new_frame(self, frame, past_frame, min_area):
# loop over the contours
for c in cnts:
# if the contour is too small, ignore it
if cv2.contourArea(c) < min_area:
countour_area = cv2.contourArea(c)
if countour_area < self.motion_detection_threshold:
continue

logger.debug("Motion detected!")
# Motion detected because there is a contour that is larger than the specified min_area
logger.debug("Motion detected! Motion level is {0} (threshold is {1})".format(countour_area, self.motion_detection_threshold))
# Motion detected because there is a contour that is larger than the specified self.motion_detection_threshold
# compute the bounding box for the contour, draw it on the frame,
(x, y, w, h) = cv2.boundingRect(c)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
Expand Down
6 changes: 4 additions & 2 deletions rpisec/rpis_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ class RpisSecurity(object):
'photo_size': '1024x768',
'gif_size': '1024x768',
'motion_size': '1024x768',
'motion_detection_threshold': '1000',
'camera_mode': 'gif',
'camera_capture_length': '3',
'telegram_users_number': '1',
'arp_ping_count': '5',
'arp_ping_count': '7',
}

def __init__(self, config_file, data_file):
Expand Down Expand Up @@ -133,6 +134,7 @@ def _str2bool(v):
self.photo_size = tuple([int(x) for x in self.photo_size.split('x')])
self.gif_size = tuple([int(x) for x in self.gif_size.split('x')])
self.motion_size = tuple([int(x) for x in self.motion_size.split('x')])
self.motion_detection_threshold = float(self.motion_detection_threshold)
self.camera_capture_length = int(self.camera_capture_length)
self.camera_mode = self.camera_mode.lower()
self.packet_timeout = int(self.packet_timeout)
Expand Down Expand Up @@ -218,8 +220,8 @@ def telegram_send_file(self, file_path):
if 'telegram_chat_ids' not in self.saved_data:
logger.error('Telegram failed to send file {0} because Telegram chat_id is not set. Send a message to the Telegram bot'.format(file_path))
return False
filename, file_extension = os.path.splitext(file_path)
try:
filename, file_extension = os.path.splitext(file_path)
if file_extension == '.mp4':
for chat_id in self.saved_data['telegram_chat_ids']:
self.bot.sendVideo(chat_id=chat_id, video=open(file_path, 'rb'), timeout=30)
Expand Down
2 changes: 2 additions & 0 deletions rpisec/rpis_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def check(self):
return
now = time.time()
if now - self.last_packet > (self.rpis.packet_timeout + 20):
if self.current != 'armed':
logger.debug("No packets detected for {0} seconds, arming".format(self.rpis.packet_timeout + 20))
self.update_state('armed')
elif now - self.last_packet > self.rpis.packet_timeout:
logger.debug("Running arp_ping_macs before arming...")
Expand Down
2 changes: 1 addition & 1 deletion rpisec/threads/capture_packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def update_time(packet):
packet_mac = set(rpis.mac_addresses) & set([packet[0].addr2, packet[0].addr3])
packet_mac_str = list(packet_mac)[0]
rpis.state.update_last_mac(packet_mac_str)
logger.debug('Packet detected from {0}'.format(packet_mac_str))
logger.debug('Packet detected from {0}. State is {1}'.format(packet_mac_str, rpis.state.current))
def calculate_filter(mac_addresses):
mac_string = ' or '.join(mac_addresses)
filter_text = (
Expand Down
9 changes: 4 additions & 5 deletions rpisec/threads/process_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,18 @@ def process_photos(rpis, camera):
logger.debug('Running arp_ping_macs before sending photos...')
rpis.arp_ping_macs()
time.sleep(2)
while True:
while not camera.queue.empty():
if rpis.state.current != 'armed':
logger.debug('Stopping photo processing as state is now {0} and clearing queue'.format(rpis.state.current))
camera.clear_queue()
break
photo = camera.queue.get()
if photo is None:
break
logger.debug('Processing the photo: {0}'.format(photo))
logger.debug('Processing the photo {0}, state is {1}'.format(photo, rpis.state.current))
rpis.state.update_triggered(True)
rpis.telegram_send_message('Motioned detected')
if rpis.telegram_send_file(photo):
camera.queue.task_done()
else:
logger.debug('Stopping photo processing as state is now {0} and clearing queue'.format(rpis.state.current))
camera.queue.queue.clear()
camera.clear_queue()
time.sleep(0.1)
8 changes: 7 additions & 1 deletion rpisec/threads/telegram_bot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-

import logging
import os
from telegram.ext import Updater, CommandHandler, RegexHandler
import _thread

Expand Down Expand Up @@ -37,7 +38,7 @@ def check_chat_id(update):

def help(bot, update):
if check_chat_id(update):
bot.sendMessage(update.message.chat_id, parse_mode='Markdown', text='/status: Request status\n/disable: Disable alarm\n/enable: Enable alarm\n/photo: Take a photo\n/gif: Take a gif\n', timeout=10)
bot.sendMessage(update.message.chat_id, parse_mode='Markdown', text='/status: Request status\n/disable: Disable alarm\n/enable: Enable alarm\n/photo: Take a photo\n/gif: Take a gif\n/reboot: reboot\n', timeout=10)

def status(bot, update):
if check_chat_id(update):
Expand All @@ -61,6 +62,10 @@ def gif(bot, update):
gif = camera.take_gif()
rpis.telegram_send_file(gif)

def reboot(bot, update):
logger.info('Rebooting after receiving reboot command')
os.system('reboot')

def error_callback(bot, update, error):
logger.error('Update "{0}" caused error "{1}"'.format(update, error))

Expand All @@ -75,6 +80,7 @@ def error_callback(bot, update, error):
dp.add_handler(CommandHandler("enable", enable), group=3)
dp.add_handler(CommandHandler("photo", photo), group=3)
dp.add_handler(CommandHandler("gif", gif), group=3)
dp.add_handler(CommandHandler("reboot", reboot), group=3)
dp.add_error_handler(error_callback)
updater.start_polling(timeout=10)
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name = 'rpi-security',
version = '1.0',
version = '1.3',
author = 'Max Williams',
author_email = '[email protected]',
url = 'https://github.com/FutureSharks/rpi-security',
Expand Down

0 comments on commit 2ddb2ab

Please sign in to comment.