bayertap 0.1.0__tar.gz
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.
Potentially problematic release.
This version of bayertap might be problematic. Click here for more details.
- bayertap-0.1.0/LICENSE +21 -0
- bayertap-0.1.0/PKG-INFO +84 -0
- bayertap-0.1.0/README.md +60 -0
- bayertap-0.1.0/bayertap/__init__.py +9 -0
- bayertap-0.1.0/bayertap/cli.py +186 -0
- bayertap-0.1.0/bayertap/v4l2.py +210 -0
- bayertap-0.1.0/bayertap.egg-info/PKG-INFO +84 -0
- bayertap-0.1.0/bayertap.egg-info/SOURCES.txt +13 -0
- bayertap-0.1.0/bayertap.egg-info/dependency_links.txt +1 -0
- bayertap-0.1.0/bayertap.egg-info/entry_points.txt +2 -0
- bayertap-0.1.0/bayertap.egg-info/requires.txt +5 -0
- bayertap-0.1.0/bayertap.egg-info/top_level.txt +1 -0
- bayertap-0.1.0/pyproject.toml +39 -0
- bayertap-0.1.0/setup.cfg +4 -0
- bayertap-0.1.0/tests/test_bayertap.py +84 -0
bayertap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serge Rabyking
|
|
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.
|
bayertap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bayertap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Passive conformance tap for bayerlink links: capture, decode, judge
|
|
5
|
+
Author: Serge Rabyking
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bayerlink/bayertap
|
|
8
|
+
Project-URL: Source, https://github.com/bayerlink/bayertap
|
|
9
|
+
Project-URL: Issues, https://github.com/bayerlink/bayertap/issues
|
|
10
|
+
Keywords: bayer,raw,hdmi,capture,v4l2,fpga,conformance,tc358743
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: bayerlink>=0.2.0
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# bayertap
|
|
26
|
+
|
|
27
|
+
**A passive conformance tap for bayerlink links.**
|
|
28
|
+
|
|
29
|
+
In a streaming netlist, a *tap* is a sink that observes a stream without
|
|
30
|
+
stalling it. This is that concept as bench equipment: point it at any V4L2
|
|
31
|
+
capture device carrying a [bayerlink](https://github.com/bayerlink/bayerlink)
|
|
32
|
+
source and it tells you whether the bytes survive — judged with the same
|
|
33
|
+
published codec the source encoded with, so the two ends cannot disagree
|
|
34
|
+
about what conformance means.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bayertap probe # which byte-lane permutation is this rig?
|
|
38
|
+
bayertap --lane-map 2,1,0 check --pattern counting # every sample, judged
|
|
39
|
+
bayertap --via tunnel check --pattern counting # through a cheap dongle
|
|
40
|
+
bayertap --from-file frame.npy check # no hardware at all
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Capture tiers
|
|
44
|
+
|
|
45
|
+
| Tier | Hardware | Path | Fidelity |
|
|
46
|
+
| --- | --- | --- | --- |
|
|
47
|
+
| **Trusted** | TC358743 HDMI→CSI-2 bridge (~$25, the PiKVM part) on any Pi or Jetson | `--via direct`, RGB888 | byte-exact |
|
|
48
|
+
| **Cheap** | MS2130S / MS2131S USB3 stick (~$15) on any Linux box | `--via direct`, RGB24 | byte-exact **if** the stick's pipeline is transparent — `check` answers that in minutes |
|
|
49
|
+
| **Drawer** | MS2109-class USB2 stick | `--via tunnel`, YUYV | **bit-exact at 1/6 capacity**, via the luma tunnel — run the source with `--luma-tunnel` |
|
|
50
|
+
| Rejected | any USB2 stick, direct | MJPEG / starved YUYV | physics: USB2 cannot carry uncompressed 1080p, and MJPEG is lossy. Refused in code, not just here |
|
|
51
|
+
|
|
52
|
+
The drawer tier deserves its sentence: a $10 dongle you already own becomes a
|
|
53
|
+
working conformance receiver **today**, because bayerlink's luma tunnel
|
|
54
|
+
carries the unchanged container as grey levels with a pilot line the decoder
|
|
55
|
+
learns the channel from. Slow, and completely sufficient for proving bytes.
|
|
56
|
+
|
|
57
|
+
## The two bring-up commands
|
|
58
|
+
|
|
59
|
+
**`probe`** resolves the byte-lane permutation — which captured channel holds
|
|
60
|
+
container byte k — by trying all six against the header's magic and CRC. Its
|
|
61
|
+
output is directly the `lane_map` for this tool's `check` *and* for the
|
|
62
|
+
FPGA-side receiver (`np2hw.bayerlink_in`). One command instead of a scope.
|
|
63
|
+
|
|
64
|
+
**`check --pattern X`** regenerates the pattern locally (patterns are pure
|
|
65
|
+
functions — nothing travels out of band) and compares **every sample**,
|
|
66
|
+
watches `frame_seq` for gaps, tolerates scanout repeats (protocol-legal),
|
|
67
|
+
and exits nonzero on any discrepancy. Point CI at it if you like.
|
|
68
|
+
|
|
69
|
+
For the TC358743 tier, remember the bridge needs an EDID loaded
|
|
70
|
+
(`v4l2-ctl --set-edid`) before it captures — and that EDID is *leverage*: it
|
|
71
|
+
is what the source reads, so advertise only RGB 4:4:4 at your one mode and
|
|
72
|
+
the source is steered into the only format the protocol accepts.
|
|
73
|
+
|
|
74
|
+
## Status
|
|
75
|
+
|
|
76
|
+
Everything except the ioctl layer is proven off-target: struct sizes pinned
|
|
77
|
+
against the kernel uapi, both decode paths (direct with lane maps, tunnel
|
|
78
|
+
with pilot learning) tested end-to-end from files. First runs against real
|
|
79
|
+
capture silicon are the bench session; the known-good device table starts
|
|
80
|
+
there.
|
|
81
|
+
|
|
82
|
+
## Licence
|
|
83
|
+
|
|
84
|
+
MIT.
|
bayertap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# bayertap
|
|
2
|
+
|
|
3
|
+
**A passive conformance tap for bayerlink links.**
|
|
4
|
+
|
|
5
|
+
In a streaming netlist, a *tap* is a sink that observes a stream without
|
|
6
|
+
stalling it. This is that concept as bench equipment: point it at any V4L2
|
|
7
|
+
capture device carrying a [bayerlink](https://github.com/bayerlink/bayerlink)
|
|
8
|
+
source and it tells you whether the bytes survive — judged with the same
|
|
9
|
+
published codec the source encoded with, so the two ends cannot disagree
|
|
10
|
+
about what conformance means.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bayertap probe # which byte-lane permutation is this rig?
|
|
14
|
+
bayertap --lane-map 2,1,0 check --pattern counting # every sample, judged
|
|
15
|
+
bayertap --via tunnel check --pattern counting # through a cheap dongle
|
|
16
|
+
bayertap --from-file frame.npy check # no hardware at all
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Capture tiers
|
|
20
|
+
|
|
21
|
+
| Tier | Hardware | Path | Fidelity |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| **Trusted** | TC358743 HDMI→CSI-2 bridge (~$25, the PiKVM part) on any Pi or Jetson | `--via direct`, RGB888 | byte-exact |
|
|
24
|
+
| **Cheap** | MS2130S / MS2131S USB3 stick (~$15) on any Linux box | `--via direct`, RGB24 | byte-exact **if** the stick's pipeline is transparent — `check` answers that in minutes |
|
|
25
|
+
| **Drawer** | MS2109-class USB2 stick | `--via tunnel`, YUYV | **bit-exact at 1/6 capacity**, via the luma tunnel — run the source with `--luma-tunnel` |
|
|
26
|
+
| Rejected | any USB2 stick, direct | MJPEG / starved YUYV | physics: USB2 cannot carry uncompressed 1080p, and MJPEG is lossy. Refused in code, not just here |
|
|
27
|
+
|
|
28
|
+
The drawer tier deserves its sentence: a $10 dongle you already own becomes a
|
|
29
|
+
working conformance receiver **today**, because bayerlink's luma tunnel
|
|
30
|
+
carries the unchanged container as grey levels with a pilot line the decoder
|
|
31
|
+
learns the channel from. Slow, and completely sufficient for proving bytes.
|
|
32
|
+
|
|
33
|
+
## The two bring-up commands
|
|
34
|
+
|
|
35
|
+
**`probe`** resolves the byte-lane permutation — which captured channel holds
|
|
36
|
+
container byte k — by trying all six against the header's magic and CRC. Its
|
|
37
|
+
output is directly the `lane_map` for this tool's `check` *and* for the
|
|
38
|
+
FPGA-side receiver (`np2hw.bayerlink_in`). One command instead of a scope.
|
|
39
|
+
|
|
40
|
+
**`check --pattern X`** regenerates the pattern locally (patterns are pure
|
|
41
|
+
functions — nothing travels out of band) and compares **every sample**,
|
|
42
|
+
watches `frame_seq` for gaps, tolerates scanout repeats (protocol-legal),
|
|
43
|
+
and exits nonzero on any discrepancy. Point CI at it if you like.
|
|
44
|
+
|
|
45
|
+
For the TC358743 tier, remember the bridge needs an EDID loaded
|
|
46
|
+
(`v4l2-ctl --set-edid`) before it captures — and that EDID is *leverage*: it
|
|
47
|
+
is what the source reads, so advertise only RGB 4:4:4 at your one mode and
|
|
48
|
+
the source is steered into the only format the protocol accepts.
|
|
49
|
+
|
|
50
|
+
## Status
|
|
51
|
+
|
|
52
|
+
Everything except the ioctl layer is proven off-target: struct sizes pinned
|
|
53
|
+
against the kernel uapi, both decode paths (direct with lane maps, tunnel
|
|
54
|
+
with pilot learning) tested end-to-end from files. First runs against real
|
|
55
|
+
capture silicon are the bench session; the known-good device table starts
|
|
56
|
+
there.
|
|
57
|
+
|
|
58
|
+
## Licence
|
|
59
|
+
|
|
60
|
+
MIT.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""bayertap -- a passive conformance tap for bayerlink links.
|
|
2
|
+
|
|
3
|
+
In a revela/np2hw netlist, a tap is a sink that observes a stream without
|
|
4
|
+
stalling it. This is that concept as bench equipment: point it at any V4L2
|
|
5
|
+
capture device carrying a bayerlink source and it tells you whether the
|
|
6
|
+
bytes survive. It judges with the same published codec the source encoded
|
|
7
|
+
with, so the two ends cannot disagree about what conformance means.
|
|
8
|
+
"""
|
|
9
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""bayertap: a passive conformance tap for bayerlink links.
|
|
2
|
+
|
|
3
|
+
Point it at any V4L2 capture device carrying a bayerlink source and it
|
|
4
|
+
tells you whether the bytes survive. Three subcommands, one per question:
|
|
5
|
+
|
|
6
|
+
check does every frame decode, does the pattern match, does
|
|
7
|
+
frame_seq advance without gaps?
|
|
8
|
+
probe which byte-lane permutation is this platform pair using?
|
|
9
|
+
save keep decoded raw frames for offline analysis.
|
|
10
|
+
|
|
11
|
+
Two capture paths, chosen by --via:
|
|
12
|
+
|
|
13
|
+
direct RGB3/BGR3 capture (TC358743 bridge, S-variant USB3 stick):
|
|
14
|
+
the container arrives as bytes, possibly lane-permuted --
|
|
15
|
+
which probe resolves and check accepts via --lane-map.
|
|
16
|
+
tunnel YUYV capture (MS2109-class USB2 dongles): only luma survives
|
|
17
|
+
that hardware, so the source must run --luma-tunnel, and the
|
|
18
|
+
container is recovered from grey levels via the pilot line.
|
|
19
|
+
|
|
20
|
+
Everything also runs from a saved .npy instead of a device (--from-file),
|
|
21
|
+
so the whole validation path is testable -- and tested -- with no capture
|
|
22
|
+
hardware at all.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from bayerlink import decode_frame, detect_lane_map, pattern, tunnel
|
|
32
|
+
from bayerlink.protocol import Header
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _validate(container: np.ndarray, expect_pattern: str | None,
|
|
36
|
+
last_seq: int | None):
|
|
37
|
+
"""One container -> (Header, list of problem strings)."""
|
|
38
|
+
problems = []
|
|
39
|
+
try:
|
|
40
|
+
header, raw = decode_frame(container)
|
|
41
|
+
except ValueError as error:
|
|
42
|
+
return None, [f"decode: {error}"]
|
|
43
|
+
if last_seq is not None:
|
|
44
|
+
gap = (header.frame_seq - last_seq) & 0xFFFFFFFF
|
|
45
|
+
if gap == 0:
|
|
46
|
+
return header, ["duplicate (same frame_seq; scanout repeat)"]
|
|
47
|
+
if gap != 1:
|
|
48
|
+
problems.append(f"frame_seq gap: {last_seq} -> {header.frame_seq}")
|
|
49
|
+
if expect_pattern is not None:
|
|
50
|
+
if not header.is_test_pattern:
|
|
51
|
+
problems.append("header does not claim a test pattern")
|
|
52
|
+
expected = pattern.generate(expect_pattern, header.width, header.height)
|
|
53
|
+
if not np.array_equal(raw, expected):
|
|
54
|
+
wrong = int((raw != expected).sum())
|
|
55
|
+
first = tuple(int(v) for v in np.argwhere(raw != expected)[0])
|
|
56
|
+
problems.append(
|
|
57
|
+
f"{wrong} of {raw.size} samples differ from {expect_pattern}; "
|
|
58
|
+
f"first at (row, col) {first}")
|
|
59
|
+
return header, problems
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _containers(args):
|
|
63
|
+
"""Yield (tag, container) from the device or the file, per --via."""
|
|
64
|
+
if args.from_file:
|
|
65
|
+
physical = np.load(args.from_file)
|
|
66
|
+
if args.via == "tunnel":
|
|
67
|
+
luma = physical[:, :, 0] if physical.ndim == 3 else physical
|
|
68
|
+
_, inner_h = tunnel.inner_display(luma.shape[1], luma.shape[0])
|
|
69
|
+
yield 0, tunnel.decode(luma, inner_height=inner_h)
|
|
70
|
+
else:
|
|
71
|
+
frame = physical[:, :, list(args.lane_map)]
|
|
72
|
+
yield 0, frame
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
from . import v4l2
|
|
76
|
+
|
|
77
|
+
device = v4l2.Device(args.device)
|
|
78
|
+
print(f"{device.path}: {device.card} ({device.driver}), "
|
|
79
|
+
f"offers {device.formats()}")
|
|
80
|
+
width, height = args.capture
|
|
81
|
+
if args.via == "tunnel":
|
|
82
|
+
for sequence, luma in device.frames(width, height, v4l2.FORMAT_YUYV):
|
|
83
|
+
_, inner_h = tunnel.inner_display(width, height)
|
|
84
|
+
try:
|
|
85
|
+
yield sequence, tunnel.decode(luma, inner_height=inner_h)
|
|
86
|
+
except ValueError as error:
|
|
87
|
+
yield sequence, error
|
|
88
|
+
else:
|
|
89
|
+
pixelformat = v4l2.FORMAT_BGR3 if args.bgr else v4l2.FORMAT_RGB3
|
|
90
|
+
for sequence, frame in device.frames(width, height, pixelformat):
|
|
91
|
+
yield sequence, frame[:, :, list(args.lane_map)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main(argv=None) -> int:
|
|
95
|
+
parser = argparse.ArgumentParser(
|
|
96
|
+
prog="bayertap",
|
|
97
|
+
description="Passive conformance tap for bayerlink links.")
|
|
98
|
+
parser.add_argument("--device", default="/dev/video0")
|
|
99
|
+
parser.add_argument("--from-file", default=None,
|
|
100
|
+
help=".npy physical frame instead of a device")
|
|
101
|
+
parser.add_argument("--via", choices=["direct", "tunnel"], default="direct",
|
|
102
|
+
help="direct: RGB capture; tunnel: YUYV luma capture "
|
|
103
|
+
"of a --luma-tunnel source")
|
|
104
|
+
parser.add_argument("--capture", type=lambda s: tuple(
|
|
105
|
+
int(v) for v in s.split("x")), default=(1280, 720),
|
|
106
|
+
help="capture geometry WxH (default 1280x720)")
|
|
107
|
+
parser.add_argument("--lane-map", type=lambda s: tuple(
|
|
108
|
+
int(v) for v in s.split(",")), default=(0, 1, 2),
|
|
109
|
+
help="byte permutation for direct capture; find it "
|
|
110
|
+
"with `bayertap probe`")
|
|
111
|
+
parser.add_argument("--bgr", action="store_true",
|
|
112
|
+
help="capture BGR3 instead of RGB3")
|
|
113
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
114
|
+
|
|
115
|
+
check = sub.add_parser("check", help="validate frames continuously")
|
|
116
|
+
check.add_argument("--pattern", default=None,
|
|
117
|
+
choices=sorted(pattern.PATTERNS),
|
|
118
|
+
help="regenerate this pattern locally and compare "
|
|
119
|
+
"every sample")
|
|
120
|
+
check.add_argument("--frames", type=int, default=0,
|
|
121
|
+
help="stop after N NEW frames (0 = forever)")
|
|
122
|
+
|
|
123
|
+
sub.add_parser("probe", help="resolve the byte-lane permutation "
|
|
124
|
+
"(direct capture of one frame)")
|
|
125
|
+
|
|
126
|
+
save = sub.add_parser("save", help="write decoded raw frames as .npy")
|
|
127
|
+
save.add_argument("--out", required=True)
|
|
128
|
+
save.add_argument("--frames", type=int, default=1)
|
|
129
|
+
|
|
130
|
+
args = parser.parse_args(argv)
|
|
131
|
+
|
|
132
|
+
if args.command == "probe":
|
|
133
|
+
if args.via == "tunnel":
|
|
134
|
+
parser.error("probe needs direct RGB capture: the tunnel is "
|
|
135
|
+
"greyscale, so lanes are indistinguishable there")
|
|
136
|
+
args.lane_map = (0, 1, 2) # probe must see raw order
|
|
137
|
+
for _, frame in _containers(args):
|
|
138
|
+
if isinstance(frame, Exception):
|
|
139
|
+
continue
|
|
140
|
+
permutation, header = detect_lane_map(frame)
|
|
141
|
+
print(f"lane_map = {permutation} "
|
|
142
|
+
f"(container byte k is on captured channel lane_map[k])")
|
|
143
|
+
print(f"header: {header.fourcc} {header.width}x{header.height} "
|
|
144
|
+
f"seq {header.frame_seq}")
|
|
145
|
+
return 0
|
|
146
|
+
return 1
|
|
147
|
+
|
|
148
|
+
seen = last_seq = None
|
|
149
|
+
good = bad = saved = 0
|
|
150
|
+
for tag, container in _containers(args):
|
|
151
|
+
if isinstance(container, Exception):
|
|
152
|
+
bad += 1
|
|
153
|
+
print(f"[{tag}] tunnel: {container}")
|
|
154
|
+
continue
|
|
155
|
+
header, problems = _validate(
|
|
156
|
+
container, getattr(args, "pattern", None), last_seq)
|
|
157
|
+
if header is not None and problems == ["duplicate (same frame_seq; "
|
|
158
|
+
"scanout repeat)"]:
|
|
159
|
+
continue # repeats are protocol-legal
|
|
160
|
+
if problems:
|
|
161
|
+
bad += 1
|
|
162
|
+
print(f"[{tag}] " + "; ".join(problems))
|
|
163
|
+
else:
|
|
164
|
+
good += 1
|
|
165
|
+
last_seq = header.frame_seq
|
|
166
|
+
if args.command == "save":
|
|
167
|
+
_, raw = decode_frame(container)
|
|
168
|
+
path = f"{args.out.rstrip('/')}/frame_{header.frame_seq:08d}.npy"
|
|
169
|
+
np.save(path, raw)
|
|
170
|
+
saved += 1
|
|
171
|
+
print(f"saved {path}")
|
|
172
|
+
elif good % 30 == 1:
|
|
173
|
+
print(f"[{tag}] ok: {header.fourcc} {header.width}x"
|
|
174
|
+
f"{header.height} seq {header.frame_seq}"
|
|
175
|
+
+ (f", pattern {args.pattern} exact"
|
|
176
|
+
if getattr(args, 'pattern', None) else ""))
|
|
177
|
+
limit = getattr(args, "frames", 0)
|
|
178
|
+
if limit and (good if args.command == "check" else saved) >= limit:
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
print(f"\n{good} good, {bad} bad")
|
|
182
|
+
return 0 if bad == 0 and good > 0 else 1
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
sys.exit(main())
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""V4L2 capture via raw ioctls: open, negotiate, mmap, stream.
|
|
2
|
+
|
|
3
|
+
The same zero-dependency posture as picam2hdmi's KMS layer, for the same
|
|
4
|
+
reason: an appliance should not carry a bindings package for the nine
|
|
5
|
+
ioctls it uses, and the V4L2 uapi is a stable kernel ABI. Every struct is
|
|
6
|
+
pinned against its known uapi size by the tests -- a mislaid ctypes field
|
|
7
|
+
fails as one assert here instead of as corruption on a bench.
|
|
8
|
+
|
|
9
|
+
This layer knows nothing about bayerlink. It negotiates a pixel format,
|
|
10
|
+
maps buffers, and yields frames; what the frames MEAN belongs to the
|
|
11
|
+
caller. That split is what lets one tool serve every capture tier -- a
|
|
12
|
+
TC358743 bridge delivering RGB888, an S-variant USB3 stick delivering
|
|
13
|
+
RGB24, an MS2109 delivering YUY2 for the luma tunnel -- they are all just
|
|
14
|
+
V4L2 devices with different formats.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ctypes
|
|
19
|
+
import fcntl
|
|
20
|
+
import mmap as _mmap
|
|
21
|
+
import os
|
|
22
|
+
import select
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
u8, u32, u64 = ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint64
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Capability(ctypes.Structure):
|
|
30
|
+
_fields_ = [("driver", ctypes.c_char * 16), ("card", ctypes.c_char * 32),
|
|
31
|
+
("bus_info", ctypes.c_char * 32), ("version", u32),
|
|
32
|
+
("capabilities", u32), ("device_caps", u32),
|
|
33
|
+
("reserved", u32 * 3)] # 104 bytes
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PixFormat(ctypes.Structure):
|
|
37
|
+
_fields_ = [("width", u32), ("height", u32), ("pixelformat", u32),
|
|
38
|
+
("field", u32), ("bytesperline", u32), ("sizeimage", u32),
|
|
39
|
+
("colorspace", u32), ("priv", u32), ("flags", u32),
|
|
40
|
+
("ycbcr_enc", u32), ("quantization", u32),
|
|
41
|
+
("xfer_func", u32)] # 48 bytes
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Format(ctypes.Structure):
|
|
45
|
+
_fields_ = [("type", u32), ("pad", u32),
|
|
46
|
+
("fmt", ctypes.c_uint8 * 200)] # 208 bytes
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def pix(self) -> PixFormat:
|
|
50
|
+
return PixFormat.from_buffer(self, 8)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RequestBuffers(ctypes.Structure):
|
|
54
|
+
_fields_ = [("count", u32), ("type", u32), ("memory", u32),
|
|
55
|
+
("capabilities", u32), ("flags", u8),
|
|
56
|
+
("reserved", u8 * 3)] # 20 bytes
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Timeval(ctypes.Structure):
|
|
60
|
+
_fields_ = [("sec", ctypes.c_long), ("usec", ctypes.c_long)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Timecode(ctypes.Structure):
|
|
64
|
+
_fields_ = [("type", u32), ("flags", u32), ("frames", u8), ("seconds", u8),
|
|
65
|
+
("minutes", u8), ("hours", u8), ("userbits", u8 * 4)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Buffer(ctypes.Structure):
|
|
69
|
+
_fields_ = [("index", u32), ("type", u32), ("bytesused", u32),
|
|
70
|
+
("flags", u32), ("field", u32), ("pad", u32),
|
|
71
|
+
("timestamp", Timeval), ("timecode", Timecode),
|
|
72
|
+
("sequence", u32), ("memory", u32),
|
|
73
|
+
("m_offset", u64), # union: offset/userptr/planes/fd
|
|
74
|
+
("length", u32), ("reserved2", u32),
|
|
75
|
+
("request_fd", u32)] # 88 bytes (LP64)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class FmtDesc(ctypes.Structure):
|
|
79
|
+
_fields_ = [("index", u32), ("type", u32), ("flags", u32),
|
|
80
|
+
("description", ctypes.c_char * 32), ("pixelformat", u32),
|
|
81
|
+
("mbus_code", u32), ("reserved", u32 * 3)] # 64 bytes
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _ioc(direction: int, nr: int, size: int) -> int:
|
|
85
|
+
return (direction << 30) | (size << 16) | (ord("V") << 8) | nr
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
IOCTL_QUERYCAP = _ioc(2, 0, ctypes.sizeof(Capability))
|
|
89
|
+
IOCTL_ENUM_FMT = _ioc(3, 2, ctypes.sizeof(FmtDesc))
|
|
90
|
+
IOCTL_S_FMT = _ioc(3, 5, ctypes.sizeof(Format))
|
|
91
|
+
IOCTL_REQBUFS = _ioc(3, 8, ctypes.sizeof(RequestBuffers))
|
|
92
|
+
IOCTL_QUERYBUF = _ioc(3, 9, ctypes.sizeof(Buffer))
|
|
93
|
+
IOCTL_QBUF = _ioc(3, 15, ctypes.sizeof(Buffer))
|
|
94
|
+
IOCTL_DQBUF = _ioc(3, 17, ctypes.sizeof(Buffer))
|
|
95
|
+
IOCTL_STREAMON = _ioc(1, 18, 4)
|
|
96
|
+
IOCTL_STREAMOFF = _ioc(1, 19, 4)
|
|
97
|
+
|
|
98
|
+
CAPTURE = 1 # V4L2_BUF_TYPE_VIDEO_CAPTURE
|
|
99
|
+
MEMORY_MMAP = 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def fourcc(text: str) -> int:
|
|
103
|
+
return int.from_bytes(text.encode("ascii"), "little")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
FORMAT_YUYV = fourcc("YUYV")
|
|
107
|
+
FORMAT_RGB3 = fourcc("RGB3") # RGB24: memory R,G,B per pixel
|
|
108
|
+
FORMAT_BGR3 = fourcc("BGR3") # memory B,G,R per pixel
|
|
109
|
+
FORMAT_MJPG = fourcc("MJPG") # rejected for payload: lossy
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def frame_array(data: np.ndarray, pixelformat: int, width: int,
|
|
113
|
+
height: int, bytesperline: int) -> np.ndarray:
|
|
114
|
+
"""Raw captured bytes -> a channel-shaped array, per format.
|
|
115
|
+
|
|
116
|
+
YUYV yields the LUMA plane (height, width) -- the only channel the luma
|
|
117
|
+
tunnel needs, extracted here so callers never index interleaved bytes.
|
|
118
|
+
RGB3/BGR3 yield (height, width, 3) in MEMORY byte order, untouched:
|
|
119
|
+
which byte is which lane is detect_lane_map()'s question, not this
|
|
120
|
+
layer's.
|
|
121
|
+
"""
|
|
122
|
+
rows = data[:height * bytesperline].reshape(height, bytesperline)
|
|
123
|
+
if pixelformat == FORMAT_YUYV:
|
|
124
|
+
return rows[:, :width * 2][:, 0::2].copy()
|
|
125
|
+
if pixelformat in (FORMAT_RGB3, FORMAT_BGR3):
|
|
126
|
+
return rows[:, :width * 3].reshape(height, width, 3).copy()
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"unhandled pixelformat {pixelformat:#010x}; this tool captures "
|
|
129
|
+
"YUYV (luma tunnel) and RGB3/BGR3 (direct), and refuses MJPEG "
|
|
130
|
+
"outright -- lossy compression cannot carry a byte container.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Device:
|
|
134
|
+
"""One V4L2 capture device, negotiated and streaming."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, path: str = "/dev/video0"):
|
|
137
|
+
self.path = path
|
|
138
|
+
self.fd = os.open(path, os.O_RDWR | os.O_NONBLOCK)
|
|
139
|
+
cap = Capability()
|
|
140
|
+
fcntl.ioctl(self.fd, IOCTL_QUERYCAP, cap)
|
|
141
|
+
self.driver = cap.driver.decode(errors="replace")
|
|
142
|
+
self.card = cap.card.decode(errors="replace")
|
|
143
|
+
self._maps: list = []
|
|
144
|
+
|
|
145
|
+
def formats(self) -> list[str]:
|
|
146
|
+
"""The fourccs the device offers, for diagnostics and refusals."""
|
|
147
|
+
found = []
|
|
148
|
+
index = 0
|
|
149
|
+
while True:
|
|
150
|
+
desc = FmtDesc(index=index, type=CAPTURE)
|
|
151
|
+
try:
|
|
152
|
+
fcntl.ioctl(self.fd, IOCTL_ENUM_FMT, desc)
|
|
153
|
+
except OSError:
|
|
154
|
+
return found
|
|
155
|
+
found.append(int(desc.pixelformat).to_bytes(4, "little")
|
|
156
|
+
.decode(errors="replace"))
|
|
157
|
+
index += 1
|
|
158
|
+
|
|
159
|
+
def set_format(self, width: int, height: int, pixelformat: int):
|
|
160
|
+
"""Negotiate; RETURNS what the device actually granted.
|
|
161
|
+
|
|
162
|
+
V4L2 is a negotiation, not a command: the driver may adjust every
|
|
163
|
+
field. The caller compares granted against requested and refuses a
|
|
164
|
+
substitute -- silently capturing at the wrong geometry is how a
|
|
165
|
+
bench lies.
|
|
166
|
+
"""
|
|
167
|
+
request = Format(type=CAPTURE)
|
|
168
|
+
pix = request.pix
|
|
169
|
+
pix.width, pix.height = width, height
|
|
170
|
+
pix.pixelformat = pixelformat
|
|
171
|
+
pix.field = 1 # V4L2_FIELD_NONE
|
|
172
|
+
fcntl.ioctl(self.fd, IOCTL_S_FMT, request)
|
|
173
|
+
granted = request.pix
|
|
174
|
+
if (granted.width, granted.height) != (width, height) or \
|
|
175
|
+
granted.pixelformat != pixelformat:
|
|
176
|
+
got = int(granted.pixelformat).to_bytes(4, "little").decode(
|
|
177
|
+
errors="replace")
|
|
178
|
+
want = int(pixelformat).to_bytes(4, "little").decode(errors="replace")
|
|
179
|
+
raise RuntimeError(
|
|
180
|
+
f"{self.path} granted {granted.width}x{granted.height} {got} "
|
|
181
|
+
f"instead of {width}x{height} {want}; it offers formats "
|
|
182
|
+
f"{self.formats()}. Refusing the substitute.")
|
|
183
|
+
return granted.width, granted.height, granted.bytesperline
|
|
184
|
+
|
|
185
|
+
def frames(self, width: int, height: int, pixelformat: int, buffers: int = 4):
|
|
186
|
+
"""Yield frames forever: (sequence, array) per :func:`frame_array`."""
|
|
187
|
+
width, height, bytesperline = self.set_format(width, height, pixelformat)
|
|
188
|
+
|
|
189
|
+
request = RequestBuffers(count=buffers, type=CAPTURE, memory=MEMORY_MMAP)
|
|
190
|
+
fcntl.ioctl(self.fd, IOCTL_REQBUFS, request)
|
|
191
|
+
for index in range(request.count):
|
|
192
|
+
buffer = Buffer(index=index, type=CAPTURE, memory=MEMORY_MMAP)
|
|
193
|
+
fcntl.ioctl(self.fd, IOCTL_QUERYBUF, buffer)
|
|
194
|
+
self._maps.append(_mmap.mmap(self.fd, buffer.length,
|
|
195
|
+
offset=buffer.m_offset))
|
|
196
|
+
fcntl.ioctl(self.fd, IOCTL_QBUF, buffer)
|
|
197
|
+
|
|
198
|
+
fcntl.ioctl(self.fd, IOCTL_STREAMON, ctypes.c_int(CAPTURE))
|
|
199
|
+
try:
|
|
200
|
+
while True:
|
|
201
|
+
select.select([self.fd], [], [])
|
|
202
|
+
buffer = Buffer(type=CAPTURE, memory=MEMORY_MMAP)
|
|
203
|
+
fcntl.ioctl(self.fd, IOCTL_DQBUF, buffer)
|
|
204
|
+
data = np.frombuffer(self._maps[buffer.index], np.uint8,
|
|
205
|
+
count=buffer.bytesused)
|
|
206
|
+
yield buffer.sequence, frame_array(
|
|
207
|
+
data, pixelformat, width, height, bytesperline)
|
|
208
|
+
fcntl.ioctl(self.fd, IOCTL_QBUF, buffer)
|
|
209
|
+
finally:
|
|
210
|
+
fcntl.ioctl(self.fd, IOCTL_STREAMOFF, ctypes.c_int(CAPTURE))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bayertap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Passive conformance tap for bayerlink links: capture, decode, judge
|
|
5
|
+
Author: Serge Rabyking
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bayerlink/bayertap
|
|
8
|
+
Project-URL: Source, https://github.com/bayerlink/bayertap
|
|
9
|
+
Project-URL: Issues, https://github.com/bayerlink/bayertap/issues
|
|
10
|
+
Keywords: bayer,raw,hdmi,capture,v4l2,fpga,conformance,tc358743
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: bayerlink>=0.2.0
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# bayertap
|
|
26
|
+
|
|
27
|
+
**A passive conformance tap for bayerlink links.**
|
|
28
|
+
|
|
29
|
+
In a streaming netlist, a *tap* is a sink that observes a stream without
|
|
30
|
+
stalling it. This is that concept as bench equipment: point it at any V4L2
|
|
31
|
+
capture device carrying a [bayerlink](https://github.com/bayerlink/bayerlink)
|
|
32
|
+
source and it tells you whether the bytes survive — judged with the same
|
|
33
|
+
published codec the source encoded with, so the two ends cannot disagree
|
|
34
|
+
about what conformance means.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bayertap probe # which byte-lane permutation is this rig?
|
|
38
|
+
bayertap --lane-map 2,1,0 check --pattern counting # every sample, judged
|
|
39
|
+
bayertap --via tunnel check --pattern counting # through a cheap dongle
|
|
40
|
+
bayertap --from-file frame.npy check # no hardware at all
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Capture tiers
|
|
44
|
+
|
|
45
|
+
| Tier | Hardware | Path | Fidelity |
|
|
46
|
+
| --- | --- | --- | --- |
|
|
47
|
+
| **Trusted** | TC358743 HDMI→CSI-2 bridge (~$25, the PiKVM part) on any Pi or Jetson | `--via direct`, RGB888 | byte-exact |
|
|
48
|
+
| **Cheap** | MS2130S / MS2131S USB3 stick (~$15) on any Linux box | `--via direct`, RGB24 | byte-exact **if** the stick's pipeline is transparent — `check` answers that in minutes |
|
|
49
|
+
| **Drawer** | MS2109-class USB2 stick | `--via tunnel`, YUYV | **bit-exact at 1/6 capacity**, via the luma tunnel — run the source with `--luma-tunnel` |
|
|
50
|
+
| Rejected | any USB2 stick, direct | MJPEG / starved YUYV | physics: USB2 cannot carry uncompressed 1080p, and MJPEG is lossy. Refused in code, not just here |
|
|
51
|
+
|
|
52
|
+
The drawer tier deserves its sentence: a $10 dongle you already own becomes a
|
|
53
|
+
working conformance receiver **today**, because bayerlink's luma tunnel
|
|
54
|
+
carries the unchanged container as grey levels with a pilot line the decoder
|
|
55
|
+
learns the channel from. Slow, and completely sufficient for proving bytes.
|
|
56
|
+
|
|
57
|
+
## The two bring-up commands
|
|
58
|
+
|
|
59
|
+
**`probe`** resolves the byte-lane permutation — which captured channel holds
|
|
60
|
+
container byte k — by trying all six against the header's magic and CRC. Its
|
|
61
|
+
output is directly the `lane_map` for this tool's `check` *and* for the
|
|
62
|
+
FPGA-side receiver (`np2hw.bayerlink_in`). One command instead of a scope.
|
|
63
|
+
|
|
64
|
+
**`check --pattern X`** regenerates the pattern locally (patterns are pure
|
|
65
|
+
functions — nothing travels out of band) and compares **every sample**,
|
|
66
|
+
watches `frame_seq` for gaps, tolerates scanout repeats (protocol-legal),
|
|
67
|
+
and exits nonzero on any discrepancy. Point CI at it if you like.
|
|
68
|
+
|
|
69
|
+
For the TC358743 tier, remember the bridge needs an EDID loaded
|
|
70
|
+
(`v4l2-ctl --set-edid`) before it captures — and that EDID is *leverage*: it
|
|
71
|
+
is what the source reads, so advertise only RGB 4:4:4 at your one mode and
|
|
72
|
+
the source is steered into the only format the protocol accepts.
|
|
73
|
+
|
|
74
|
+
## Status
|
|
75
|
+
|
|
76
|
+
Everything except the ioctl layer is proven off-target: struct sizes pinned
|
|
77
|
+
against the kernel uapi, both decode paths (direct with lane maps, tunnel
|
|
78
|
+
with pilot learning) tested end-to-end from files. First runs against real
|
|
79
|
+
capture silicon are the bench session; the known-good device table starts
|
|
80
|
+
there.
|
|
81
|
+
|
|
82
|
+
## Licence
|
|
83
|
+
|
|
84
|
+
MIT.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
bayertap/__init__.py
|
|
5
|
+
bayertap/cli.py
|
|
6
|
+
bayertap/v4l2.py
|
|
7
|
+
bayertap.egg-info/PKG-INFO
|
|
8
|
+
bayertap.egg-info/SOURCES.txt
|
|
9
|
+
bayertap.egg-info/dependency_links.txt
|
|
10
|
+
bayertap.egg-info/entry_points.txt
|
|
11
|
+
bayertap.egg-info/requires.txt
|
|
12
|
+
bayertap.egg-info/top_level.txt
|
|
13
|
+
tests/test_bayertap.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bayertap
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bayertap"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Passive conformance tap for bayerlink links: capture, decode, judge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Serge Rabyking" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
keywords = ["bayer", "raw", "hdmi", "capture", "v4l2", "fpga", "conformance", "tc358743"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Multimedia :: Video :: Capture",
|
|
20
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["numpy", "bayerlink>=0.2.0"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = ["pytest"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/bayerlink/bayertap"
|
|
29
|
+
Source = "https://github.com/bayerlink/bayertap"
|
|
30
|
+
Issues = "https://github.com/bayerlink/bayertap/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
bayertap = "bayertap.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
include = ["bayertap*"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
bayertap-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Everything that does not need capture hardware, which is everything but
|
|
2
|
+
the ioctls: struct sizes pinned to the uapi, frame_array per format, and the
|
|
3
|
+
whole check pipeline end to end from files -- the same code paths a device
|
|
4
|
+
feeds, minus the device.
|
|
5
|
+
"""
|
|
6
|
+
import ctypes
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
from bayerlink import encode_frame, pattern, tunnel
|
|
11
|
+
from bayerlink.protocol import FLAG_TEST_PATTERN
|
|
12
|
+
|
|
13
|
+
from bayertap import cli, v4l2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_struct_sizes_match_the_kernel_uapi():
|
|
17
|
+
expected = {v4l2.Capability: 104, v4l2.PixFormat: 48, v4l2.Format: 208,
|
|
18
|
+
v4l2.RequestBuffers: 20, v4l2.Buffer: 88, v4l2.FmtDesc: 64}
|
|
19
|
+
for struct, size in expected.items():
|
|
20
|
+
assert ctypes.sizeof(struct) == size, struct.__name__
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_frame_array_extracts_luma_from_yuyv_and_passes_rgb_untouched():
|
|
24
|
+
height, width, stride = 4, 8, 8 * 2 + 4 # padded line
|
|
25
|
+
yuyv = np.arange(height * stride, dtype=np.uint8)
|
|
26
|
+
luma = v4l2.frame_array(yuyv, v4l2.FORMAT_YUYV, width, height, stride)
|
|
27
|
+
assert luma.shape == (height, width)
|
|
28
|
+
assert np.array_equal(luma[0], yuyv[:width * 2:2])
|
|
29
|
+
|
|
30
|
+
stride = 8 * 3
|
|
31
|
+
rgb = np.arange(height * stride, dtype=np.uint8)
|
|
32
|
+
frame = v4l2.frame_array(rgb, v4l2.FORMAT_RGB3, width, height, stride)
|
|
33
|
+
assert frame.shape == (height, width, 3)
|
|
34
|
+
assert np.array_equal(frame.reshape(-1), rgb)
|
|
35
|
+
|
|
36
|
+
with pytest.raises(ValueError, match="refuses MJPEG"):
|
|
37
|
+
v4l2.frame_array(rgb, v4l2.FORMAT_MJPG, width, height, stride)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _direct_file(tmp_path, lane_perm=(0, 1, 2), seq=4):
|
|
41
|
+
raw = pattern.generate("corners", 32, 8)
|
|
42
|
+
container = encode_frame(raw, "RGGB", frame_seq=seq, display=(32, 12),
|
|
43
|
+
flags=FLAG_TEST_PATTERN)
|
|
44
|
+
scrambled = np.empty_like(container)
|
|
45
|
+
for k in range(3):
|
|
46
|
+
scrambled[:, :, lane_perm[k]] = container[:, :, k]
|
|
47
|
+
path = tmp_path / "direct.npy"
|
|
48
|
+
np.save(path, scrambled)
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_check_from_file_direct_passes_and_fails_honestly(tmp_path, capsys):
|
|
53
|
+
path = _direct_file(tmp_path)
|
|
54
|
+
code = cli.main(["--from-file", str(path), "check", "--pattern", "corners"])
|
|
55
|
+
assert code == 0 and "1 good, 0 bad" in capsys.readouterr().out
|
|
56
|
+
|
|
57
|
+
code = cli.main(["--from-file", str(path), "check", "--pattern", "checker"])
|
|
58
|
+
out = capsys.readouterr().out
|
|
59
|
+
assert code == 1 and "differ from checker" in out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_check_applies_the_lane_map(tmp_path, capsys):
|
|
63
|
+
path = _direct_file(tmp_path, lane_perm=(2, 0, 1))
|
|
64
|
+
# Without the map: not a bayerlink frame at all.
|
|
65
|
+
assert cli.main(["--from-file", str(path), "check"]) == 1
|
|
66
|
+
capsys.readouterr()
|
|
67
|
+
# detect_lane_map's answer, applied: (container byte k is on channel...)
|
|
68
|
+
assert cli.main(["--from-file", str(path), "--lane-map", "2,0,1",
|
|
69
|
+
"check", "--pattern", "corners"]) == 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_check_from_file_via_tunnel(tmp_path, capsys):
|
|
73
|
+
display = (192, 40)
|
|
74
|
+
inner_w, inner_h = tunnel.inner_display(*display)
|
|
75
|
+
raw = pattern.generate("counting", inner_w * 2, 20)
|
|
76
|
+
container = encode_frame(raw, "BGGR", frame_seq=0,
|
|
77
|
+
display=(inner_w, inner_h),
|
|
78
|
+
flags=FLAG_TEST_PATTERN)
|
|
79
|
+
grey = tunnel.encode(container, display)
|
|
80
|
+
path = tmp_path / "tunnel.npy"
|
|
81
|
+
np.save(path, grey)
|
|
82
|
+
code = cli.main(["--from-file", str(path), "--via", "tunnel",
|
|
83
|
+
"check", "--pattern", "counting"])
|
|
84
|
+
assert code == 0 and "1 good, 0 bad" in capsys.readouterr().out
|