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