diff --git a/CHANGELOG.md b/CHANGELOG.md index 015332c..7027bf2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -472,7 +472,7 @@ This update focuses on improving the reliability of Predator, especially when op - Added per-device resolution configuration. - Added the ability to disable capture devices without removing them from the configuration entirely. - Predator now changes the status light color when a video is being locked. - - Added standalone tool for triggering dashcam saving via a GPIO input. + - Dashcam video save events can now be triggered using buttons via GPIO pins. - Moved the status lighting configuration to the "general" section. - Updated configuration back-end. - Predator can now automatically update the configuration file between versions when configuration values are added or removed. diff --git a/assets/support/configdefault.json b/assets/support/configdefault.json index 050ccc6..1dd5f3b 100755 --- a/assets/support/configdefault.json +++ b/assets/support/configdefault.json @@ -144,6 +144,8 @@ "saving": { "directory": "saved_dashcam", "trigger": "dashcam_lock_trigger", + "trigger_gpio": { + }, "segment_length": 60, "looped_recording": { "mode": "manual", diff --git a/assets/support/configoutline.json b/assets/support/configoutline.json index 971efb0..375081e 100755 --- a/assets/support/configoutline.json +++ b/assets/support/configoutline.json @@ -138,6 +138,7 @@ "saving": { "directory": "str", "trigger": "str", + "trigger_gpio": "dict", "segment_length": "+float", "looped_recording": { "mode": ["automatic", "manual"], diff --git a/config.py b/config.py index ef14353..08dcab7 100755 --- a/config.py +++ b/config.py @@ -316,7 +316,7 @@ def highest_different_index(config_active, config_default, index): # This functi return [] -ignore_indexes = [["realtime", "image", "camera", "devices"], ["dashcam", "stamps", "relay", "triggers"], ["dashcam", "capture", "video", "devices"]] # Configure values specified here will not be checked by the check_defaults_missing() and check_defaults_extra() functions. +ignore_indexes = [["realtime", "image", "camera", "devices"], ["dashcam", "saving", "trigger_gpio"], ["dashcam", "stamps", "relay", "triggers"], ["dashcam", "capture", "video", "devices"]] # Configure values specified here will not be checked by the check_defaults_missing() and check_defaults_extra() functions. # This function checks for values that exist in the default config that aren't present in the active config. def check_defaults_missing(config_defaults, config_active, index=[], missing_values=[]): @@ -358,14 +358,14 @@ def update_config(): extra_values = check_defaults_extra(config_default, config_active) if (len(missing_values) > 0): - print(style.yellow + "The following values were present in the default configuration, but not the active configuration. They may have been added in an update. The default values have been inserted into the active configuration." + style.end) + print(style.yellow + "The following values were present in the default configuration, but not the active configuration. They may have been added in an update. The default values will be inserted into the active configuration." + style.end) for value in missing_values: print(" " + '>'.join(map(str, value))) print(style.faint + "Continuing in 5 seconds" + style.end) time.sleep(5) if (len(extra_values) > 0): - print(style.yellow + "The following values were present in the active configuration, but not the default configuration. They may have been removed in an update. These values have been removed from the active configuration." + style.end) + print(style.yellow + "The following values were present in the active configuration, but not the default configuration. They may have been removed in an update. These values will be removed from the active configuration." + style.end) for value in extra_values: index = highest_different_index(config_default, config_active, value) if (get_nested_value(index, config_active) != None): # Check to see if this index hasn't already been removed. diff --git a/dashcam.py b/dashcam.py index b2e169f..ba5efbb 100755 --- a/dashcam.py +++ b/dashcam.py @@ -53,10 +53,15 @@ import lighting # Import the lighting.py script. update_status_lighting = lighting.update_status_lighting # Load the status lighting update function from the lighting script. +must_import_gpiozero = False for stamp in config["dashcam"]["stamps"]["relay"]["triggers"]: # Check to see if there are any GPIO relay stamps active. if (config["dashcam"]["stamps"]["relay"]["triggers"][stamp]["enabled"] == True): # Check to see if at least one relay stamp is enabled. - from gpiozero import Button # Import GPIOZero + must_import_gpiozero = True break # Exit the loop, since GPIOZero has already been imported. +if (len(config["dashcam"]["saving"]["trigger_gpio"]) > 0): + must_import_gpiozero = True +if (must_import_gpiozero == True): + from gpiozero import Button # Import GPIOZero @@ -145,6 +150,41 @@ + +trigger_file_location = config["general"]["interface_directory"] + "/" + config["dashcam"]["saving"]["trigger"] # Define the path of the dashcam lock trigger file. +trigger_file_location = trigger_file_location.replace("//", "/") # Remove any duplicate slashes in the file path. + +last_trigger_file_created = 0 +def create_trigger_file(): + global last_trigger_file_created + if (time.time() - last_trigger_file_created < 1): + if (os.path.isdir(config["general"]["interface_directory"]) == False): # Check to see if the interface directory has not yet been created. + os.system("mkdir -p '" + str(config["general"]["interface_directory"]) + "'") + os.system("chmod -R 777 '" + str(config["general"]["interface_directory"]) + "'") + os.system("touch '" + trigger_file_location + "'") + else: + print("Supressed duplicate trigger file") + last_strigger_file_created + +def watch_button(pin, hold_time=0.2, event=create_trigger_file): + print("Watching pin " + str(pin)) + button = Button(pin) + time_pressed = 0 + last_triggered = 0 + while True: + if (button.is_pressed and time_pressed == 0): # Check to see if the button was just pressed. + print("Pressed" + str(pin)) + time_pressed = time.time() + elif (button.is_pressed and time.time() - time_pressed >= hold_time): # Check to see if the button is being held, and the time threshold has been reached. + print("Triggered " + str(pin)) + event() + elif (button.is_pressed == False): # If the button is not pressed, reset the timer. + time_pressed = 0 + + time.sleep(hold_time/10) + + + def merge_audio_video(video_file, audio_file, output_file, audio_offset=0): debug_message("Merging audio and video files") @@ -762,6 +802,13 @@ def start_dashcam_recording(dashcam_devices, directory, background=False): # Thi update_status_lighting("normal") # Initialize the status lighting to normal. + + + button_watch_threads = {} + for pin in config["dashcam"]["saving"]["trigger_gpio"]: + button_watch_threads[int(pin)] = threading.Thread(target=watch_button, args=[int(pin)], name="ButtonWatch" + str(pin)) + button_watch_threads[int(pin)].start() + dashcam_process = [] # Create a placeholder list to store the dashcam processes. iteration_counter = 0 # Set the iteration counter to 0 so that we can increment it for each recording device specified. global parked diff --git a/docs/CONFIGURE.md b/docs/CONFIGURE.md index 923a4a9..126fc6b 100755 --- a/docs/CONFIGURE.md +++ b/docs/CONFIGURE.md @@ -185,7 +185,11 @@ This document describes the configuration values found `config.json`. - `trigger` is the name of a file inside the interface directory that will trigger Predator to save the current and previous dashcam segments. - To trigger a save, create this file in the interface directory. Predator will save the video then automatically remove the trigger file. - When this file is created, Predator will immediately save the previous and current dashcam video segments. Once the current segment is done recording, Predator will re-save it, such that the saved video doesn't cut off at the moment the saved was triggered. - - If Predator is terminated between the initial save and the second save, only video captured after the save trigger shouldn't be saved. + - If Predator is terminated between the initial save and the second save, only video captured after the save trigger won't be saved. + - `trigger_gpio` contains GPIO pins that will be monitored for button presses to create the dashcam save trigger file. + - This allows hardwired buttons to trigger dashcam video saves. + - Each entry in this configuration section uses the GPIO pin number as a key, and contains the following values: + - `name` is a human friendly name for the pin, and can be set to any plain text string. - `segment_length` is a number that sets how many seconds long each video segment will be before another segment is created. - `looped_recording` contains settings that control how and when Predator will erase old dashcam segments to make space for new ones. - `mode` determines the method by which Predator determines how to erase old files. This can only be set to one of 3 strings: diff --git a/tools/gpio_dashcam_save_trigger.py b/tools/gpio_dashcam_save_trigger.py index 34b5d5b..10fda97 100644 --- a/tools/gpio_dashcam_save_trigger.py +++ b/tools/gpio_dashcam_save_trigger.py @@ -9,6 +9,7 @@ import os # Required to interact with certain operating system functions import json # Required to process JSON data import time +import threading from gpiozero import Button from signal import pause @@ -28,20 +29,38 @@ trigger_file_location = trigger_file_location.replace("//", "/") # Remove any duplicate slashes in the file path. if (os.path.isdir(config["general"]["interface_directory"]) == False): # Check to see if the interface directory has not yet been created. os.system("mkdir -p '" + str(config["general"]["interface_directory"]) + "'") + os.system("chmod -R 777 '" + str(config["general"]["interface_directory"]) + "'") -last_trigger_time = 0 def create_trigger_file(): - global last_trigger_time - if (time.time() - last_trigger_time > 1): # Check to see if at least 1 second has passed since the last dash-cam save trigger. - os.system("touch '" + trigger_file_location + "'") - last_trigger_time = time.time() + os.system("touch '" + trigger_file_location + "'") +def watch_button(pin, hold_time=0.2, event=create_trigger_file): + print("Watching pin " + str(pin)) + button = Button(pin) -buttons = [] + time_pressed = 0 + last_triggered = 0 + while True: + if (button.is_pressed and time_pressed == 0): # Check to see if the button was just pressed. + print("Pressed" + str(pin)) + time_pressed = time.time() + elif (button.is_pressed and time.time() - time_pressed < hold_time): # Check to see if the button is being held, but the time threshold hasn't been reached. + pass + #print("Holding") + elif (button.is_pressed and time.time() - time_pressed >= hold_time): # Check to see if the button is being held, and the time threshold has been reached. + if (time.time() - last_triggered > 1): + print("Triggered " + str(pin)) + event() + last_triggered = time.time() + elif (button.is_pressed == False): # If the button is not pressed, reset the timer. + time_pressed = 0 + + time.sleep(0.02) + + +button_watch_threads = {} for pin in gpio_pins: - buttons.append(Button(pin)) -for button in buttons: - button.when_pressed = create_trigger_file + button_watch_threads[pin] = threading.Thread(target=watch_button, args=[pin], name="ButtonWatch" + str(pin)) + button_watch_threads[pin].start() -pause()