Age | Commit message (Collapse) | Author |
|
Rename the Capture class to CameraSession, to prepare for multi-camera
support that will gather more camera-related operations than capture in
that class.
While at it, remove an unneeded blank line.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
The options parser currently ignores non-option arguments silently,
which is confusing. It's a common error to run 'cam -c1 -C 10' and
expect only 10 frames to be captured. As the -C option takes an optional
argument, the number 10 is interpreted as a non-option argument instead
of the argument to the -C option.
Fail parsing with an appropriate message and print usage information
when a non-option argument is found. The parser may be extended later to
accept non-option arguments when the application has a use for them.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
The OptionValue toKeyValues() and toArray() functions return a copy of
the values. This is unnecessary, and can cause use-after-free issues
when taking references to the return values. Return references instead
to optimize the implementation and avoid issues.
The behaviour of the two functions is now undefined in case of an option
type mismatch. The current implementation catches this with an
assertion.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
The roles() and updateConfiguration() functions check if the OptStream
OptionValue they receive is empty by first casting it to an array. To
prepare for the toArray() function not allowing such a cast when the
option value is empty, test if the option value is empty instead.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
Add a convenience helper to check if an option value is empty, based on
the value type. This is useful as a shortcut syntax to check if an
option has been set.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
While OptionValue cast operators to int and std::string allow useful
shortcut syntaxes, the cast operators to KeyValueParser::Options and
std::vector<OptionValue> are less useful. A an explicit static_cast call
would be more cumbersome to write than an explicit .toKeyValues() or
toArray(), and implicit cast hide too much of what's going on.
Drop those two cast operators, and replace the only implicit cast
occurrence with .toKeyValues(). While at it, drop the local opts
variable in StreamKeyValueParser::roles() as it isn't used.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Add support for creating a tree-based hieararchy of options instead of a
flat list. This is useful to support options that need to be interpreted
in the context of a particular occurrence of an array option.
The usage text automatically documents the options in their
corresponding context:
Options:
-c, --camera camera ... Specify which camera to operate on, by id or by index
-h, --help Display this help message
-I, --info Display information about stream(s)
-l, --list List all cameras
--list-controls List cameras controls
-p, --list-properties List cameras properties
-m, --monitor Monitor for hotplug and unplug camera events
Options valid in the context of --camera:
-C, --capture[=count] Capture until interrupted by user or until <count> frames captured
-F, --file[=filename] Write captured frames to disk
If the file name ends with a '/', it sets the directory in which
to write files, using the default file name. Otherwise it sets the
full file path and name. The first '#' character in the file name
is expanded to the camera index, stream name and frame sequence number.
The default file name is 'frame-#.bin'.
-s, --stream key=value[,key=value,...] ... Set configuration of a camera stream
height=integer Height in pixels
pixelformat=string Pixel format name
role=string Role for the stream (viewfinder, video, still, raw)
width=integer Width in pixels
--strict-formats Do not allow requested stream format(s) to be adjusted
--metadata Print the metadata for completed requests
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
When printing usage information for a key-value parser, the
documentation of the keys and values is printed in the second column of
the usage text:
-s, --stream key=value[,key=value,...] ... Set configuration of a camera stream
height=integer Height in pixels
pixelformat=string Pixel format name
role=string Role for the stream (viewfinder, video, still, raw)
width=integer Width in pixels
-h, --help Display this help message
This results in long lines. Improve this by moving the key description
to the first column, and aligning the value description as other option
description text:
-s, --stream key=value[,key=value,...] ... Set configuration of a camera stream
height=integer Height in pixels
pixelformat=string Pixel format name
role=string Role for the stream (viewfinder, video, still, raw)
width=integer Width in pixels
-h, --help Display this help message
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Copying the OptionsParser class would result in the optionsMap_ entries
pointing to Option entries of the original instance. As there's no use
case for copying the class, disable copying.
Disable copying of KeyValueParser as well for consistency as there's no
use case either.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
To prepare for code reuse, split the printing of options out of
OptionsParser::usage() to a separate function.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Add an Option::optionName() function that returns a string describing
the option name, with leading dashes. As a result,
OptionsParser::parseValueError() function becomes a single-line function
and can be inlined in its caller.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
To prepare for usage of the OptionsParser::Options class in OptionValue,
move the definition of the OptionValue class after OptionsParser. There
is no functional change.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Before extending the option parser, document its existing API.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
The Option structure is an internal implementation detail and shouldn't
be exposed in the API. Move it to options.cpp. This requires moving the
inline constructors and destructors for the KeyValueParser and
OptionsParser classes to options.cpp as well, as they need a full
definition of the Option structure.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
The KeyValueParser::usage() function is meant to be called from an
OptionsParser or another KeyValueParser only. Make it private, and set
the OptionsParser class as a friend of the KeyValueParser class.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
libcamera names header files based on the classes they define. The
buffer.h file is an exception. Rename it to framebuffer.h.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Umang Jain <umang.jain@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Reviewed-by: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Now that we have a libcamera_private, make the public only dependency
libcamera_public so that it is clear which interface is being linked.
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Implement support for the new '--metadata' option by printing the
value of each metadata entry associated with a completed Request.
As sample of the output, running on raspberry pi, looks like the
following:
3050.205672 (30.01 fps) stream0 seq: 000033 bytesused: 720000
ScalerCrop = (0x2)/3280x2460
ExposureTime = 13969
AeLocked = true
DigitalGain = 1.000721
Lux = 771.204224
ColourGains = [ 1.561101, 1.629698 ]
ColourTemperature = 4289
SensorBlackLevels = [ 4096, 4096, 4096, 4096 ]
ColourCorrectionMatrix = [ 1.691066, -0.599756, -0.091317, -0.437452, 1.983766, -0.546314, -0.083429, -0.722407, 1.805836 ]
AnalogueGain = 2.000000
SensorTimestamp = 3050205672000
3050.238999 (30.01 fps) stream0 seq: 000034 bytesused: 720000
ScalerCrop = (0x2)/3280x2460
ExposureTime = 13969
AeLocked = true
DigitalGain = 1.000709
Lux = 771.232422
ColourGains = [ 1.560868, 1.630029 ]
ColourTemperature = 4289
SensorBlackLevels = [ 4096, 4096, 4096, 4096 ]
ColourCorrectionMatrix = [ 1.691081, -0.599726, -0.091362, -0.437497, 1.983627, -0.546130, -0.083420, -0.722523, 1.805943 ]
AnalogueGain = 2.000000
SensorTimestamp = 3050238999000
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
Add the "--metadata" option to the cam tool, which will be used to
print the metadata associated with a completed capture request.
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
Add the application and adaptation layers being built to the meson
summary. The summary now prints
libcamera 0.0.0
Configuration
Enabled pipelines: ipu3
raspberrypi
rkisp1
simple
uvcvideo
vimc
Android support: True
GStreamer support: True
V4L2 emulation support: True
cam application: True
qcam application: True
Unit tests: True
Subprojects
libyuv: YES
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Hirokazu Honda <hiroh@chromium.org>
|
|
Building the cam application isn't always desired, for instance in a
production environment that only needs the libcamera library. Add a
meson option to disable it.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
In preparation to register the Location property only if the firware
interface provides it, do not assume it is available and build the
camera name using the camera sensor model as a fallback.
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
The cam option --capture=N is suppose to only capture N requests. But if
the processing done for each request is large (such as writing it to a
slow disk) the current implementation could queue more than N requests
before the exit condition is detected and capturing stopped.
Solve this by only queueing N requests while still waiting for N
requests to complete before exiting.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
Cam uses libevent to deal with threads and idle loop while still
implementing its own event queue. This creates issues when the event
loop is terminated as it might get stuck in the idle loop if exit() is
called while the thread is busy with dispatchCalls().
Solve this by removing the custom event execution and instead injecting
the calls as events to the base event loop.
Reported-by: Sebastian Fricke <sebastian.fricke@posteo.net>
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
The 'event' variable name is usually used for events added to the base
event loop, not the loop itself. Rename the struct event_base member to
base_ as a preparation for future work adding events to the loop.
There is no functional change.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
The description text lists "stillraw" as a stream role option. This is
incorrect, it should be listed as "raw" instead.
Signed-off-by: Naushir Patuck <naush@raspberrypi.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
The request completion handler is invoked in the camera manager thread,
which shouldn't be blocked for large amounts of time. As writing the
frames to disk can be a time-consuming process, move request processing
to the main thread by queueing an event to the event loop.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
|
|
Add a deferred cals queue to the EventLoop class to support queuing
calls from a different thread and processing them in the event loop's
thread.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
|
|
To prepare for removal of the EventDispatcher from the libcamera public
API, switch to libevent to handle the event loop.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
|
|
The base class of polymorphic classes is required to declare a
destructor. Several of these are empty, and can thus be declared as
defaulted.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Umang Jain <email@uajain.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
The parseValue function is given the optarg directly from the getopt
library, but the function retains the same name.
This causes an shadowed variable of the global optarg variable to be
present in the parseValue function. While this is not harmful, rename it
to work towards disabling shadowed variables.
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
|
|
operator[] doesn't check if the option exists in the values_ map, so it
can return a pointer to location outside the map.
Fix by returning an empty OptionValue if the option is not found.
Signed-off-by: Tomi Valkeinen <tomi.valkeinen@iki.fi>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
[Kieran: Adjust s_empty naming to 'empty']
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Initialize last_ to 0 to avoid uninitialized use.
Signed-off-by: Tomi Valkeinen <tomi.valkeinen@iki.fi>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Allow reuse of the Request object by implementing reuse(). This means
the applications now have the responsibility of freeing the Request
objects, so make all libcamera users (cam, qcam, v4l2-compat, gstreamer,
android) do so.
Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
|
|
Instead of only printing the camera ID which is not intended for humans
to read and parse create a more user-friendly string when printing
camera names. The ID is still printed as it is one option used to select
camera using the --camera option.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Umang Jain <email@uajain.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
With the buffer copy removed from all pipelines for raw capture
rename StillCaptureRaw to Raw to better describe the role.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: David Plowman <david.plowman@raspberrypi.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
|
|
The payload size in a captured framebuffer is usually equal to the
buffer size. However, for compressed formats, the payload may be
smaller Only write the payload when capturing to a file.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Print the timestamp of the captured buffer in addition to the frame
rate, as this is more precise information that can help debugging issue.
The log changes from
Using camera \_SB_.PCI0.XHC_.RHUB.HS05-5:1.0-0bda:58f4
Capture until user interrupts by SIGINT
fps: 0.00 stream0 seq: 000000 bytesused: 169968
fps: 28.57 stream0 seq: 000002 bytesused: 170352
fps: 31.25 stream0 seq: 000003 bytesused: 170428
fps: 32.26 stream0 seq: 000004 bytesused: 170672
fps: 27.78 stream0 seq: 000005 bytesused: 170568
to
Using camera \_SB_.PCI0.XHC_.RHUB.HS05-5:1.0-0bda:58f4
Capture until user interrupts by SIGINT
93473.942525 (0.00 fps) stream0 seq: 000000 bytesused: 183940
93474.006528 (15.62 fps) stream0 seq: 000002 bytesused: 183512
93474.038525 (31.25 fps) stream0 seq: 000003 bytesused: 183760
93474.074530 (27.77 fps) stream0 seq: 000004 bytesused: 182704
93474.106581 (31.20 fps) stream0 seq: 000005 bytesused: 182768
While at it, compute the frame rate on the buffer timestamps instead of
sampling the clock in the request completion handler.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
|
|
We build libcamera with -Wno-unused-parameter and this doesn't cause
much issue internally. However, it prevents catching unused parameters
in inline functions defined in public headers. This can lead to
compilation warnings for applications compiled without
-Wno-unused-parameter.
To catch those issues, remove -Wno-unused-parameter and fix all the
related warnings with [[maybe_unused]].
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
|
|
The Stream pointer just acts as a key in the Request object. There is no
good use-case to modify a stream from a pointer retrieved from the
Request, make it const. This allows pipeline handlers to better express
that the Stream pointer is retrieved in a Request should just be treated
as a key.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Declare a using directive for the map of Stream to FrameBuffer. Update
all users of Request::buffers() to use the new usage directive.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
When converting Camera::name() to Camera::id() one variable in cam was
left unnoticed, rename it to cameraId.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
Rename Camera::name() to camera::id() to better describe what it
represents, a unique and stable ID for the camera. While at it improve
the documentation for the camera ID to describe it needs to be stable
for a camera between resets of the system.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
The different error paths in init() are out of sync. Instead of fixing
them switch to using the cleanup() function which does the right thing
for all cases.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
|
|
Add --monitor to monitor new hotplug and unplug camera events from
the CameraManager.
Signed-off-by: Umang Jain <email@uajain.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Replace hex input for pixel formats with their format names, for input in
cam and qcam.
Hence, remove the todo.
Signed-off-by: Kaaira Gupta <kgupta@es.iitr.ac.in>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
Extend the '--capture' option with and optional numerical argument to be
able to specify how many frames to capture before exiting. If the
optional argument is not provided the old behavior of running until the
user interrupts with a SIGINT is retained.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
Prepare for the ability to exit the event loop based on conditions in
the request complete handler by caching the pointer instead of passing
it as an argument.
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
Add a '--strict-formats' option which fails the camera configuration
step if the format is adjusted,
Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
|
Several headers belonging to cam and qcam use __LIBCAMERA_*_H__ as a
header guard. They're not part of the libcamera core, use __CAM_*_H__
and __QCAM_*_H__ instead, similarly to all other headers of cam and
qcam.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
|