weston/clients/touch-calibrator.c
Pekka Paalanen 96dc449259 Rename matrix.h to libweston/matrix.h
matrix.h is a public installed header and even used by libweston.h.

See "Rename compositor.h to libweston/libweston.h" for rationale.

Signed-off-by: Pekka Paalanen <pekka.paalanen@collabora.com>
2019-04-18 12:31:46 +03:00

971 lines
21 KiB
C

/*
* Copyright 2012 Intel Corporation
* Copyright 2017-2018 Collabora, Ltd.
* Copyright 2017-2018 General Electric Company
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "config.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cairo.h>
#include <math.h>
#include <assert.h>
#include <getopt.h>
#include <errno.h>
#include <wayland-client.h>
#include "clients/window.h"
#include "shared/helpers.h"
#include <libweston/matrix.h>
#include "weston-touch-calibration-client-protocol.h"
enum exit_code {
CAL_EXIT_SUCCESS = 0,
CAL_EXIT_ERROR = 1,
CAL_EXIT_CANCELLED = 2,
};
static int debug_;
static int verbose_;
#define pr_ver(...) do { \
if (verbose_) \
printf(__VA_ARGS__); \
} while (0)
#define pr_dbg(...) do { \
if (debug_) \
fprintf(stderr, __VA_ARGS__); \
} while (0)
static void
pr_err(const char *fmt, ...) WL_PRINTF(1, 2);
/* Our points for the calibration must be not be on a line */
static const struct {
float x_ratio, y_ratio;
} test_ratios[] = {
{ 0.15, 0.10 }, /* three points for calibration */
{ 0.85, 0.13 },
{ 0.20, 0.80 },
{ 0.70, 0.75 } /* and one for verification */
};
#define NR_SAMPLES ((int)ARRAY_LENGTH(test_ratios))
struct point {
double x;
double y;
};
struct sample {
int ind;
struct point drawn; /**< drawn point, pixels */
struct weston_touch_coordinate *pending;
struct point drawn_cal; /**< drawn point, converted */
bool conv_done;
struct point touched; /**< touch point, normalized */
bool touch_done;
};
struct poly {
struct color {
double r, g, b, a;
} color;
int n_verts;
const struct point *verts;
};
/** Touch event handling state machine
*
* Only a complete down->up->frame sequence should be accepted with user
* feedback "right", and anything that deviates from that (invalid_touch,
* cancel, multiple touch-downs) needs to undo the current sample and
* possibly show user feedback "wrong".
*
* <STATE>
* - <triggers>: <actions>
*
* IDLE
* - touch down: sample, -> DOWN
* - touch up: no-op
* - frame: no-op
* - invalid_touch: (undo), wrong, -> WAIT
* - cancel: no-op
* DOWN (first touch down)
* - touch down: undo, wrong, -> WAIT
* - touch up: -> UP
* - frame: no-op
* - invalid_touch: undo, wrong, -> WAIT
* - cancel: undo, -> IDLE
* UP (first touch was down and up)
* - touch down: undo, wrong, -> WAIT
* - touch up: no-op
* - frame: right, touch finish, -> WAIT
* - invalid_touch: undo, wrong, -> WAIT
* - cancel: undo, -> IDLE
* WAIT (show user feedback)
* - touch down: no-op
* - touch up: no-op
* - frame, cancel, timer: if num_tp == 0 && timer_done -> IDLE
* - invalid_touch: no-op
*/
enum touch_state {
STATE_IDLE,
STATE_DOWN,
STATE_UP,
STATE_WAIT
};
struct calibrator {
struct sample samples[NR_SAMPLES];
int current_sample;
struct display *display;
struct weston_touch_calibration *calibration;
struct weston_touch_calibrator *calibrator;
struct window *window;
struct widget *widget;
int n_devices_listed;
char *match_name;
char *device_name;
int width;
int height;
bool cancelled;
const struct poly *current_poly;
bool exiting;
struct toytimer wait_timer;
bool timer_pending;
enum touch_state state;
int num_tp; /* touch points down count */
};
static struct sample *
current_sample(struct calibrator *cal)
{
return &cal->samples[cal->current_sample];
}
static void
sample_start(struct calibrator *cal, int i)
{
struct sample *s = &cal->samples[i];
assert(i >= 0 && i < NR_SAMPLES);
s->ind = i;
s->drawn.x = round(test_ratios[i].x_ratio * cal->width);
s->drawn.y = round(test_ratios[i].y_ratio * cal->height);
s->pending = NULL;
s->conv_done = false;
s->touch_done = false;
cal->current_sample = i;
}
static struct point
wire_to_point(uint32_t xu, uint32_t yu)
{
struct point p = {
.x = (double)xu / 0xffffffff,
.y = (double)yu / 0xffffffff
};
return p;
}
static void
sample_touch_down(struct calibrator *cal, uint32_t xu, uint32_t yu)
{
struct sample *s = current_sample(cal);
s->touched = wire_to_point(xu, yu);
s->touch_done = true;
pr_dbg("Down[%d] (%f, %f)\n", s->ind, s->touched.x, s->touched.y);
}
static void
coordinate_result_handler(void *data, struct weston_touch_coordinate *interface,
uint32_t xu, uint32_t yu)
{
struct sample *s = data;
weston_touch_coordinate_destroy(s->pending);
s->pending = NULL;
s->drawn_cal = wire_to_point(xu, yu);
s->conv_done = true;
pr_dbg("Conv[%d] (%f, %f)\n", s->ind, s->drawn_cal.x, s->drawn_cal.y);
}
struct weston_touch_coordinate_listener coordinate_listener = {
coordinate_result_handler
};
static void
sample_undo(struct calibrator *cal)
{
struct sample *s = current_sample(cal);
pr_dbg("Undo[%d]\n", s->ind);
s->touch_done = false;
s->conv_done = false;
if (s->pending) {
weston_touch_coordinate_destroy(s->pending);
s->pending = NULL;
}
}
static void
sample_finish(struct calibrator *cal)
{
struct sample *s = current_sample(cal);
pr_dbg("Finish[%d]\n", s->ind);
assert(!s->pending && !s->conv_done);
s->pending = weston_touch_calibrator_convert(cal->calibrator,
(int32_t)s->drawn.x,
(int32_t)s->drawn.y);
weston_touch_coordinate_add_listener(s->pending,
&coordinate_listener, s);
if (cal->current_sample + 1 < NR_SAMPLES) {
sample_start(cal, cal->current_sample + 1);
} else {
pr_dbg("got all touches\n");
cal->exiting = true;
}
}
/*
* Calibration algorithm:
*
* The equation we want to apply at event time where x' and y' are the
* calibrated co-ordinates.
*
* x' = Ax + By + C
* y' = Dx + Ey + F
*
* For example "zero calibration" would be A=1.0 B=0.0 C=0.0, D=0.0, E=1.0,
* and F=0.0.
*
* With 6 unknowns we need 6 equations to find the constants:
*
* x1' = Ax1 + By1 + C
* y1' = Dx1 + Ey1 + F
* ...
* x3' = Ax3 + By3 + C
* y3' = Dx3 + Ey3 + F
*
* In matrix form:
*
* x1' x1 y1 1 A
* x2' = x2 y2 1 x B
* x3' x3 y3 1 C
*
* So making the matrix M we can find the constants with:
*
* A x1'
* B = M^-1 x x2'
* C x3'
*
* (and similarly for D, E and F)
*
* For the calibration the desired values x, y are the same values at which
* we've drawn at.
*
*/
static int
compute_calibration(struct calibrator *cal, float *result)
{
struct weston_matrix m;
struct weston_matrix inverse;
struct weston_vector x_calib;
struct weston_vector y_calib;
int i;
assert(NR_SAMPLES >= 3);
/*
* x1 y1 1 0
* x2 y2 1 0
* x3 y3 1 0
* 0 0 0 1
*/
weston_matrix_init(&m);
for (i = 0; i < 3; i++) {
m.d[i + 0] = cal->samples[i].touched.x;
m.d[i + 4] = cal->samples[i].touched.y;
m.d[i + 8] = 1.0f;
}
m.type = WESTON_MATRIX_TRANSFORM_OTHER;
if (weston_matrix_invert(&inverse, &m) < 0) {
pr_err("non-invertible matrix during computation\n");
return -1;
}
for (i = 0; i < 3; i++) {
x_calib.f[i] = cal->samples[i].drawn_cal.x;
y_calib.f[i] = cal->samples[i].drawn_cal.y;
}
x_calib.f[3] = 0.0f;
y_calib.f[3] = 0.0f;
/* Multiples into the vector */
weston_matrix_transform(&inverse, &x_calib);
weston_matrix_transform(&inverse, &y_calib);
for (i = 0; i < 3; i++)
result[i] = x_calib.f[i];
for (i = 0; i < 3; i++)
result[i + 3] = y_calib.f[i];
return 0;
}
static int
verify_calibration(struct calibrator *cal, const float *r)
{
double thr = 0.1; /* accepted error radius */
struct point e; /* expected value; error */
const struct sample *s = &cal->samples[3];
/* transform raw touches through the matrix */
e.x = r[0] * s->touched.x + r[1] * s->touched.y + r[2];
e.y = r[3] * s->touched.x + r[4] * s->touched.y + r[5];
/* compute error */
e.x -= s->drawn_cal.x;
e.y -= s->drawn_cal.y;
pr_dbg("calibration test error: %f, %f\n", e.x, e.y);
if (e.x * e.x + e.y * e.y < thr * thr)
return 0;
pr_err("Calibration verification failed, too large error.\n");
return -1;
}
static void
send_calibration(struct calibrator *cal, float *values)
{
struct wl_array matrix;
float *f;
int i;
wl_array_init(&matrix);
for (i = 0; i < 6; i++) {
f = wl_array_add(&matrix, sizeof *f);
*f = values[i];
}
weston_touch_calibration_save(cal->calibration,
cal->device_name, &matrix);
wl_array_release(&matrix);
}
static const struct point cross_verts[] = {
{ 0.1, 0.2 },
{ 0.2, 0.1 },
{ 0.5, 0.4 },
{ 0.8, 0.1 },
{ 0.9, 0.2 },
{ 0.6, 0.5 },
{ 0.9, 0.8 },
{ 0.8, 0.9 },
{ 0.5, 0.6 },
{ 0.2, 0.9 },
{ 0.1, 0.8 },
{ 0.4, 0.5 },
};
/* a red cross, for "wrong" */
static const struct poly cross = {
.color = { 0.7, 0.0, 0.0, 1.0 },
.n_verts = ARRAY_LENGTH(cross_verts),
.verts = cross_verts
};
static const struct point check_verts[] = {
{ 0.5, 0.7 },
{ 0.8, 0.1 },
{ 0.9, 0.1 },
{ 0.55, 0.8 },
{ 0.45, 0.8 },
{ 0.3, 0.5 },
{ 0.4, 0.5 }
};
/* a green check mark, for "right" */
static const struct poly check = {
.color = { 0.0, 0.7, 0.0, 1.0 },
.n_verts = ARRAY_LENGTH(check_verts),
.verts = check_verts
};
static void
draw_poly(cairo_t *cr, const struct poly *poly)
{
int i;
cairo_set_source_rgba(cr, poly->color.r, poly->color.g,
poly->color.b, poly->color.a);
cairo_move_to(cr, poly->verts[0].x, poly->verts[0].y);
for (i = 1; i < poly->n_verts; i++)
cairo_line_to(cr, poly->verts[i].x, poly->verts[i].y);
cairo_close_path(cr);
cairo_fill(cr);
}
static void
feedback_show(struct calibrator *cal, const struct poly *what)
{
cal->current_poly = what;
widget_schedule_redraw(cal->widget);
toytimer_arm_once_usec(&cal->wait_timer, 1000 * 1000);
cal->timer_pending = true;
}
static void
feedback_hide(struct calibrator *cal)
{
cal->current_poly = NULL;
widget_schedule_redraw(cal->widget);
}
static void
try_enter_state_idle(struct calibrator *cal)
{
if (cal->num_tp != 0)
return;
if (cal->timer_pending)
return;
cal->state = STATE_IDLE;
feedback_hide(cal);
if (cal->exiting)
display_exit(cal->display);
}
static void
enter_state_wait(struct calibrator *cal)
{
assert(cal->timer_pending);
cal->state = STATE_WAIT;
}
static void
wait_timer_done(struct toytimer *tt)
{
struct calibrator *cal = container_of(tt, struct calibrator, wait_timer);
assert(cal->state == STATE_WAIT);
cal->timer_pending = false;
try_enter_state_idle(cal);
}
static void
redraw_handler(struct widget *widget, void *data)
{
struct calibrator *cal = data;
struct sample *s = current_sample(cal);
struct rectangle allocation;
cairo_surface_t *surface;
cairo_t *cr;
widget_get_allocation(cal->widget, &allocation);
assert(allocation.width == cal->width);
assert(allocation.height == cal->height);
surface = window_get_surface(cal->window);
cr = cairo_create(surface);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
cairo_paint(cr);
if (!cal->current_poly) {
cairo_translate(cr, s->drawn.x, s->drawn.y);
cairo_set_line_width(cr, 2.0);
cairo_set_source_rgb(cr, 0.7, 0.0, 0.0);
cairo_move_to(cr, 0, -10.0);
cairo_line_to(cr, 0, 10.0);
cairo_stroke(cr);
cairo_move_to(cr, -10.0, 0);
cairo_line_to(cr, 10.0, 0.0);
cairo_stroke(cr);
} else {
cairo_scale(cr, allocation.width, allocation.height);
draw_poly(cr, cal->current_poly);
}
cairo_destroy(cr);
cairo_surface_destroy(surface);
}
static struct calibrator *
calibrator_create(struct display *display, const char *match_name)
{
struct calibrator *cal;
cal = zalloc(sizeof *cal);
if (!cal)
abort();
cal->match_name = match_name ? strdup(match_name) : NULL;
cal->window = window_create_custom(display);
cal->widget = window_add_widget(cal->window, cal);
window_inhibit_redraw(cal->window);
window_set_title(cal->window, "Touchscreen calibrator");
cal->display = display;
widget_set_redraw_handler(cal->widget, redraw_handler);
toytimer_init(&cal->wait_timer, CLOCK_MONOTONIC,
display, wait_timer_done);
cal->state = STATE_IDLE;
cal->num_tp = 0;
return cal;
}
static void
configure_handler(void *data, struct weston_touch_calibrator *interface,
int32_t width, int32_t height)
{
struct calibrator *cal = data;
pr_dbg("Configure calibrator window to size %ix%i\n", width, height);
cal->width = width;
cal->height = height;
window_schedule_resize(cal->window, width, height);
window_uninhibit_redraw(cal->window);
sample_start(cal, 0);
widget_schedule_redraw(cal->widget);
}
static void
cancel_calibration_handler(void *data, struct weston_touch_calibrator *interface)
{
struct calibrator *cal = data;
pr_dbg("calibration cancelled by the display server, quitting.\n");
cal->cancelled = true;
display_exit(cal->display);
}
static void
invalid_touch_handler(void *data, struct weston_touch_calibrator *interface)
{
struct calibrator *cal = data;
pr_dbg("invalid touch\n");
switch (cal->state) {
case STATE_IDLE:
case STATE_DOWN:
case STATE_UP:
sample_undo(cal);
feedback_show(cal, &cross);
enter_state_wait(cal);
break;
case STATE_WAIT:
/* no-op */
break;
}
}
static void
down_handler(void *data, struct weston_touch_calibrator *interface,
uint32_t time, int32_t id, uint32_t xu, uint32_t yu)
{
struct calibrator *cal = data;
cal->num_tp++;
switch (cal->state) {
case STATE_IDLE:
sample_touch_down(cal, xu, yu);
cal->state = STATE_DOWN;
break;
case STATE_DOWN:
case STATE_UP:
sample_undo(cal);
feedback_show(cal, &cross);
enter_state_wait(cal);
break;
case STATE_WAIT:
/* no-op */
break;
}
if (cal->current_poly)
return;
}
static void
up_handler(void *data, struct weston_touch_calibrator *interface,
uint32_t time, int32_t id)
{
struct calibrator *cal = data;
cal->num_tp--;
if (cal->num_tp < 0) {
pr_dbg("Unmatched touch up.\n");
cal->num_tp = 0;
}
switch (cal->state) {
case STATE_DOWN:
cal->state = STATE_UP;
break;
case STATE_IDLE:
case STATE_UP:
case STATE_WAIT:
/* no-op */
break;
}
}
static void
motion_handler(void *data, struct weston_touch_calibrator *interface,
uint32_t time, int32_t id, uint32_t xu, uint32_t yu)
{
/* motion is ignored */
}
static void
frame_handler(void *data, struct weston_touch_calibrator *interface)
{
struct calibrator *cal = data;
switch (cal->state) {
case STATE_IDLE:
case STATE_DOWN:
/* no-op */
break;
case STATE_UP:
feedback_show(cal, &check);
sample_finish(cal);
enter_state_wait(cal);
break;
case STATE_WAIT:
try_enter_state_idle(cal);
break;
}
}
static void
cancel_handler(void *data, struct weston_touch_calibrator *interface)
{
struct calibrator *cal = data;
cal->num_tp = 0;
switch (cal->state) {
case STATE_IDLE:
/* no-op */
break;
case STATE_DOWN:
case STATE_UP:
sample_undo(cal);
try_enter_state_idle(cal);
break;
case STATE_WAIT:
try_enter_state_idle(cal);
break;
}
}
struct weston_touch_calibrator_listener calibrator_listener = {
configure_handler,
cancel_calibration_handler,
invalid_touch_handler,
down_handler,
up_handler,
motion_handler,
frame_handler,
cancel_handler
};
static void
calibrator_show(struct calibrator *cal)
{
struct wl_surface *surface = window_get_wl_surface(cal->window);
cal->calibrator =
weston_touch_calibration_create_calibrator(cal->calibration,
surface,
cal->device_name);
weston_touch_calibrator_add_listener(cal->calibrator,
&calibrator_listener, cal);
}
static void
calibrator_destroy(struct calibrator *cal)
{
toytimer_fini(&cal->wait_timer);
if (cal->calibrator)
weston_touch_calibrator_destroy(cal->calibrator);
if (cal->calibration)
weston_touch_calibration_destroy(cal->calibration);
if (cal->widget)
widget_destroy(cal->widget);
if (cal->window)
window_destroy(cal->window);
free(cal->match_name);
free(cal->device_name);
free(cal);
}
static void
touch_device_handler(void *data, struct weston_touch_calibration *c,
const char *device, const char *head)
{
struct calibrator *cal = data;
cal->n_devices_listed++;
if (!cal->match_name) {
printf("device \"%s\" - head \"%s\"\n", device, head);
return;
}
if (cal->device_name)
return;
if (strcmp(cal->match_name, device) == 0 ||
strcmp(cal->match_name, head) == 0)
cal->device_name = strdup(device);
}
struct weston_touch_calibration_listener touch_calibration_listener = {
touch_device_handler
};
static void
global_handler(struct display *display, uint32_t name,
const char *interface, uint32_t version, void *data)
{
struct calibrator *cal = data;
if (strcmp(interface, "weston_touch_calibration") == 0) {
cal->calibration = display_bind(display, name,
&weston_touch_calibration_interface, 1);
weston_touch_calibration_add_listener(cal->calibration,
&touch_calibration_listener,
cal);
}
}
static int
calibrator_run(struct calibrator *cal)
{
struct wl_display *dpy;
struct sample *s;
bool wait;
int i;
int ret;
float result[6];
calibrator_show(cal);
display_run(cal->display);
if (cal->cancelled)
return CAL_EXIT_CANCELLED;
/* remove the window, no more input events */
widget_destroy(cal->widget);
cal->widget = NULL;
window_destroy(cal->window);
cal->window = NULL;
/* wait for all conversions to return */
dpy = display_get_display(cal->display);
do {
wait = false;
for (i = 0; i < NR_SAMPLES; i++)
if (cal->samples[i].pending)
wait = true;
if (wait) {
ret = wl_display_roundtrip(dpy);
if (ret < 0)
return CAL_EXIT_ERROR;
}
} while (wait);
for (i = 0; i < NR_SAMPLES; i++) {
s = &cal->samples[i];
if (!s->conv_done || !s->touch_done)
return CAL_EXIT_ERROR;
}
if (compute_calibration(cal, result) < 0)
return CAL_EXIT_ERROR;
if (verify_calibration(cal, result) < 0)
return CAL_EXIT_ERROR;
pr_ver("Calibration values:");
for (i = 0; i < 6; i++)
pr_ver(" %f", result[i]);
pr_ver("\n");
send_calibration(cal, result);
ret = wl_display_roundtrip(dpy);
if (ret < 0)
return CAL_EXIT_ERROR;
return CAL_EXIT_SUCCESS;
}
static void
pr_err(const char *fmt, ...)
{
va_list argp;
va_start(argp, fmt);
fprintf(stderr, "%s error: ", program_invocation_short_name);
vfprintf(stderr, fmt, argp);
va_end(argp);
}
static void
help(void)
{
fprintf(stderr, "Compute a touchscreen calibration matrix for "
"a Wayland compositor by\n"
"having the user touch points on the screen.\n\n");
fprintf(stderr, "Usage: %s [options...] name\n\n",
program_invocation_short_name);
fprintf(stderr,
"Where 'name' can be a touch device sys path or a head name.\n"
"If 'name' is not given, all devices available for "
"calibration will be listed.\n"
"If 'name' is given, it must be exactly as listed.\n"
"Options:\n"
" --debug Print messages to help debugging.\n"
" -h, --help Display this help message\n"
" -v, --verbose Print list header and calibration result.\n");
}
int
main(int argc, char *argv[])
{
struct display *display;
struct calibrator *cal;
int c;
char *match_name = NULL;
int exit_code = CAL_EXIT_SUCCESS;
static const struct option opts[] = {
{ "help", no_argument, NULL, 'h' },
{ "debug", no_argument, &debug_, 1 },
{ "verbose", no_argument, &verbose_, 1 },
{ 0, 0, NULL, 0 }
};
while ((c = getopt_long(argc, argv, "hv", opts, NULL)) != -1) {
switch (c) {
case 'h':
help();
return CAL_EXIT_SUCCESS;
case 'v':
verbose_ = 1;
break;
case 0:
break;
default:
return CAL_EXIT_ERROR;
}
}
if (optind < argc)
match_name = argv[optind++];
if (optind < argc) {
pr_err("extra arguments given.\n\n");
help();
return CAL_EXIT_ERROR;
}
display = display_create(&argc, argv);
if (!display)
return CAL_EXIT_ERROR;
cal = calibrator_create(display, match_name);
if (!cal)
return CAL_EXIT_ERROR;
display_set_user_data(display, cal);
display_set_global_handler(display, global_handler);
if (!match_name)
pr_ver("Available touch devices:\n");
/* Roundtrip to get list of available touch devices,
* first globals, then touch_device events */
wl_display_roundtrip(display_get_display(display));
wl_display_roundtrip(display_get_display(display));
if (!cal->calibration) {
exit_code = CAL_EXIT_ERROR;
pr_err("the Wayland server does not expose the calibration interface.\n");
} else if (cal->device_name) {
exit_code = calibrator_run(cal);
} else if (match_name) {
exit_code = CAL_EXIT_ERROR;
pr_err("\"%s\" was not found.\n", match_name);
} else if (cal->n_devices_listed == 0) {
fprintf(stderr, "No devices listed.\n");
}
calibrator_destroy(cal);
display_destroy(display);
return exit_code;
}