This repository has no description
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()