Compare commits

...
Sign in to create a new pull request.

12 commits

12 changed files with 149 additions and 50 deletions

View file

@ -3,7 +3,6 @@ from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate from flask_migrate import Migrate
from phytopi.config import Config from phytopi.config import Config
# from phytopi.camera.camera_pi import Camera
app = Flask(__name__, instance_relative_config=True) app = Flask(__name__, instance_relative_config=True)
@ -11,6 +10,11 @@ app.config.from_object(Config)
db = SQLAlchemy(app) db = SQLAlchemy(app)
migrate = Migrate(app, db) migrate = Migrate(app, db)
# camera = Camera(app=app)
from phytopi.errors import bp as errors_bp
from phytopi.camera import bp as camera_bp
app.register_blueprint(errors_bp)
app.register_blueprint(camera_bp)
from phytopi import routes, models from phytopi import routes, models

View file

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('camera', __name__)
from phytopi.camera import camera

90
phytopi/camera/camera.py Normal file
View file

@ -0,0 +1,90 @@
import io
import os
import sys
import stat
import subprocess
import signal
from time import sleep
from enum import Enum
from PIL import Image
from phytopi import db, models
from phytopi.models import CameraSettings
class CameraActionError(Exception):
pass
class CameraStatus(Enum):
STOPPED = 0
IDLE = 1
TIMELAPSE = 2
VIDEO = 3
SHOT = 4
class CameraWorker(object):
def __init__(self, pidfile='/tmp/raspimjpeg.pid', executable='/usr/local/bin/raspimjpeg', fifo='/dev/shm/mjpeg/FIFO'):
self.pidfile = pidfile
self.executable = executable
self.fifo = fifo
self.proc = None
self.pid = None
self.status = CameraStatus.STOPPED
def start(self):
# check if process is already running
if os.path.isfile(self.pidfile):
with open(self.pidfile, 'r') as fh:
self.pid = int(fh.read())
self.status = CameraStatus.IDLE
return
# create FIFO, if not existent
try:
os.mkfifo(self.fifo)
except FileExistsError:
pass
# if not, spawn new process and create pid file
self.proc = subprocess.Popen([self.executable], preexec_fn=os.setsid)
self.pid = self.proc.pid
with open(self.pidfile, 'w') as fh:
fh.write(str(self.pid))
self.status = CameraStatus.IDLE
def stop(self):
os.killpg(os.getpgid(self.pid), signal.SIGTERM)
os.unlink(self.fifo)
os.unlink(self.pidfile)
self.status = CameraStatus.STOPPED
def _send_cmd(self, cmd):
with open(self.fifo, 'w') as fh:
fh.write('{}\n'.format(cmd))
def _send_cmds(self, cmds):
for cmd in cmds:
self._send_cmd(cmd)
# don't flood the fifo
sleep(1)
def start_timelapse(self, interval=300):
if self.status == CameraStatus.IDLE:
cmds = ['tv {}'.format(interval), 'tl 1']
self._send_cmds(cmds)
self.status = CameraStatus.TIMELAPSE
else:
raise CameraActionError('Camera not idle!')
def stop_timelapse(self):
if self.status == CameraStatus.TIMELAPSE:
self._send_cmd('tl 0')
self.status = CameraStatus.IDLE
else:
raise CameraActionError('Camera not in timelapse mode!')
def get_frame(self, thumbnail=False):
output = io.BytesIO()
img = Image.open('/dev/shm/mjpeg/cam.jpg')
img.save(output, format='JPEG')
output.seek(0, 0)
return output.getvalue()

View file

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('errors', __name__)
from phytopi.errors import handlers

View file

@ -1,6 +1,8 @@
from datetime import datetime from datetime import datetime
from phytopi import db from phytopi import db
class Dataset(db.Model): class Dataset(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(64), index=True) title = db.Column(db.String(64), index=True)
@ -29,17 +31,16 @@ class CameraSettings(db.Model):
fps = db.Column(db.Integer, default=5) fps = db.Column(db.Integer, default=5)
width = db.Column(db.Integer, default=3280) width = db.Column(db.Integer, default=3280)
height = db.Column(db.Integer, default=2464) height = db.Column(db.Integer, default=2464)
fix_shutter = db.Column(db.Boolean, default=False) exposure_mode = db.Column(db.String(64), default='auto')
fix_wb = db.Column(db.Boolean, default=False)
def __init__(self, name, iso=100, fps=5, width=3280, height=2464, fix_shutter=False, fix_wb=False): def __init__(self, name, iso=100, fps=5, width=3280, height=2464, exposure_mode='auto'):
self.name = name self.name = name
self.iso = iso self.iso = iso
self.fps = fps self.fps = fps
self.width = width self.width = width
self.height = height self.height = height
self.fix_shutter = fix_shutter self.exposure_mode = exposure_mode
self.fix_wb = fix_wb
def __repr__(self): def __repr__(self):
return '<CameraSettings {}>'.format(self.name) return '<CameraSettings {}>'.format(self.name)

Binary file not shown.

View file

@ -1,6 +1,6 @@
from flask import render_template, Response, flash, jsonify, request, stream_with_context, send_file from flask import render_template, Response, flash, jsonify, request, stream_with_context, send_file
from phytopi.camera.camera_pi import Camera from phytopi.camera.camera import CameraWorker, CameraStatus
from phytopi.forms import CameraSettingsForm from phytopi.forms import CameraSettingsForm
from phytopi import app from phytopi import app
@ -10,46 +10,59 @@ from phytopi.models import Dataset
from io import BytesIO from io import BytesIO
import zipstream import zipstream
camera = Camera(app=app) camera = CameraWorker(app=app)
# camera = app.camera camera.start()
save_frames = False
@app.route('/') @app.route('/')
@app.route('/index') @app.route('/index')
def index(): def index():
content = {'timelapse': save_frames} global camera
if camera.status == CameraStatus.TIMELAPSE:
timelapse = True
else:
timelapse = False
content = {'timelapse': timelapse}
return render_template('index.html', content=content) return render_template('index.html', content=content)
@app.route('/toggle_timelapse', methods=['GET', 'POST']) @app.route('/toggle_timelapse', methods=['GET', 'POST'])
def start_stop_timelapse(): def start_stop_timelapse():
global save_frames # global save_frames
global current_dataset global current_dataset
global camera global camera
if save_frames: # if save_frames:
save_frames = False # save_frames = False
timelapse_interval = None # timelapse_interval = None
btn_text = "Start" # btn_text = "Start"
btn_class = 'btn-primary' # btn_class = 'btn-primary'
camera.current_dataset = None # camera.current_dataset = None
camera.last_saved = None # camera.last_saved = None
print(" > switched off timelapse mode") # print(" > switched off timelapse mode")
else: # else:
save_frames = True # save_frames = True
timelapse_interval = 1200 # timelapse_interval = 1200
# btn_text = "Stop"
# btn_class = 'btn-danger'
# dataset = Dataset()
# db.session.add(dataset)
# db.session.commit()
# camera.current_dataset = dataset.id
# print(" > switched on timelapse mode")
# camera.set_timelapse_interval(timelapse_interval)
if camera.status == CameraStatus.IDLE:
btn_text = "Stop" btn_text = "Stop"
btn_class = 'btn-danger' btn_class = 'btn-danger'
dataset = Dataset() camera.start_timelapse()
db.session.add(dataset) elif camera.status == CameraStatus.TIMELAPSE:
db.session.commit() btn_text = "Start"
camera.current_dataset = dataset.id btn_class = 'btn-primary'
print(" > switched on timelapse mode") camera.stop_timelapse()
camera.set_timelapse_interval(timelapse_interval)
return jsonify(btn_text=btn_text, btn_class=btn_class) return jsonify(btn_text=btn_text, btn_class=btn_class)
def gen(camera): def gen(camera):
# def gen():
"""Video streaming generator function.""" """Video streaming generator function."""
while True: while True:
frame = camera.get_frame(thumbnail=True) frame = camera.get_frame(thumbnail=True)

View file

@ -1,19 +0,0 @@
alembic>=1.0.1
Click>=7.0
Flask>=1.0.2
Flask-Migrate>=2.3.0
Flask-SQLAlchemy>=2.3.2
Flask-WTF>=0.14.2
itsdangerous>=1.1.0
Jinja2>=2.10
Mako>=1.0.7
MarkupSafe>=1.0
picamera>=1.13
Pillow>=5.3.0
python-dateutil>=2.7.5
python-editor>=1.0.3
six>=1.11.0
SQLAlchemy>=1.2.12
Werkzeug>=0.14.1
WTForms>=2.2.1
zipstream>=1.1.4