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