This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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