diff --git a/README.md b/README.md index 528ebc5..b8bb03e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ -# bee_track -Tracking software to run on pi +# Bumblebee Tracking System +This is a web-based bee tracking system that runs on a Raspberry Pi that may be accessed via a mobile phone. It controls a collection of cameras and captures data. -# Getting the pi set up +This code repository contains two systems: + +- a front-end user interface implemented in HTML and Javascript; +- a back-end API implemented using the Flask web framework. + +# Installation + +## Getting the RPi set up To make it possible to ssh into, use: @@ -23,13 +30,13 @@ Edit `/etc/wpa_supplicant/wpa_supplicant.conf`, enter: psk="PASSWORD" } -# Installation +# Dependencies -Download the aravis library: +Download the Aravis library: git clone https://github.com/AravisProject/aravis.git -Or donwload earlier version from +Or download earlier version from http://ftp.gnome.org/pub/GNOME/sources/aravis/0.6/ then if you need the viewer (although I did find I had to split these installs). @@ -105,18 +112,78 @@ frontend <--"HTTP 5000"--> backend ## API architecture -The Flask application has four components which run in separate threads: +The Flask application has four main components, which are workers that run in separate threads: -- Cameras -- Triggers -- Rotation -- Tracking +- Trigger: handles triggering the camera and flashes using GPIO pins. +- Cameras: reads in image data from a camera. +- Rotation: sends a rotation signal to a stepper motor via GPIO pins. +- Tracking: takes images from the greyscale camera's image queue (`cam.photo_queue`) and looks for the tag Each thread has a worker process with a configuration message queue. +## Camera capture process + +### Camera exposure trigger + +The `Trigger` class waits for the instruction to activate the exposure pin. + +Photos are captured in "sets" which are synchronised with the flash sequence. + +1. Click "Start" on the GUI. +2. API activates `Trigger.run` event +3. (Optional) Delay start: wait +4. Activate flashes (via their GPIO pins) +5. Wait for preparation time (20 microseconds) +6. Record a photo capture in the exposure log. +7. Increment the trigger index (photo exposure counter). +8. Trigger camera exposure (activate GPIO pin) +9. The camera captured data based on the GPIO pin activation. +10. Wait for the exposure/trigger time (30 microseconds) +11. Deactivate the trigger pins for the camera and flashes. + +### Camera data retrieval + +The `Camera` class gets photos from the camera's data buffer. + +## User interface + +The front-end is implemented using jQuery, a popular JavaScript library for manipulating web pages. In simple terms, each HTML form widget in [webinterface/index.html](./webinterface/index.html) has an associated behaviour determined by the JavaScript code in [webinterface/track.js](./webinterface/track.js), which listens for certain events such as button clicks and makes a call to the back-end API using AJAX (Asynchronous JavaScript and XML). + +| Button | Button ID | Endpoint | Behaviour | +| -------------- | --------- | -------- | --------------------------- | +| Capture: Start | `#start` | `/start` | Starts camera data capture. | +| Capture: Stop | `#stop` | `/stop` | Stops camera data capture. | +| | | | | + +There is a JavaScript function called `msg()` that prints message to the "console" which is a HTML text area. + # Usage -TODO +The user interface is designed to control the bee tracker system. + +1. Access the user interface +2. Enter a label for your files +3. Under capture, click "Start" +4. Under capture, click "Stop" + +## API usage + +The API returns HTTP responses. + +```bash +$ curl http://192.168.50.58:5000/getimagecount +42 +``` + +# Configuration + +There are endpoints to set and get configuration options for each of the components. + +The configuration options are stored in this file, which is loaded when the app starts up. + +``` +/home/pi/bee_track/webinterface/configvals.pkl +``` # Development diff --git a/bee_track/battery.py b/bee_track/battery.py index 84feaaf..a540534 100644 --- a/bee_track/battery.py +++ b/bee_track/battery.py @@ -25,6 +25,10 @@ def scale(v): def read_batteries(): + """ + Get battery level information. + """ + scaleval = 0.0164 # Open SPI bus spi = spidev.SpiDev() diff --git a/bee_track/camera.py b/bee_track/camera.py index 3b768f2..d6112d7 100644 --- a/bee_track/camera.py +++ b/bee_track/camera.py @@ -2,13 +2,16 @@ import multiprocessing import pickle import threading +import logging -# TODO what's this? +# https://github.com/SheffieldMLtracking/QueueBuffer from QueueBuffer import QueueBuffer import numpy as np from configurable import Configurable +logger = logging.getLogger(__name__) + class CameraException(Exception): pass @@ -45,6 +48,9 @@ def downscalecolour(img, blocksize): class Camera(Configurable): + """ + A worker to reads in image data from a camera. + """ def __init__(self, message_queue, record, cam_trigger, cam_id=None): """ @@ -55,6 +61,7 @@ def __init__(self, message_queue, record, cam_trigger, cam_id=None): self.record = record self.label = multiprocessing.Array('c', 100) self.index = multiprocessing.Value('i', 0) + 'Incrementing identifier number per capture (i.e. image count)' self.savephotos = multiprocessing.Value('b', True) self.fastqueue = multiprocessing.Value('b', False) ###THIS WILL STOP PROCESSING self.test = multiprocessing.Value('b', False) @@ -86,34 +93,41 @@ def camera_trigger(self): """ raise NotImplementedError - # def trigger(self): - # print("Triggering Camera") - # self.cam_trigger.set() + def get_photo(self, get_raw: bool = False) -> np.ndarray: + """ + Retrieve image data from the camera. + + This is a synchronous (blocking) function. - def get_photo(self, getraw: bool = False): - """Blocking, returns a photo numpy array""" + :returns: Photo data (numpy array) + """ raise NotImplementedError def worker(self): - print("Camera worker started") + """ + Get image data from the camera. + """ + logger.info("Camera worker started") self.setup_camera() - t = threading.Thread(target=self.camera_trigger) - t.start() - t = threading.Thread(target=self.camera_config_worker) - t.start() - print("Camera setup complete") + + # Start threads to listen for camera trigger and configuration messages + threading.Thread(target=self.camera_trigger).start() + threading.Thread(target=self.camera_config_worker).start() + + logger.info("Camera setup complete") + + # ??? last_photo_object = None - while True: - # print("waiting for photo") - if self.debug: print( - 'Blocking started for getting photo at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) - photo = self.get_photo(getraw=self.fastqueue.value) - print(".", end="", flush=True) - if self.debug: print('Got photo at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) + # Indefinite loop: transfer image data from the camera + while True: + logger.debug('Blocking started for getting photo at %s' % ( + datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) + photo = self.get_photo(get_raw=self.fastqueue.value) + logger.debug('Got photo at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) if photo is None: - print("Photo failed") + logger.info("Photo failed") rec = None for r in self.record: @@ -121,31 +135,32 @@ def worker(self): rec = r break if rec is None: - print("WARNING: Failed to find associated photo record") + logger.info("WARNING: Failed to find associated photo record") photo_object = {'index': self.index.value, 'record': rec} if bool(self.return_full_colour.value): - if self.debug: print( + logger.debug( 'generating greyscale copy at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) colorphoto = photo if photo is not None: + # ??? if self.fastqueue.value: # photo = downscale(np.mean(photo,2),10) photo = np.mean(photo[::10, ::10, :], 2) else: photo = np.mean(photo, 2) - if self.debug: print( + logger.debug( 'averaging completed at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) photo = photo.astype(np.ubyte) - if self.debug: print( + logger.debug( 'type conversion completed at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) if not self.fastqueue.value: colorphoto = colorphoto.astype(np.ubyte) - if self.debug: print( + logger.debug( 'colour type conv completed at%s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) photo_object['colorimg'] = colorphoto - if self.debug: print( + logger.debug( 'greyscale copy completed at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) else: if photo is not None: @@ -191,12 +206,14 @@ def worker(self): camidstr, triggertime_string, self.label.value.decode('utf-8'), self.index.value) photo_object['filename'] = filename self.message_queue.put("Saved Photo: %s" % filename) - if self.debug: print( - 'starting save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) - pickle.dump(photo_object, open(filename, 'wb')) - if self.debug: print( - 'finished save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) - if self.info: print("Saved photo as %s" % filename) + if self.debug: + print('starting save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) + with open(filename, 'wb') as file: + pickle.dump(photo_object, file) + if self.debug: + print('finished save at %s' % (datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f"))) + if self.info: + print("Saved photo as %s" % filename) else: filename = 'photo_object_%s_%s.np' % ( camidstr, datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S.%f")) @@ -206,7 +223,7 @@ def worker(self): self.message_queue.put("Saved Photo: %s" % filename) # np.save(open('photo_%04i.np' % self.index.value,'wb'),photo.astype(np.ubyte)) print("Incrementing camera index, from %d" % self.index.value) - self.index.value = self.index.value + 1 + self.index.value += 1 def close(self): """ diff --git a/bee_track/camera_aravis.py b/bee_track/camera_aravis.py index 206b5a7..23abcd7 100644 --- a/bee_track/camera_aravis.py +++ b/bee_track/camera_aravis.py @@ -1,16 +1,14 @@ +""" +This module contains the Aravis Camera class. +""" + import logging -import sys -import time -import pickle -import ctypes -import threading import gc import os -from datetime import datetime as dt -from multiprocessing import Queue import numpy as np # TODO what's this gi.repository? +# PyGObject https://gnome.pages.gitlab.gnome.org/pygobject/ from gi.repository import Aravis from bee_track.camera import Camera @@ -20,7 +18,9 @@ class AravisCamera(Camera): """ - TODO + Aravis Camera interface + + https://lazka.github.io/pgi-docs/Aravis-0.8/ """ @classmethod @@ -33,7 +33,7 @@ def update_device_list(cls): Aravis.update_device_list() @classmethod - def count_devices(cls) -> int: + def get_n_devices(cls) -> int: """ Retrieves the number of currently online devices. """ @@ -45,15 +45,13 @@ def count_devices(cls) -> int: return n_cams - @staticmethod - def get_camera_ids() -> list[str]: + @classmethod + def get_device_ids(cls) -> list[str]: """ Get camera identifiers """ - # TODO convert to generator? (how long does get_device_id take to run?) - ids = list() - for i in range(n_cams): + for i in range(cls.get_n_devices()): # https://lazka.github.io/pgi-docs/Aravis-0.8/functions.html#Aravis.get_device_id dev_id: str = Aravis.get_device_id(i) logger.info("Found camera: %s" % dev_id) @@ -64,6 +62,7 @@ def setup_camera(self): print("PROCESS ID: ", os.getpid()) os.system("sudo chrt -f -p 1 %d" % os.getpid()) Aravis.enable_interface("Fake") + # https://lazka.github.io/pgi-docs/Aravis-0.8/classes/Camera.html self.aravis_camera = Aravis.Camera.new(self.cam_id) self.width = int(2048) self.height = int(1536) @@ -194,39 +193,51 @@ def setup_camera(self): print("Ready") def camera_config_worker(self): + """ + Listen for new settings/options in the camera configuration queue and set them on the device. + """ while True: + # Wait for a new configuration option to arrive config = self.config_camera_queue.get() - print("Got:") - print(config) - if config[0] == 'exposure': - self.aravis_camera.set_exposure_time(config[1]) - if config[0] == 'delay': - aravis_device = self.aravis_camera.get_device() - aravis_device.set_integer_feature_value("StrobeLineDelay", config[1]) - if config[0] == 'predelay': - aravis_device = self.aravis_camera.get_device() - aravis_device.set_integer_feature_value("StrobeLinePreDelay", config[1]) + print("Got:", config) + key = config[0] + value = config[1] + match key: + case 'exposure': + self.aravis_camera.set_exposure_time(value) + case 'delay': + aravis_device = self.aravis_camera.get_device() + aravis_device.set_integer_feature_value("StrobeLineDelay", value) + case 'predelay': + aravis_device = self.aravis_camera.get_device() + aravis_device.set_integer_feature_value("StrobeLinePreDelay", value) + case _: + raise ValueError(config) def camera_trigger(self): + """ + Software camera trigger + """ + while True: - if self.debug: print("WAITING FOR TRIGGER") + if self.debug: + print("WAITING FOR TRIGGER") + # Wait for software trigger self.cam_trigger.wait() - if self.debug: print("Software Trigger...") + if self.debug: + print("Software Trigger...") self.aravis_camera.software_trigger() self.cam_trigger.clear() - def get_photo(self, getraw=False): - if self.debug: print(self.cam_id, self.stream.get_n_buffers()) - if self.debug: print(self.cam_id, "waiting for photo...") + def get_photo(self, get_raw: bool = False): + if self.debug: + print(self.cam_id, self.stream.get_n_buffers()) + print(self.cam_id, "waiting for photo...") buffer = self.stream.pop_buffer() - # buffer = None - # while buffer is None: - # print("...") - # time.sleep(np.random.rand()*1) #wait between 0 and 1 second - # buffer = self.stream.timeout_pop_buffer(1000) #blocking for 1ms + if self.debug: + print(self.cam_id, "got buffer...") - if self.debug: print(self.cam_id, "got buffer...") if buffer is None: self.message_queue.put(self.cam_id + " Buffer read failed") print(self.cam_id, "buffer read failed") @@ -242,17 +253,20 @@ def get_photo(self, getraw=False): return None print("Stream statistics") print(self.stream.get_statistics()) - if self.debug: print(self.cam_id, "buffer ok") - if getraw: - raw = np.frombuffer(buffer.get_data(), dtype=np.uint8) # no type conversion! - else: - raw = np.frombuffer(buffer.get_data(), dtype=np.uint8).astype(float) + if self.debug: + print(self.cam_id, "buffer ok") + + # Get image data + raw = np.frombuffer(buffer.get_data(), dtype=np.uint8) + if not get_raw: + raw = raw.astype(float) self.stream.push_buffer(buffer) if bool(self.return_full_colour.value): print(">>>") - return np.reshape(raw, [self.height, self.width, 3]) + new_shape = [self.height, self.width, 3] else: - return np.reshape(raw, [self.height, self.width]) + new_shape = [self.height, self.width] + return np.reshape(raw, new_shape) def close(self): """ diff --git a/bee_track/configurable.py b/bee_track/configurable.py index 36d0e09..f4a5194 100644 --- a/bee_track/configurable.py +++ b/bee_track/configurable.py @@ -1,4 +1,5 @@ import multiprocessing.sharedctypes +import threading class Configurable: diff --git a/bee_track/core.py b/bee_track/core.py index 4ffe2cd..244f917 100644 --- a/bee_track/core.py +++ b/bee_track/core.py @@ -15,6 +15,7 @@ from glob import glob from datetime import datetime as dt from typing import Optional +import threading import psutil import flask @@ -80,7 +81,8 @@ def setdatetime(timestring): def addtoconfigvals(component, field, value): try: - configvals = pickle.load(open('configvals.pkl', 'rb')) + with open('configvals.pkl', 'rb') as file: + configvals = pickle.load(file) if component not in configvals: configvals[component] = {} # if field not in configvals[component]: configvals[component][field] = value @@ -115,12 +117,16 @@ def setfromconfigvals(): Set app config values based on saved values. """ try: + # Load configuration file with open('configvals.pkl', 'rb') as file: configvals = pickle.load(file) app.logger.info("Loaded %s", file.name) + + # Set options for the components for component, fields in configvals.items(): for field, value in fields.items(): set(component, field, value) + # TODO don't ignore all errors except: # FileNotFoundError: pass @@ -145,8 +151,11 @@ def getdiskfree(): @app.route('/getbattery') def getbattery(): + """ + Log battery info to file + """ batstr = read_batteries() - # Log battery info to file + # Append to text file with open("battery_status.txt", "a") as battery: battery.write(batstr) return batstr @@ -257,10 +266,11 @@ def startup(): multiprocessing.Process(target=trigger.worker).start() # Initialise cameras: get list of Aravis cameras - cam_ids = AravisCamera.get_camera_ids() + cam_ids = AravisCamera.get_device_ids() if not cam_ids: raise bee_track.camera.NoCamerasFoundError - # Iterate over available cameras ands tart a process for each one + + # Iterate over available cameras and start a process for each one for cam_id in cam_ids: app.logger.info("Camera %s", cam_id) camera = AravisCamera(message_queue, trigger.record, cam_trigger, cam_id=cam_id) @@ -297,19 +307,17 @@ def start(): """ Start camera data capture. """ - # TODO what is the index? - nextindex = max([camera.index.value for camera in cameras] + [trigger.index.value]) + # Find the highest value of the index for any camera or the trigger + next_index = max([camera.index.value for camera in cameras] + [trigger.index.value]) - # reset indicies + # Set all camera indices and the trigger index to that value for camera in cameras: - camera.index.value = nextindex - trigger.index.value = nextindex - - # for camera in cameras: - # app.logger.info(camera.index.value) - # app.logger.info(trigger.index.value) + camera.index.value = next_index + trigger.index.value = next_index + # Enable the trigger event trigger.run.set() + return "Collection Started" @@ -318,6 +326,7 @@ def stop(): """ Stop camera data capture """ + # Disable the trigger event trigger.run.clear() return "Collection Stopped" @@ -396,27 +405,20 @@ def zip(): return "Zipping Started" -from threading import Thread -from time import sleep - - def threaded_function(): - while (True): + # TODO this should probably be a separate service, not part of the web app + while True: app.logger.info("running auto zip") # zip() #disabled - sleep(600) + raise NotImplementedError + time.sleep(600) -thread = Thread(target=threaded_function) +# TODO what's this for ??? +thread = threading.Thread(target=threaded_function) thread.start() -# import threading -# ticker = threading.Event() -# while not ticker.wait(600): -# app.logger.info("AUTO ZIP") -# zip() - @app.route('/update') def update(): # res = "" @@ -454,10 +456,13 @@ def lowresmaximg(img, blocksize=10): @app.route('/getimagecount') def getimagecount(): + """ + Get the total number of images. + """ try: - return str(cameras[0].index.value - 1) # gets index of current image... - # return str(cameras[0].photo_queue.len()) - except Empty: + # Get index of current image + return str(cameras[0].index.value - 1) + except IndexError: return "No items" @@ -467,7 +472,7 @@ def getimagewithindex(photo_queue, idx): if item is None: continue if item['index'] == idx: return item - return None + return @app.route('/getimage//') @@ -496,7 +501,7 @@ def getimage(number, camera_id=0): track['y'] = int(track['y']) newtracklist.append(track) else: - newtracklist = [] + newtracklist = list() return jsonify( {'index': photoitem['index'], 'photo': img.tolist(), 'record': photoitem['record'], 'track': newtracklist}) @@ -567,6 +572,7 @@ def getimagecentre(number: int, camera_id: int = 0): def main(): startup() + # Listen on all incoming addresses app.run(host="0.0.0.0") diff --git a/bee_track/rotate.py b/bee_track/rotate.py index 4a947be..348cb9d 100644 --- a/bee_track/rotate.py +++ b/bee_track/rotate.py @@ -1,4 +1,3 @@ -import datetime import multiprocessing import time @@ -11,7 +10,7 @@ class Rotate(Configurable): """ - This is a worker that sends a rotation signal via GPIO pins. + This is a worker that sends a rotation signal to a stepper motor via GPIO pins. """ def __init__(self, message_queue): diff --git a/bee_track/tracking.py b/bee_track/tracking.py index e243b2e..e8d0352 100644 --- a/bee_track/tracking.py +++ b/bee_track/tracking.py @@ -93,7 +93,7 @@ def __init__(self, img, tagloc): class Tracking(Configurable): """ - TODO + Takes images from the greyscale camera's image queue (cam.photo_queue) and looks for the retro-reflector tag. """ def __init__(self, message_queue, photo_queue): diff --git a/bee_track/trigger.py b/bee_track/trigger.py index f6d37c5..4bd6362 100644 --- a/bee_track/trigger.py +++ b/bee_track/trigger.py @@ -1,6 +1,7 @@ import logging import time import datetime +from enum import Enum import multiprocessing import RPi.GPIO as GPIO @@ -10,112 +11,234 @@ logger = logging.getLogger(__name__) +class FlashSequence(Enum): + """ + Flash sequence options + """ + ALL = 0 + "Fire every flash" + TWO_AT_A_TIME = 1 + "Fire two flashes at once" + IN_SEQUENCE = 2 + "Flash all the flashes sequentially" + NONE = 9 + "Disable all flashes" + + class Trigger(Configurable): """ - Send a signal to the camera to take an exposure. + A worker to handle triggering the GPIO pins, etc. + + E.g. Send a signal to the camera to take an exposure. + + Raspberry Pi GPIO module basics + https://sourceforge.net/p/raspberry-gpio-python/wiki/BasicUsage/ """ - def __init__(self, message_queue, cam_trigger, t=2.0): + def __init__(self, message_queue, cam_trigger: multiprocessing.Event, t: float = 2.0): super().__init__(message_queue) print("Initialising Trigger Control") self.cam_trigger = cam_trigger + "Camera exposure event" self.manager = multiprocessing.Manager() - self.flashselection = self.manager.list() + self.flashselection: list = self.manager.list() self.index = multiprocessing.Value('i', 0) - self.record = self.manager.list() + "Incrementing identifier number per trigger event" + self.record: list = self.manager.list() + "Camera exposure trigger event log" self.direction = 0 self.flash_select_pins = [14, 15, 18, 23] # [8,10,12,16] #Board->BCM pins + "Custom flash selection" self.trigger_pin = 24 # 18 #Board->BCM pins + # ??? self.flashselection.append(0) self.flashselection.append(1) self.flashselection.append(2) self.flashselection.append(3) self.t = multiprocessing.Value('d', t) + "Time interval/seconds" self.ds = multiprocessing.Value('d', 0) - self.flashseq = multiprocessing.Value('i', 0) # 0 = flash all, 1 = flash 2 at a time, 1 = flash in sequence, - self.skipnoflashes = multiprocessing.Value('i', 0) # how many to skip + "Delayed start (proportion of time interval)" + self.flashseq = multiprocessing.Value('i', FlashSequence.ALL) + "Flash sequence" + self.skipnoflashes = multiprocessing.Value('i', 0) + "How many flashes to skip???" self.preptime = 0.02 + "Preparation time/seconds" self.triggertime = 0.03 # this will end up at least 200us + "Trigger exposure time" self.seqn = 0 - GPIO.setmode(GPIO.BCM) # GPIO.BOARD) #Board->BCM + "Flash sequence position tracker" + self.set_up_gpio() + self.run = multiprocessing.Event() + "Trigger activation flag" + self.mode = GPIO.BCM # GPIO.BOARD, GPIO.BCM or None + "Pin numbering mode. See: https://sourceforge.net/p/raspberry-gpio-python/wiki/BasicUsage/" + + def set_up_gpio(self): + """ + Set up GPIO + + https://sourceforge.net/p/raspberry-gpio-python/wiki/BasicUsage/ + """ + + # Set GPIO pin numbering mode + GPIO.setmode(self.mode) + + # Set up pins as outputs (GPIO.OUT is output) GPIO.setup(self.trigger_pin, GPIO.OUT) for pin in self.flash_select_pins: GPIO.setup(pin, GPIO.OUT) + time.sleep(0.5) + + # Disable those output pins + self.deactivate_flashes() + self.deactivate() + + print("Running") + + @property + def flash_count(self) -> int: + """ + The number of flashes + """ + return len(self.flashselection) + + def activate_flashes(self): + """ + Activate the GPIO pins to configure the flashes that should fire + """ + + # All flashes + if self.flashseq.value == FlashSequence.ALL: + for flash in self.flashselection: + GPIO.output(self.flash_select_pins[flash], True) + + # Two at a time + if self.flashseq.value == FlashSequence.TWO_AT_A_TIME: + # Activate two flashes based on our position in the sequence + GPIO.output(self.flash_select_pins[self.flashselection[self.seqn]], True) + GPIO.output(self.flash_select_pins[self.flashselection[self.seqn + 1]], True) + + # Loop through the flash sequence + self.seqn += 2 + if self.seqn >= self.flash_count: + self.seqn = 0 + + # Flash in sequence + if self.flashseq.value == FlashSequence.IN_SEQUENCE: + GPIO.output(self.flash_select_pins[self.flashselection[self.seqn]], True) + + # Loop through the flash sequence + self.seqn += 1 + if self.seqn >= self.flash_count: + self.seqn = 0 + + # Custom flash selection + if self.flashseq.value == FlashSequence.NONE: + self.deactivate_flashes() + + def deactivate_flashes(self): + """ + Deactivate all flash GPIO pins + """ for pin in self.flash_select_pins: GPIO.output(pin, False) - GPIO.output(self.trigger_pin, False) - print("Running") - self.run = multiprocessing.Event() - def trigger_camera(self, fireflash: bool, endofset: bool): - """ - Send trigger to camera (and flash) - - fireflash = boolean: true=fire flash - endofset = boolean: whether this is the last photo of a set (this will then tell the tracking system to look for the bee). - """ - logger.debug("Photo: Flash" if fireflash else "Photo: No Flash") - - if fireflash: - if self.flashseq.value == 0: - for flash in self.flashselection: - GPIO.output(self.flash_select_pins[flash], True) - if self.flashseq.value == 1: - GPIO.output(self.flash_select_pins[self.flashselection[self.seqn]], True) - GPIO.output(self.flash_select_pins[self.flashselection[self.seqn + 1]], True) - self.seqn += 2 - if self.seqn >= len(self.flashselection): - self.seqn = 0 - if self.flashseq.value == 2: - GPIO.output(self.flash_select_pins[self.flashselection[self.seqn]], True) - self.seqn += 1 - if self.seqn >= len(self.flashselection): - self.seqn = 0 - if self.flashseq.value == 9: - for pin in self.flash_select_pins: - GPIO.output(pin, False) + def trigger_camera(self, fire_flash: bool, end_of_set: bool): + """ + Send trigger to camera (and flash, optionally) to take a photograph exposure. + + @param fire_flash: Whether to activate the flashes + @param end_of_set: whether this is the last photo of a set (tells the tracking system to look for the bee) + """ + logger.debug("Photo: Flash" if fire_flash else "Photo: No Flash") + + # Activate flash + if fire_flash: + self.activate_flashes() else: - for pin in self.flash_select_pins: - GPIO.output(pin, False) + # Deactivate all flash GPIO pins + self.deactivate_flashes() + + # Wait for preparation time time.sleep(self.preptime) - triggertime = time.time() # TODO Why are these two different? - triggertimestring = datetime.datetime.now() # need to convert to string later - triggertimestring = triggertimestring.strftime("%Y%m%d_%H:%M:%S.%f") - self.record.append( - {'index': self.index.value, 'endofset': endofset, 'direction': self.direction, 'flash': fireflash, - 'flashselection': list(self.flashselection), 'triggertime': triggertime, - 'triggertimestring': triggertimestring}) + # Get current timestamp + triggertime = time.time() # TODO Why are these two different? + triggertime_datetime = datetime.datetime.now() # need to convert to string later + # triggertime = triggertime_datetime.timestamp() + triggertimestring = triggertime_datetime.strftime("%Y%m%d_%H:%M:%S.%f") + + # Log the photo capture + record = { + 'index': self.index.value, 'endofset': end_of_set, 'direction': self.direction, 'flash': fire_flash, + 'flashselection': list(self.flashselection), 'triggertime': triggertime, + 'triggertimestring': triggertimestring + } + self.record.append(record) + + # Increment exposure count print("Incrementing trigger index from %d" % self.index.value) - self.index.value = self.index.value + 1 + self.index.value += 1 # Software trigger... + # This communicates to the software camera interface # self.cam_trigger.set() # Trigger via pin... - GPIO.output(self.trigger_pin, True) - + self.activate() time.sleep(self.triggertime) - for pin in self.flash_select_pins: - GPIO.output(pin, False) - # (trigger via pin)... + # Deactivate + self.deactivate_flashes() + self.deactivate() + + def activate(self): + """ + Turn on the trigger GPIO pin + """ + GPIO.output(self.trigger_pin, True) + + def deactivate(self): + """ + Turn off the trigger GPIO pin + """ GPIO.output(self.trigger_pin, False) def worker(self): + """ + Tell the camera to take exposures when the trigger flag is activated. + """ + + # ??? skipcount = 0 + + # Loop until the run event is deactivated (cleared) while True: self.run.wait() + + # Delayed start delaystart = self.ds.value * self.t.value time.sleep(delaystart) + + # Increment skip counter skipcount += 1 - skipnoflashphoto = (skipcount <= self.skipnoflashes.value) - self.trigger_camera(True, skipnoflashphoto) + # ??? + skipnoflashphoto = skipcount <= self.skipnoflashes.value + + # Capture data + self.trigger_camera(fire_flash=True, end_of_set=skipnoflashphoto) + + # Is something different happening when we reach the end of a set? What's a set? + # ??? if not skipnoflashphoto: - self.trigger_camera(False, True) + self.trigger_camera(fire_flash=False, end_of_set=True) skipcount = 0 + # Wait for ??? time.sleep(self.t.value - self.triggertime * 2 - self.preptime * 2 - delaystart) else: + # Wait for ??? time.sleep(self.t.value - self.triggertime - self.preptime - delaystart) diff --git a/startup b/startup index 7b06a35..0b9dd1d 100755 --- a/startup +++ b/startup @@ -2,7 +2,7 @@ sleep 20 export GI_TYPELIB_PATH=$GI_TYPELIB_PATH:/home/pi/aravis/build/src export LD_LIBRARY_PATH=/home/pi/aravis/build/src cd /home/pi/bee_track/webinterface -python3 -m http.server & +python3 -m http.server 8000 & sudo sysctl -w net.core.rmem_max=67108864 net.core.rmem_default=67108864 sudo sysctl -w net.ipv4.route.flush=1 diff --git a/webinterface/track.js b/webinterface/track.js index 0a71c41..0de48b1 100644 --- a/webinterface/track.js +++ b/webinterface/track.js @@ -12,16 +12,20 @@ function getdatestring() { return st } +// Function to display a message in the console and scroll to the bottom function msg(message) { + // Ensure the message ends with a new-line character if (message.slice(-1) != '\n') { message = message + "\n"; } - $("#console").val($("#console").val() + message);// + "\n"); + // Append message to console + $("#console").val($("#console").val() + message); + + // Scroll the console to the bottom to display the latest message var con = $('#console'); con.scrollTop(con[0].scrollHeight - con.height()); } - image = 0 imagecount = 0 $("input#url").val(window.location.hostname + ":5000") @@ -37,6 +41,7 @@ $.ajax({ }); $('button#start').click(function () { msg('Starting...'); + // Construct the URL of the API endpoint url = "http://" + $('input#url').val() + "/start"; $.ajax({ url: url, @@ -74,7 +79,9 @@ $('button#imagedownx10').click(function () { image = image - 10; }) +// Regular status updates +// Get free disk space on tracker setInterval(function () { url = "http://" + $('input#url').val() + "/getdiskfree"; $.ajax({ @@ -85,6 +92,7 @@ setInterval(function () { }); }, 60000); +// Get battery level setInterval(function () { url = "http://" + $('input#url').val() + "/getbattery"; $.ajax({ @@ -102,7 +110,6 @@ setInterval(function () { setInterval(function () { - url = "http://" + $('input#url').val() + "/getimagecount"; $.ajax({ url: url,