Guest User

Untitled

a guest
Mar 5th, 2026
20
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.75 KB | None | 0 0
  1. import datetime
  2. import os
  3. import select
  4. import struct
  5.  
  6. import kaiten.constants
  7. import kaiten.generators
  8. import kaiten.log
  9.  
  10. import kaiten._bwcamera
  11.  
  12. # Format numbers according to the kaiten API
  13. FMT_YUYV = 1
  14. FMT_JPEG = 2
  15.  
  16. class CameraSettings(object):
  17. def __init__(self, width, height, format_nr, nr_buffers):
  18. self.width = width
  19. self.height = height
  20. self.format_nr = format_nr
  21.  
  22. # Don't set < 2. More buffers == more latency, but
  23. # too few buffers hurts the framerate.
  24. self.nr_buffers = nr_buffers
  25.  
  26. # Mapping of known camera types to standard resolution/formats
  27. SUPPORTED_CAMERAS = {
  28. 'Venus USB1.1 Camera': CameraSettings(320, 240, FMT_YUYV, 2),
  29. 'NMG HD WebCam': CameraSettings(640, 480, FMT_JPEG, 3),
  30. }
  31.  
  32. class NoImageError(Exception):
  33. pass
  34.  
  35. class GeneratorLock(object):
  36. """
  37. Like a thread lock, but for generators. Usage:
  38.  
  39. lock = GeneratorLock()
  40. ...
  41. yield from lock.get_lock()
  42. with lock:
  43. ...
  44. """
  45. def __init__(self):
  46. self._locked = False
  47.  
  48. def get_lock(self):
  49. while self._locked: yield
  50.  
  51. def __enter__(self):
  52. if self._locked:
  53. raise RuntimeError("Tried to grab lock without calling get_lock()")
  54. self._locked = True
  55.  
  56. def __exit__(self, e, v, tb):
  57. self._locked = False
  58.  
  59. class CameraContract(object):
  60. """
  61. Wrapper around the camera IO generator. When the camera IO generator is
  62. running, it will constantly reschedule this generator so that this
  63. generator will never be iterated. When it is not running, this contract
  64. periodically tries to (re)start it.
  65. """
  66. def __init__(self, server, device_path=kaiten.constants.camera_dev_path):
  67. self._server = server
  68. self._dev_path = device_path
  69. self._log = kaiten.log.getlogger(self)
  70. self._camera = None
  71. self._init_fail_count = 0
  72. self._duration = datetime.timedelta(seconds=30)
  73. self._recurring_clients = []
  74. self._one_shot_clients = []
  75. self._one_shot_camera_frame_callbacks = []
  76.  
  77. # Assume the device exists so that we only log when it does not
  78. self._dev_exists = True
  79.  
  80. # Keep track of which clients we have not finished sending
  81. # a frame to. To limit server load we will not send another
  82. # frame to such clients.
  83. self._pending_clients = set()
  84.  
  85. # On each iteration, we only grab a packed frame copy if a
  86. # client requests it, but we only grab one copy per iteration
  87. self._frame = None
  88.  
  89. # Also limit the server load by capping the number of
  90. # clients we send a frame to on each iteration
  91. self._max_clients_per_frame = 5
  92.  
  93. def request_frame(self, client):
  94. """
  95. Request that a single frame be sent to the given client.
  96. This will only succeed if a client is not already awaiting
  97. a frame that is still queued for sending. This will also
  98. unsubscribe the client from a stream.
  99. """
  100. if client in self._recurring_clients:
  101. self._recurring_clients.remove(client)
  102. return False
  103. elif client in self._one_shot_clients:
  104. return False
  105. elif client in self._pending_clients:
  106. return False
  107. else:
  108. self._one_shot_clients.append(client)
  109. return True
  110.  
  111. def request_stream(self, client):
  112. """
  113. Subscribe the given client to a stream of frames.
  114. """
  115. if client not in self._recurring_clients:
  116. self._recurring_clients.append(client)
  117.  
  118. def end_stream(self, client):
  119. """
  120. Subscribe the given client to a stream of frames.
  121. """
  122. if client in self._recurring_clients:
  123. self._recurring_clients.remove(client)
  124.  
  125. def is_streaming(self):
  126. return bool(self._recurring_clients)
  127.  
  128. def save_jpeg(self, path):
  129. if self._camera is None:
  130. raise NoImageError
  131. yield from self._camera.save_jpeg(path)
  132.  
  133. def close(self):
  134. if self._camera:
  135. self._camera.close()
  136. self._server._iopoll.unregister(self._camera)
  137. self._camera = None
  138.  
  139. def contract_duration(self):
  140. return self._duration
  141.  
  142. def expected_run_time(self):
  143. return datetime.timedelta(seconds=kaiten.constants.normal_generator_time)
  144.  
  145. def _ensure_frame(self):
  146. if self._frame is not None:
  147. return
  148. elif self._camera is None:
  149. self._frame = Camera.empty_frame
  150. else:
  151. self._frame = self._camera.get_latest_packed_frame()
  152.  
  153. def set_one_shot_frame_callback(self, callback):
  154. self._one_shot_camera_frame_callbacks.append(callback)
  155.  
  156. def _send_frame(self, client):
  157. def sent_callback():
  158. self._pending_clients -= {client}
  159. self._pending_clients |= {client}
  160. self._ensure_frame()
  161. method = 'camera_frame'
  162. params = {}
  163. self._server.notify_client(
  164. client, method, params, sent_callback, self._frame
  165. )
  166.  
  167. def _check_client(self, client):
  168. """ Determine if this client should be sent a frame """
  169. if client in self._pending_clients:
  170. return False
  171. data_rate_limiter = getattr(client, 'data_rate_limiter', None)
  172. if data_rate_limiter:
  173. # We need to know the size of the frame to limit on data rate
  174. self._ensure_frame()
  175. return data_rate_limiter.try_data(len(self._frame))
  176. return True
  177.  
  178. def _defer(self):
  179. self._server.reschedule_contract_generator(self)
  180.  
  181. def _send_frames(self):
  182. self._defer()
  183.  
  184. client_count = 0
  185. client_recur = []
  186.  
  187. #giving the one shot callbacks priority over clients
  188. if self._one_shot_camera_frame_callbacks:
  189. client_count += 1
  190. self._ensure_frame()
  191. callback = self._one_shot_camera_frame_callbacks.pop(0)
  192. callback(self._frame)
  193.  
  194. while client_count < self._max_clients_per_frame:
  195. if self._one_shot_clients:
  196. client = self._one_shot_clients.pop(0)
  197. elif self._recurring_clients:
  198. client = self._recurring_clients.pop(0)
  199. client_recur.append(client)
  200. else:
  201. break
  202. if not self._check_client(client):
  203. continue
  204. client_count += 1
  205. try:
  206. self._send_frame(client)
  207. except Exception:
  208. # Blame the client
  209. self._pending_clients -= {client}
  210. if client in client_recur:
  211. client_recur.remove(client)
  212.  
  213. self._recurring_clients.extend(client_recur)
  214. self._frame = None
  215.  
  216. def _hotplug_update(self):
  217. """
  218. We don't have actual hotplug notifications for the camera, we
  219. just periodically check if the device exists. This function
  220. performs that check and logs whenever the status changes.
  221. """
  222. dev_exists = os.path.exists(self._dev_path)
  223. if self._dev_exists and not dev_exists:
  224. self._log.warning('Camera device disappeared')
  225. if not self._dev_exists and dev_exists:
  226. self._log.info('Camera device found')
  227. self._dev_exists = dev_exists
  228.  
  229. def __next__(self):
  230. self._hotplug_update()
  231. if not self._dev_exists:
  232. # Close the camera if it exists, but don't log anything
  233. # here since we already logged the device disappearance
  234. self._init_fail_count = 0
  235. self.close()
  236. return
  237. if self._camera:
  238. self._log.info('Dropping unresponsive camera object')
  239. self.close()
  240. try:
  241. self._camera = Camera(self._dev_path, frame_cb=self._send_frames)
  242. self._server._iopoll.register(self._camera)
  243. except Exception:
  244. self._init_fail_count += 1
  245. msg = 'Failed to start camera stream'
  246. if self._init_fail_count > 2:
  247. return
  248. elif self._init_fail_count == 2:
  249. msg += ' (further failures will not be logged)'
  250. self._log.error(msg, exc_info=True)
  251. else:
  252. self._init_fail_count = 0
  253. self._log.info('Started camera stream')
  254.  
  255.  
  256. class Camera(kaiten.generators.IOGenerator):
  257. """
  258. Camera object to interact with and store the state of the camera.
  259. """
  260.  
  261. def __init__(self, device_path, frame_cb=None):
  262. self._log = kaiten.log.getlogger(self)
  263. self._dev = kaiten._bwcamera.Camera(device_path)
  264. self._frame_cb = frame_cb
  265. self._latest_buffer = None
  266. self._lock = GeneratorLock()
  267.  
  268. self._initialize()
  269.  
  270. super(Camera, self).__init__(self._stream(), self._dev, True, False)
  271.  
  272. def _set_format(self):
  273. """
  274. _set_format will attempt to set the width, height, and image encoding
  275. according to the type of camera detected. It will also create buffers
  276. """
  277. camera_type = self._dev.card
  278. settings = SUPPORTED_CAMERAS[camera_type]
  279.  
  280. # TODO allow custom format via config file or args
  281. self._dev.set_format(settings.width, settings.height,
  282. settings.format_nr)
  283.  
  284. self._dev.create_buffers(settings.nr_buffers)
  285.  
  286. # when you set the format, sometimes the device will reject it
  287. # and set different parameters. We check if this happens here.
  288. fmt = self._dev.format
  289. if fmt.width != settings.width or fmt.height != settings.height:
  290. self._log.warning(
  291. "Actual w/h (%d/%d) differs from requested (%d/%d)",
  292. fmt.width, fmt.height,
  293. settings.width, settings.height)
  294.  
  295. empty_frame = struct.pack("!IIII", 16, 0, 0, 0)
  296.  
  297. def get_latest_packed_frame(self):
  298. """
  299. get_latest_packed_frame will pack the cached image data
  300. into the following structure:
  301. uint32 total_blob_size
  302. uint32 image_width
  303. uint32 image_height
  304. uint32 pixelformat (0 = Invalid image, 1 = YUYV, 2 = JPEG)
  305. char[] latest_cached_image
  306. """
  307. # Because we do not yield and only make a copy of _latest_buffer,
  308. # we do not need to acquire self._lock here
  309. if self._latest_buffer is not None:
  310. fmt = self._latest_buffer.format
  311. data = memoryview(self._latest_buffer)
  312. total_size = 16 + len(data)
  313. header = struct.pack("!IIII", total_size, fmt.width,
  314. fmt.height, fmt.format_nr)
  315. return header + data
  316. else:
  317. return self.empty_frame
  318.  
  319. def save_jpeg(self, path, rows_per_chunk=10):
  320. """
  321. save_jpeg will attempt to encode the current frame to a jpeg and save
  322. it to the supplied path on the filesystem. This is CPU intensive, so
  323. it is implemented as a generator that must be exhausted to complete
  324. the encoding.
  325. """
  326. yield from self._lock.get_lock()
  327. with self._lock:
  328. if self._latest_buffer is None:
  329. raise NoImageError()
  330. fmt = self._latest_buffer.format
  331. data = memoryview(self._latest_buffer)
  332. if fmt.format_nr == FMT_JPEG:
  333. with open(path, 'wb') as f:
  334. f.write(data)
  335. return
  336. writer = kaiten._bwcamera.JpegWriter(fmt.width, fmt.height, path)
  337. # Each pixel is two bytes
  338. chunk_size = 2 * fmt.width * rows_per_chunk
  339. for index in range(0, len(data), chunk_size):
  340. writer.send(data[index:index+chunk_size])
  341. yield
  342.  
  343. def _initialize(self):
  344. """
  345. _initialize opens the camera device, sets the format and starts the
  346. camera stream.
  347. """
  348. self._dev.open_device()
  349. self._set_format()
  350.  
  351. # Start the stream
  352. self._dev.start_stream()
  353.  
  354. # Queue up all but one buffer
  355. for buff in self._dev.buffers[:-1]:
  356. self._dev.queue_buffer(buff)
  357.  
  358. def _stream(self):
  359. """
  360. _stream will start pulling frames from the video device into
  361. self._latest_buffer, which will be a CameraBuffer Instance.
  362. """
  363. try:
  364. next_buff = self._dev.buffers[-1]
  365. while True:
  366. # Don't requeue the current buffer if we are using it
  367. yield from self._lock.get_lock()
  368.  
  369. # Swap the buffers around
  370. self._dev.queue_buffer(next_buff)
  371. next_buff = self._dev.dequeue_buffer()
  372. self._latest_buffer = next_buff
  373.  
  374. if self._frame_cb:
  375. self._frame_cb()
  376.  
  377. yield
  378. finally:
  379. self._cleanup()
  380.  
  381. def _cleanup(self):
  382. """
  383. _cleanup is a utility function that attempts to shut off
  384. the camera stream, clear all existing buffers, and close
  385. the camera file descriptor.
  386.  
  387. Please note, that if a camera object exists, and is closed
  388. without calling cleanup, it is likely a reboot will be
  389. required to utilize the camera again.
  390. """
  391. try:
  392. self._dev.stop_stream()
  393. except Exception:
  394. self._log.warning('stop_stream failed', exc_info=True)
  395.  
  396. self._latest_buffer = None
  397. self._dev.clear_buffers()
  398.  
  399. try:
  400. self._dev.close_device()
  401. except Exception:
  402. self._log.warning('close_device failed', exc_info=True)
  403.  
  404. def __del__(self):
  405. self._cleanup()
  406.  
  407.  
  408.  
Add Comment
Please, Sign In to add comment