an eink camera running on an rpi zero 2 w
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 10import websockets 11import asyncio 12 13# Setup logging 14logger = logging.getLogger('camera_server') 15logger.setLevel(logging.INFO) 16formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 17file_handler = logging.FileHandler('/home/kierank/camera_server.log') 18file_handler.setFormatter(formatter) 19stream_handler = logging.StreamHandler() 20stream_handler.setFormatter(formatter) 21logger.addHandler(file_handler) 22logger.addHandler(stream_handler) 23 24class Config: 25 BUTTON_PIN = 17 26 PHOTO_DIR = "/home/kierank/photos" 27 WEB_PORT = 80 28 WS_PORT = 8765 29 PHOTO_RESOLUTION = (2592, 1944) 30 CAMERA_SETTLE_TIME = 1 31 DEBOUNCE_DELAY = 0.2 32 POLL_INTERVAL = 0.01 33 34def validate_photo_dir(): 35 if not os.path.isabs(Config.PHOTO_DIR): 36 raise ValueError("PHOTO_DIR must be an absolute path") 37 if not os.access(Config.PHOTO_DIR, os.W_OK): 38 raise PermissionError(f"No write access to {Config.PHOTO_DIR}") 39 40# Ensure photo directory exists and is valid 41validate_photo_dir() 42os.makedirs(Config.PHOTO_DIR, exist_ok=True) 43 44# Set up GPIO 45GPIO.setmode(GPIO.BCM) 46GPIO.setup(Config.BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) 47 48# WebSocket clients set 49connected_clients = set() 50 51# Create a simple HTML gallery template - using triple quotes properly 52HTML_TEMPLATE = """<!DOCTYPE html> 53<html> 54<head> 55 <title>Inkpress: Gallery</title> 56 <meta name="viewport" content="width=device-width, initial-scale=1"> 57 <style> 58 body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }} 59 h1 {{ text-align: center; }} 60 .gallery {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }} 61 .photo {{ border: 1px solid #ddd; padding: 5px; }} 62 .photo img {{ width: 100%; height: auto; }} 63 .photo .actions {{ text-align: center; margin-top: 5px; }} 64 .photo .actions a {{ margin: 0 5px; }} 65 </style> 66 <script> 67 const ws = new WebSocket('ws://' + window.location.hostname + ':8765'); 68 ws.onmessage = function(event) {{ 69 if(event.data === 'reload') {{ 70 window.location.reload(); 71 }} 72 }}; 73 74 function deletePhoto(filename) {{ 75 if(confirm('Are you sure you want to delete this photo?')) {{ 76 fetch('/delete/' + filename, {{ 77 method: 'POST' 78 }}).then(response => {{ 79 if(response.ok) {{ 80 window.location.reload(); 81 }} 82 }}); 83 }} 84 }} 85 </script> 86</head> 87<body> 88 <h1>Inkpress: Gallery</h1> 89 <div class="gallery"> 90 {photo_items} 91 </div> 92</body> 93</html> 94""" 95 96class PhotoHandler(http.server.SimpleHTTPRequestHandler): 97 def __init__(self, *args, **kwargs): 98 super().__init__(*args, directory=Config.PHOTO_DIR, **kwargs) 99 100 def do_GET(self): 101 if self.path == '/': 102 self.send_response(200) 103 self.send_header('Content-type', 'text/html') 104 self.send_header('X-Content-Type-Options', 'nosniff') 105 self.send_header('X-Frame-Options', 'DENY') 106 self.send_header('X-XSS-Protection', '1; mode=block') 107 self.end_headers() 108 109 # Generate photo gallery HTML 110 photo_items = "" 111 try: 112 files = sorted(os.listdir(Config.PHOTO_DIR), reverse=True) 113 for filename in files: 114 if filename.lower().endswith(('.jpg', '.jpeg', '.png')): 115 timestamp = filename.replace('photo_', '').replace('.jpg', '') 116 photo_items += f""" 117 <div class="photo"> 118 <img src="/{filename}" alt="{timestamp}"> 119 <div class="actions"> 120 <a href="/{filename}" download>Download</a> 121 <a href="#" onclick="deletePhoto('{filename}'); return false;">Delete</a> 122 </div> 123 </div> 124 """ 125 126 if not photo_items: 127 photo_items = "<p style='grid-column: 1/-1; text-align: center;'>No photos yet. Press the button to take a photo!</p>" 128 except Exception as e: 129 logger.error(f"Error generating gallery: {str(e)}") 130 photo_items = f"<p>Error loading photos: {str(e)}</p>" 131 132 html = HTML_TEMPLATE.format(photo_items=photo_items) 133 self.wfile.write(html.encode()) 134 else: 135 super().do_GET() 136 137 def do_POST(self): 138 if self.path.startswith('/delete/'): 139 filename = self.path[8:] # Remove '/delete/' prefix 140 file_path = os.path.join(Config.PHOTO_DIR, filename) 141 142 try: 143 if os.path.exists(file_path) and os.path.isfile(file_path): 144 os.remove(file_path) 145 logger.info(f"Deleted photo: {filename}") 146 self.send_response(200) 147 self.send_header('Content-type', 'text/plain') 148 self.end_headers() 149 self.wfile.write(b"File deleted successfully") 150 asyncio.run(notify_clients()) 151 else: 152 self.send_response(404) 153 self.send_header('Content-type', 'text/plain') 154 self.end_headers() 155 self.wfile.write(b"File not found") 156 except Exception as e: 157 logger.error(f"Error deleting file {filename}: {str(e)}") 158 self.send_response(500) 159 self.send_header('Content-type', 'text/plain') 160 self.end_headers() 161 self.wfile.write(b"Error deleting file") 162 else: 163 self.send_response(404) 164 self.end_headers() 165 166async def websocket_handler(websocket, path): 167 connected_clients.add(websocket) 168 try: 169 await websocket.wait_closed() 170 finally: 171 connected_clients.remove(websocket) 172 173async def notify_clients(): 174 if connected_clients: 175 await asyncio.gather( 176 *[client.send('reload') for client in connected_clients] 177 ) 178 179def take_photo(): 180 """ 181 Captures a photo using the Raspberry Pi camera. 182 183 The photo is saved with a timestamp in the configured photo directory. 184 The camera is configured for still capture at the specified resolution. 185 186 Raises: 187 IOError: If there's an error accessing the camera or saving the file 188 """ 189 try: 190 with Picamera2() as picam2: 191 config = picam2.create_still_configuration(main={"size": Config.PHOTO_RESOLUTION}) 192 picam2.configure(config) 193 picam2.start() 194 time.sleep(Config.CAMERA_SETTLE_TIME) 195 196 timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 197 filename = f"{Config.PHOTO_DIR}/photo_{timestamp}.jpg" 198 logger.info(f"Taking photo: {filename}") 199 200 picam2.capture_file(filename) 201 logger.info("Photo taken successfully") 202 203 # Notify websocket clients to reload 204 asyncio.run(notify_clients()) 205 except IOError as e: 206 logger.error(f"IO Error while taking photo: {str(e)}") 207 except Exception as e: 208 logger.error(f"Unexpected error while taking photo: {str(e)}") 209 210def run_server(): 211 try: 212 handler = PhotoHandler 213 with socketserver.TCPServer(("", Config.WEB_PORT), handler) as httpd: 214 logger.info(f"Web server started at port {Config.WEB_PORT}") 215 httpd.serve_forever() 216 except Exception as e: 217 logger.error(f"Server error: {str(e)}") 218 219def cleanup(): 220 try: 221 # Instead of getting/creating a new loop, we'll work with the running loop 222 loop = asyncio.get_running_loop() 223 224 # Create a new event loop for cleanup operations if needed 225 cleanup_loop = asyncio.new_event_loop() 226 asyncio.set_event_loop(cleanup_loop) 227 228 # Close all websocket connections 229 for websocket in connected_clients.copy(): 230 cleanup_loop.run_until_complete(websocket.close()) 231 232 # Cancel all tasks in the main loop 233 for task in asyncio.all_tasks(loop): 234 task.cancel() 235 236 cleanup_loop.close() 237 238 except RuntimeError: 239 # Handle case where there is no running loop 240 logger.info("No running event loop found during cleanup") 241 except Exception as e: 242 logger.error(f"Error during cleanup: {str(e)}") 243 244def main(): 245 logger.info("Camera and web server starting") 246 server = None 247 ws_server = None 248 loop = None 249 250 try: 251 socketserver.TCPServer.allow_reuse_port = True 252 253 # Start HTTP server 254 server = socketserver.TCPServer(("", Config.WEB_PORT), PhotoHandler) 255 server_thread = threading.Thread(target=server.serve_forever, daemon=True) 256 server_thread.start() 257 258 # Create new event loop for websockets 259 loop = asyncio.new_event_loop() 260 asyncio.set_event_loop(loop) 261 262 # Start WebSocket server 263 ws_server = websockets.serve(websocket_handler, "0.0.0.0", Config.WS_PORT) 264 loop.run_until_complete(ws_server) 265 ws_thread = threading.Thread( 266 target=loop.run_forever, 267 daemon=True 268 ) 269 ws_thread.start() 270 271 logger.info("Camera and web server started") 272 273 previous_state = GPIO.input(Config.BUTTON_PIN) 274 while True: 275 current_state = GPIO.input(Config.BUTTON_PIN) 276 277 if current_state == False and previous_state == True: 278 logger.info("Button press detected") 279 take_photo() 280 time.sleep(Config.DEBOUNCE_DELAY) 281 282 previous_state = current_state 283 time.sleep(Config.POLL_INTERVAL) 284 285 except KeyboardInterrupt: 286 logger.info("Program stopped by user") 287 except Exception as e: 288 logger.error(f"Unexpected error: {str(e)}") 289 finally: 290 if server: 291 server.shutdown() 292 server.server_close() 293 if loop: 294 loop.stop() 295 GPIO.cleanup() 296 logger.info("GPIO cleaned up") 297 cleanup() 298 time.sleep(0.5) 299 300if __name__ == "__main__": 301 main()