supercamera 0.1.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,6 @@
1
+ """Python driver for USB endoscopes using the supercamera/useeplus protocol."""
2
+
3
+ from supercamera.camera import Camera, CameraInfo, list_devices
4
+ from supercamera.validate import is_valid_jpeg
5
+
6
+ __all__ = ["Camera", "CameraInfo", "list_devices", "is_valid_jpeg"]
supercamera/camera.py ADDED
@@ -0,0 +1,344 @@
1
+ """USB communication with supercamera devices (Oasis, Depstech, etc.)."""
2
+
3
+ import usb.core
4
+ import usb.util
5
+ import time
6
+
7
+ # Known vendor/product ID pairs for supercamera devices
8
+ KNOWN_DEVICES = [
9
+ (0x2CE3, 0x3828),
10
+ (0x0329, 0x2022),
11
+ ]
12
+
13
+ # Interface 1 (com.useeplus.protocol) endpoints
14
+ EP_OUT = 0x01
15
+ EP_IN = 0x81
16
+
17
+ # Interface 0 (iAP) endpoints
18
+ EP_IAP_OUT = 0x02
19
+ EP_IAP_IN = 0x82
20
+
21
+ # Protocol commands (reverse-engineered from C++ PoC and Linux driver)
22
+ MAGIC_INIT = bytes([0xFF, 0x55, 0xFF, 0x55, 0xEE, 0x10])
23
+ CONNECT_CMD = bytes([0xBB, 0xAA, 0x05, 0x00, 0x00])
24
+
25
+ HEADER_SIZE = 12
26
+ JPEG_SOI = bytes([0xFF, 0xD8])
27
+ JPEG_EOI = bytes([0xFF, 0xD9])
28
+
29
+
30
+ class CameraInfo:
31
+ """Info about a detected supercamera device."""
32
+
33
+ def __init__(self, usb_dev):
34
+ self._dev = usb_dev
35
+
36
+ @property
37
+ def vendor_id(self):
38
+ return self._dev.idVendor
39
+
40
+ @property
41
+ def product_id(self):
42
+ return self._dev.idProduct
43
+
44
+ @property
45
+ def serial_number(self):
46
+ try:
47
+ return self._dev.serial_number
48
+ except Exception:
49
+ return None
50
+
51
+ @property
52
+ def manufacturer(self):
53
+ try:
54
+ return self._dev.manufacturer
55
+ except Exception:
56
+ return None
57
+
58
+ @property
59
+ def product(self):
60
+ try:
61
+ return self._dev.product
62
+ except Exception:
63
+ return None
64
+
65
+ @property
66
+ def bus(self):
67
+ return self._dev.bus
68
+
69
+ @property
70
+ def address(self):
71
+ return self._dev.address
72
+
73
+ def __repr__(self):
74
+ return (
75
+ f"CameraInfo({self.vendor_id:04x}:{self.product_id:04x} "
76
+ f"serial={self.serial_number!r} bus={self.bus} addr={self.address})"
77
+ )
78
+
79
+
80
+ def list_devices():
81
+ """Find all connected supercamera devices.
82
+
83
+ Returns:
84
+ list[CameraInfo]: Info about each detected device.
85
+ """
86
+ found = []
87
+ for vid, pid in KNOWN_DEVICES:
88
+ devs = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
89
+ for d in devs:
90
+ found.append(CameraInfo(d))
91
+ return found
92
+
93
+
94
+ class Camera:
95
+ """USB endoscope camera using the useeplus/supercamera protocol.
96
+
97
+ Works as a drop-in for cv2.VideoCapture:
98
+
99
+ from supercamera import Camera
100
+ cam = Camera()
101
+ ret, frame = cam.read() # returns numpy array (requires opencv-python)
102
+ cam.release()
103
+
104
+ Or use as a context manager:
105
+
106
+ with Camera() as cam:
107
+ ret, frame = cam.read()
108
+
109
+ For raw JPEG bytes without OpenCV dependency:
110
+
111
+ cam = Camera()
112
+ jpeg_bytes = cam.read_jpeg()
113
+ cam.release()
114
+
115
+ With multiple cameras, select by serial number or index:
116
+
117
+ cameras = supercamera.list_devices()
118
+ cam = Camera(serial="022018050100030")
119
+ cam = Camera(index=1) # second camera
120
+ """
121
+
122
+ def __init__(self, serial=None, index=0, timeout=5.0):
123
+ """Open a supercamera device.
124
+
125
+ Args:
126
+ serial: Serial number string to match a specific camera.
127
+ index: Which camera to open if multiple are found (0-based).
128
+ Ignored if serial is provided.
129
+ timeout: Seconds to wait for a frame before giving up.
130
+ """
131
+ self._dev = None
132
+ self._streaming = False
133
+ self._frames_read = 0
134
+ self._serial = serial
135
+ self._index = index
136
+ self._timeout = timeout
137
+ self._open()
138
+
139
+ def _find_device(self):
140
+ all_devs = []
141
+ for vid, pid in KNOWN_DEVICES:
142
+ devs = list(usb.core.find(find_all=True, idVendor=vid, idProduct=pid))
143
+ all_devs.extend(devs)
144
+
145
+ if not all_devs:
146
+ raise RuntimeError(
147
+ "No supercamera devices found. Is it plugged in?\n"
148
+ "Known USB IDs: " + ", ".join(f"{v:04x}:{p:04x}" for v, p in KNOWN_DEVICES)
149
+ )
150
+
151
+ if self._serial is not None:
152
+ for dev in all_devs:
153
+ try:
154
+ if dev.serial_number == self._serial:
155
+ return dev
156
+ except Exception:
157
+ continue
158
+ serials = []
159
+ for dev in all_devs:
160
+ try:
161
+ serials.append(dev.serial_number)
162
+ except Exception:
163
+ serials.append("???")
164
+ raise RuntimeError(
165
+ f"No camera with serial {self._serial!r} found. "
166
+ f"Available: {serials}"
167
+ )
168
+
169
+ if self._index >= len(all_devs):
170
+ raise RuntimeError(
171
+ f"Camera index {self._index} out of range. "
172
+ f"Found {len(all_devs)} device(s)."
173
+ )
174
+
175
+ return all_devs[self._index]
176
+
177
+ def _open(self):
178
+ dev = self._find_device()
179
+ self._dev = dev
180
+
181
+ # Detach kernel drivers
182
+ for intf in [0, 1]:
183
+ try:
184
+ if dev.is_kernel_driver_active(intf):
185
+ dev.detach_kernel_driver(intf)
186
+ except Exception:
187
+ pass
188
+
189
+ dev.set_configuration()
190
+ usb.util.claim_interface(dev, 0)
191
+ usb.util.claim_interface(dev, 1)
192
+
193
+ # Drain pending heartbeat data from iAP interface
194
+ for _ in range(30):
195
+ try:
196
+ dev.read(EP_IAP_IN, 512, timeout=100)
197
+ except usb.core.USBError:
198
+ break
199
+
200
+ # Activate bulk endpoints on interface 1
201
+ dev.set_interface_altsetting(interface=1, alternate_setting=1)
202
+ dev.clear_halt(EP_OUT)
203
+
204
+ # Send init sequence
205
+ dev.write(EP_IAP_OUT, MAGIC_INIT, timeout=1000)
206
+ dev.write(EP_OUT, CONNECT_CMD, timeout=1000)
207
+ time.sleep(0.3)
208
+
209
+ self._streaming = True
210
+ self._frames_read = 0
211
+
212
+ # Skip first frame (always partial/corrupt after connect)
213
+ self._read_jpeg_internal()
214
+
215
+ def _read_jpeg_internal(self):
216
+ """Read one complete JPEG frame from USB. Returns bytes or None."""
217
+ buf = bytearray()
218
+ in_frame = False
219
+ deadline = time.monotonic() + self._timeout
220
+
221
+ while time.monotonic() < deadline:
222
+ try:
223
+ data = bytes(self._dev.read(EP_IN, 65536, timeout=1000))
224
+ except usb.core.USBError:
225
+ continue
226
+
227
+ # Strip 12-byte protocol header
228
+ payload = data
229
+ if len(data) >= HEADER_SIZE and data[0] == 0xAA and data[1] == 0xBB:
230
+ payload = data[HEADER_SIZE:]
231
+
232
+ soi_pos = payload.find(JPEG_SOI)
233
+ if soi_pos >= 0 and not in_frame:
234
+ in_frame = True
235
+ buf = bytearray(payload[soi_pos:])
236
+ elif in_frame:
237
+ buf.extend(payload)
238
+
239
+ if in_frame:
240
+ eoi_pos = buf.find(JPEG_EOI)
241
+ if eoi_pos >= 0:
242
+ self._frames_read += 1
243
+ return bytes(buf[:eoi_pos + 2])
244
+
245
+ return None
246
+
247
+ def read_jpeg(self):
248
+ """Read one JPEG frame as raw bytes.
249
+
250
+ Returns:
251
+ bytes or None: JPEG data, or None if read failed.
252
+ """
253
+ if not self._streaming:
254
+ raise RuntimeError("Camera is not streaming. Call open() or use Camera().")
255
+ return self._read_jpeg_internal()
256
+
257
+ def read(self):
258
+ """Read one frame as a numpy array (BGR, like OpenCV).
259
+
260
+ Returns:
261
+ tuple: (success: bool, frame: numpy.ndarray or None)
262
+ """
263
+ try:
264
+ import cv2
265
+ import numpy as np
266
+ except ImportError:
267
+ raise ImportError(
268
+ "opencv-python and numpy are required for read(). "
269
+ "Install them with: pip install opencv-python numpy\n"
270
+ "Or use read_jpeg() for raw JPEG bytes without extra dependencies."
271
+ )
272
+
273
+ jpeg = self.read_jpeg()
274
+ if jpeg is None:
275
+ return False, None
276
+
277
+ arr = np.frombuffer(jpeg, dtype=np.uint8)
278
+ frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
279
+ if frame is None:
280
+ return False, None
281
+ return True, frame
282
+
283
+ def release(self):
284
+ """Stop streaming and release the USB device."""
285
+ if self._dev is None:
286
+ return
287
+
288
+ if self._streaming:
289
+ try:
290
+ self._dev.set_interface_altsetting(interface=1, alternate_setting=0)
291
+ except Exception:
292
+ pass
293
+ self._streaming = False
294
+
295
+ for intf in [1, 0]:
296
+ try:
297
+ usb.util.release_interface(self._dev, intf)
298
+ except Exception:
299
+ pass
300
+
301
+ try:
302
+ self._dev.reset()
303
+ except Exception:
304
+ pass
305
+
306
+ self._dev = None
307
+
308
+ @property
309
+ def serial_number(self):
310
+ if self._dev is None:
311
+ return None
312
+ try:
313
+ return self._dev.serial_number
314
+ except Exception:
315
+ return None
316
+
317
+ @property
318
+ def bus(self):
319
+ return self._dev.bus if self._dev else None
320
+
321
+ @property
322
+ def address(self):
323
+ return self._dev.address if self._dev else None
324
+
325
+ @property
326
+ def is_opened(self):
327
+ return self._streaming and self._dev is not None
328
+
329
+ @property
330
+ def resolution(self):
331
+ return (640, 480)
332
+
333
+ @property
334
+ def frames_read(self):
335
+ return self._frames_read
336
+
337
+ def __enter__(self):
338
+ return self
339
+
340
+ def __exit__(self, *args):
341
+ self.release()
342
+
343
+ def __del__(self):
344
+ self.release()
supercamera/cli.py ADDED
@@ -0,0 +1,120 @@
1
+ """CLI tool to capture frames from a supercamera USB endoscope."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(
9
+ description="Capture JPEG frames from a USB endoscope (supercamera/useeplus protocol)"
10
+ )
11
+ parser.add_argument(
12
+ "-n", "--num-frames", type=int, default=1,
13
+ help="Number of frames to capture (default: 1)"
14
+ )
15
+ parser.add_argument(
16
+ "-o", "--output", default="frame",
17
+ help="Output filename prefix (default: 'frame')"
18
+ )
19
+ parser.add_argument(
20
+ "-s", "--serial", default=None,
21
+ help="Serial number of the camera to use"
22
+ )
23
+ parser.add_argument(
24
+ "-i", "--index", type=int, default=0,
25
+ help="Camera index if multiple are connected (default: 0)"
26
+ )
27
+ parser.add_argument(
28
+ "-l", "--list", action="store_true",
29
+ help="List connected cameras and exit"
30
+ )
31
+ parser.add_argument(
32
+ "--show", action="store_true",
33
+ help="Display frames using OpenCV (requires opencv-python)"
34
+ )
35
+ args = parser.parse_args()
36
+
37
+ if args.list:
38
+ _list_cameras()
39
+ return
40
+
41
+ from supercamera import Camera
42
+
43
+ try:
44
+ cam = Camera(serial=args.serial, index=args.index)
45
+ except RuntimeError as e:
46
+ print(f"Error: {e}", file=sys.stderr)
47
+ sys.exit(1)
48
+
49
+ print(f"Connected: {cam.resolution[0]}x{cam.resolution[1]} (serial: {cam.serial_number})")
50
+
51
+ if args.show:
52
+ _live_view(cam)
53
+ else:
54
+ _capture(cam, args.num_frames, args.output)
55
+
56
+
57
+ def _list_cameras():
58
+ from supercamera import list_devices
59
+
60
+ cameras = list_devices()
61
+ if not cameras:
62
+ print("No supercamera devices found.")
63
+ sys.exit(1)
64
+
65
+ print(f"Found {len(cameras)} device(s):\n")
66
+ for i, cam in enumerate(cameras):
67
+ print(f" [{i}] {cam.vendor_id:04x}:{cam.product_id:04x}"
68
+ f" serial={cam.serial_number}"
69
+ f" ({cam.manufacturer} {cam.product})"
70
+ f" bus={cam.bus} addr={cam.address}")
71
+
72
+
73
+ def _capture(cam, num_frames, prefix):
74
+ try:
75
+ for i in range(1, num_frames + 1):
76
+ jpeg = cam.read_jpeg()
77
+ if jpeg is None:
78
+ print(f"Failed to read frame {i}", file=sys.stderr)
79
+ continue
80
+ if num_frames == 1:
81
+ fname = f"{prefix}.jpg"
82
+ else:
83
+ fname = f"{prefix}_{i:03d}.jpg"
84
+ with open(fname, "wb") as f:
85
+ f.write(jpeg)
86
+ print(f"Saved {fname} ({len(jpeg)} bytes)")
87
+ finally:
88
+ cam.release()
89
+
90
+
91
+ def _live_view(cam):
92
+ try:
93
+ import cv2
94
+ except ImportError:
95
+ print("Live view requires opencv-python: pip install opencv-python", file=sys.stderr)
96
+ sys.exit(1)
97
+
98
+ print("Live view — press 'q' to quit, 's' to save a frame")
99
+ saved = 0
100
+ try:
101
+ while True:
102
+ ret, frame = cam.read()
103
+ if not ret:
104
+ continue
105
+ cv2.imshow("supercamera", frame)
106
+ key = cv2.waitKey(1) & 0xFF
107
+ if key == ord("q"):
108
+ break
109
+ elif key == ord("s"):
110
+ saved += 1
111
+ fname = f"capture_{saved:03d}.jpg"
112
+ cv2.imwrite(fname, frame)
113
+ print(f"Saved {fname}")
114
+ finally:
115
+ cam.release()
116
+ cv2.destroyAllWindows()
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -0,0 +1,32 @@
1
+ """JPEG frame validation."""
2
+
3
+
4
+ def is_valid_jpeg(data):
5
+ """Check if JPEG data is structurally valid.
6
+
7
+ Checks SOI/EOI markers and attempts a full decode if Pillow is available.
8
+
9
+ Returns:
10
+ bool: True if the JPEG is valid.
11
+ """
12
+ if not data or len(data) < 4:
13
+ return False
14
+
15
+ # Must start with SOI and end with EOI
16
+ if data[0] != 0xFF or data[1] != 0xD8:
17
+ return False
18
+ if data[-2] != 0xFF or data[-1] != 0xD9:
19
+ return False
20
+
21
+ # Try full decode (catches truncation, corrupted huffman tables, etc.)
22
+ try:
23
+ from PIL import Image
24
+ import io
25
+ img = Image.open(io.BytesIO(data))
26
+ img.load()
27
+ except ImportError:
28
+ pass # No Pillow, markers-only check is the best we can do
29
+ except Exception:
30
+ return False
31
+
32
+ return True
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: supercamera
3
+ Version: 0.1.0
4
+ Summary: Python driver for USB endoscopes using the supercamera/useeplus protocol (Oasis, Depstech, etc.)
5
+ Project-URL: Homepage, https://github.com/Revise-Robotics/supercamera-endoscope
6
+ Project-URL: Issues, https://github.com/Revise-Robotics/supercamera-endoscope/issues
7
+ Author: g
8
+ License-Expression: MIT
9
+ Keywords: borescope,camera,endoscope,oasis,opencv,supercamera,usb,useeplus
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Multimedia :: Video :: Capture
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Requires-Python: >=3.8
20
+ Requires-Dist: pyusb>=1.2.0
21
+ Provides-Extra: all
22
+ Requires-Dist: numpy; extra == 'all'
23
+ Requires-Dist: opencv-python; extra == 'all'
24
+ Requires-Dist: pillow; extra == 'all'
25
+ Provides-Extra: opencv
26
+ Requires-Dist: numpy; extra == 'opencv'
27
+ Requires-Dist: opencv-python; extra == 'opencv'
28
+ Provides-Extra: validate
29
+ Requires-Dist: pillow; extra == 'validate'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # supercamera
33
+
34
+ Python driver for USB endoscopes that use the **supercamera / useeplus protocol** — the cheap endoscopes sold under brands like Oasis, Depstech, and others that only work with the "UseeePlus" mobile app.
35
+
36
+ These devices don't implement standard UVC, so they won't show up as webcams. This package talks to them directly over USB and gives you JPEG frames or numpy arrays.
37
+
38
+ ## Supported devices
39
+
40
+ | USB ID | Device name | Chip |
41
+ |--------|-------------|------|
42
+ | `2ce3:3828` | supercamera (Geek szitman) | Common |
43
+ | `0329:2022` | supercamera (variant) | Common |
44
+
45
+ Check yours with `lsusb` (Linux) or `system_profiler SPUSBDataType` (macOS).
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install supercamera
51
+ ```
52
+
53
+ For OpenCV/numpy support (`.read()` returns numpy arrays):
54
+
55
+ ```bash
56
+ pip install supercamera[opencv]
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### Python API
62
+
63
+ ```python
64
+ from supercamera import Camera
65
+
66
+ # Like cv2.VideoCapture
67
+ with Camera() as cam:
68
+ ret, frame = cam.read() # numpy array (BGR), requires opencv
69
+ # or
70
+ jpeg_bytes = cam.read_jpeg() # raw JPEG, no extra deps
71
+ ```
72
+
73
+ ### Multiple cameras
74
+
75
+ ```python
76
+ from supercamera import Camera, list_devices
77
+
78
+ list_devices() # returns list of CameraInfo with bus/address
79
+ Camera(index=0) # first camera
80
+ Camera(index=1) # second camera
81
+ ```
82
+
83
+ Note: these devices often share the same serial number, so use `index` or `bus`/`address` to distinguish them.
84
+
85
+ ### CLI
86
+
87
+ ```bash
88
+ supercamera --list # list connected cameras
89
+ supercamera # capture one frame
90
+ supercamera -n 10 # capture 10 frames
91
+ supercamera -i 1 # use second camera
92
+ supercamera --show # live view (requires opencv-python)
93
+ ```
94
+
95
+ ### Frame validation
96
+
97
+ ```python
98
+ from supercamera import is_valid_jpeg
99
+ is_valid_jpeg(jpeg_bytes) # True/False (uses Pillow full decode if available)
100
+ ```
101
+
102
+ ### OpenCV pipeline example
103
+
104
+ ```python
105
+ import cv2
106
+ from supercamera import Camera
107
+
108
+ cam = Camera()
109
+ while True:
110
+ ret, frame = cam.read()
111
+ if not ret:
112
+ continue
113
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
114
+ cv2.imshow("endoscope", gray)
115
+ if cv2.waitKey(1) & 0xFF == ord("q"):
116
+ break
117
+ cam.release()
118
+ ```
119
+
120
+ ## How it works
121
+
122
+ These endoscopes use a proprietary USB protocol (`com.useeplus.protocol`) instead of UVC. The driver:
123
+
124
+ 1. Claims USB interfaces, sends magic init (`ff 55 ff 55 ee 10`) and connect command (`bb aa 05 00 00`)
125
+ 2. Reads bulk USB packets containing JPEG-encoded 640x480 frames
126
+ 3. Properly resets the device on disconnect so it's ready for the next session
127
+
128
+ Resolution is **640x480** regardless of what the product listing claims.
129
+
130
+ ## Testing
131
+
132
+ Plug in one or both cameras, then:
133
+
134
+ ```bash
135
+ python test_camera.py
136
+ ```
137
+
138
+ Runs: device listing, single capture, 3x reconnect (no replug), OpenCV read, and two-camera simultaneous capture (if two connected).
139
+
140
+ ## Platform notes
141
+
142
+ - **macOS**: Works out of the box. No kernel extensions needed.
143
+ - **Linux**: Works. You may need a udev rule for non-root access:
144
+ ```bash
145
+ echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="2ce3", ATTR{idProduct}=="3828", MODE="0666"' | \
146
+ sudo tee /etc/udev/rules.d/99-supercamera.rules
147
+ sudo udevadm control --reload-rules
148
+ ```
149
+ - **Windows**: Should work with [libusb](https://libusb.info/) + [Zadig](https://zadig.akeo.ie/) driver. Untested.
150
+
151
+ ## Credits
152
+
153
+ Protocol reverse-engineered by:
154
+ - [hbens/geek-szitman-supercamera](https://github.com/hbens/geek-szitman-supercamera) (C++ PoC, CC0)
155
+ - [MAkcanca/useeplus-linux-driver](https://github.com/MAkcanca/useeplus-linux-driver) (Linux kernel driver)
156
+
157
+ ## License
158
+
159
+ MIT
@@ -0,0 +1,8 @@
1
+ supercamera/__init__.py,sha256=L6mHirGmONzMDZ2tQriqmYCCcteuBIeqY61ckwxbnu4,261
2
+ supercamera/camera.py,sha256=M2KEv5mIBAZEtA4rDOXfO9S8EqSFF45sNt6JHoABSvE,9389
3
+ supercamera/cli.py,sha256=JAAEsjFCYofhgVu2lYLTHXEd0rwv9HPvDjGJO7khE1M,3395
4
+ supercamera/validate.py,sha256=aGKlnKKUP7_GOjYufLs54c6kXcakv2ZL1xvn_oKfgqs,823
5
+ supercamera-0.1.0.dist-info/METADATA,sha256=KDbZlnTWbPdlGzphN_9Jbp8a2JZw1926i3WetVx80Ko,4810
6
+ supercamera-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ supercamera-0.1.0.dist-info/entry_points.txt,sha256=sQR_ix78oyldDgDVR1rXfcrAotyvAkZNja7j9mhd8Mk,53
8
+ supercamera-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ supercamera = supercamera.cli:main