Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion checkbox-core-snap/series20/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ parts:
- libcap2-bin
- libfdt1
- libglu1-mesa
- libsvm3
- lsb-release
- lshw
- mesa-utils
Expand All @@ -378,9 +379,9 @@ parts:
- python3-evdev
- python3-gi
- python3-natsort
- python3-opencv
- python3-pil
- python3-psutil
- python3-pyqrcode
- python3-serial
- python3-yaml
- python3-pyscard
Expand Down
1 change: 0 additions & 1 deletion checkbox-core-snap/series22/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ parts:
- python3-natsort
- python3-pil
- python3-psutil
- python3-pyqrcode
- python3-serial
- python3-yaml
- python3-pyscard
Expand Down
1 change: 0 additions & 1 deletion checkbox-core-snap/series24/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,6 @@ parts:
- python3-natsort
- python3-pil
- python3-psutil
- python3-pyqrcode
- python3-serial
- python3-yaml
- python3-pyscard
Expand Down
10 changes: 7 additions & 3 deletions providers/base/bin/camera_test_rpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@
]


def capture():
def capture(device):
path = os.path.expandvars("$PLAINBOX_SESSION_SHARE")
print("Images will be written to:\n{}\n".format(path), flush=True)

for mode_no, (res, fr) in enumerate(test_res):
with picamera.PiCamera(resolution=res, framerate=fr) as camera:
print("Camera initialised, wait to settle...", flush=True)
time.sleep(2)

print("Resolution: {}".format(camera.resolution))
print("Framerate: {}".format(camera.framerate))
file = "picam_{}.jpg".format(mode_no + 1)

file = "picam_{}_{}.jpg".format(mode_no + 1, device.split("/")[-1])
camera.capture(os.path.join(path, file))
print("Image {} captured\n".format(file))

Expand All @@ -56,8 +59,9 @@ def main():
parser = argparse.ArgumentParser(description="PiCamera Tests")
parser.add_argument("--device", default="/dev/vchiq", type=str)
args = parser.parse_args()

print("Resolutions test on device: {}".format(args.device), flush=True)
return capture()
return capture(args.device)


if __name__ == "__main__":
Expand Down
128 changes: 0 additions & 128 deletions providers/base/bin/roundtrip_qr.py

This file was deleted.

65 changes: 65 additions & 0 deletions providers/base/tests/test_camera_test_rpi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright 2026 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import sys
import unittest
from unittest.mock import MagicMock, call, patch

sys.modules["picamera"] = MagicMock()
from camera_test_rpi import main, capture, test_res # noqa: E402


class CameraTestRPITests(unittest.TestCase):
"""Test cases for the camera_test_rpi module."""

@patch("builtins.print", MagicMock())
@patch("camera_test_rpi.time.sleep", MagicMock())
@patch("camera_test_rpi.os.path")
@patch("camera_test_rpi.picamera.PiCamera")
def test_capture(self, mock_picamera, mock_path):
mock_path.expandvars.return_value = "/tmp/session"
mock_path.join.side_effect = [
"/tmp/session/picam_{}_vchiq.jpg".format(i)
for i in range(1, len(test_res) + 2)
]

capture("/dev/vchiq")

# Verify the camera calls
expected_picamera_calls = [
call(resolution=res, framerate=fr) for res, fr in test_res
]
self.assertEqual(mock_picamera.call_count, len(test_res))
self.assertEqual(mock_picamera.call_args_list, expected_picamera_calls)

# Verify the capture calls
expected_capture_calls = [
call("/tmp/session/picam_{}_{}.jpg".format(index, "vchiq"))
for index in range(1, len(test_res) + 1)
]
camera = mock_picamera.return_value.__enter__.return_value
camera.capture.assert_has_calls(expected_capture_calls)

@patch("camera_test_rpi.capture")
def test_main_passes_device(self, mock_capture):
sys.argv = ["camera_test_rpi.py", "--device", "/dev/video0"]
main()
mock_capture.assert_called_once_with("/dev/video0")

@patch("camera_test_rpi.capture")
def test_main_default_device(self, mock_capture):
sys.argv = ["camera_test_rpi.py"]
main()
mock_capture.assert_called_once_with("/dev/vchiq")
4 changes: 1 addition & 3 deletions providers/base/units/camera/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ Jobs
- **led**
- **still**
- **multiple-resolution-images-rpi**
- **multiple-resolution-images-rpi**
- **roundtrip-qrcode**
- **camera-quality**: Computes the quality of the image using the brisque score
It depends on python3-opencv and libsvm3.
Comment thread
fernando79513 marked this conversation as resolved.
- **camera-quality-rpi**: RPi-specific variant of the BRISQUE image quality check.
Comment thread
fernando79513 marked this conversation as resolved.
Outdated

**NOTE:**

The python svm library is vendorized and imports `libsvm.so.3` from the
`LD_LIBRARY_PATH`. If the version is updated in the future, it should be
changed manually in the `svm.py` file under
`checkbox-support/vendor/brisque/svm`.

74 changes: 26 additions & 48 deletions providers/base/units/camera/jobs.pxu
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,6 @@ command:
_summary: This Automated test attempts to detect a camera.
user: root

plugin: shell
category_id: com.canonical.plainbox::camera
id: camera/detect-rpi
estimated_duration: 1.0
imports: from com.canonical.plainbox import manifest
requires:
manifest.has_rpi_camera == 'True'
command:
udev_resource.py -f MMAL | grep "category: MMAL"
_summary: Detect presence of a MMAL camera.

unit: template
category_id: com.canonical.plainbox::camera
template-resource: device
Expand Down Expand Up @@ -191,6 +180,17 @@ command:
_description:
This test will attach the image used for the BRISQUE score.

plugin: shell
category_id: com.canonical.plainbox::camera
id: camera/detect-rpi
estimated_duration: 1.0
imports: from com.canonical.plainbox import manifest
requires:
manifest.has_rpi_camera == 'True'
command:
udev_resource.py -f MMAL | grep "category: MMAL"
_summary: Detect presence of a MMAL camera.

unit: template
template-resource: device
template-filter: device.category == 'MMAL' and device.name != ''
Expand Down Expand Up @@ -225,50 +225,28 @@ estimated_duration: 1s
after: camera/multiple-resolution-images-rpi_{name}
requires: cpuinfo.platform == 'armv7l'
command:
[ -f "$PLAINBOX_SESSION_SHARE"/picam_1.jpg ] &&
cat "$PLAINBOX_SESSION_SHARE"/picam_1.jpg
[ -f "$PLAINBOX_SESSION_SHARE"/picam_1_{name}.jpg ] &&
cat "$PLAINBOX_SESSION_SHARE"/picam_1_{name}.jpg
_description:
This test will attach one of the images used for the multiple resolution
images test.
images test on rpi.

unit: template
template-engine: jinja2
template-resource: device
template-filter: device.category in ('CAPTURE', 'MMAL') and device.name != ''
template-filter: device.category == 'MMAL' and device.name != ''
template-unit: job
plugin: shell
category_id: com.canonical.plainbox::camera
id: camera/roundtrip-qrcode_{{ name }}
template-id: camera/roundtrip-qrcode_name
_summary: Test video output and camera {{ name }} by displaying and reading a QR code
estimated_duration: 5.0
depends:
{%- if category == 'MMAL' %}
camera/detect-rpi
{%- else %}
camera/detect
{% endif -%}
requires:
{#
If the device that generated this test is MMAL, check that we are on armhf
(libmmal doesn't exist for amd64 or arm64).
See: https://github.com/waveform80/picamera/issues/716#issuecomment-1063878114
#}
(device.name == '{{name}}' and device.category == 'MMAL' and dpkg.architecture == 'armhf') or (device.name == '{{ name}}' and device.category == 'CAPTURE')
{%- if __on_ubuntucore__ %}
os.release >= '19.1'
{%- else %}
os.release >= '19.1'
package.name == 'python3-zbar'
package.name == 'python3-pyqrcode'
package.name == 'python3-pil'
{% endif -%}
id: camera/camera-quality-rpi_{name}
template-id: camera/camera-quality-rpi_name
_summary: Webcam BRISQUE score for rpi devices on {name}
estimated_duration: 20s
depends: camera/detect-rpi
after: camera/multiple-resolution-images-rpi_{name}
requires: cpuinfo.platform == 'armv7l' and os.release >= '20'
command:
roundtrip_qr.py {{ name }}
camera_quality_test.py -f "$PLAINBOX_SESSION_SHARE"/picam_1_{name}.jpg
_purpose:
Generates a QR code representing a random string of ASCII letters. This is
written to tty1 using ASCII escape codes. Either the PiCamera python module or
a GStreamer pipeline is used to capture an image of the display. An attempt
to decode a QR code in the image is then made and data compared against the
random string.
user: root
Uses an image of the camera to get the quality based on a No-Reference image
quality assessment algorithm called BRISQUE. This test will timeout and fail
if the quality has not been computed within 120 seconds.
Loading
Loading