This repository has no description
1import RPi.GPIO as GPIO
2import time
3from picamera2 import Picamera2
4from datetime import datetime
5import os
6import logging
7import http.server
8import socketserver
9import threading
10
11# Setup logging
12logger = logging.getLogger('camera_server')
13logger.setLevel(logging.INFO)
14formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
15file_handler = logging.FileHandler('/home/kierank/camera_server.log')
16file_handler.setFormatter(formatter)
17stream_handler = logging.StreamHandler()
18stream_handler.setFormatter(formatter)
19logger.addHandler(file_handler)
20logger.addHandler(stream_handler)
21
22class Config:
23 BUTTON_PIN = 17
24 PHOTO_DIR = "/home/kierank/photos"
25 WEB_PORT = 80
26 PHOTO_RESOLUTION = (2592, 1944)
27 CAMERA_SETTLE_TIME = 1
28 DEBOUNCE_DELAY = 0.2
29 POLL_INTERVAL = 0.01
30
31def validate_photo_dir():
32 if not os.path.isabs(Config.PHOTO_DIR):
33 raise ValueError("PHOTO_DIR must be an absolute path")
34 if not os.access(Config.PHOTO_DIR, os.W_OK):
35 raise PermissionError(f"No write access to {Config.PHOTO_DIR}")
36
37# Ensure photo directory exists and is valid
38validate_photo_dir()
39os.makedirs(Config.PHOTO_DIR, exist_ok=True)
40
41# Set up GPIO
42GPIO.setmode(GPIO.BCM)
43GPIO.setup(Config.BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
44
45# Create a simple HTML gallery template - using triple quotes properly
46HTML_TEMPLATE = """<!DOCTYPE html>
47<html>
48<head>
49 <title>Inkpress: Gallery</title>
50 <meta name="viewport" content="width=device-width, initial-scale=1">
51 <style>
52 body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
53 h1 {{ text-align: center; }}
54 .gallery {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }}
55 .photo {{ border: 1px solid #ddd; padding: 5px; }}
56 .photo img {{ width: 100%; height: auto; }}
57 .photo a {{ display: block; text-align: center; margin-top: 5px; }}
58 button {{ display: block; margin: 10px auto; padding: 5px 10px; }}
59 </style>
60</head>
61<body>
62 <h1>Inkpress: Gallery</h1>
63 <button onclick="location.reload()">Refresh Gallery</button>
64 <div class="gallery">
65 {photo_items}
66 </div>
67</body>
68</html>
69"""
70
71class PhotoHandler(http.server.SimpleHTTPRequestHandler):
72 def __init__(self, *args, **kwargs):
73 super().__init__(*args, directory=Config.PHOTO_DIR, **kwargs)
74
75 def do_GET(self):
76 if self.path == '/':
77 self.send_response(200)
78 self.send_header('Content-type', 'text/html')
79 self.send_header('X-Content-Type-Options', 'nosniff')
80 self.send_header('X-Frame-Options', 'DENY')
81 self.send_header('X-XSS-Protection', '1; mode=block')
82 self.end_headers()
83
84 # Generate photo gallery HTML
85 photo_items = ""
86 try:
87 files = sorted(os.listdir(Config.PHOTO_DIR), reverse=True)
88 for filename in files:
89 if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
90 timestamp = filename.replace('photo_', '').replace('.jpg', '')
91 photo_items += f"""
92 <div class="photo">
93 <img src="/{filename}" alt="{timestamp}">
94 <a href="/{filename}" download>Download</a>
95 </div>
96 """
97
98 if not photo_items:
99 photo_items = "<p style='grid-column: 1/-1; text-align: center;'>No photos yet. Press the button to take a photo!</p>"
100 except Exception as e:
101 logger.error(f"Error generating gallery: {str(e)}")
102 photo_items = f"<p>Error loading photos: {str(e)}</p>"
103
104 html = HTML_TEMPLATE.format(photo_items=photo_items)
105 self.wfile.write(html.encode())
106 else:
107 super().do_GET()
108
109def take_photo():
110 """
111 Captures a photo using the Raspberry Pi camera.
112
113 The photo is saved with a timestamp in the configured photo directory.
114 The camera is configured for still capture at the specified resolution.
115
116 Raises:
117 IOError: If there's an error accessing the camera or saving the file
118 """
119 try:
120 with Picamera2() as picam2:
121 config = picam2.create_still_configuration(main={"size": Config.PHOTO_RESOLUTION})
122 picam2.configure(config)
123 picam2.start()
124 time.sleep(Config.CAMERA_SETTLE_TIME)
125
126 timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
127 filename = f"{Config.PHOTO_DIR}/photo_{timestamp}.jpg"
128 logger.info(f"Taking photo: {filename}")
129
130 picam2.capture_file(filename)
131 logger.info("Photo taken successfully")
132 except IOError as e:
133 logger.error(f"IO Error while taking photo: {str(e)}")
134 except Exception as e:
135 logger.error(f"Unexpected error while taking photo: {str(e)}")
136
137def run_server():
138 try:
139 handler = PhotoHandler
140 with socketserver.TCPServer(("", Config.WEB_PORT), handler) as httpd:
141 logger.info(f"Web server started at port {Config.WEB_PORT}")
142 httpd.serve_forever()
143 except Exception as e:
144 logger.error(f"Server error: {str(e)}")
145
146def main():
147 logger.info("Camera and web server starting")
148 server = None
149
150 try:
151 server = socketserver.TCPServer(("", Config.WEB_PORT), PhotoHandler)
152 server_thread = threading.Thread(target=server.serve_forever, daemon=True)
153 server_thread.start()
154
155 previous_state = GPIO.input(Config.BUTTON_PIN)
156 while True:
157 current_state = GPIO.input(Config.BUTTON_PIN)
158
159 if current_state == False and previous_state == True:
160 logger.info("Button press detected")
161 take_photo()
162 time.sleep(Config.DEBOUNCE_DELAY)
163
164 previous_state = current_state
165 time.sleep(Config.POLL_INTERVAL)
166
167 except KeyboardInterrupt:
168 logger.info("Program stopped by user")
169 except Exception as e:
170 logger.error(f"Unexpected error: {str(e)}")
171 finally:
172 if server:
173 server.shutdown()
174 server.server_close()
175 GPIO.cleanup()
176 logger.info("GPIO cleaned up")
177
178if __name__ == "__main__":
179 main()