this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: do reactive updates and auto reconnect to websocket

+90 -20
+90 -20
src/camera_server.py
··· 9 9 import threading 10 10 import websockets 11 11 import asyncio 12 + import json 12 13 13 14 # Setup logging 14 15 logger = logging.getLogger('camera_server') ··· 48 49 # WebSocket clients set 49 50 connected_clients = set() 50 51 51 - # Create a simple HTML gallery template - using triple quotes properly 52 + # Create a simple HTML gallery template - using triple quotes properly and making sure to escape curly braces 52 53 HTML_TEMPLATE = """<!DOCTYPE html> 53 54 <html> 54 55 <head> ··· 57 58 <style> 58 59 body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }} 59 60 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; }} 61 + .gallery {{ display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }} 62 + .photo {{ border: 1px solid #ddd; padding: 5px; animation: fadeIn 0.1s; flex: 0 1 200px; }} 62 63 .photo img {{ width: 100%; height: auto; }} 63 64 .photo .actions {{ text-align: center; margin-top: 5px; }} 64 65 .photo .actions a {{ margin: 0 5px; }} 66 + @keyframes fadeIn {{ from {{ opacity: 0; }} to {{ opacity: 1; }} }} 67 + @keyframes fadeOut {{ from {{ opacity: 1; }} to {{ opacity: 0; }} }} 65 68 </style> 66 69 <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(); 70 + let ws; 71 + const RECONNECT_DELAY = 1000; 72 + 73 + function connect() {{ 74 + ws = new WebSocket('ws://' + window.location.hostname + ':8765'); 75 + 76 + ws.onmessage = function(event) {{ 77 + const data = JSON.parse(event.data); 78 + 79 + if (data.action === 'new_photo') {{ 80 + addPhoto(data.filename, data.timestamp); 81 + }} else if (data.action === 'delete_photo') {{ 82 + removePhoto(data.filename); 83 + }} 84 + }}; 85 + 86 + ws.onclose = function() {{ 87 + console.log('WebSocket connection closed. Reconnecting...'); 88 + setTimeout(connect, RECONNECT_DELAY); 89 + }}; 90 + 91 + ws.onerror = function(err) {{ 92 + console.error('WebSocket error:', err); 93 + ws.close(); 94 + }}; 95 + }} 96 + 97 + connect(); 98 + 99 + function addPhoto(filename, timestamp) {{ 100 + const gallery = document.querySelector('.gallery'); 101 + const noPhotosMsg = gallery.querySelector('p'); 102 + if (noPhotosMsg) {{ 103 + noPhotosMsg.remove(); 71 104 }} 72 - }}; 105 + 106 + const photoDiv = document.createElement('div'); 107 + photoDiv.className = 'photo'; 108 + photoDiv.id = `photo-${{filename}}`; 109 + 110 + photoDiv.innerHTML = ` 111 + <img src="/${{filename}}" alt="${{timestamp}}"> 112 + <div class="actions"> 113 + <a href="/${{filename}}" download>Download</a> 114 + <a href="#" onclick="deletePhoto('${{filename}}'); return false;">Delete</a> 115 + </div> 116 + `; 117 + 118 + gallery.insertBefore(photoDiv, gallery.firstChild); 119 + }} 120 + 121 + function removePhoto(filename) {{ 122 + const photoDiv = document.getElementById(`photo-${{filename}}`); 123 + if (photoDiv) {{ 124 + setTimeout(() => {{ 125 + photoDiv.remove(); 126 + const gallery = document.querySelector('.gallery'); 127 + if (gallery.children.length === 0) {{ 128 + const noPhotosMsg = document.createElement('p'); 129 + noPhotosMsg.style = 'text-align: center;'; 130 + noPhotosMsg.textContent = 'No photos yet. Press the button to take a photo!'; 131 + gallery.appendChild(noPhotosMsg); 132 + }} 133 + }}, 100); 134 + }} 135 + }} 73 136 74 137 function deletePhoto(filename) {{ 75 - if(confirm('Are you sure you want to delete this photo?')) {{ 138 + if (confirm('Are you sure you want to delete this photo?')) {{ 76 139 fetch('/delete/' + filename, {{ 77 140 method: 'POST' 78 141 }}).then(response => {{ 79 142 if(response.ok) {{ 80 - window.location.reload(); 143 + removePhoto(filename); 81 144 }} 82 145 }}); 83 146 }} ··· 114 177 if filename.lower().endswith(('.jpg', '.jpeg', '.png')): 115 178 timestamp = filename.replace('photo_', '').replace('.jpg', '') 116 179 photo_items += f""" 117 - <div class="photo"> 180 + <div class="photo" id="photo-{filename}"> 118 181 <img src="/{filename}" alt="{timestamp}"> 119 182 <div class="actions"> 120 183 <a href="/{filename}" download>Download</a> ··· 147 210 self.send_header('Content-type', 'text/plain') 148 211 self.end_headers() 149 212 self.wfile.write(b"File deleted successfully") 150 - asyncio.run(notify_clients()) 213 + asyncio.run(notify_clients('delete_photo', {'filename': filename})) 151 214 else: 152 215 self.send_response(404) 153 216 self.send_header('Content-type', 'text/plain') ··· 170 233 finally: 171 234 connected_clients.remove(websocket) 172 235 173 - async def notify_clients(): 236 + async def notify_clients(action, data): 174 237 if connected_clients: 238 + message = { 239 + 'action': action, 240 + **data 241 + } 175 242 await asyncio.gather( 176 - *[client.send('reload') for client in connected_clients] 243 + *[client.send(json.dumps(message)) for client in connected_clients] 177 244 ) 178 245 179 246 def take_photo(): ··· 194 261 time.sleep(Config.CAMERA_SETTLE_TIME) 195 262 196 263 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}") 264 + filename = f"photo_{timestamp}.jpg" 265 + filepath = os.path.join(Config.PHOTO_DIR, filename) 266 + logger.info(f"Taking photo: {filepath}") 199 267 200 - picam2.capture_file(filename) 268 + picam2.capture_file(filepath) 201 269 logger.info("Photo taken successfully") 202 270 203 - # Notify websocket clients to reload 204 - asyncio.run(notify_clients()) 271 + # Notify websocket clients about new photo 272 + asyncio.run(notify_clients('new_photo', { 273 + 'filename': filename, 274 + 'timestamp': timestamp 275 + })) 205 276 except IOError as e: 206 277 logger.error(f"IO Error while taking photo: {str(e)}") 207 278 except Exception as e: ··· 295 366 GPIO.cleanup() 296 367 logger.info("GPIO cleaned up") 297 368 cleanup() 298 - time.sleep(0.5) 299 369 300 370 if __name__ == "__main__": 301 371 main()