an eink camera running on an rpi zero 2 w
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()