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 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()