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 a {{ display: block; text-align: center; margin-top: 5px; }}
64 </style>
65 <script>
66 const ws = new WebSocket('ws://' + window.location.hostname + ':8765');
67 ws.onmessage = function(event) {{
68 if(event.data === 'reload') {{
69 window.location.reload();
70 }}
71 }};
72 </script>
73</head>
74<body>
75 <h1>Inkpress: Gallery</h1>
76 <div class="gallery">
77 {photo_items}
78 </div>
79</body>
80</html>
81"""
82
83class PhotoHandler(http.server.SimpleHTTPRequestHandler):
84 def __init__(self, *args, **kwargs):
85 super().__init__(*args, directory=Config.PHOTO_DIR, **kwargs)
86
87 def do_GET(self):
88 if self.path == '/':
89 self.send_response(200)
90 self.send_header('Content-type', 'text/html')
91 self.send_header('X-Content-Type-Options', 'nosniff')
92 self.send_header('X-Frame-Options', 'DENY')
93 self.send_header('X-XSS-Protection', '1; mode=block')
94 self.end_headers()
95
96 # Generate photo gallery HTML
97 photo_items = ""
98 try:
99 files = sorted(os.listdir(Config.PHOTO_DIR), reverse=True)
100 for filename in files:
101 if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
102 timestamp = filename.replace('photo_', '').replace('.jpg', '')
103 photo_items += f"""
104 <div class="photo">
105 <img src="/{filename}" alt="{timestamp}">
106 <a href="/{filename}" download>Download</a>
107 </div>
108 """
109
110 if not photo_items:
111 photo_items = "<p style='grid-column: 1/-1; text-align: center;'>No photos yet. Press the button to take a photo!</p>"
112 except Exception as e:
113 logger.error(f"Error generating gallery: {str(e)}")
114 photo_items = f"<p>Error loading photos: {str(e)}</p>"
115
116 html = HTML_TEMPLATE.format(photo_items=photo_items)
117 self.wfile.write(html.encode())
118 else:
119 super().do_GET()
120
121async def websocket_handler(websocket, path):
122 connected_clients.add(websocket)
123 try:
124 await websocket.wait_closed()
125 finally:
126 connected_clients.remove(websocket)
127
128async def notify_clients():
129 if connected_clients:
130 await asyncio.gather(
131 *[client.send('reload') for client in connected_clients]
132 )
133
134def take_photo():
135 """
136 Captures a photo using the Raspberry Pi camera.
137
138 The photo is saved with a timestamp in the configured photo directory.
139 The camera is configured for still capture at the specified resolution.
140
141 Raises:
142 IOError: If there's an error accessing the camera or saving the file
143 """
144 try:
145 with Picamera2() as picam2:
146 config = picam2.create_still_configuration(main={"size": Config.PHOTO_RESOLUTION})
147 picam2.configure(config)
148 picam2.start()
149 time.sleep(Config.CAMERA_SETTLE_TIME)
150
151 timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
152 filename = f"{Config.PHOTO_DIR}/photo_{timestamp}.jpg"
153 logger.info(f"Taking photo: {filename}")
154
155 picam2.capture_file(filename)
156 logger.info("Photo taken successfully")
157
158 # Notify websocket clients to reload
159 asyncio.run(notify_clients())
160 except IOError as e:
161 logger.error(f"IO Error while taking photo: {str(e)}")
162 except Exception as e:
163 logger.error(f"Unexpected error while taking photo: {str(e)}")
164
165def run_server():
166 try:
167 handler = PhotoHandler
168 with socketserver.TCPServer(("", Config.WEB_PORT), handler) as httpd:
169 logger.info(f"Web server started at port {Config.WEB_PORT}")
170 httpd.serve_forever()
171 except Exception as e:
172 logger.error(f"Server error: {str(e)}")
173
174def cleanup():
175 try:
176 # Instead of getting/creating a new loop, we'll work with the running loop
177 loop = asyncio.get_running_loop()
178
179 # Create a new event loop for cleanup operations if needed
180 cleanup_loop = asyncio.new_event_loop()
181 asyncio.set_event_loop(cleanup_loop)
182
183 # Close all websocket connections
184 for websocket in connected_clients.copy():
185 cleanup_loop.run_until_complete(websocket.close())
186
187 # Cancel all tasks in the main loop
188 for task in asyncio.all_tasks(loop):
189 task.cancel()
190
191 cleanup_loop.close()
192
193 except RuntimeError:
194 # Handle case where there is no running loop
195 logger.info("No running event loop found during cleanup")
196 except Exception as e:
197 logger.error(f"Error during cleanup: {str(e)}")
198
199def main():
200 logger.info("Camera and web server starting")
201 server = None
202 ws_server = None
203 loop = None
204
205 try:
206 socketserver.TCPServer.allow_reuse_port = True
207
208 # Start HTTP server
209 server = socketserver.TCPServer(("", Config.WEB_PORT), PhotoHandler)
210 server_thread = threading.Thread(target=server.serve_forever, daemon=True)
211 server_thread.start()
212
213 # Create new event loop for websockets
214 loop = asyncio.new_event_loop()
215 asyncio.set_event_loop(loop)
216
217 # Start WebSocket server
218 ws_server = websockets.serve(websocket_handler, "0.0.0.0", Config.WS_PORT)
219 loop.run_until_complete(ws_server)
220 ws_thread = threading.Thread(
221 target=loop.run_forever,
222 daemon=True
223 )
224 ws_thread.start()
225
226 logger.info("Camera and web server started")
227
228 previous_state = GPIO.input(Config.BUTTON_PIN)
229 while True:
230 current_state = GPIO.input(Config.BUTTON_PIN)
231
232 if current_state == False and previous_state == True:
233 logger.info("Button press detected")
234 take_photo()
235 time.sleep(Config.DEBOUNCE_DELAY)
236
237 previous_state = current_state
238 time.sleep(Config.POLL_INTERVAL)
239
240 except KeyboardInterrupt:
241 logger.info("Program stopped by user")
242 except Exception as e:
243 logger.error(f"Unexpected error: {str(e)}")
244 finally:
245 if server:
246 server.shutdown()
247 server.server_close()
248 if loop:
249 loop.stop()
250 GPIO.cleanup()
251 logger.info("GPIO cleaned up")
252 cleanup()
253 time.sleep(0.5)
254
255if __name__ == "__main__":
256 main()