Skip to content

Commit

Permalink
Refactor the code for better scalability.
Browse files Browse the repository at this point in the history
  • Loading branch information
grapeot committed Oct 3, 2021
1 parent 56f8a79 commit afe921a
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 207 deletions.
145 changes: 145 additions & 0 deletions ZWOCamera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from threading import Condition, Thread
from os import system
from time import sleep, time
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
import zwoasi as asi

SDK_PATH = 'ASI_linux_mac_SDK_V1.20/lib/armv7/libASICamera2.so'
asi.init(SDK_PATH)


# The worker thread that does the heavy lifting
class ZWOCamera(Thread):
def __init__(self, output_stream, latest_stream, logger, interval=0):
super(CameraCapture, self).__init__()
self.terminate = False
self.interval = interval
self.last_gain = 0
self.last_exposure = 0
self.server = None # Optional hook for updating the server
self.logger = logger
self.initialize_camera()

self.logger.info('Camera initialization complete.')
self.stream = output_stream
self.latest_stream = latest_stream
self.start()

def initialize_camera(self):
self.logger.info('Initializing camera...')
sleep(2)
num_cameras = asi.get_num_cameras()
if num_cameras == 0:
raise RuntimeError('No ZWO camera was detected.')
try:
cameras_found = asi.list_cameras()
self.camera = asi.Camera(0)
except Exception as e:
# When the power is stable, this case is usually not recoverable except restart
self.logger.error(e)
self.logger.error("About to retry once")
try:
self.camera = asi.Camera(0)
except Exception as e:
self.logger.error(e)
self.logger.error("Still failed. About to restart in 60 seconds.")
sleep(60)
self.logger.error("About to restart now.")
system("reboot now")

camera_info = self.camera.get_camera_property()
self.logger.info(camera_info)
controls = self.camera.get_controls()
self.logger.info(controls)

self.camera.set_image_type(asi.ASI_IMG_RAW8)

self.camera.set_control_value(asi.ASI_BANDWIDTHOVERLOAD,
self.camera.get_controls()['BandWidth']['DefaultValue'],
auto=True)

# Set auto exposure value
self.whbi = self.camera.get_roi_format()
self.camera.auto_wb()
# Uncomment to enable manual white balance
# self.camera.set_control_value(asi.ASI_WB_B, 99)
# self.camera.set_control_value(asi.ASI_WB_R, 75)
self.camera.set_control_value(asi.ASI_AUTO_MAX_GAIN, 425)
self.camera.set_control_value(asi.ASI_AUTO_MAX_BRIGHTNESS, 130)
self.camera.set_control_value(asi.ASI_EXPOSURE,
100000,
auto=True)
self.camera.set_control_value(asi.ASI_GAIN,
0,
auto=True)
self.camera.set_control_value(controls['AutoExpMaxExpMS']['ControlType'], 3000)
# Uncomment to enable flip
# self.camera.set_control_value(asi.ASI_FLIP, 3)
self.camera.start_video_capture()

def run(self):
self.logger.info('Start capturing...')
last_timestamp = 0
try:
while not self.terminate:
if time() < last_timestamp + self.interval:
sleep(0.1)
continue
last_timestamp = time()
# self.logger.debug('About to take photo.')
settings = self.camera.get_control_values()
self.logger.debug('Gain {gain:d} Exposure: {exposure:f}'.format(gain=settings['Gain'],
exposure=settings['Exposure']))
self.last_gain = settings['Gain']
self.last_exposure = settings['Exposure']
try:
img = self.camera.capture_video_frame(timeout=max(1000, 500 + 2 * settings['Exposure'] / 1000))
if self.server is not None:
self.server.last_update_timestamp = time()
except Exception as e:
self.logger.error(e)
self.camera.stop_exposure()
self.camera.stop_video_capture()
self.camera.close()
self.initialize_camera()
# Set the exposure and gain to the last known good value to reduce the auto exposure time
self.camera.set_control_value(asi.ASI_EXPOSURE,
self.last_exposure,
auto=True)
self.camera.set_control_value(asi.ASI_GAIN,
self.last_gain,
auto=True)
continue
# convert the numpy array to PIL image
mode = None
if len(img.shape) == 3:
img = img[:, :, ::-1] # Convert BGR to RGB
if self.whbi[3] == asi.ASI_IMG_RAW16:
mode = 'I;16'
image = Image.fromarray(img, mode=mode)
# Add some annotation
draw = ImageDraw.Draw(image)
pstring = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
draw.text((15, 15), pstring, fill='black')
# Write to the stream
image.save(self.stream, format='jpeg', quality=90)
image.save(self.latest_stream, format='jpeg', quality=90)
# Adaptive auto exposure. 7am-7pm => 120, 7pm-7am => 180.
# Do this only once in an hour
now = datetime.now()
if now.minute == 0 and now.second <= 1:
if 7 <= datetime.now().hour <= 18:
self.camera.set_control_value(asi.ASI_AUTO_MAX_BRIGHTNESS, 100)
# Too bright. Reduce gain and favor longer exposure
self.camera.set_control_value(asi.ASI_GAIN,
0,
auto=True)
self.camera.set_control_value(asi.ASI_EXPOSURE,
100000,
auto=True)
else:
self.camera.set_control_value(asi.ASI_AUTO_MAX_BRIGHTNESS, 160)
finally:
self.camera.stop_video_capture()
self.camera.stop_exposure()
213 changes: 6 additions & 207 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from datetime import datetime
from fractions import Fraction
from os.path import join, exists
from os import mkdir
from os import mkdir, system
from threading import Condition, Thread
from http import server
from Streaming import StreamingOutput, StreamingServer, StreamingHandler
from queue import Queue
from streaming import StreamingOutput, StreamingServer, StreamingHandler
from copy import deepcopy
from sys import exit
from utils import NetworkChecker
import socketserver
import json
import requests
Expand All @@ -22,6 +22,7 @@
import sys
import numpy as np
import zwoasi as asi
from ZWOCamera import ZWOCamera

# Set up logging
logger = logging.getLogger(__name__)
Expand All @@ -36,213 +37,11 @@
logger.addHandler(consoleHandler)
logger.addHandler(fileHandler)

SDK_PATH = 'ASI_linux_mac_SDK_V1.20/lib/armv7/libASICamera2.so'
asi.init(SDK_PATH)


# The worker thread to save the files in the backend
class FileSaver(Thread):
def __init__(self):
super(FileSaver, self).__init__()
self.q = Queue()
self.start()

def run(self):
while True:
img, fn = self.q.get()
try:
img.save(fn, 'JPEG', quality=90)
logger.info('Saved img to {}.'.format(fn))
except:
logger.warning('Saving file failed.')
finally:
del img


# Checks Internet connection, and will restart the network service if it cannot access a host.
class NetworkChecker(Thread):
def __init__(self):
super(NetworkChecker, self).__init__()
self.last_check_timestamp = 0
self.error_count = 0
self.has_tried_networking = False
self.terminate = False
# By default, it checks a connection with a timeout of 10 seconds, checks once 30 seconds.
# So in the case of network connection lost, it will take 400 seconds to respond.
self.CHECK_TIMEOUT = 10
self.CHECK_INTERVAL = 30
self.CHECK_URL = 'https://bing.com/'
self.MAX_ERROR_COUNT = 10
self.start()

def run(self):
logger.info('Network checker launches.')
while not self.terminate:
try:
if time() < self.last_check_timestamp + self.CHECK_INTERVAL:
sleep(0.1)
continue
self.last_check_timestamp = time()
requests.get(self.CHECK_URL, timeout=self.CHECK_TIMEOUT)
self.error_count = 0
self.has_tried_networking = False
logger.debug('Check {} succeeded.'.format(self.CHECK_URL))
except Exception as e:
self.error_count += 1
logger.error(e)
logger.error('NetworkChecker error count = {}'.format(self.error_count))
if self.error_count == 10:
if not self.has_tried_networking:
# First try to restart the networking service
logger.error('About to restart the networking service.')
system('service networking restart')
else:
logger.error("Still failed. About to restart in 60 seconds.")
sleep(60)
logger.error("About to restart now.")
system("reboot now")


# The worker thread that does the heavy lifting
class CameraCapture(Thread):
def __init__(self, output_stream, latest_stream, interval=0):
super(CameraCapture, self).__init__()
self.terminate = False
self.interval = interval
self.last_gain = 0
self.last_exposure = 0
self.server = None # Optional hook for updating the server
self.initialize_camera()

logger.info('Camera initialization complete.')
self.stream = output_stream
self.latest_stream = latest_stream
self.start()

def initialize_camera(self):
logger.info('Initializing camera...')
sleep(2)
num_cameras = asi.get_num_cameras()
if num_cameras == 0:
raise RuntimeError('No ZWO camera was detected.')
try:
cameras_found = asi.list_cameras()
self.camera = asi.Camera(0)
except Exception as e:
# When the power is stable, this case is usually not recoverable except restart
logger.error(e)
logger.error("About to retry once")
try:
self.camera = asi.Camera(0)
except Exception as e:
logger.error(e)
logger.error("Still failed. About to restart in 60 seconds.")
sleep(60)
logger.error("About to restart now.")
system("reboot now")

camera_info = self.camera.get_camera_property()
logger.info(camera_info)
controls = self.camera.get_controls()
logger.info(controls)

self.camera.set_image_type(asi.ASI_IMG_RAW8)

self.camera.set_control_value(asi.ASI_BANDWIDTHOVERLOAD,
self.camera.get_controls()['BandWidth']['DefaultValue'],
auto=True)

# Set auto exposure value
self.whbi = self.camera.get_roi_format()
self.camera.auto_wb()
# Uncomment to enable manual white balance
# self.camera.set_control_value(asi.ASI_WB_B, 99)
# self.camera.set_control_value(asi.ASI_WB_R, 75)
self.camera.set_control_value(asi.ASI_AUTO_MAX_GAIN, 425)
self.camera.set_control_value(asi.ASI_AUTO_MAX_BRIGHTNESS, 130)
self.camera.set_control_value(asi.ASI_EXPOSURE,
100000,
auto=True)
self.camera.set_control_value(asi.ASI_GAIN,
0,
auto=True)
self.camera.set_control_value(controls['AutoExpMaxExpMS']['ControlType'], 3000)
# Uncomment to enable flip
# self.camera.set_control_value(asi.ASI_FLIP, 3)
self.camera.start_video_capture()

def run(self):
logger.info('Start capturing...')
last_timestamp = 0
try:
while not self.terminate:
if time() < last_timestamp + self.interval:
sleep(0.1)
continue
last_timestamp = time()
#logger.debug('About to take photo.')
settings = self.camera.get_control_values()
logger.debug('Gain {gain:d} Exposure: {exposure:f}'.format(gain=settings['Gain'],
exposure=settings['Exposure']))
self.last_gain = settings['Gain']
self.last_exposure = settings['Exposure']
try:
img = self.camera.capture_video_frame(timeout=max(1000, 500 + 2 * settings['Exposure'] / 1000))
if self.server is not None:
self.server.last_update_timestamp = time()
except Exception as e:
logger.error(e)
self.camera.stop_exposure()
self.camera.stop_video_capture()
self.camera.close()
self.initialize_camera()
# Set the exposure and gain to the last known good value to reduce the auto exposure time
self.camera.set_control_value(asi.ASI_EXPOSURE,
self.last_exposure,
auto=True)
self.camera.set_control_value(asi.ASI_GAIN,
self.last_gain,
auto=True)
continue
error_count = 0
# convert the numpy array to PIL image
mode = None
if len(img.shape) == 3:
img = img[:, :, ::-1] # Convert BGR to RGB
if self.whbi[3] == asi.ASI_IMG_RAW16:
mode = 'I;16'
image = Image.fromarray(img, mode=mode)
# Add some annotation
draw = ImageDraw.Draw(image)
pstring = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
draw.text((15, 15), pstring, fill='black')
# Write to the stream
image.save(self.stream, format='jpeg', quality=90)
image.save(self.latest_stream, format='jpeg', quality=90)
# Adaptive auto exposure. 7am-7pm => 120, 7pm-7am => 180.
# Do this only once in an hour
now = datetime.now()
if now.minute == 0 and now.second <= 1:
if 7 <= datetime.now().hour <= 18:
self.camera.set_control_value(asi.ASI_AUTO_MAX_BRIGHTNESS, 100)
# Too bright. Reduce gain and favor longer exposure
self.camera.set_control_value(asi.ASI_GAIN,
0,
auto=True)
self.camera.set_control_value(asi.ASI_EXPOSURE,
100000,
auto=True)
else:
self.camera.set_control_value(asi.ASI_AUTO_MAX_BRIGHTNESS, 160)
finally:
self.camera.stop_video_capture()
self.camera.stop_exposure()

if __name__ == '__main__':
stream_output = StreamingOutput()
latest_output = StreamingOutput()
network_checker = NetworkChecker()
thread = CameraCapture(stream_output, latest_output, 1)
network_checker = NetworkChecker(logger)
thread = ZWOCamera(stream_output, latest_output, logger, 1)
try:
address = ('', 8000)
server = StreamingServer(address, StreamingHandler, stream_output, latest_output)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
zwoasi
picamera
File renamed without changes.
Loading

0 comments on commit afe921a

Please sign in to comment.