octv2_server.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. #!/usr/bin/env python3
  2. """
  3. OCTv2 (Oreo Cookie Thrower v2) - Raspberry Pi Server
  4. Handles commands from the iOS app to control hardware and camera
  5. """
  6. import socket
  7. import json
  8. import threading
  9. import time
  10. import logging
  11. from datetime import datetime
  12. from typing import Dict, Any, Optional
  13. import io
  14. import os
  15. # Camera imports (uncomment when on Pi)
  16. try:
  17. from picamera2 import Picamera2
  18. import RPi.GPIO as GPIO
  19. CAMERA_AVAILABLE = True
  20. GPIO_AVAILABLE = True
  21. except ImportError:
  22. print("Camera/GPIO not available - running in simulation mode")
  23. CAMERA_AVAILABLE = False
  24. GPIO_AVAILABLE = False
  25. # Configure logging
  26. logging.basicConfig(
  27. level=logging.INFO,
  28. format='%(asctime)s - %(levelname)s - %(message)s'
  29. )
  30. logger = logging.getLogger(__name__)
  31. class OCTv2Server:
  32. def __init__(self, host='0.0.0.0', port=8080):
  33. self.host = host
  34. self.port = port
  35. self.running = False
  36. self.clients = []
  37. # Hardware state
  38. self.current_angle = 30.0
  39. self.is_auto_mode = True
  40. self.is_homed = False
  41. # Camera state
  42. self.camera = None
  43. self.streaming_clients = []
  44. self.stream_thread = None
  45. # GPIO pins (adjust for your hardware)
  46. self.SERVO_PIN = 18
  47. self.STEPPER_PINS = [19, 20, 21, 22] # Example stepper motor pins
  48. self.FIRE_PIN = 23
  49. self.setup_hardware()
  50. self.setup_camera()
  51. def setup_hardware(self):
  52. """Initialize GPIO and hardware components"""
  53. if not GPIO_AVAILABLE:
  54. logger.info("GPIO not available - simulating hardware")
  55. return
  56. try:
  57. GPIO.setmode(GPIO.BCM)
  58. GPIO.setup(self.SERVO_PIN, GPIO.OUT)
  59. GPIO.setup(self.FIRE_PIN, GPIO.OUT)
  60. # Setup stepper motor pins
  61. for pin in self.STEPPER_PINS:
  62. GPIO.setup(pin, GPIO.OUT)
  63. GPIO.output(pin, False)
  64. # Initialize servo
  65. self.servo = GPIO.PWM(self.SERVO_PIN, 50) # 50Hz
  66. self.servo.start(0)
  67. logger.info("Hardware initialized successfully")
  68. except Exception as e:
  69. logger.error(f"Hardware setup failed: {e}")
  70. def setup_camera(self):
  71. """Initialize camera"""
  72. if not CAMERA_AVAILABLE:
  73. logger.info("Camera not available - simulating camera")
  74. return
  75. try:
  76. self.camera = Picamera2()
  77. # Configure camera for streaming and photos
  78. config = self.camera.create_preview_configuration(
  79. main={"size": (640, 480)},
  80. lores={"size": (320, 240)},
  81. display="lores"
  82. )
  83. self.camera.configure(config)
  84. self.camera.start()
  85. logger.info("Camera initialized successfully")
  86. except Exception as e:
  87. logger.error(f"Camera setup failed: {e}")
  88. def start_server(self):
  89. """Start the TCP server"""
  90. self.running = True
  91. server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  92. server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  93. try:
  94. server_socket.bind((self.host, self.port))
  95. server_socket.listen(5)
  96. logger.info(f"OCTv2 Server listening on {self.host}:{self.port}")
  97. while self.running:
  98. try:
  99. client_socket, address = server_socket.accept()
  100. logger.info(f"Client connected from {address}")
  101. client_thread = threading.Thread(
  102. target=self.handle_client,
  103. args=(client_socket, address)
  104. )
  105. client_thread.daemon = True
  106. client_thread.start()
  107. except Exception as e:
  108. if self.running:
  109. logger.error(f"Error accepting client: {e}")
  110. except Exception as e:
  111. logger.error(f"Server error: {e}")
  112. finally:
  113. server_socket.close()
  114. self.cleanup()
  115. def handle_client(self, client_socket, address):
  116. """Handle individual client connections"""
  117. self.clients.append(client_socket)
  118. try:
  119. while self.running:
  120. data = client_socket.recv(1024)
  121. if not data:
  122. break
  123. try:
  124. command = json.loads(data.decode('utf-8'))
  125. logger.info(f"Received command: {command}")
  126. response = self.process_command(command, client_socket)
  127. if response:
  128. client_socket.send(json.dumps(response).encode('utf-8'))
  129. except json.JSONDecodeError:
  130. logger.error("Invalid JSON received")
  131. except Exception as e:
  132. logger.error(f"Error processing command: {e}")
  133. except Exception as e:
  134. logger.error(f"Client {address} error: {e}")
  135. finally:
  136. if client_socket in self.clients:
  137. self.clients.remove(client_socket)
  138. if client_socket in self.streaming_clients:
  139. self.streaming_clients.remove(client_socket)
  140. client_socket.close()
  141. logger.info(f"Client {address} disconnected")
  142. def process_command(self, command: Dict[str, Any], client_socket) -> Optional[Dict[str, Any]]:
  143. """Process commands from iOS app"""
  144. action = command.get('action')
  145. timestamp = command.get('timestamp')
  146. logger.info(f"Processing action: {action}")
  147. if action == 'aim_left':
  148. return self.aim_left()
  149. elif action == 'aim_right':
  150. return self.aim_right()
  151. elif action == 'fire':
  152. angle = command.get('angle', self.current_angle)
  153. return self.fire_oreo(angle)
  154. elif action == 'home':
  155. return self.home_device()
  156. elif action == 'set_mode':
  157. mode = command.get('mode', 'auto')
  158. return self.set_mode(mode)
  159. elif action == 'capture_photo':
  160. return self.capture_photo(client_socket)
  161. elif action == 'start_video_stream':
  162. return self.start_video_stream(client_socket)
  163. elif action == 'stop_video_stream':
  164. return self.stop_video_stream(client_socket)
  165. elif action == 'status':
  166. return self.get_status()
  167. else:
  168. return {'error': f'Unknown action: {action}'}
  169. def aim_left(self) -> Dict[str, Any]:
  170. """Move aim left by a small increment"""
  171. if self.current_angle > 0:
  172. self.current_angle = max(0, self.current_angle - 5)
  173. self.move_to_angle(self.current_angle)
  174. return {'status': 'success', 'angle': self.current_angle}
  175. return {'status': 'error', 'message': 'Already at minimum angle'}
  176. def aim_right(self) -> Dict[str, Any]:
  177. """Move aim right by a small increment"""
  178. if self.current_angle < 60:
  179. self.current_angle = min(60, self.current_angle + 5)
  180. self.move_to_angle(self.current_angle)
  181. return {'status': 'success', 'angle': self.current_angle}
  182. return {'status': 'error', 'message': 'Already at maximum angle'}
  183. def fire_oreo(self, angle: float) -> Dict[str, Any]:
  184. """Fire an Oreo at the specified angle"""
  185. logger.info(f"FIRING OREO at {angle} degrees!")
  186. # Move to target angle first
  187. self.current_angle = angle
  188. self.move_to_angle(angle)
  189. time.sleep(0.5) # Wait for positioning
  190. # Fire mechanism
  191. if GPIO_AVAILABLE:
  192. GPIO.output(self.FIRE_PIN, True)
  193. time.sleep(0.1) # Fire pulse
  194. GPIO.output(self.FIRE_PIN, False)
  195. else:
  196. logger.info("SIMULATED: Fire mechanism activated!")
  197. return {
  198. 'status': 'success',
  199. 'message': f'Oreo fired at {angle}°',
  200. 'angle': angle
  201. }
  202. def move_to_angle(self, angle: float):
  203. """Move servo to specified angle (0-60 degrees)"""
  204. if GPIO_AVAILABLE:
  205. # Convert angle to servo duty cycle
  206. duty = 2 + (angle / 60) * 10 # 2-12% duty cycle for 0-60°
  207. self.servo.ChangeDutyCycle(duty)
  208. time.sleep(0.1)
  209. self.servo.ChangeDutyCycle(0) # Stop sending signal
  210. else:
  211. logger.info(f"SIMULATED: Moving to {angle} degrees")
  212. def home_device(self) -> Dict[str, Any]:
  213. """Home the device to its reference position"""
  214. logger.info("Homing device...")
  215. # Move to home position (usually 0 degrees)
  216. self.current_angle = 0
  217. self.move_to_angle(0)
  218. self.is_homed = True
  219. return {
  220. 'status': 'success',
  221. 'message': 'Device homed successfully',
  222. 'angle': 0
  223. }
  224. def set_mode(self, mode: str) -> Dict[str, Any]:
  225. """Set operating mode (auto/manual)"""
  226. self.is_auto_mode = (mode.lower() == 'auto')
  227. logger.info(f"Mode set to: {mode}")
  228. return {
  229. 'status': 'success',
  230. 'mode': mode,
  231. 'auto_mode': self.is_auto_mode
  232. }
  233. def capture_photo(self, client_socket) -> Dict[str, Any]:
  234. """Capture a high-resolution photo"""
  235. if not CAMERA_AVAILABLE:
  236. logger.info("SIMULATED: Photo captured")
  237. return {'status': 'success', 'message': 'Photo captured (simulated)'}
  238. try:
  239. # Capture high-res photo
  240. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  241. filename = f"octv2_photo_{timestamp}.jpg"
  242. # Save to Pi storage
  243. self.camera.capture_file(filename)
  244. # Optionally send photo back to app
  245. with open(filename, 'rb') as f:
  246. photo_data = f.read()
  247. client_socket.send(photo_data)
  248. return {
  249. 'status': 'success',
  250. 'filename': filename,
  251. 'message': 'Photo captured and saved'
  252. }
  253. except Exception as e:
  254. logger.error(f"Photo capture failed: {e}")
  255. return {'status': 'error', 'message': str(e)}
  256. def start_video_stream(self, client_socket) -> Dict[str, Any]:
  257. """Start video streaming to client"""
  258. if client_socket not in self.streaming_clients:
  259. self.streaming_clients.append(client_socket)
  260. if not self.stream_thread or not self.stream_thread.is_alive():
  261. self.stream_thread = threading.Thread(target=self.video_stream_worker)
  262. self.stream_thread.daemon = True
  263. self.stream_thread.start()
  264. return {'status': 'success', 'message': 'Video stream started'}
  265. def stop_video_stream(self, client_socket) -> Dict[str, Any]:
  266. """Stop video streaming to client"""
  267. if client_socket in self.streaming_clients:
  268. self.streaming_clients.remove(client_socket)
  269. return {'status': 'success', 'message': 'Video stream stopped'}
  270. def video_stream_worker(self):
  271. """Worker thread for video streaming"""
  272. if not CAMERA_AVAILABLE:
  273. logger.info("SIMULATED: Video streaming started")
  274. return
  275. try:
  276. while self.streaming_clients:
  277. # Capture frame
  278. stream = io.BytesIO()
  279. self.camera.capture_file(stream, format='jpeg')
  280. frame_data = stream.getvalue()
  281. # Send to all streaming clients
  282. for client in self.streaming_clients[:]: # Copy list to avoid modification issues
  283. try:
  284. client.send(frame_data)
  285. except Exception as e:
  286. logger.error(f"Failed to send frame to client: {e}")
  287. self.streaming_clients.remove(client)
  288. time.sleep(0.1) # ~10 FPS
  289. except Exception as e:
  290. logger.error(f"Video streaming error: {e}")
  291. def get_status(self) -> Dict[str, Any]:
  292. """Return current device status"""
  293. return {
  294. 'status': 'success',
  295. 'angle': self.current_angle,
  296. 'auto_mode': self.is_auto_mode,
  297. 'homed': self.is_homed,
  298. 'streaming_clients': len(self.streaming_clients),
  299. 'total_clients': len(self.clients)
  300. }
  301. def cleanup(self):
  302. """Clean up resources"""
  303. self.running = False
  304. if self.camera and CAMERA_AVAILABLE:
  305. self.camera.stop()
  306. if GPIO_AVAILABLE:
  307. if hasattr(self, 'servo'):
  308. self.servo.stop()
  309. GPIO.cleanup()
  310. logger.info("Server shutdown complete")
  311. def main():
  312. """Main entry point"""
  313. print("🍪 OCTv2 (Oreo Cookie Thrower v2) Server Starting...")
  314. server = OCTv2Server()
  315. try:
  316. server.start_server()
  317. except KeyboardInterrupt:
  318. print("\n🛑 Shutting down OCTv2 server...")
  319. server.cleanup()
  320. if __name__ == "__main__":
  321. main()