/* * cyclabile - CYC Light Augmented Bike Light Emitter * * Copyright (C) 2018 Antonio Ospite * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include "projective_split.h" #include "fps-meter.h" #define M_PI 3.14159265358979323846 /* pi */ /* Rotary encoder properties. */ #define PULSES_PER_REVOLUTION 120 /* Video projector property. */ #define HEIGHT_IN_PIXELS 480 /* Bike setup properties */ #define WHEEL_RADIUS_IN_METERS 0.33 #define HEIGHT_IN_METERS 0.92 #define PIXELS_PER_METER (HEIGHT_IN_PIXELS / HEIGHT_IN_METERS) static unsigned int run = 1; static struct { char *image_file; char *event_device; unsigned int width; unsigned int height; unsigned int x_correction; unsigned int y_correction; unsigned int zoom_mode; unsigned int power_mode; } config = { .image_file = NULL, .event_device = NULL, .width = 800, .height = 480, .x_correction = 200, .y_correction = 0, .zoom_mode = AM7XXX_ZOOM_ORIGINAL, .power_mode = AM7XXX_POWER_LOW, }; #ifdef USE_EQEP #define POSITION_FILE "/sys/devices/platform/ocp/48304000.epwmss/48304180.eqep/position" static int read_position(int *position) { FILE *file; int ret = 0; file = fopen(POSITION_FILE, "r"); if (file == NULL) { fprintf(stderr, "Cannot open %s\n", POSITION_FILE); return -errno; } ret = fscanf(file, "%d", position); if (ret != 1) ret = -EINVAL; else ret = 0; fclose(file); return ret; } #else static int read_position(int *position) { (void)position; return 0; } #endif static int mod(int a, int b) { int c = a % b; return (c < 0) ? c + b : c; } static int projector_device_init(am7xxx_context **ctx, am7xxx_device **dev, unsigned int device_index) { int log_level = AM7XXX_LOG_INFO; int ret; ret = am7xxx_init(ctx); if (ret < 0) { perror("am7xxx_init"); goto err; } am7xxx_set_log_level(*ctx, log_level); ret = am7xxx_open_device(*ctx, dev, device_index); if (ret < 0) { perror("am7xxx_open_device"); goto err; } ret = am7xxx_set_zoom_mode(*dev, config.zoom_mode); if (ret < 0) { perror("am7xxx_set_zoom_mode"); goto err_close_device; } ret = am7xxx_set_power_mode(*dev, config.power_mode); if (ret < 0) { perror("am7xxx_set_power_mode"); goto err_close_device; } return 0; err_close_device: am7xxx_close_device(*dev); err: am7xxx_shutdown(*ctx); return ret; } static void unset_run(int signo) { (void) signo; run = 0; } static int set_signal_handler(int signum, void (*signal_handler)(int)) { struct sigaction new_action; struct sigaction old_action; int ret; new_action.sa_handler = signal_handler; sigemptyset(&new_action.sa_mask); new_action.sa_flags = 0; ret = sigaction(signum, NULL, &old_action); if (ret < 0) { perror("sigaction on old_action"); goto out; } if (old_action.sa_handler != SIG_IGN) { ret = sigaction(signum, &new_action, NULL); if (ret < 0) { perror("sigaction on new_action"); goto out; } } out: return ret; } static struct p *calc_perspective_map(void) { PTA *src; PTA *dst; BOX *viewport; l_float32 *transform_coeffs; struct p *perspective_map = NULL; int ret; src = ptaCreate(4); if (src == NULL) return NULL; ptaAddPt(src, 0, 0); ptaAddPt(src, config.width, 0); ptaAddPt(src, config.width, config.height); ptaAddPt(src, 0, config.height); dst = ptaCreate(4); if (dst == NULL) goto out_destroy_pta_src; ptaAddPt(dst, config.x_correction, config.y_correction); ptaAddPt(dst, config.width - config.x_correction, config.y_correction); ptaAddPt(dst, config.width, config.height); ptaAddPt(dst, 0, config.height); transform_coeffs = NULL; ret = getProjectiveXformCoeffs(dst, src, &transform_coeffs); if (ret != 0) goto out_destroy_pta_dst; viewport = boxCreateValid(0, 0, config.width, config.height); if (viewport == NULL) goto out_free_transform_coeffs; perspective_map = _pixProjectiveSampled_precalc_map(viewport, transform_coeffs); boxDestroy(&viewport); out_free_transform_coeffs: LEPT_FREE(transform_coeffs); out_destroy_pta_dst: ptaDestroy(&dst); out_destroy_pta_src: ptaDestroy(&src); return perspective_map; } static void usage(char *name) { printf("usage: %s [OPTIONS]\n\n", name); printf("OPTIONS:\n"); printf("\t-f \t\tthe image file to upload\n"); printf("\t-e \t\tthe event device to get input from\n"); printf("\t-x \t\tthe perspective correction in the X direction, in pixels (default 200)\n"); printf("\t-y \t\tthe perspective correction in the Y direction, in pixels (default 50)\n"); printf("\t-Z \t\tthe display zoom mode, between %d (original) and %d (test)\n", AM7XXX_ZOOM_ORIGINAL, AM7XXX_ZOOM_TEST); printf("\t-h \t\t\tthis help message\n"); printf("\n\nEXAMPLES OF USE:\n"); printf("\t%s -f road.png -e /dev/input/event5\n", name); printf("\t%s -f narrow-road.png -e /dev/input/event0 -x 130 -Z 1\n", name); } int main(int argc, char *argv[]) { PIX *image; PIX *transformed_image; BOX *viewport; struct p *perspective_map; tjhandle jpeg_compressor; unsigned char *out_buf; unsigned long out_buf_size; am7xxx_context *projector_ctx = NULL; am7xxx_device *projector_dev = NULL; int input_fd; struct input_event ev[64]; unsigned int i; int position = 0; int old_position = 0; int position_delta; float distance; struct fps_meter_stats fps_stats; int y = 0; int lastframe_limit; int ret; int opt; while ((opt = getopt(argc, argv, "e:f:x:y:P:Z:h")) != -1) { switch (opt) { case 'e': if (config.event_device != NULL) fprintf(stderr, "Warning: event device already specified\n"); config.event_device = optarg; break; case 'f': if (config.image_file != NULL) fprintf(stderr, "Warning: image file already specified\n"); config.image_file = optarg; break; case 'x': config.x_correction = atoi(optarg); /* atoi() is as nasty as it is easy */ break; case 'y': config.y_correction = atoi(optarg); /* atoi() is as nasty as it is easy */ break; case 'Z': config.zoom_mode = atoi(optarg); /* atoi() is as nasty as it is easy */ switch(config.zoom_mode) { case AM7XXX_ZOOM_ORIGINAL: case AM7XXX_ZOOM_H: case AM7XXX_ZOOM_H_V: case AM7XXX_ZOOM_TEST: break; default: fprintf(stderr, "Invalid zoom mode value, must be between %d and %d\n", AM7XXX_ZOOM_ORIGINAL, AM7XXX_ZOOM_TEST); ret = -EINVAL; goto out; } break; case 'P': config.power_mode = atoi(optarg); /* atoi() is as nasty as it is easy */ switch(config.power_mode) { case AM7XXX_POWER_OFF: case AM7XXX_POWER_LOW: case AM7XXX_POWER_MIDDLE: case AM7XXX_POWER_HIGH: case AM7XXX_POWER_TURBO: break; default: fprintf(stderr, "Invalid power mode value, must be between %d and %d\n", AM7XXX_POWER_OFF, AM7XXX_POWER_TURBO); ret = -EINVAL; goto out; } break; case 'h': usage(argv[0]); ret = 0; goto out; default: /* '?' */ usage(argv[0]); ret = -EINVAL; goto out; } } if (config.image_file == NULL) { fprintf(stderr, "An image file MUST be specified with the -f option.\n\n"); usage(argv[0]); ret = -EINVAL; goto out; } if (config.event_device == NULL) { fprintf(stderr, "An event device MUST be specified with the -e option.\n\n"); usage(argv[0]); ret = -EINVAL; goto out; } ret = set_signal_handler(SIGINT, unset_run); if (ret < 0) { perror("setting SIGINT"); goto out; } ret = set_signal_handler(SIGTERM, unset_run); if (ret < 0) { perror("setting SIGTERM"); goto out; } image = pixRead(config.image_file); if (image == NULL) { fprintf(stderr, "Cannot load image\n"); ret = -EINVAL; goto out; } lastframe_limit = (pixGetHeight(image) - config.height); config.width = pixGetWidth(image); transformed_image = pixCreate(config.width, config.height, pixGetDepth(image)); if (transformed_image == NULL) { fprintf(stderr, "Cannot create image\n"); ret = -EINVAL; goto out_destroy_image; } /* Transform only a part of the whole image */ viewport = boxCreateValid(0, y, config.width, config.height); if (viewport == NULL) { ret = -EINVAL; goto out_destroy_transformed_image; } perspective_map = calc_perspective_map(); if (perspective_map == NULL) { ret = -ENOMEM; goto out_destroy_viewport; } /* jpeg encoder init */ jpeg_compressor = tjInitCompress(); if (jpeg_compressor == NULL) { fprintf(stderr, "tjInitCompress failed: %s\n", tjGetErrorStr()); ret = -ENOMEM; goto out_free_perspective_map; } out_buf_size = tjBufSize(config.width, config.height, TJSAMP_420); ret = (int) out_buf_size; if (ret < 0) { fprintf(stderr, "tjBufSize failed: %s\n", tjGetErrorStr()); goto out_jpeg_destroy; } out_buf = malloc(out_buf_size); if (out_buf == NULL) { ret = -ENOMEM; goto out_jpeg_destroy; } /* output init */ ret = projector_device_init(&projector_ctx, &projector_dev, 0); if (ret < 0) goto out_free_out_buf; /* input event init */ input_fd = open(config.event_device, O_RDONLY | O_NONBLOCK); if (input_fd < 0) { perror("open"); ret = input_fd; goto out_am7xxx_shutdown; } ret = read_position(&old_position); if (ret < 0) goto out_cleanup; fps_meter_init(&fps_stats); while (run) { fps_meter_update(&fps_stats); errno = 0; ret = read(input_fd, ev, sizeof(ev)); if (ret < (int)sizeof(ev[0]) && errno != EAGAIN && errno != EWOULDBLOCK) { perror("read"); ret = -errno; goto out_cleanup; } /* * Handle keypad input to adjust the perspective map. * This can go in a preliminar "setup" stage instead of the * main loop. */ if (ret >= (int)sizeof(ev[0])) { for (i = 0; i < ret / sizeof(ev[0]); i++) { /* * This is for testing the software with the * mouse wheel... */ if (ev[i].type == EV_REL && ev[i].code == REL_WHEEL) { position += ev[i].value; } else if (ev[i].type == EV_KEY && ev[i].value > 0) { switch(ev[i].code) { case KEY_UP: if (config.y_correction < config.height) config.y_correction += 1; break; case KEY_DOWN: if (config.y_correction > 0) config.y_correction -= 1; break; case KEY_LEFT: if (config.x_correction > 0) config.x_correction -= 1; break; case KEY_RIGHT: if (config.x_correction < config.width) config.x_correction += 1; break; default: break; } free(perspective_map); perspective_map = calc_perspective_map(); if (perspective_map == NULL) { ret = -ENOMEM; goto out_cleanup; } pixClearAll(transformed_image); break; } } } ret = read_position(&position); if (ret < 0) break; position_delta = old_position - position; old_position = position; /* distance in meters */ distance = 2 * WHEEL_RADIUS_IN_METERS * M_PI / PULSES_PER_REVOLUTION * position_delta; #ifdef DEBUG fprintf(stderr, "position_delta: %d distance: %f pixels: %d\n", position_delta, distance, (int) (distance * PIXELS_PER_METER)); #endif /* convert distance to pixels */ y += distance * PIXELS_PER_METER; y = mod(y, lastframe_limit); boxSetGeometry(viewport, -1, y, -1, -1); /* Apply the perspective transformation */ transformed_image = _pixProjectiveSampled_apply_map_dest_roi(image, perspective_map, L_BRING_IN_BLACK, transformed_image, viewport); ret = tjCompress2(jpeg_compressor, (unsigned char *)pixGetData(transformed_image), config.width, 0, config.height, TJPF_XBGR, &out_buf, &out_buf_size, TJSAMP_420, 70, TJFLAG_NOREALLOC | TJFLAG_FASTDCT); if (ret < 0) { tjGetErrorStr(); fprintf(stderr, "tjCompress2 failed: %s\n", tjGetErrorStr()); break; } ret = am7xxx_send_image_async(projector_dev, AM7XXX_IMAGE_FORMAT_JPEG, config.width, config.height, out_buf, out_buf_size); if (ret < 0) { perror("am7xxx_send_image"); break; } } ret = 0; out_cleanup: close(input_fd); out_am7xxx_shutdown: am7xxx_set_zoom_mode(projector_dev, AM7XXX_ZOOM_TEST); am7xxx_set_power_mode(projector_dev, AM7XXX_POWER_OFF); am7xxx_close_device(projector_dev); am7xxx_shutdown(projector_ctx); out_free_out_buf: free(out_buf); out_jpeg_destroy: tjDestroy(jpeg_compressor); out_free_perspective_map: free(perspective_map); out_destroy_viewport: boxDestroy(&viewport); out_destroy_transformed_image: pixDestroy(&transformed_image); out_destroy_image: pixDestroy(&image); out: return ret; }