adafruit-circuitpython-usb-host-camera 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: adafruit-circuitpython-usb-host-camera
3
+ Version: 1.0.0
4
+ Summary: CircuitPython USB host driver for cameras
5
+ Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera
8
+ Keywords: adafruit,blinka,circuitpython,micropython,usb_host_camera,camera,webcam,usb,host,usbhost,picture,image,video
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: System :: Hardware
13
+ Classifier: Programming Language :: Python :: 3
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+ Requires-Dist: Adafruit-Blinka
17
+ Requires-Dist: adafruit-circuitpython-usb-host-descriptors
18
+ Provides-Extra: optional
19
+ Dynamic: license-file
20
+
21
+ Introduction
22
+ ============
23
+
24
+
25
+ .. image:: https://readthedocs.org/projects/adafruit-circuitpython-usb-host-camera/badge/?version=latest
26
+ :target: https://docs.circuitpython.org/projects/usb_host_camera/en/latest/
27
+ :alt: Documentation Status
28
+
29
+
30
+ .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg
31
+ :target: https://adafru.it/discord
32
+ :alt: Discord
33
+
34
+
35
+ .. image:: https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera/workflows/Build%20CI/badge.svg
36
+ :target: https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera/actions
37
+ :alt: Build Status
38
+
39
+
40
+ .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
41
+ :target: https://github.com/astral-sh/ruff
42
+ :alt: Code Style: Ruff
43
+
44
+ CircuitPython USB host driver for cameras
45
+
46
+
47
+ Dependencies
48
+ =============
49
+ This driver depends on:
50
+
51
+ * `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
52
+
53
+ Please ensure all dependencies are available on the CircuitPython filesystem.
54
+ This is easily achieved by downloading
55
+ `the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
56
+ or individual libraries can be installed using
57
+ `circup <https://github.com/adafruit/circup>`_.
58
+
59
+
60
+ `Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_
61
+
62
+ Installing from PyPI
63
+ =====================
64
+
65
+ On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
66
+ PyPI <https://pypi.org/project/adafruit-circuitpython-usb-host-camera/>`_.
67
+ To install for current user:
68
+
69
+ .. code-block:: shell
70
+
71
+ pip3 install adafruit-circuitpython-usb-host-camera
72
+
73
+ To install system-wide (this may be required in some cases):
74
+
75
+ .. code-block:: shell
76
+
77
+ sudo pip3 install adafruit-circuitpython-usb-host-camera
78
+
79
+ To install in a virtual environment in your current project:
80
+
81
+ .. code-block:: shell
82
+
83
+ mkdir project-name && cd project-name
84
+ python3 -m venv .venv
85
+ source .env/bin/activate
86
+ pip3 install adafruit-circuitpython-usb-host-camera
87
+
88
+ Installing to a Connected CircuitPython Device with Circup
89
+ ==========================================================
90
+
91
+ Make sure that you have ``circup`` installed in your Python environment.
92
+ Install it with the following command if necessary:
93
+
94
+ .. code-block:: shell
95
+
96
+ pip3 install circup
97
+
98
+ With ``circup`` installed and your CircuitPython device connected use the
99
+ following command to install:
100
+
101
+ .. code-block:: shell
102
+
103
+ circup install adafruit_usb_host_camera
104
+
105
+ Or the following command to update an existing version:
106
+
107
+ .. code-block:: shell
108
+
109
+ circup update
110
+
111
+ Usage Example
112
+ =============
113
+
114
+ .. code-block:: python
115
+
116
+ import time
117
+
118
+ import adafruit_usb_host_camera
119
+
120
+ camera = None
121
+ while camera is None:
122
+ try:
123
+ # Finds the camera among the attached devices (a keyboard, a mouse...)
124
+ camera = adafruit_usb_host_camera.UVCCamera()
125
+ except ValueError:
126
+ print("Plug a USB camera into the USB host port")
127
+ time.sleep(2)
128
+
129
+ print("Camera modes:", camera.modes)
130
+ mode = camera.find_mode(640, 480)
131
+ print("Using", mode)
132
+ camera.start(mode)
133
+ frame = camera.capture()
134
+ camera.stop()
135
+
136
+ camera.save_jpeg("/saves/photo.jpg", frame)
137
+ print("Saved /saves/photo.jpg,", len(frame), "bytes")
138
+
139
+ Documentation
140
+ =============
141
+ API documentation for this library can be found on `Read the Docs <https://docs.circuitpython.org/projects/usb_host_camera/en/latest/>`_.
142
+
143
+ For information on building library documentation, please check out
144
+ `this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
145
+
146
+ Contributing
147
+ ============
148
+
149
+ Contributions are welcome! Please read our `Code of Conduct
150
+ <https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera/blob/HEAD/CODE_OF_CONDUCT.md>`_
151
+ before contributing to help this project stay welcoming.
@@ -0,0 +1,6 @@
1
+ adafruit_usb_host_camera.py,sha256=CFZF3Fys28nkw8g6LbZUPnlWL_D32c6R9C2-zWyiFU0,23690
2
+ adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/licenses/LICENSE,sha256=IrDmGxfX4xdm-jZka6acotFMss63SMR3pHWyFp7jA3o,1100
3
+ adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/METADATA,sha256=lu1NuXKIwXHV1NsvoGDQMzkoENkXSVMJx-se_mkVGis,4832
4
+ adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/top_level.txt,sha256=UfDIw7qlHytH3MCtD0hB2hMkuEIcqpbSsFJ3ziCyZrE,25
6
+ adafruit_circuitpython_usb_host_camera-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Tim Cocks for Adafruit Industries
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ adafruit_usb_host_camera
@@ -0,0 +1,587 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ `adafruit_usb_host_camera`
6
+ ================================================================================
7
+
8
+ CircuitPython USB host driver for UVC (USB Video Class) cameras.
9
+
10
+ Negotiates a video format with the camera, then reads whole frames. MJPEG
11
+ frames are complete JPEG images (see `UVCCamera.add_huffman_tables`) that can
12
+ be saved or decoded with ``jpegio``. YUY2 (uncompressed) frames are raw 4:2:2
13
+ pixels.
14
+
15
+ * Author(s): Tim Cocks
16
+
17
+ Implementation Notes
18
+ --------------------
19
+
20
+ **Hardware:**
21
+
22
+ * `Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_
23
+
24
+ **Software and Dependencies:**
25
+
26
+ * Adafruit CircuitPython firmware for the supported boards:
27
+ https://circuitpython.org/downloads
28
+
29
+ """
30
+
31
+ import struct
32
+ import time
33
+
34
+ import adafruit_usb_host_descriptors
35
+ import usb.core
36
+ from micropython import const
37
+
38
+ try:
39
+ import displayio
40
+ import jpegio
41
+ except ImportError:
42
+ jpegio = None # only UVCCamera.bitmap needs these
43
+
44
+ __version__ = "1.0.0"
45
+ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Camera.git"
46
+
47
+ _DIR_IN = const(0x80)
48
+ _REQ_CLASS_IF_OUT = const(0x21)
49
+ _REQ_CLASS_IF_IN = const(0xA1)
50
+ _REQ_STD_IF_OUT = const(0x01)
51
+ _REQ_SET_INTERFACE = const(11)
52
+ _SET_CUR = const(0x01)
53
+ _GET_CUR = const(0x81)
54
+
55
+ _VS_PROBE_CONTROL = const(0x01)
56
+ _VS_COMMIT_CONTROL = const(0x02)
57
+
58
+ _CLASS_VIDEO = const(0x0E)
59
+ _SUBCLASS_VC = const(1)
60
+ _SUBCLASS_VS = const(2)
61
+ _CS_INTERFACE = const(0x24)
62
+ _VC_HEADER = const(0x01)
63
+ _VS_FORMAT_UNCOMPRESSED = const(0x04)
64
+ _VS_FRAME_UNCOMPRESSED = const(0x05)
65
+ _VS_FORMAT_MJPEG = const(0x06)
66
+ _VS_FRAME_MJPEG = const(0x07)
67
+
68
+ # Payload header bmHeaderInfo bits
69
+ _HDR_FID = const(0x01)
70
+ _HDR_EOF = const(0x02)
71
+ _HDR_ERR = const(0x40)
72
+
73
+ # Isochronous reads return records of a 16-bit length then that many bytes.
74
+ # This length marks packets the firmware could not keep.
75
+ _LOST = const(0xFFFF)
76
+
77
+ FORMAT_MJPEG = "MJPEG"
78
+ FORMAT_YUY2 = "YUY2"
79
+
80
+
81
+ class Mode:
82
+ """One frame size and format the camera offers."""
83
+
84
+ def __init__(self, format_type, format_index, frame_index, width, height, interval, max_size):
85
+ self.format = format_type
86
+ """``FORMAT_MJPEG``, ``FORMAT_YUY2``, or the raw character code"""
87
+ self.format_index = format_index
88
+ self.frame_index = frame_index
89
+ self.width = width
90
+ self.height = height
91
+ self.interval = interval
92
+ """Default frame interval in 100 ns units"""
93
+ self.max_frame_size = max_size
94
+ """Largest frame in bytes, from the frame descriptor"""
95
+
96
+ def __repr__(self):
97
+ return f"<Mode {self.format} {self.width}x{self.height}>"
98
+
99
+
100
+ def find_camera():
101
+ """Return the first attached ``usb.core.Device`` that has a UVC streaming
102
+ interface, or None. Other devices (keyboards, mice, hubs...) are skipped."""
103
+ for device in usb.core.find(find_all=True):
104
+ try:
105
+ cfg = adafruit_usb_host_descriptors.get_configuration_descriptor(device, 0)
106
+ except usb.core.USBError:
107
+ continue # could not read it, so not one we can drive
108
+ i = 0
109
+ while i + 6 < len(cfg) and cfg[i]:
110
+ if (
111
+ cfg[i + 1] == adafruit_usb_host_descriptors.DESC_INTERFACE
112
+ and cfg[i + 5] == _CLASS_VIDEO
113
+ and cfg[i + 6] == _SUBCLASS_VS
114
+ ):
115
+ return device
116
+ i += cfg[i]
117
+ return None
118
+
119
+
120
+ class UVCCamera:
121
+ """A USB Video Class camera attached to the USB host port.
122
+
123
+ :param device: a ``usb.core.Device`` for the camera. The default is the
124
+ first camera `find_camera` sees, so other devices such as a keyboard
125
+ can share the host ports.
126
+ """
127
+
128
+ def __init__(self, device=None):
129
+ if device is None:
130
+ device = find_camera()
131
+ if device is None:
132
+ raise ValueError("No UVC camera found")
133
+ self.device = device
134
+ self.modes = []
135
+ """The `Mode` objects the camera offers"""
136
+ self.dropped_frames = 0
137
+ """Frames discarded because they were not valid JPEG data"""
138
+ self.lost_packets = 0
139
+ """Gaps in the stream: the firmware dropped its oldest packets because
140
+ `capture` was not called for too long, or a packet arrived corrupted"""
141
+ self.read_errors = 0
142
+ """Reads that failed outright"""
143
+ self.max_frame_size = 0
144
+ self._vc_interface = None
145
+ self._vs_interface = None
146
+ self._uvc_version = 0x0100
147
+ # (alternate setting, endpoint address, is_bulk, max packet size)
148
+ self._endpoints = []
149
+ self._parse(adafruit_usb_host_descriptors.get_configuration_descriptor(device, 0))
150
+ if self._vs_interface is None or not self._endpoints:
151
+ raise ValueError("No UVC streaming interface found")
152
+ device.set_configuration()
153
+ self._streaming = None
154
+ self._ep = None
155
+ self._bulk = False
156
+ self._read_buf = None
157
+ self._frame = None
158
+ self._bitmap = None
159
+ self._decoder = None
160
+
161
+ def _parse(self, cfg):
162
+ i = 0
163
+ kind = None
164
+ format_type = None
165
+ format_index = 0
166
+ alt = 0
167
+ while i < len(cfg):
168
+ length = cfg[i]
169
+ if length == 0:
170
+ break
171
+ dtype = cfg[i + 1]
172
+ if dtype == adafruit_usb_host_descriptors.DESC_INTERFACE:
173
+ kind = None
174
+ if cfg[i + 5] == _CLASS_VIDEO:
175
+ kind = cfg[i + 6]
176
+ if kind == _SUBCLASS_VC and self._vc_interface is None:
177
+ self._vc_interface = cfg[i + 2]
178
+ elif kind == _SUBCLASS_VS and self._vs_interface is None:
179
+ self._vs_interface = cfg[i + 2]
180
+ elif kind == _SUBCLASS_VS and cfg[i + 2] != self._vs_interface:
181
+ kind = None # only drive the first streaming interface
182
+ alt = cfg[i + 3]
183
+ elif dtype == adafruit_usb_host_descriptors.DESC_ENDPOINT and kind == _SUBCLASS_VS:
184
+ address = cfg[i + 2]
185
+ if address & _DIR_IN:
186
+ mps = cfg[i + 4] | cfg[i + 5] << 8
187
+ size = (mps & 0x7FF) * (((mps >> 11) & 3) + 1)
188
+ self._endpoints.append((alt, address, cfg[i + 3] & 3 == 2, size))
189
+ elif dtype == _CS_INTERFACE and kind == _SUBCLASS_VC:
190
+ if cfg[i + 2] == _VC_HEADER:
191
+ self._uvc_version = cfg[i + 3] | cfg[i + 4] << 8
192
+ elif dtype == _CS_INTERFACE and kind == _SUBCLASS_VS:
193
+ sub = cfg[i + 2]
194
+ if sub == _VS_FORMAT_MJPEG:
195
+ format_type = FORMAT_MJPEG
196
+ format_index = cfg[i + 3]
197
+ elif sub == _VS_FORMAT_UNCOMPRESSED:
198
+ format_type = bytes(cfg[i + 5 : i + 9]).decode()
199
+ format_index = cfg[i + 3]
200
+ elif sub in {_VS_FRAME_MJPEG, _VS_FRAME_UNCOMPRESSED}:
201
+ width, height = struct.unpack_from("<HH", cfg, i + 5)
202
+ max_size, interval = struct.unpack_from("<II", cfg, i + 17)
203
+ self.modes.append(
204
+ Mode(
205
+ format_type,
206
+ format_index,
207
+ cfg[i + 3],
208
+ width,
209
+ height,
210
+ interval,
211
+ max_size,
212
+ )
213
+ )
214
+ i += length
215
+
216
+ def find_mode(self, width=None, height=None, format_type=FORMAT_MJPEG):
217
+ """Return the `Mode` matching ``format_type`` whose size is closest to
218
+ ``width`` x ``height`` (smallest available when no size is given)."""
219
+ best = None
220
+ best_score = None
221
+ for mode in self.modes:
222
+ if mode.format != format_type:
223
+ continue
224
+ if width is None:
225
+ score = mode.width * mode.height
226
+ else:
227
+ score = abs(mode.width - width) + abs(mode.height - height)
228
+ if best is None or score < best_score:
229
+ best = mode
230
+ best_score = score
231
+ return best
232
+
233
+ def _probe(self, request, selector, data):
234
+ bm = _REQ_CLASS_IF_IN if request & 0x80 else _REQ_CLASS_IF_OUT
235
+ self.device.ctrl_transfer(bm, request, selector << 8, self._vs_interface, data)
236
+
237
+ def start(self, mode, max_packet_size=None, frame_interval=None, read_size=8192):
238
+ """Negotiate ``mode`` with the camera and start streaming.
239
+
240
+ :param Mode mode: the format and frame size to stream
241
+ :param int max_packet_size: largest isochronous packet to accept per
242
+ 1 ms USB frame, which limits the bandwidth the camera may use
243
+ :param int frame_interval: frame interval in 100 ns units, overriding
244
+ the mode's default. It must be one the camera supports, for
245
+ example 2000000 for 5 fps.
246
+ :param int read_size: bytes to request per isochronous read
247
+ """
248
+ size = 26 if self._uvc_version < 0x0110 else 34
249
+ probe = bytearray(size)
250
+ interval = frame_interval or mode.interval
251
+ struct.pack_into("<HBBI", probe, 0, 1, mode.format_index, mode.frame_index, interval)
252
+ self._probe(_SET_CUR, _VS_PROBE_CONTROL, probe)
253
+ self._probe(_GET_CUR, _VS_PROBE_CONTROL, probe)
254
+ self._probe(_SET_CUR, _VS_COMMIT_CONTROL, probe)
255
+ negotiated, max_payload = struct.unpack_from("<II", probe, 18)
256
+ self.max_frame_size = max(negotiated, mode.max_frame_size)
257
+
258
+ # Bulk cameras stream on alternate setting 0. For isochronous ones use
259
+ # the smallest allowed alternate setting whose packets hold the
260
+ # camera's payload, or the largest allowed one if none do.
261
+ choice = None
262
+ for ep in self._endpoints:
263
+ packet = ep[3]
264
+ if ep[2]:
265
+ choice = ep
266
+ break
267
+ if max_packet_size and packet > max_packet_size:
268
+ continue
269
+ if choice is None:
270
+ choice = ep
271
+ elif choice[3] < max_payload:
272
+ if packet > choice[3]:
273
+ choice = ep
274
+ elif max_payload <= packet < choice[3]:
275
+ choice = ep
276
+ if choice is None:
277
+ raise ValueError("No streaming endpoint fits max_packet_size")
278
+ alt, self._ep, self._bulk, packet = choice
279
+ if not self._bulk or alt:
280
+ self.device.ctrl_transfer(
281
+ _REQ_STD_IF_OUT, _REQ_SET_INTERFACE, alt, self._vs_interface, None
282
+ )
283
+ # A bulk read ends at each payload boundary (short packet), so the
284
+ # buffer must hold a whole payload. Isochronous reads return whole
285
+ # records, so a read that leaves room for a largest record means the
286
+ # firmware had nothing more buffered (see capture()).
287
+ self._slack = packet + 2
288
+ size = max_payload if self._bulk else max(read_size, 2 * self._slack)
289
+ if self._read_buf is None or len(self._read_buf) != size:
290
+ self._read_buf = bytearray(size)
291
+ if self._frame is None or len(self._frame) < self.max_frame_size:
292
+ self._frame = None
293
+ self._frame = bytearray(self.max_frame_size)
294
+ self._pos = 0
295
+ self._count = 0
296
+ self._records = False
297
+ self._caught_up = False
298
+ self._fid = None # None until synchronized to a frame boundary
299
+ self._reset_assembly(None)
300
+ self._streaming = mode
301
+
302
+ def _reset_assembly(self, buffer):
303
+ self._out = buffer # the buffer the state below refers to
304
+ self._n = 0 # bytes of the frame being assembled
305
+ self._base = 0 # where it starts in the frame buffer
306
+ self._good = True
307
+ self._done = 0 # length of the complete frame just before it
308
+
309
+ def stop(self):
310
+ """Stop streaming."""
311
+ if self._streaming is None:
312
+ return
313
+ self.device.ctrl_transfer(_REQ_STD_IF_OUT, _REQ_SET_INTERFACE, 0, self._vs_interface, None)
314
+ self._streaming = None
315
+
316
+ def capture(self, buffer=None, timeout=2):
317
+ """Return the newest complete frame.
318
+
319
+ The firmware buffers about 200 ms of an isochronous stream. When the
320
+ caller takes longer than a frame interval between calls, the frames
321
+ that piled up meanwhile are skipped and the newest complete one is
322
+ returned, so the caller never falls behind until that buffer
323
+ overflows. Nothing counts the skipped frames; to get every frame,
324
+ keep up with the camera or ask it for a lower rate with the
325
+ ``frame_interval`` argument of `start`.
326
+
327
+ :param bytearray buffer: optional buffer to fill; it must hold
328
+ ``max_frame_size`` bytes and be the same object on every call.
329
+ The default is an internal buffer that the next call overwrites.
330
+ :param float timeout: seconds to wait for a whole frame
331
+ :return: a memoryview of the frame data, at the start of the buffer
332
+ """
333
+ mode = self._streaming
334
+ if mode is None:
335
+ raise RuntimeError("call start() first")
336
+ out = memoryview(buffer if buffer is not None else self._frame)
337
+ if buffer is not self._out:
338
+ self._reset_assembly(buffer)
339
+ self._fid = None
340
+ check_jpeg = mode.format == FORMAT_MJPEG
341
+ buf = self._read_buf
342
+ view = memoryview(buf)
343
+ size = len(out)
344
+ deadline = time.monotonic() + timeout
345
+ # A complete frame occupies out[base - done:base] while the next one
346
+ # assembles at out[base:base + n].
347
+ n = self._n
348
+ base = self._base
349
+ good = self._good
350
+ done = self._done
351
+ while True:
352
+ if self._pos >= self._count:
353
+ if done and self._caught_up:
354
+ break
355
+ if time.monotonic() > deadline:
356
+ self._n, self._base, self._good, self._done = n, base, good, done
357
+ raise RuntimeError("Timed out waiting for a frame")
358
+ self._pos = self._count = 0
359
+ try:
360
+ count = self.device.read(self._ep, buf, 100)
361
+ except usb.core.USBTimeoutError:
362
+ continue
363
+ except usb.core.USBError:
364
+ good = False # data lost
365
+ self.read_errors += 1
366
+ continue
367
+ if count < 2:
368
+ continue
369
+ # A raw UVC payload starts with the header length and then
370
+ # header flags with the end-of-header bit (0x80) set, so it never
371
+ # looks like a record length.
372
+ first = buf[0] | buf[1] << 8
373
+ self._records = not self._bulk and (first < 0x8000 or first == _LOST)
374
+ self._count = count
375
+ self._caught_up = count + self._slack <= len(buf)
376
+
377
+ pos = self._pos
378
+ if self._records:
379
+ length = buf[pos] | buf[pos + 1] << 8
380
+ pos += 2
381
+ if length == _LOST:
382
+ # Packets are missing here, so the frame in progress is
383
+ # unusable and what follows may be the tail of a later
384
+ # one. Resynchronize at the next end of frame.
385
+ self._pos = pos
386
+ self.lost_packets += 1
387
+ n = 0
388
+ good = True
389
+ self._fid = None
390
+ continue
391
+ end = pos + length
392
+ else:
393
+ end = self._count
394
+ if end - pos < 2 or buf[pos] > end - pos:
395
+ self._pos = end
396
+ continue
397
+ header_len = buf[pos]
398
+ info = buf[pos + 1]
399
+ fid = info & _HDR_FID
400
+ if self._fid is None:
401
+ # Skip the rest of the frame in progress when streaming began.
402
+ self._pos = end
403
+ if info & _HDR_EOF:
404
+ self._fid = fid ^ 1
405
+ continue
406
+ if fid != self._fid:
407
+ self._fid = fid
408
+ if n:
409
+ # A new frame began without an EOF on the last one. Finish
410
+ # that frame and handle this payload on the next pass.
411
+ if good and self._complete(out, base, n, check_jpeg):
412
+ done = n
413
+ base += n
414
+ n = 0
415
+ good = True
416
+ # Only look for a newer frame when another one of
417
+ # this size fits behind it and reads return promptly.
418
+ if not self._records or 2 * done > size:
419
+ break
420
+ continue
421
+ n = 0
422
+ good = True
423
+ continue
424
+ self._pos = end
425
+ if info & _HDR_ERR:
426
+ good = False
427
+ data = end - pos - header_len
428
+ if data > 0 and good:
429
+ if base + n + data > size:
430
+ good = False
431
+ else:
432
+ # Assigning into a slice of a big buffer moves everything
433
+ # after the slice (about 19 ms for a 150 KB frame buffer);
434
+ # filling a view of just the right size does not.
435
+ out[base + n : base + n + data][:] = view[pos + header_len : end]
436
+ n += data
437
+ if info & _HDR_EOF:
438
+ self._fid = fid ^ 1
439
+ if good and self._complete(out, base, n, check_jpeg):
440
+ done = n
441
+ base += n
442
+ if not self._records or 2 * done > size:
443
+ n = 0
444
+ good = True
445
+ break
446
+ n = 0
447
+ good = True
448
+ # Move the complete frame to the start of the buffer, and the frame in
449
+ # progress right behind it.
450
+ start = base - done
451
+ if start:
452
+ out[0:done][:] = out[start:base]
453
+ if n:
454
+ out[done : done + n][:] = out[base : base + n]
455
+ base = done
456
+ self._n, self._base, self._good, self._done = n, base, good, 0
457
+ return out[:done]
458
+
459
+ def _complete(self, out, base, n, check_jpeg):
460
+ if n == 0:
461
+ return False
462
+ if check_jpeg and (n < 4 or out[base] != 0xFF or out[base + 1] != 0xD8):
463
+ self.dropped_frames += 1
464
+ return False
465
+ return True
466
+
467
+ @staticmethod
468
+ def add_huffman_tables(frame):
469
+ """Return an MJPEG frame as a standalone JPEG.
470
+
471
+ MJPEG cameras usually leave out the Huffman tables, which most decoders
472
+ and image viewers need. This inserts the standard ones when missing.
473
+
474
+ :param frame: the frame from `capture`
475
+ :return: ``bytes`` of a complete JPEG image
476
+ """
477
+ if _has_dht(frame):
478
+ return bytes(frame)
479
+ return b"\xff\xd8" + _DHT + bytes(frame[2:])
480
+
481
+ def save_jpeg(self, path, frame=None):
482
+ """Write an MJPEG frame to ``path`` as a standalone JPEG file.
483
+
484
+ :param str path: the file to write
485
+ :param frame: the frame from `capture`. The default captures a new
486
+ one, which needs the camera to be streaming.
487
+ """
488
+ if frame is None:
489
+ frame = self.capture()
490
+ with open(path, "wb") as f:
491
+ if _has_dht(frame):
492
+ f.write(frame)
493
+ else:
494
+ f.write(b"\xff\xd8")
495
+ f.write(_DHT)
496
+ f.write(frame[2:])
497
+
498
+ @property
499
+ def bitmap(self):
500
+ """A ``displayio.Bitmap`` of the camera image.
501
+
502
+ The first access captures a frame and creates the bitmap at its size;
503
+ later ones return the same bitmap unchanged. Call `update_bitmap` to
504
+ show a new frame in it. Show it with a
505
+ ``displayio.ColorConverter(input_colorspace=displayio.Colorspace.RGB565_SWAPPED)``
506
+ pixel shader.
507
+ """
508
+ if self._bitmap is None:
509
+ self.update_bitmap()
510
+ return self._bitmap
511
+
512
+ def update_bitmap(self):
513
+ """Capture a frame with `capture` and decode it into `bitmap`,
514
+ creating the bitmap the first time. A later, larger mode is shrunk by
515
+ halves to fit.
516
+
517
+ Needs an MJPEG mode and the ``jpegio`` module.
518
+
519
+ :return: the `bitmap`
520
+ """
521
+ mode = self._streaming
522
+ if mode is None:
523
+ raise RuntimeError("call start() first")
524
+ if mode.format != FORMAT_MJPEG:
525
+ raise ValueError("bitmap needs an MJPEG mode")
526
+ if jpegio is None:
527
+ raise RuntimeError("bitmap needs jpegio")
528
+ if self._decoder is None:
529
+ self._decoder = jpegio.JpegDecoder()
530
+ width, height = self._decoder.open(self.add_huffman_tables(self.capture()))
531
+ bitmap = self._bitmap
532
+ if bitmap is None:
533
+ bitmap = self._bitmap = displayio.Bitmap(width, height, 65536)
534
+ scale = 0
535
+ while (width >> scale) > bitmap.width or (height >> scale) > bitmap.height:
536
+ scale += 1
537
+ self._decoder.decode(bitmap, scale=scale)
538
+ return bitmap
539
+
540
+
541
+ # The standard Huffman tables that MJPEG streams leave out (ITU-T T.81 K.3),
542
+ # as one DHT segment.
543
+ _DHT = (
544
+ b"\xff\xc4\x01\xa2"
545
+ b"\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00"
546
+ b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b"
547
+ b"\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00"
548
+ b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b"
549
+ b"\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01\x7d"
550
+ b"\x01\x02\x03\x00\x04\x11\x05\x12\x21\x31\x41\x06\x13\x51\x61\x07"
551
+ b"\x22\x71\x14\x32\x81\x91\xa1\x08\x23\x42\xb1\xc1\x15\x52\xd1\xf0"
552
+ b"\x24\x33\x62\x72\x82\x09\x0a\x16\x17\x18\x19\x1a\x25\x26\x27\x28"
553
+ b"\x29\x2a\x34\x35\x36\x37\x38\x39\x3a\x43\x44\x45\x46\x47\x48\x49"
554
+ b"\x4a\x53\x54\x55\x56\x57\x58\x59\x5a\x63\x64\x65\x66\x67\x68\x69"
555
+ b"\x6a\x73\x74\x75\x76\x77\x78\x79\x7a\x83\x84\x85\x86\x87\x88\x89"
556
+ b"\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7"
557
+ b"\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5"
558
+ b"\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2"
559
+ b"\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8"
560
+ b"\xf9\xfa"
561
+ b"\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02\x77"
562
+ b"\x00\x01\x02\x03\x11\x04\x05\x21\x31\x06\x12\x41\x51\x07\x61\x71"
563
+ b"\x13\x22\x32\x81\x08\x14\x42\x91\xa1\xb1\xc1\x09\x23\x33\x52\xf0"
564
+ b"\x15\x62\x72\xd1\x0a\x16\x24\x34\xe1\x25\xf1\x17\x18\x19\x1a\x26"
565
+ b"\x27\x28\x29\x2a\x35\x36\x37\x38\x39\x3a\x43\x44\x45\x46\x47\x48"
566
+ b"\x49\x4a\x53\x54\x55\x56\x57\x58\x59\x5a\x63\x64\x65\x66\x67\x68"
567
+ b"\x69\x6a\x73\x74\x75\x76\x77\x78\x79\x7a\x82\x83\x84\x85\x86\x87"
568
+ b"\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5"
569
+ b"\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3"
570
+ b"\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda"
571
+ b"\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf2\xf3\xf4\xf5\xf6\xf7\xf8"
572
+ b"\xf9\xfa"
573
+ )
574
+
575
+
576
+ def _has_dht(frame):
577
+ """Scan the JPEG marker segments before the scan data for a DHT."""
578
+ i = 2
579
+ n = len(frame)
580
+ while i + 4 <= n and frame[i] == 0xFF:
581
+ marker = frame[i + 1]
582
+ if marker == 0xC4:
583
+ return True
584
+ if marker == 0xDA:
585
+ return False
586
+ i += 2 + (frame[i + 2] << 8 | frame[i + 3])
587
+ return False