/* This file is part of TALER Copyright (C) 2019, 2020 Taler Systems SA TALER 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, or (at your option) any later version. TALER 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 TALER; see the file COPYING. If not, see */ /** * @file main.c * @brief runs the payment logic for a Taler-enabled snack machine * @author Marco Boss * @author Christian Grothoff * @author Dominik Hofer * */ #include "config.h" #include #include #include #include #include #if HAVE_SYS_UN_H #include #endif #if HAVE_NETINET_IN_H #include #endif #if HAVE_NETINET_IP_H #include /* superset of previous */ #endif #include #include #include #include #include #include #include #include #include #include #if HAVE_QRENCODE_H #include #endif #include #include #include /* for adafruit pitft display */ #include /* Constants */ #define MAX_SIZE_RX_BUFFER 256 /* Constants */ #define MAX_SIZE_TX_BUFFER 256 /** * Disable i18n support. */ #define _(s) (s) /* FIXME Adjusted Time out because low internet connection at 36C3 */ #define BACKEND_POLL_TIMEOUT GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MINUTES, 5) #define NFC_FAILURE_RETRY_FREQ GNUNET_TIME_UNIT_SECONDS /** * How long do we wait at most for an ACK from MDB? */ #define MAX_ACK_LATENCY GNUNET_TIME_UNIT_SECONDS /** * Timeout in milliseconds for libnfc operations. */ #define NFC_TIMEOUT 500 #define MAX_HTTP_RETRY_FREQ GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MILLISECONDS, 500) #define MAX_HTTP_RETRY_FREQ GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MILLISECONDS, 500) /** * How long could it take at most for us to notify the Taler merchant * backend to grant a refund to a user if dispensing the product * failed? (Very conservative value here, for vending machines brewing * coffee or something complex that could fail.) */ #define MAX_REFUND_DELAY GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MINUTES, 5) /** * Code returned by libnfc in case of success. */ #define APDU_SUCCESS "\x90\x00" /** * Code returned by libnfc in case Taler wallet is not installed. */ #define APDU_NOT_FOUND "\x6a\x82" /* upper and lower bounds for mifare targets uid length */ /** * Upper lenght of the uid for a valid MIFARE target */ #define UID_LEN_UPPER 7 /** * Lower lenght of the uid for a valid MIFARE target */ #define UID_LEN_LOWER 4 /* Commands for communication via MDB/ICP */ /* VMC commands */ #define VMC_CMD_START 0x02 #define VMC_CMD_END 0x03 #define VMC_CMD_RESET 0x10 /** * Acknowledgement */ #define VMC_ACKN 0x00 /** * Request for configuration. */ #define VMC_CONF 0x11 #define VMC_READER_CONF 0x00 #define VMC_SETUP_MAX_MIN_PRICES 0x01 /** * Machine is polling for something. */ #define VMC_POLL 0x12 /** * Vending, with sub-command. */ #define VMC_VEND 0x13 #define VMC_VEND_REQUEST 0x00 #define VMC_VEND_CANCEL 0x01 #define VMC_VEND_SUCCESS 0x02 #define VMC_VEND_FAILURE 0x03 #define VMC_VEND_SESSION_COMPLETE 0x04 /** * VMC Revalue Request */ #define VMC_REVALUE 0x15 #define VMC_REVALUE_REQUEST 0x00 #define VMC_REVALUE_LIMIT_REQUEST 0x01 /** * Commands for the reader (our device). */ #define VMC_READER 0x14 #define VMC_READER_DISABLE 0x00 #define VMC_READER_ENABLE 0x01 #define VMC_READER_CANCEL 0x02 #define VMC_REQUEST_ID 0x17 /** * Out of sequence. */ #define VMC_OOSQ 0xB0 /** * Request to retransmit last command. */ #define VMC_RETR 0xAA /* Reader commands */ /* Reader Not Acknowledge */ #define READER_NACK "FF" /* Config Data */ /* Refer to the mdb interface specifications v4.2 p.288 */ #define READER_CONFIG "01" #define READER_FEATURE_LEVEL "01" #define READER_COUNTRYCODE "0972" #define READER_SCALE_FACTOR "0A" #define READER_DECIMAL_PLACES "02" #define READER_MAX_RESPONSE_TIME "07" #define READER_MISC_OPTIONS "0D" /* Session Commands */ /* Refer to the mdb interface specifications v4.2 p.131 */ #define READER_BEGIN_SESSION "03" #define READER_FUNDS_AVAILABLE "000A" #define READER_END_SESSION "07" /* Vend Commands */ /* Refer to the mdb interface specifications v4.2 p.134 */ #define READER_VEND_APPROVE "05" #define READER_VEND_AMOUNT "FFFE" #define READER_VEND_DENIED "06" /* Revalue */ #define READER_REVALUE_APPROVED "0D" #define READER_REVALUE_APPROVED "0D" #define READER_REVALUE_LIMIT "0F" #define READER_REVALUE_LIMIT_AMOUNT "FFFE" /* Cancelled Command */ #define READER_CANCELLED "08" /* Display Request for Sold Out product */ #define READER_DISPLAY_REQUEST "02" #define READER_DISPLAY_REQUEST_TIME "32" #define READER_DISPLAY_SOLD_OUT \ "202020202020202050726f6475637420736f6c64206f75742020202020202020" #define READER_DISPLAY_INTERNAL_ERROR \ "202020496e7465726e616c204572726f72202d2054727920416761696e202020i" #define READER_DISPLAY_BACKEND_NOT_REACHABLE \ "20202020204261636b656e64206e6f7420726561636861626c65202020202020" /* Unused reader commands */ #define READER_SESSION_CANCEL_REQUEST "04" #define READER_REVALUE_DENIED "0E" /** * How long are we willing to wait for MDB during * shutdown? */ #define SHUTDOWN_MDB_TIMEOUT GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MILLISECONDS, 100) /** * Datatype for mdb subcommands and data */ struct MdbBlock { /** * Data containing a mdb command or the data of an mdb command */ uint8_t *bin; /** * Size of the data referenced by *bin */ size_t bin_size; }; /** * Datatype for mdb command */ struct MdbCommand { /** * Name of the command for the logging */ const char *name; /** * Data block containing the information about the mdb command */ struct MdbBlock cmd; /** * Data block containing the information about the mdb command data */ struct MdbBlock data; }; /** * Struct holding the information for a product to sell */ struct Product { /** * The price for the product */ struct TALER_Amount price; /** * Description (or name) of the product */ char *description; /** * Which instance should be used for billing? * Full URL, replaces the default base URL * for orders involving this product. NULL if * we should use the base_url. */ char *instance; /** * Preview image to embed in the contract, NULL for * no preview. Already base64 encoded. */ char *preview; /** * Number of the product in the vending machine */ unsigned long long number; /** * Set to #GNUNET_YES if this product was found * to have been sold out (VEND failure). */ int sold_out; /** * Key for the product (optional, needed to test the application without vending machine) */ char key; }; /** * Handle for a payment */ struct PaymentActivity { /** * Handle to a PUT /proposal operation */ struct TALER_MERCHANT_ProposalOperation *po; /** * Handle for a /check-payment operation. */ struct TALER_MERCHANT_CheckPaymentOperation *cpo; /** * The product being sold. */ struct Product *product; /** * Order ID for pending order */ char *order_id; /** * URI needed to pay the pending order */ char *taler_pay_uri; /** * NFC device */ nfc_device *pnd; /** * Target to send the data via NFC */ nfc_target nt; /** * Current task running */ struct GNUNET_SCHEDULER_Task *task; /** * Tasks delayed */ struct GNUNET_SCHEDULER_Task *delay_task; /** * Payment checking delayed task */ struct GNUNET_SCHEDULER_Task *delay_pay_task; /** * What is the price the user is paying? */ struct TALER_Amount amount; /** * Member to see if the wallet already received a uri * GNUNET_YES, GNUNET_NO * If yes, tunneling can be offered to the wallet */ int wallet_has_uri; /** * Set to #GNUNET_YES once the product has been paid * (and we are in the process of yielding the product). */ int paid; }; /** * Data structures associated with the MDB. */ struct MdbHandle { /** * Buffer to save the received data from UART */ uint8_t rxBuffer[MAX_SIZE_RX_BUFFER]; /** * Buffer to save the data to send via UART */ uint8_t txBuffer[MAX_SIZE_TX_BUFFER]; /** * Reference to scheduler task to read from UART */ struct GNUNET_SCHEDULER_Task *rtask; /** * Reference to scheduler task to write to UART */ struct GNUNET_SCHEDULER_Task *wtask; /** * Reference to the mdb cmd which will be sent next */ const struct MdbCommand *cmd; /** * Reference to the mdb cmd which was sent last */ const struct MdbCommand *last_cmd; /** * Current read offset in @e rxBuffer. */ size_t rx_off; /** * Current write offset in @e txBuffer. */ size_t tx_off; /** * Number of bytes in @e txBuffer with the serialized data of the * @e last_cmd. */ size_t tx_len; /** * Time out to wait for an acknoweledge received via the mdb bus */ struct GNUNET_TIME_Absolute ack_timeout; /** * Backup of the config data to restore the configuration of the UART before closing it */ struct termios uart_opts_backup; /** * Indicates if a vend session is running or not */ int session_running; /** * File descriptor to the UART device file */ int uartfd; }; /** * Handle for the Framebuffer device */ struct Display { /** * File descriptor for the screen */ int devicefd; /** * File descriptor to set backlight information */ int backlightfd; /** * The display memory to set the pixel information */ uint16_t *memory; /** * Original screen information */ struct fb_var_screeninfo orig_vinfo; /** * Variable screen information (color depth ...) */ struct fb_var_screeninfo var_info; /** * Fixed screen informtaion */ struct fb_fix_screeninfo fix_info; }; /** * Handle for the Cancel Button */ struct CancelButton { /** * File descriptor to read the state of the cancel button gpio pin */ int cancelbuttonfd; }; /** * DLL of pending refund operations. */ struct Refund { /** * DLL next pointer. */ struct Refund *next; /** * DLL prev pointer. */ struct Refund *prev; /** * Handle to the ongoing operation. */ struct TALER_MERCHANT_RefundIncreaseOperation *rio; }; /** * DLL head of refunds. */ static struct Refund *refund_head; /** * DLL tail of refunds. */ static struct Refund *refund_tail; /** * NFC context used by the NFC reader */ static nfc_context *context; /** * Global return value */ static int global_ret; /** * Flag set to remember that we are in shutdown. */ static int in_shutdown; /** * Flag set to remember that MDB needs shutdown * (because we were actually able to talk to MDB). */ static int mdb_active; /** * Reference to the keyboard task */ static struct GNUNET_SCHEDULER_Task *keyboard_task; /** * Reference to the cancel button task */ static struct GNUNET_SCHEDULER_Task *cancelbutton_task; /** * Curl context for communication with taler backend */ static struct GNUNET_CURL_Context *ctx; /** * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule(). */ static struct GNUNET_CURL_RescheduleContext *rc; /** * Currency read from configuration file */ static char *currency; /** * Taler Backend url read from configuration file */ static char *backend_base_url; /** * Taler fulfillment url read from configuration file */ static char *fulfillment_url; /** * Fulfillment message to display after successfull payment, read from configuration file */ static char *fulfillment_msg; /** * Authorization for the taler backend */ static char *authorization; /** * Handle for the payment */ static struct PaymentActivity *payment_activity; /** * Products read from configuration file */ static struct Product *products; /** * Amount of products */ static unsigned int products_length; /** * Data associated with the MDB session. */ static struct MdbHandle mdb; /** * MDB response to the request for configuration. */ static struct MdbCommand cmd_reader_config_data; /** * Ask MDB to begin session (with "infinite" money) */ static struct MdbCommand cmd_begin_session; /** * Refuse vending request (payment failed) */ static struct MdbCommand cmd_deny_vend; /** * Approve vending request (payment succeeded) */ static struct MdbCommand cmd_approve_vend; /** * Confirm cancellation by machine. */ static struct MdbCommand cmd_reader_cancelled; /** * Approve Revalue */ static struct MdbCommand cmd_revalue_approved; /** * Send Revalue Limit Amount */ static struct MdbCommand cmd_revalue_amount; /** * Send NACK */ static struct MdbCommand cmd_reader_NACK; /** * Display Request for Sold Out */ static struct MdbCommand cmd_reader_display_sold_out; /** * Display Request for Error Message */ static struct MdbCommand cmd_reader_display_internal_error; /** * Display Request for Error Message */ static struct MdbCommand cmd_reader_display_backend_not_reachable; /** * Terminate session. */ static struct MdbCommand endSession; /** * Name of the framebuffer device (i.e. /dev/fb1). */ static char *framebuffer_device_filename; /** * Name of the backlight file of @e framebuffer_device_filename (i.e. /sys/class/backlight/soc:backlight/brightness). */ static char *framebuffer_backlight_filename; /** * Global option '-i' to invert backlight on/off values */ static int backlight_invert; /** * Standard backlight on value */ static char backlight_on = '1'; /** * Standard backlight off value */ static char backlight_off = '0'; /** * State for the implementation of the 'cancel' button. */ static struct CancelButton cancel_button; /** * Name of the UART device with the MDB (i.e. /dev/ttyAMA0). */ static char *uart_device_filename; /** * Global option '-d' to disable MDB set. */ static int disable_mdb; /** * Global option '-t' to disable stdin / terminal. */ static int disable_tty; /** * Global option '-s' to enable sold-out detection. */ static int sold_out_enabled; /** * Taler wallet application identifier */ static const uint8_t taler_aid[] = { 0xF0, 0x00, 0x54, 0x41, 0x4c, 0x45, 0x52 }; /** * NFC select file command to select wallet aid */ static const uint8_t select_file[] = { 0x00, 0xA4, 0x04, 0x00, 0x07 }; /** * NFC put command to send data to the wallet */ static const uint8_t put_data[] = { 0x00, 0xDA, 0x01, 0x00, 0x7c, 0x01 }; #if FUTURE_FEATURES /* tunneling */ static const uint8_t get_data[] = { 0x00, 0xCA, 0x01, 0x00, 0x00, 0x00 }; #endif /** * Handle for the framebuffer device */ static struct Display qrDisplay; #if HAVE_QRENCODE_H #include /** * @brief Create the QR code to pay and display it on screen * * @param uri what text to show in the QR code */ static void show_qrcode (const char *uri) { QRinput *qri; QRcode *qrc; unsigned int size; char *upper; char *base; size_t xOff; size_t yOff; const char *dddash; if (0 > qrDisplay.devicefd) return; /* no display, no dice */ /* find the fourth '/' in the payto://pay/hostname/-uri */ dddash = strchr (uri, '/'); if (NULL != dddash) dddash = strchr (dddash + 1, '/'); if (NULL != dddash) dddash = strchr (dddash + 1, '/'); if (NULL != dddash) dddash = strchr (dddash + 1, '/'); if (NULL == dddash) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "taler://pay/-URI malformed: `%s'\n", uri); return; } qri = QRinput_new2 (0, QR_ECLEVEL_L); if (NULL == qri) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, "QRinput_new2"); return; } /* convert all characters until the fourth '/' to upper case. The rest _should_ be upper case in a NICE setup, but we can't warrant it and must not touch those. */ base = GNUNET_strndup (uri, dddash - uri); GNUNET_STRINGS_utf8_toupper (base, base); GNUNET_asprintf (&upper, "%s%s", base, dddash); GNUNET_free (base); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Showing QR code for `%s'\n", upper); /* first try encoding as uppercase-only alpha-numerical QR code (much smaller encoding); if that fails, also try using binary encoding (in case nick contains special characters). */ if ( (0 != QRinput_append (qri, QR_MODE_AN, strlen (upper), (unsigned char *) upper)) && (0 != QRinput_append (qri, QR_MODE_8, strlen (upper), (unsigned char *) upper))) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, "QRinput_append"); GNUNET_free (upper); return; } GNUNET_free (upper); qrc = QRcode_encodeInput (qri); if (NULL == qrc) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, "QRcode_encodeInput"); QRinput_free (qri); return; } /* set QR-code border */ size = GNUNET_MIN (qrDisplay.var_info.xres, qrDisplay.var_info.yres); unsigned int nwidth = qrc->width + 8; /* +8 for 4 pixel border */ xOff = 4 * size / nwidth; yOff = 4 * size / nwidth; /* calculate offset to show the code centered */ if (qrDisplay.var_info.xres < qrDisplay.var_info.yres) yOff += (qrDisplay.var_info.yres - qrDisplay.var_info.xres) / 2; else xOff += (qrDisplay.var_info.xres - qrDisplay.var_info.yres) / 2; for (unsigned int x = 0; x < qrDisplay.var_info.xres - 2 * xOff; x++) for (unsigned int y = 0; y < qrDisplay.var_info.yres - 2 * yOff; y++) { unsigned int xoff = x * nwidth / size; unsigned int yoff = y * nwidth / size; unsigned int off = xoff + yoff * qrc->width; if ( (xoff >= (unsigned) qrc->width) || (yoff >= (unsigned) qrc->width) ) continue; /* set the pixels in the display memory */ qrDisplay.memory[(y + yOff) * qrDisplay.var_info.xres + (x + xOff)] = (0 == (qrc->data[off] & 1)) ? 0xFFFF : 0x0000; } QRcode_free (qrc); QRinput_free (qri); /* turn on backlight if supported */ if (0 < qrDisplay.backlightfd) (void) ! write (qrDisplay.backlightfd, &backlight_on, 1); } #endif static void run_mdb_event_loop (void); /** * @brief Cleanup all the data when a order has succeeded or got cancelled * * @param pa the payment activity to clean up */ static void cleanup_payment (struct PaymentActivity *pa) { if (NULL != pa->pnd) { nfc_abort_command (pa->pnd); nfc_close (pa->pnd); } if (NULL != cancelbutton_task) { GNUNET_SCHEDULER_cancel (cancelbutton_task); cancelbutton_task = NULL; } if (NULL != pa->po) TALER_MERCHANT_proposal_cancel (pa->po); if (NULL != pa->cpo) TALER_MERCHANT_check_payment_cancel (pa->cpo); GNUNET_CURL_gnunet_scheduler_reschedule (&rc); if (NULL != pa->task) GNUNET_SCHEDULER_cancel (pa->task); if (NULL != pa->delay_task) GNUNET_SCHEDULER_cancel (pa->delay_task); if (NULL != pa->delay_pay_task) GNUNET_SCHEDULER_cancel (pa->delay_pay_task); if (NULL != pa->taler_pay_uri) { #if HAVE_QRENCODE_H if (NULL != qrDisplay.memory) memset (qrDisplay.memory, 0xFF, qrDisplay.var_info.xres * qrDisplay.var_info.yres * sizeof (uint16_t)); if (0 < qrDisplay.backlightfd) (void) ! write (qrDisplay.backlightfd, &backlight_off, 1); #endif GNUNET_free (pa->taler_pay_uri); } GNUNET_free_non_null (pa->order_id); GNUNET_free (pa); } /** * @brief Shutdown the mdb communication tasks */ static void mdb_shutdown (void) { if (NULL != mdb.rtask) { GNUNET_SCHEDULER_cancel (mdb.rtask); mdb.rtask = NULL; } if (NULL != mdb.wtask) { GNUNET_SCHEDULER_cancel (mdb.wtask); mdb.wtask = NULL; } if (disable_mdb) return; /* restore UART */ if (0 != tcsetattr (mdb.uartfd, TCSAFLUSH, &mdb.uart_opts_backup)) { printf ("Failed to restore uart discipline\n"); global_ret = EXIT_FAILURE; } if (-1 != mdb.uartfd) { (void) close (mdb.uartfd); mdb.uartfd = -1; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Shutdown complete (including MDB)\n"); } /** * @brief Shutdown the application. * * @param cls closure */ static void shutdown_task (void *cls) { struct Refund *r; (void) cls; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Shutdown initiated\n"); while (NULL != (r = refund_head)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Pending refund operation aborted due to shutdown\n"); GNUNET_CONTAINER_DLL_remove (refund_head, refund_tail, r); TALER_MERCHANT_refund_increase_cancel (r->rio); GNUNET_free (r); } if (NULL != context) { nfc_exit (context); context = NULL; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "NFC down\n"); if (NULL != payment_activity) { cleanup_payment (payment_activity); payment_activity = NULL; } if (NULL != cancelbutton_task) { GNUNET_SCHEDULER_cancel (cancelbutton_task); cancelbutton_task = NULL; } if (NULL != keyboard_task) { GNUNET_SCHEDULER_cancel (keyboard_task); keyboard_task = NULL; } /* last ditch saying nicely goodbye to MDB */ in_shutdown = GNUNET_YES; mdb.cmd = &endSession; run_mdb_event_loop (); if (NULL != ctx) { GNUNET_CURL_fini (ctx); ctx = NULL; } if (NULL != rc) { GNUNET_CURL_gnunet_rc_destroy (rc); rc = NULL; } if ( (MAP_FAILED != qrDisplay.memory) && (NULL != qrDisplay.memory) ) { /* free the display data */ munmap (qrDisplay.memory, qrDisplay.fix_info.smem_len); qrDisplay.memory = NULL; /* reset original state */ if (0 > ioctl (qrDisplay.devicefd, FBIOPUT_VSCREENINFO, &qrDisplay.orig_vinfo)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "failed to reset originial display state\n"); } /* close the device */ close (qrDisplay.devicefd); qrDisplay.devicefd = -1; if (0 < qrDisplay.backlightfd) close (qrDisplay.backlightfd); qrDisplay.backlightfd = -1; } if (0 < cancel_button.cancelbuttonfd) { close (cancel_button.cancelbuttonfd); cancel_button.cancelbuttonfd = open ("/sys/class/gpio/unexport", O_WRONLY); if (0 > cancel_button.cancelbuttonfd) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unable to open /gpio/unexport for cancel button\n"); } (void) ! write (cancel_button.cancelbuttonfd, "23", 2); close (cancel_button.cancelbuttonfd); } /* free the allocated productes read from config file */ if (NULL != products) { for (unsigned int i = 0; i < products_length; i++) { GNUNET_free (products[i].description); GNUNET_free_non_null (products[i].instance); GNUNET_free_non_null (products[i].preview); } GNUNET_array_grow (products, products_length, 0); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Shutdown complete (except for MDB)\n"); } static void check_payment_again (void *cls); static void connect_target (void *cls); static void wallet_select_aid (void *cls); /** * @brief Transmit the pay uri from taler to the wallet application via NFC * * @param cls closure */ static void wallet_transmit_uri (void *cls) { struct PaymentActivity *pa = cls; /* response array for APDU response status word */ uint8_t response[] = { 0x00, 0x00 }; size_t slen = strlen (pa->taler_pay_uri); uint8_t message[sizeof (put_data) + slen]; pa->delay_task = NULL; /* append the pay uri to the put data command */ memcpy (message, put_data, sizeof (put_data)); memcpy (&message[sizeof (put_data)], pa->taler_pay_uri, slen); /* send the put data command via nfc */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Sending 'PUT DATA' command for `%s' to wallet\n", pa->taler_pay_uri); if (0 > nfc_initiator_transceive_bytes (pa->pnd, message, sizeof (message), response, sizeof(response), NFC_TIMEOUT)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to send command via NFC\n"); pa->task = GNUNET_SCHEDULER_add_now (&connect_target, pa); return; } /* check if the transmission succeeded */ if (0 != memcmp (response, APDU_SUCCESS, sizeof (response))) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "'PUT DATA' command transmission failed, return code: %x%x\n", response[0], response[1]); pa->task = GNUNET_SCHEDULER_add_now (&connect_target, pa); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "'PUT DATA' command sent successfully via NFC\n"); pa->wallet_has_uri = GNUNET_YES; /* FIXME: or just offer Internet service here? */ /* transmit the uri again later, there can be many external failures, for example the taler wallet app was not opened and thus did not receive the data */ pa->delay_task = GNUNET_SCHEDULER_add_delayed (MAX_HTTP_RETRY_FREQ, &wallet_transmit_uri, pa); } /** * @brief Select the taler wallet app via NFC on the target selected with * @e connect_target() * (check if it is installed on the smartphone) * * @param cls closure */ static void wallet_select_aid (void *cls) { struct PaymentActivity *pa = cls; /* response array for APDU response status word */ uint8_t response[] = { 0x00, 0x00 }; uint8_t message[sizeof(select_file) + sizeof(taler_aid)]; pa->task = NULL; /* append the taler wallet aid to the select file command */ memcpy (message, select_file, sizeof (select_file)); memcpy (&message[sizeof (select_file)], taler_aid, sizeof (taler_aid)); /* send the select file command via nfc */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to find Taler wallet on NFC\n"); if (0 > nfc_initiator_transceive_bytes (pa->pnd, message, sizeof (message), response, sizeof (response), NFC_TIMEOUT)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Failed to transceive with NFC app, trying to find another NFC client\n"); pa->task = GNUNET_SCHEDULER_add_now (&connect_target, pa); return; } /* check if the transmission succeeded */ if (0 == memcmp (response, APDU_SUCCESS, sizeof (response))) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Taler wallet found over NFC\n"); pa->delay_task = GNUNET_SCHEDULER_add_now (&wallet_transmit_uri, pa); return; } /* if the transmission was not successful chack if the app is available at all */ if (0 == memcmp (response, APDU_NOT_FOUND, sizeof (response))) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Taler wallet NOT found on this device\n"); pa->task = GNUNET_SCHEDULER_add_now (&connect_target, pa); return; } /* If the upper cases did not match, there was an unknown APDU status returned */ GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "AID selection failure, return code: %x%x, trying to find another NFC client\n", response[0], response[1]); /* start the selection again */ pa->task = GNUNET_SCHEDULER_add_delayed (NFC_FAILURE_RETRY_FREQ, &connect_target, pa); } /** * @brief Connect the NFC reader with a compatible NFC target * * @param cls closure */ static void connect_target (void *cls) { struct PaymentActivity *pa = cls; /* nfc modulation used */ const nfc_modulation nmMifare = { .nmt = NMT_ISO14443A, .nbr = NBR_212, }; pa->task = NULL; /* set the uid len to zero (maybe it is still set from earlier selections) */ pa->nt.nti.nai.szUidLen = 0; /* poll for a fitting nfc target (we use the shortest time possible to not block the scheduler) */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to find NFC client\n"); if (0 > nfc_initiator_poll_target (pa->pnd, &nmMifare, 1, 0x01, 0x01, &pa->nt)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Failed to connect to NFC target\n"); } /* if the uid lenght are out of bound abort */ else if ( (pa->nt.nti.nai.szUidLen > UID_LEN_UPPER) || (pa->nt.nti.nai.szUidLen < UID_LEN_LOWER) ) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to connect, wrong NFC modulation\n"); } else { /* the target was successfully selected, now we have to check if the taler wallet is installed on it */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Found NFC client\n"); pa->task = GNUNET_SCHEDULER_add_now (&wallet_select_aid, pa); return; } /* if no target was found try again */ pa->task = GNUNET_SCHEDULER_add_delayed (NFC_FAILURE_RETRY_FREQ, &connect_target, pa); } /** * @brief Open the NFC reader. * * @param cls closure */ static void open_nfc_reader (void *cls) { struct PaymentActivity *pa = cls; pa->task = NULL; /* open the nfc reader via libnfc's open */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to open NFC device\n"); pa->pnd = nfc_open (context, NULL); if (NULL == pa->pnd) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Payment inititation: Unable to open NFC device\n"); pa->task = GNUNET_SCHEDULER_add_delayed (NFC_FAILURE_RETRY_FREQ, &open_nfc_reader, pa); return; } /* initialize the reader as initiator */ if (0 > nfc_initiator_init (pa->pnd)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to initialize NFC device: %s\n", nfc_strerror (pa->pnd)); cleanup_payment (pa); GNUNET_assert (payment_activity == pa); payment_activity = NULL; return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "NFC in operation %s / %s\n", nfc_device_get_name (pa->pnd), nfc_device_get_connstring (pa->pnd)); /* the nfc reader was opened successfully, now try to find a mobile device as a target */ pa->task = GNUNET_SCHEDULER_add_now (&connect_target, pa); } static void start_read_keyboard (void); /** * @brief Callback to process a GET /check-payment request * * @param cls closure * @param http_status HTTP status code for this request * @param obj raw response body * @param paid #GNUNET_YES if the payment is settled, #GNUNET_NO if not * settled, #GNUNET_SYSERR on error * (note that refunded payments are returned as paid!) * @param refunded #GNUNET_YES if there is at least on refund on this payment, * #GNUNET_NO if refunded, #GNUNET_SYSERR or error * @param refunded_amount amount that was refunded, NULL if there * was no refund * @param taler_pay_uri the URI that instructs the wallets to process * the payment */ static void check_payment_cb (void *cls, unsigned int http_status, const json_t *obj, int paid, int refunded, struct TALER_Amount *refund_amount, const char *taler_pay_uri) { struct PaymentActivity *pa = cls; (void) refunded; (void) refund_amount; (void) obj; GNUNET_assert (payment_activity == pa); pa->cpo = NULL; if (MHD_HTTP_OK != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Backend request to /check-payment failed: %u\n", http_status); mdb.cmd = &cmd_reader_display_backend_not_reachable; run_mdb_event_loop (); cleanup_payment (pa); GNUNET_assert (payment_activity == pa); payment_activity = NULL; return; } if (paid) { mdb.cmd = &cmd_approve_vend; payment_activity->paid = GNUNET_YES; run_mdb_event_loop (); if ((disable_mdb) && (! disable_tty)) { GNUNET_SCHEDULER_cancel (keyboard_task); keyboard_task = NULL; start_read_keyboard (); } return; } else { /* Start to check for payment. Note that we do this even before we talked successfully to the wallet via NFC because we MAY show the QR code in the future and in that case the payment may happen anytime even before the NFC communication succeeds. */ if ( (NULL == pa->cpo) && (NULL == pa->delay_pay_task) ) { pa->delay_pay_task = GNUNET_SCHEDULER_add_delayed (MAX_HTTP_RETRY_FREQ, &check_payment_again, pa); } } if (NULL == pa->taler_pay_uri) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to talk to wallet to give it pay URI `%s'\n", taler_pay_uri); GNUNET_assert (NULL == pa->pnd); pa->taler_pay_uri = GNUNET_strdup (taler_pay_uri); #if HAVE_QRENCODE_H show_qrcode (taler_pay_uri); #endif pa->task = GNUNET_SCHEDULER_add_now (&open_nfc_reader, pa); } } /** * @brief Check the payment status again * * @param cls closure */ static void check_payment_again (void *cls) { struct PaymentActivity *pa = cls; struct Product *p = pa->product; pa->delay_pay_task = NULL; GNUNET_assert (NULL == pa->cpo); pa->cpo = TALER_MERCHANT_check_payment (ctx, (NULL == p->instance) ? backend_base_url : p->instance, pa->order_id, NULL /* snack machine, no Web crap */, BACKEND_POLL_TIMEOUT, &check_payment_cb, pa); } /** * @brief Callback for a PUT /order request * * @param cls closure * @param http_status HTTP status code for this request * @param ec Taler error code * @param obj raw response body * @param order_id order ID of the order created */ static void proposal_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *obj, const char *order_id) { (void) obj; struct PaymentActivity *pa = cls; pa->po = NULL; if (MHD_HTTP_OK != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to setup order with backend: %u/%d\n", http_status, (int) ec); json_dumpf (obj, stderr, 0); mdb.cmd = &cmd_reader_display_backend_not_reachable; run_mdb_event_loop (); cleanup_payment (pa); GNUNET_assert (payment_activity == pa); payment_activity = NULL; return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Backend successfully created order `%s'\n", order_id); pa->order_id = GNUNET_strdup (order_id); pa->cpo = TALER_MERCHANT_check_payment (ctx, backend_base_url, pa->order_id, NULL /* snack machine, no Web crap */, GNUNET_TIME_UNIT_ZERO, &check_payment_cb, pa); } static void start_read_cancel_button (); /** * @brief Launch a new order * * @param product information for product to sell * @return payment activity for the order, NULL on failure */ static struct PaymentActivity * launch_payment (struct Product *product) { struct PaymentActivity *pa; json_t *orderReq; char *fulflmntUrl; struct GNUNET_ShortHashCode uuid; char *uuid_s; /* We need to ensure that every fulfillment URL is unique; most easily done by adding a random nonce, as we may not have a reliable counter or clock on the machine */ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &uuid, sizeof (uuid)); uuid_s = GNUNET_STRINGS_data_to_string_alloc (&uuid, sizeof (uuid)); /* create the fulfillment url, e.g. "taler://fulfillment-success/Enjoy+your+ice+cream!"; */ GNUNET_asprintf (&fulflmntUrl, "%s%s%s#%s", fulfillment_url, fulfillment_msg, product->description, uuid_s); GNUNET_free (uuid_s); /* create the json object for the order request */ if (NULL != product->preview) { orderReq = json_pack ("{ s:s, s:s, s:o, s:s, s:o }", "summary", product->description, "preview", product->preview, "amount", TALER_JSON_from_amount (&product->price), "fulfillment_url", fulflmntUrl ,"auto_refund", GNUNET_JSON_from_time_rel ( MAX_REFUND_DELAY) ); } else { orderReq = json_pack ("{ s:s}, s:o, s:s, s:o }", "summary", product->description, "amount", TALER_JSON_from_amount (&product->price), "fulfillment_url", fulflmntUrl ,"auto_refund", GNUNET_JSON_from_time_rel ( MAX_REFUND_DELAY) ); } GNUNET_free (fulflmntUrl); if (NULL == orderReq) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "json_pack failed (out of memory?)\n"); return NULL; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Creating order for `%s' at backend `%s'\n", product->description, (NULL == product->instance) ? backend_base_url : product->instance); pa = GNUNET_new (struct PaymentActivity); pa->product = product; pa->amount = product->price; /* put the order on the merchant's backend */ pa->po = TALER_MERCHANT_order_put (ctx, (NULL == product->instance) ? backend_base_url : product->instance, orderReq, &proposal_cb, pa); json_decref (orderReq); if (NULL == pa->po) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "TALER_MERCHANT_order_put failed (out of memory?)\n"); GNUNET_free (pa); return NULL; } /* Start to read the button on the VM to cancel this payment */ if (0 < cancel_button.cancelbuttonfd) start_read_cancel_button (); return pa; } /** * @brief Vending successful, conclude payment activity. */ static void vend_success () { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "MDB vend success received\n"); GNUNET_break (NULL != payment_activity); if (NULL != payment_activity) { cleanup_payment (payment_activity); payment_activity = NULL; } } /** * @brief Callback to process a POST /refund request * * @param cls closure * @param http_status HTTP status code for this request * @param ec taler-specific error code * @param obj the response body */ static void refund_complete_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *obj) { struct Refund *r = cls; (void) obj; r->rio = NULL; if (MHD_HTTP_OK != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to grant consumer refund: %u/%d\n", http_status, ec); } GNUNET_CONTAINER_DLL_remove (refund_head, refund_tail, r); GNUNET_free (r); } /** * @brief Vending failed, provide refund. */ static void vend_failure () { struct Product *p; struct Refund *r; if (NULL == payment_activity) { GNUNET_break (0); return; } p = payment_activity->product; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received MDB vend failure for `%s', refunding customer\n", p->description); p->sold_out = GNUNET_YES; mdb.cmd = &endSession; mdb.session_running = GNUNET_NO; r = GNUNET_new (struct Refund); r->rio = TALER_MERCHANT_refund_increase (ctx, (NULL == p->instance) ? backend_base_url : p->instance, payment_activity->order_id, &payment_activity->amount, "failed to dispense product", &refund_complete_cb, r); if (NULL == r->rio) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to launch refund interaction with the merchant backend!\n"); GNUNET_free (r); } else { GNUNET_CONTAINER_DLL_insert (refund_head, refund_tail, r); } if (NULL != payment_activity) { cleanup_payment (payment_activity); payment_activity = NULL; } } /** * @brief Read the character from stdin and activate the selected task * * @param cls closure */ static void read_keyboard_command (void *cls) { (void) cls; int input; keyboard_task = NULL; input = getchar (); if ( (EOF == input) || ('x' == (char) input) ) { GNUNET_SCHEDULER_shutdown (); return; } if (NULL != payment_activity) { switch ((char) input) { case 'c': if (GNUNET_NO == payment_activity->paid) { mdb.cmd = &cmd_deny_vend; } else { mdb.cmd = &endSession; mdb.session_running = GNUNET_NO; } run_mdb_event_loop (); cleanup_payment (payment_activity); payment_activity = NULL; break; case 'a': payment_activity->paid = GNUNET_YES; mdb.cmd = &cmd_approve_vend; run_mdb_event_loop (); break; case 'n': if (disable_mdb) { vend_failure (); } else { fprintf (stderr, "Cannot fail to vend at this time, waiting for payment\n"); } break; case 'y': if (disable_mdb) { vend_success (); } else { fprintf (stderr, "Cannot succeed to vend at this time, waiting for payment\n"); } break; default: fprintf (stderr, "Unknown command `%c'\n", input); break; } start_read_keyboard (); return; } if (NULL != payment_activity) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Purchase activity already pending\n"); start_read_keyboard (); return; } for (unsigned int i = 0; i < products_length; i++) if (((char) input) == products[i].key) { if ( (sold_out_enabled) && (GNUNET_YES == products[i].sold_out) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Product %s sold out, denying vend\n", products[i].description); mdb.cmd = &cmd_reader_display_sold_out; run_mdb_event_loop (); start_read_keyboard (); return; } payment_activity = launch_payment (&products[i]); start_read_keyboard (); return; } fprintf (stderr, "Unknown command '%c'\n", (char) input); start_read_keyboard (); } /** * @brief Read the state of the cancel button GPIO pin * * @param cls closure */ static void cancel_button_pressed (void *cls) { (void) cls; char value; cancelbutton_task = NULL; if (1 != read (cancel_button.cancelbuttonfd, &value, 1)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "read"); start_read_cancel_button (); return; } lseek (cancel_button.cancelbuttonfd, 0, SEEK_SET); /* This point should only be reached when a order is pending, because * the scheduler read file gets added in the function "launch_payment". * But anyway safe check, else do nothing */ if (NULL != payment_activity) { if ('1' == value) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Cancel button pressed, canceling current order\n"); if (GNUNET_NO == payment_activity->paid) { /* The current payment was not paid already, deny it */ mdb.cmd = &cmd_deny_vend; } else { /* The order was paid and if we know this, then it is also yielded, * just end the current session */ mdb.cmd = &endSession; mdb.session_running = GNUNET_NO; } run_mdb_event_loop (); cleanup_payment (payment_activity); payment_activity = NULL; } else { start_read_cancel_button (); } } } /** * @brief Wait for a keyboard input */ static void start_read_keyboard () { static struct GNUNET_DISK_FileHandle fh = { STDIN_FILENO }; GNUNET_assert (NULL == keyboard_task); if (NULL == payment_activity) { for (unsigned int i = 0; i < products_length; i++) printf ("'%c' to buy %s\n", products[i].key, products[i].description); } else { if (GNUNET_NO == payment_activity->paid) { printf ("'a' to fake payment for the last purchase\n" "'c' to cancel last purchase\n"); } else { if (disable_mdb) printf ("'y' to simulate product successfully dispensed\n" "'n' to simulate product failed to be dispensed\n"); } } printf ("'x' to quit\n"); printf ("Waiting for keyboard input\n"); keyboard_task = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, &fh, &read_keyboard_command, NULL); } /** * @brief Wait for cancel button during payment activity */ static void start_read_cancel_button () { struct GNUNET_DISK_FileHandle fh = { cancel_button.cancelbuttonfd }; GNUNET_assert (NULL == cancelbutton_task); cancelbutton_task = GNUNET_SCHEDULER_add_read_file ( GNUNET_TIME_UNIT_FOREVER_REL, &fh, &cancel_button_pressed, NULL); } /** * @brief Send data to the vmc via the uart bus * * @param cls closure */ static void write_mdb_command (void *cls) { int did_write = 0; (void) cls; mdb.wtask = NULL; if (disable_mdb) { /* check if there is a cmd to send and overwrite last command */ if (NULL == mdb.cmd) return; mdb.last_cmd = mdb.cmd; mdb.cmd = NULL; run_mdb_event_loop (); return; } /* if command was sent partially, send the rest */ if (mdb.tx_off < mdb.tx_len) { ssize_t ret = write (mdb.uartfd, &mdb.txBuffer[mdb.tx_off], mdb.tx_len - mdb.tx_off); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Wrote %d bytes to MDB\n", (int) ret); did_write = 1; if (-1 == ret) { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, "write", uart_device_filename); if (in_shutdown) mdb_shutdown (); else GNUNET_SCHEDULER_shutdown (); return; } mdb.tx_off += ret; /* if command was sent sucessfully start the timer for ACK timeout */ if ( (ret > 0) && (mdb.tx_off == mdb.tx_len) ) mdb.ack_timeout = GNUNET_TIME_relative_to_absolute (MAX_ACK_LATENCY); } /* ongoing write incomplete, continue later */ if (mdb.tx_off < mdb.tx_len) { run_mdb_event_loop (); return; } if (NULL != mdb.last_cmd) { struct GNUNET_TIME_Relative del; /* Still waiting for ACK! -> delay write task */ del = GNUNET_TIME_absolute_get_remaining (mdb.ack_timeout); if (0 != del.rel_value_us) { if (did_write) run_mdb_event_loop (); else mdb.wtask = GNUNET_SCHEDULER_add_delayed (del, &write_mdb_command, NULL); return; } GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "MDB failed to acknowledge command `%s' within timeout\n", mdb.last_cmd->name); mdb.last_cmd = NULL; if (in_shutdown) { mdb_shutdown (); return; } } if (NULL == mdb.cmd) return; /* if there is a command to send calculate length and checksum */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Sending command `%s'\n", mdb.cmd->name); mdb.tx_off = 0; mdb.tx_len = mdb.cmd->cmd.bin_size + mdb.cmd->data.bin_size + 1; GNUNET_assert (mdb.tx_len <= sizeof (mdb.txBuffer)); { /* calculate checksum: sum up command and data, take just the LSB of the result */ uint32_t chkSum = 0; for (size_t idx = 0; idx < mdb.cmd->cmd.bin_size; idx++) chkSum += mdb.txBuffer[idx] = mdb.cmd->cmd.bin[idx]; for (size_t idx = 0; idx < mdb.cmd->data.bin_size; idx++) chkSum += mdb.txBuffer[idx + mdb.cmd->cmd.bin_size] = mdb.cmd->data.bin[idx]; mdb.txBuffer[mdb.cmd->cmd.bin_size + mdb.cmd->data.bin_size] = (uint8_t) (chkSum & 0xFF); } mdb.last_cmd = mdb.cmd; mdb.cmd = NULL; run_mdb_event_loop (); } /** * @brief MDB acknowledged the last command, proceed. */ static void handle_ack () { if (&cmd_begin_session == mdb.last_cmd) mdb.session_running = GNUNET_YES; if (&cmd_deny_vend == mdb.last_cmd) { mdb.session_running = GNUNET_NO; mdb.cmd = &endSession; } if (&cmd_reader_display_sold_out == mdb.last_cmd) mdb.cmd = &cmd_deny_vend; if (&cmd_reader_display_internal_error == mdb.last_cmd) mdb.cmd = &cmd_deny_vend; if (&cmd_reader_display_backend_not_reachable == mdb.last_cmd) mdb.cmd = &cmd_deny_vend; mdb.last_cmd = NULL; /* Cause the write-task to be re-scheduled now */ if (NULL != mdb.wtask) { GNUNET_SCHEDULER_cancel (mdb.wtask); mdb.wtask = NULL; } } /** * @brief Parse received command from the VMC * * @param hex received command from VMC * @param hex_len number of characters in @a hex */ static void handle_command (const char *hex, size_t hex_len) { unsigned int cmd; unsigned int tmp = 0; uint32_t chkSum; /* if the received command is 0 or not a multiple of 2 we cannot parse it */ if (0 == hex_len) return; if (0 != (hex_len % 2)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received unexpected input `%.*s'\n", (int) hex_len, hex); GNUNET_break_op (0); return; } /* convert the received 2 bytes from ASCII to hex */ if (1 != sscanf (hex, "%2X", &cmd)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received non-HEX input `%.*s'\n", (int) hex_len, hex); GNUNET_break_op (0); return; } /* parse the first byte (cmd) and the second byte (subcmd) */ switch (cmd) { case VMC_VEND: { unsigned int subcmd; GNUNET_break (GNUNET_YES == mdb.session_running); if (4 > hex_len) { GNUNET_break_op (0); return; } if (1 != sscanf (&hex[2], "%2X", &subcmd)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received non-HEX input `%.*s'\n", (int) hex_len - 2, &hex[2]); GNUNET_break_op (0); return; } switch (subcmd) { case VMC_VEND_REQUEST: { /* Calculate the checksum and check it */ chkSum = cmd; for (size_t offset = 1; offset < ((hex_len / 2)); offset++) { chkSum += tmp; if (1 != sscanf (hex + (2 * offset), "%2X", &tmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received non-HEX input `%.*s'\n", (int) hex_len, hex); GNUNET_break_op (0); return; } } if ( ((uint8_t) (chkSum & 0xFF)) != tmp) { mdb.cmd = &cmd_reader_display_internal_error; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received command with wrong checksum `%.*s'\n", (int) hex_len, hex); break; } unsigned int product; GNUNET_break (GNUNET_YES == mdb.session_running); /* NOTE: hex[4..7] contain the price */ if (12 > hex_len) { GNUNET_break_op (0); return; } if (1 != sscanf (&hex[8], "%4X", &product)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received non-HEX input `%.*s'\n", (int) hex_len - 8, &hex[8]); GNUNET_break_op (0); return; } /* compare the received product number with the defined product numbers */ for (unsigned int i = 0; i < products_length; i++) if (product == products[i].number) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Product %u selected on MDB\n", product); if ( (sold_out_enabled) && (GNUNET_YES == products[i].sold_out) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Product %s sold out, denying vend\n", products[i].description); mdb.cmd = &cmd_reader_display_sold_out; return; } payment_activity = launch_payment (&products[i]); return; } GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unknown product %u selected on MDB, denying vend\n", product); mdb.cmd = &cmd_deny_vend; break; } case VMC_VEND_SUCCESS: GNUNET_break (GNUNET_YES == mdb.session_running); vend_success (); break; case VMC_VEND_FAILURE: { GNUNET_break (GNUNET_YES == mdb.session_running); vend_failure (); break; } case VMC_VEND_SESSION_COMPLETE: { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received MDB session complete\n"); mdb.session_running = GNUNET_NO; mdb.cmd = &endSession; if (NULL != payment_activity) { cleanup_payment (payment_activity); payment_activity = NULL; } } break; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unknown MDB sub-command %X of command %X\n", subcmd, cmd); break; } break; } case VMC_REVALUE: { unsigned int subcmd; if (4 > hex_len) { GNUNET_break_op (0); return; } if (1 != sscanf (&hex[2], "%2X", &subcmd)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received non-HEX input `%.*s'\n", (int) hex_len - 2, &hex[2]); GNUNET_break_op (0); return; } switch (subcmd) { case VMC_REVALUE_REQUEST: { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received request for revalue via MDB\n"); mdb.cmd = &cmd_revalue_approved; break; } case VMC_REVALUE_LIMIT_REQUEST: { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received request for revalue limit amount via MDB\n"); mdb.cmd = &cmd_revalue_amount; break; } default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unknown MDB sub-command %X of command %X\n", subcmd, cmd); break; } break; } case VMC_CONF: { unsigned int subcmd; if (4 > hex_len) { GNUNET_break_op (0); return; } if (1 != sscanf (&hex[2], "%2X", &subcmd)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Received non-HEX input `%.*s'\n", (int) hex_len - 2, &hex[2]); GNUNET_break_op (0); return; } switch (subcmd) { case VMC_READER_CONF: { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received request for configuration via MDB\n"); mdb.cmd = &cmd_reader_config_data; break; } case VMC_SETUP_MAX_MIN_PRICES: { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received max and min prices via MDB\n"); break; } default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unknown MDB sub-command %X of command %X\n", subcmd, cmd); break; } break; } case VMC_POLL: GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Received POLL from MDB (ignored)\n"); break; case VMC_CMD_RESET: { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Received RESET from MDB (ignored)\n"); break; } case VMC_READER: { unsigned int subcmd; if (4 > hex_len) { GNUNET_break_op (0); return; } if (1 != sscanf (&hex[2], "%2X", &subcmd)) { GNUNET_break_op (0); return; } switch (subcmd) { case VMC_READER_DISABLE: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received Reader Disable via MDB\n"); mdb.session_running = GNUNET_NO; if (NULL != payment_activity) { cleanup_payment (payment_activity); payment_activity = NULL; } for (unsigned int i = 0; i < products_length; i++) { if ( (sold_out_enabled) && (0 != strcmp (products[i].description, "empty")) && (GNUNET_YES == products[i].sold_out) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resetting sold out state of product %s\n", products[i].description); products[i].sold_out = GNUNET_NO; } } break; case VMC_READER_ENABLE: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received Reader Enable via MDB\n"); mdb.session_running = GNUNET_NO; break; case VMC_READER_CANCEL: mdb.cmd = &cmd_reader_cancelled; mdb.session_running = GNUNET_NO; break; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unknown MDB sub-command %X of command %X\n", subcmd, cmd); break; } break; } case VMC_REQUEST_ID: GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Received VMC request ID, no need to handle (done by HW)\n"); break; case VMC_ACKN: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received acknowledgement (for command `%s') from MDB\n", (NULL != mdb.last_cmd) ? mdb.last_cmd->name : "?"); handle_ack (); break; case VMC_OOSQ: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received command out of sequence from MDB (for command `%s')\n", (NULL != mdb.last_cmd) ? mdb.last_cmd->name : "?"); mdb.session_running = GNUNET_NO; if (mdb.last_cmd != &endSession) mdb.cmd = &endSession; else mdb.last_cmd = NULL; break; case VMC_RETR: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received request to resend previous data from MDB (previous command was `%s')\n", (NULL != mdb.last_cmd) ? mdb.last_cmd->name : "?"); GNUNET_break (NULL == mdb.cmd); GNUNET_break (NULL != mdb.last_cmd); mdb.cmd = mdb.last_cmd; mdb.last_cmd = NULL; break; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received unknown MDB command %X\n", cmd); break; } } /** * @brief Read in the sent commands of the VMC controller * * @param cls closure */ static void read_mdb_command (void *cls) { ssize_t ret; size_t cmdStartIdx; size_t cmdEndIdx; (void) cls; /* don't read if the mdb bus is disabled (only for testing) */ GNUNET_assert (! disable_mdb); mdb.rtask = NULL; ret = read (mdb.uartfd, &mdb.rxBuffer[mdb.rx_off], sizeof (mdb.rxBuffer) - mdb.rx_off); if (-1 == ret) { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, "read", uart_device_filename); GNUNET_SCHEDULER_shutdown (); return; } mdb.rx_off += ret; while (mdb.rx_off > 0) { mdb_active = GNUNET_YES; /* find begining of command */ for (cmdStartIdx = 0; cmdStartIdx < mdb.rx_off; cmdStartIdx++) if (VMC_CMD_START == mdb.rxBuffer[cmdStartIdx]) break; if (cmdStartIdx == mdb.rx_off) { mdb.rx_off = 0; break; } /* find end of command */ for (cmdEndIdx = cmdStartIdx; cmdEndIdx < mdb.rx_off; cmdEndIdx++) if (VMC_CMD_END == mdb.rxBuffer[cmdEndIdx]) break; if (cmdEndIdx == mdb.rx_off) { /* check to make sure rxBuffer was big enough in principle */ if ( (cmdStartIdx == 0) && (mdb.rx_off == sizeof (mdb.rxBuffer)) ) { /* Developer: if this happens, try increasing rxBuffer! */ GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); return; } /* move cmd in buffer to the beginning of the buffer */ memmove (mdb.rxBuffer, &mdb.rxBuffer[cmdStartIdx], mdb.rx_off - cmdStartIdx); mdb.rx_off -= cmdStartIdx; break; } /* if the full command was received parse it */ handle_command ((const char *) &mdb.rxBuffer[cmdStartIdx + 1], cmdEndIdx - cmdStartIdx - 1); /* move the data after the processed command to the left */ memmove (mdb.rxBuffer, &mdb.rxBuffer[cmdEndIdx + 1], mdb.rx_off - cmdEndIdx + 1); mdb.rx_off -= (cmdEndIdx + 1); } if (in_shutdown) { mdb_shutdown (); return; } run_mdb_event_loop (); } /** * @brief Mdb event loop to start read and write tasks */ static void run_mdb_event_loop () { struct GNUNET_DISK_FileHandle fh = { mdb.uartfd }; /* begin session if no cmd waits for sending and no cmd is received from the VMC */ if ( (GNUNET_NO == mdb.session_running) && (NULL == mdb.cmd) && (NULL == mdb.last_cmd) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Begining MDB session\n"); mdb.cmd = &cmd_begin_session; } /* start write task if he doesn't exist and if there is a cmd waiting to get sent */ if ( (NULL == mdb.wtask) && ( (NULL != mdb.cmd) || (in_shutdown) || (mdb.tx_len > mdb.tx_off) ) ) { if (disable_mdb || ! mdb_active) mdb.wtask = GNUNET_SCHEDULER_add_now (&write_mdb_command, NULL); else mdb.wtask = GNUNET_SCHEDULER_add_write_file ((in_shutdown) ? SHUTDOWN_MDB_TIMEOUT : GNUNET_TIME_UNIT_FOREVER_REL, &fh, &write_mdb_command, NULL); } if ( (disable_mdb) && (NULL != mdb.last_cmd) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Faking acknowledgement (for command `%s') from MDB\n", mdb.last_cmd->name); handle_ack (); } /* start read task if he doesn't exist and the mdb communication is not disabled (only for testing) */ if ( (NULL == mdb.rtask) && (! disable_mdb) ) mdb.rtask = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, &fh, &read_mdb_command, NULL); } /** * @brief Read the products from the configuration file * * @param cls closure in this case the name of the configuration file to read from * @param section section of the config file to read from */ static void read_products (void *cls, const char *section) { struct Product tmpProduct; char *tmpKey; char *thumbnail_fn; /* if the current section is not a product skip it */ if (0 != strncmp (section, "product-", strlen ("product-"))) return; /* the current section is a product, parse its specifications and store it in a temporary product */ if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cls, section, "description", &tmpProduct.description)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "description"); return; } if (sold_out_enabled) { if (0 == strcmp (tmpProduct.description, "empty")) { tmpProduct.sold_out = GNUNET_YES; } else { tmpProduct.sold_out = GNUNET_NO; } } if (GNUNET_OK != TALER_config_get_amount (cls, section, "price", &tmpProduct.price)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "price"); GNUNET_free (tmpProduct.description); return; } if (0 != strcasecmp (currency, tmpProduct.price.currency)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "price", "currency missmatch"); GNUNET_free (tmpProduct.description); return; } if (GNUNET_OK == GNUNET_CONFIGURATION_get_value_string (cls, section, "key", &tmpKey)) { tmpProduct.key = tmpKey[0]; GNUNET_free (tmpKey); } else { /* no key */ tmpProduct.key = '\0'; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cls, section, "instance", &tmpProduct.instance)) tmpProduct.instance = NULL; tmpProduct.preview = NULL; if (GNUNET_OK == GNUNET_CONFIGURATION_get_value_string (cls, section, "thumbnail", &thumbnail_fn)) { struct GNUNET_DISK_FileHandle *fh; fh = GNUNET_DISK_file_open (thumbnail_fn, GNUNET_DISK_OPEN_READ, GNUNET_DISK_PERM_NONE); if (NULL == fh) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not open thumbnail `%s'\n", thumbnail_fn); } else { off_t flen; if (GNUNET_OK != GNUNET_DISK_file_handle_size (fh, &flen)) { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, "stat", thumbnail_fn); } else { void *thumb; size_t len; ssize_t ret; len = (size_t) flen; thumb = GNUNET_malloc (len); ret = GNUNET_DISK_file_read (fh, thumb, len); if ( (ret < 0) || (len != ((size_t) ret)) ) { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, "read", thumbnail_fn); } else { const char *mime_type = ""; const char *ext; char *base64; ext = strrchr (thumbnail_fn, '.'); if (NULL != ext) { static const struct { const char *ext; const char *mime; } mimes[] = { { ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".svg", "image/svg" }, { NULL, NULL } }; for (unsigned int i = 0; NULL != mimes[i].ext; i++) if (0 == strcasecmp (mimes[i].ext, ext)) { mime_type = mimes[i].mime; break; } } (void) GNUNET_STRINGS_base64_encode (thumb, len, &base64); GNUNET_asprintf (&tmpProduct.preview, "data:%s;base64,%s", mime_type, base64); GNUNET_free (base64); } GNUNET_free (thumb); } GNUNET_break (GNUNET_OK == GNUNET_DISK_file_close (fh)); } GNUNET_free (thumbnail_fn); } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_number (cls, section, "number", &tmpProduct.number)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "number"); GNUNET_free (tmpProduct.description); GNUNET_free_non_null (tmpProduct.instance); GNUNET_free_non_null (tmpProduct.preview); return; } /* append the temporary product to the existing products */ GNUNET_array_append (products, products_length, tmpProduct); } /** * @brief Initialise the uart device to send mdb commands */ static int mdb_init () { struct termios uart_opts_raw; if (disable_mdb) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Running with MDB disabled!\n"); run_mdb_event_loop (); return GNUNET_OK; } /* open uart connection */ if (0 > (mdb.uartfd = open (uart_device_filename, O_RDWR | O_NOCTTY | O_NDELAY))) { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, "open", uart_device_filename); return GNUNET_SYSERR; } if (0 != tcgetattr (mdb.uartfd, &mdb.uart_opts_backup)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "tcgetattr"); return GNUNET_SYSERR; } uart_opts_raw = mdb.uart_opts_backup; /* reset current uart discipline */ memset (&uart_opts_raw, 0, sizeof(uart_opts_raw)); /* set baudrate */ cfsetispeed (&uart_opts_raw, B9600); cfsetospeed (&uart_opts_raw, B9600); /* set options */ uart_opts_raw.c_cflag &= ~PARENB; uart_opts_raw.c_cflag &= ~CSTOPB; uart_opts_raw.c_cflag &= ~CSIZE; uart_opts_raw.c_cflag |= CS8; /* 19200 bps, 8 databits, ignore cd-signal, allow reading */ uart_opts_raw.c_cflag |= (CLOCAL | CREAD); uart_opts_raw.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); uart_opts_raw.c_iflag = IGNPAR; uart_opts_raw.c_oflag &= ~OPOST; uart_opts_raw.c_cc[VMIN] = 0; uart_opts_raw.c_cc[VTIME] = 50; tcflush (mdb.uartfd, TCIOFLUSH); if (0 != tcsetattr (mdb.uartfd, TCSAFLUSH, &uart_opts_raw)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "tcsetattr"); return GNUNET_SYSERR; } /* if the configuration of the uart was sucessfull start the mdb write and read tasks */ run_mdb_event_loop (); return GNUNET_OK; } /** * @brief Start the application * * @param cls closure * @param args arguments left * @param cfgfile config file name * @param cfg handle for the configuation file */ static void run (void *cls, char *const *args, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *cfg) { (void) cls; (void) args; (void) cfgfile; /* parse the configuration file */ if (GNUNET_OK != TALER_config_get_currency (cfg, ¤cy)) { global_ret = EXIT_FAILURE; return; } /* parse the devices, if no config entry is found, a standard is used */ if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_filename (cfg, "taler-mdb", "UART_DEVICE", &uart_device_filename)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "UART_DEVICE"); uart_device_filename = GNUNET_strdup ("/dev/ttyAMA0"); } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_filename (cfg, "taler-mdb", "FRAMEBUFFER_DEVICE", &framebuffer_device_filename)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "FRAMEBUFFER_DEVICE"); framebuffer_device_filename = GNUNET_strdup ("/dev/fb1"); } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_filename (cfg, "taler-mdb", "FRAMEBUFFER_BACKLIGHT", &framebuffer_backlight_filename)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "FRAMEBUFFER_BACKLIGHT"); framebuffer_backlight_filename = GNUNET_strdup ( "/sys/class/backlight/soc:backlight/brightness"); } /* parse the taler configurations */ if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "taler-mdb", "backend-base-url", &backend_base_url)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "backend-base-url"); global_ret = EXIT_FAILURE; return; } { char *auth; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "taler-mdb", "backend-authorization", &auth)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "backend-authorization"); global_ret = EXIT_FAILURE; return; } /* concat the authorization e.g. Authorization: ApiKey sandbox */ GNUNET_asprintf (&authorization, "%s: %s", MHD_HTTP_HEADER_AUTHORIZATION, auth); GNUNET_free (auth); } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "taler-mdb", "fulfillment-url", &fulfillment_url)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "fulfillment-url"); global_ret = EXIT_FAILURE; return; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "taler-mdb", "fulfillment-msg", &fulfillment_msg)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler-mdb", "fulfillment-msgcurrency"); global_ret = EXIT_FAILURE; return; } /* parse the products */ GNUNET_CONFIGURATION_iterate_sections (cfg, &read_products, (void *) cfg); GNUNET_assert (NULL != products); GNUNET_SCHEDULER_add_shutdown (&shutdown_task, NULL); /* initialize mdb */ if (GNUNET_OK != mdb_init ()) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unable to initialize MDB (mdb_init() failed)\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } /* initialize nfc */ nfc_init (&context); if (NULL == context) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unable to initialize NFC (nfc_init() failed)\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } /* initialize HTTP client */ ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc); rc = GNUNET_CURL_gnunet_rc_create (ctx); /* setup authorization */ GNUNET_assert (GNUNET_OK == GNUNET_CURL_append_header (ctx, authorization)); /* open gpio pin for cancel button */ { int efd; efd = open ("/sys/class/gpio/export", O_WRONLY); if (-1 == efd) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unable to open /gpio/export for cancel button\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } if (2 != write (efd, "23", 2)) GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, "write", "/sys/class/gpio/export"); GNUNET_assert (0 == close (efd)); } /* set direction: input */ { int dfd; dfd = open ("/sys/class/gpio/gpio23/direction", O_WRONLY); if (-1 == dfd) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unable to open /gpio/gpio23/direction for cancel button\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } if (2 != write (dfd, "in", 2)) GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, "write", "/sys/class/gpio/gpio23/direction"); GNUNET_assert (0 == close (dfd)); } /* actually open fd for reading the state */ cancel_button.cancelbuttonfd = open ("/sys/class/gpio/gpio23/value", O_RDONLY); if (-1 == cancel_button.cancelbuttonfd) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unable to open /gpio/gpio23/value for cancel button\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } #if HAVE_QRENCODE_H /* open the framebuffer device */ qrDisplay.devicefd = open (framebuffer_device_filename, O_RDWR); if (-1 != qrDisplay.devicefd) { /* read information about the screen */ ioctl (qrDisplay.devicefd, FBIOGET_VSCREENINFO, &qrDisplay.var_info); /* store current screeninfo for reset */ qrDisplay.orig_vinfo = qrDisplay.var_info; if (16 != qrDisplay.var_info.bits_per_pixel) { /* Change variable info to 16 bit per pixel */ qrDisplay.var_info.bits_per_pixel = 16; if (0 > ioctl (qrDisplay.devicefd, FBIOPUT_VSCREENINFO, &qrDisplay.var_info)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, "ioctl(FBIOPUT_VSCREENINFO)"); return; } } /* Get fixed screen information */ if (0 > ioctl (qrDisplay.devicefd, FBIOGET_FSCREENINFO, &qrDisplay.fix_info)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, "ioctl(FBIOGET_FSCREENINFO)"); return; } /* get pointer onto frame buffer */ qrDisplay.memory = mmap (NULL, qrDisplay.fix_info.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, qrDisplay.devicefd, 0); if (MAP_FAILED == qrDisplay.memory) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, "mmap"); return; } /* set the screen to white */ memset (qrDisplay.memory, 0xFF, qrDisplay.var_info.xres * qrDisplay.var_info.yres * sizeof (uint16_t)); /* open backlight file to turn display backlight on and off */ qrDisplay.backlightfd = open ( framebuffer_backlight_filename, O_WRONLY); if (0 > qrDisplay.backlightfd) { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, "open", framebuffer_backlight_filename); } else { /* if '-i' flag was set, invert the on and off values */ if (backlight_invert) { backlight_on = '0'; backlight_off = '1'; } /* turn off the backlight */ (void) ! write (qrDisplay.backlightfd, &backlight_off, 1); } } else { GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, "open", framebuffer_device_filename); } #endif if (! disable_tty) start_read_keyboard (); } /** * @brief Convert the ASCII cmd in @hex to hex and store it in the mdb data struct @blk * * @param *blk pointer with reference to the mdb datablock * @param *hex pointer to the string containing the cmd data */ static void parse_block (struct MdbBlock *blk, const char *hex) { if (NULL == hex) { blk->bin_size = 0; blk->bin = NULL; return; } blk->bin_size = strlen (hex) / 2; blk->bin = GNUNET_malloc (blk->bin_size); for (size_t idx = 0; idx < blk->bin_size; idx++) { unsigned int val; GNUNET_assert (1 == sscanf (&hex[idx * 2], "%2X", &val)); blk->bin[idx] = (uint8_t) val; } } /** * @brief Create a new mdb command * * @param *name pointer to the string containing the command name * @param *cmd pointer to the string containing the command * @param *data pointer to the string containing the command data * @return structure of type MdbCommand holding the given information by the parameters */ static struct MdbCommand setup_mdb_cmd (const char *name, const char *cmd, const char *data) { struct MdbCommand cmdNew; cmdNew.name = (NULL == name) ? "No Cmd Name Set" : name; parse_block (&cmdNew.cmd, cmd); parse_block (&cmdNew.data, data); return cmdNew; } int main (int argc, char*const*argv) { struct termios tty_opts_backup, tty_opts_raw; int have_tty; int ret; /* the available command line options */ struct GNUNET_GETOPT_CommandLineOption options[] = { GNUNET_GETOPT_option_flag ('d', "disable-mdb", "disable all interactions with the MDB (for testing without machine)", &disable_mdb), GNUNET_GETOPT_option_flag ('s', "enable-soldout", "enable detection of sold-out products, preventing vend operations of the respective product until the process is restarted", &sold_out_enabled), GNUNET_GETOPT_option_flag ('t', "disable-tty", "disable all keyboard interactions (for running from systemd)", &disable_tty), GNUNET_GETOPT_option_flag ('i', "backlight-invert", "invert the backlight on/off values (standard on = 1)", &backlight_invert), GNUNET_GETOPT_OPTION_END }; if (! disable_tty) { have_tty = isatty (STDIN_FILENO); if (have_tty) { if (0 != tcgetattr (STDIN_FILENO, &tty_opts_backup)) fprintf (stderr, "Failed to get terminal discipline\n"); tty_opts_raw = tty_opts_backup; tty_opts_raw.c_lflag &= ~(ECHO | ECHONL | ICANON); if (0 != tcsetattr (STDIN_FILENO, TCSANOW, &tty_opts_raw)) fprintf (stderr, "Failed to set terminal discipline\n"); } } /* make the needed commands for the communication with the vending machine controller */ cmd_reader_config_data = setup_mdb_cmd ("Reader Config", READER_CONFIG, READER_FEATURE_LEVEL READER_COUNTRYCODE READER_SCALE_FACTOR READER_DECIMAL_PLACES READER_MAX_RESPONSE_TIME READER_MISC_OPTIONS); cmd_begin_session = setup_mdb_cmd ("Begin Session", READER_BEGIN_SESSION, READER_FUNDS_AVAILABLE); cmd_approve_vend = setup_mdb_cmd ("Approve Vend", READER_VEND_APPROVE, READER_VEND_AMOUNT); cmd_reader_cancelled = setup_mdb_cmd ("Confirm cancellation", READER_CANCELLED, NULL); cmd_deny_vend = setup_mdb_cmd ("Deny Vend", READER_VEND_DENIED, NULL); endSession = setup_mdb_cmd ("End Session", READER_END_SESSION, NULL); cmd_revalue_approved = setup_mdb_cmd ("Reader Approve Revalue", READER_REVALUE_APPROVED, NULL); cmd_revalue_amount = setup_mdb_cmd ("Send Revalue Limit Amount", READER_REVALUE_LIMIT, READER_REVALUE_LIMIT_AMOUNT); cmd_reader_NACK = setup_mdb_cmd ("Reader NACK", READER_NACK, NULL); cmd_reader_display_sold_out = setup_mdb_cmd ("Display Sold Out", READER_DISPLAY_REQUEST, READER_DISPLAY_REQUEST_TIME READER_DISPLAY_SOLD_OUT); cmd_reader_display_internal_error = setup_mdb_cmd ( "Display Communication Error", READER_DISPLAY_REQUEST, READER_DISPLAY_REQUEST_TIME READER_DISPLAY_INTERNAL_ERROR); cmd_reader_display_backend_not_reachable = setup_mdb_cmd ( "Display Backend not reachable", READER_DISPLAY_REQUEST, READER_DISPLAY_REQUEST_TIME READER_DISPLAY_BACKEND_NOT_REACHABLE); ret = GNUNET_PROGRAM_run (argc, argv, "taler-mdb", "This is an application for snack machines to pay with GNU Taler via NFC.\n", options, &run, NULL); if (! disable_tty) { if (have_tty) { /* Restore previous TTY settings */ if (0 != tcsetattr (STDIN_FILENO, TCSANOW, &tty_opts_backup)) fprintf (stderr, "Failed to restore terminal discipline\n"); } } if (GNUNET_OK != ret) return 1; return global_ret; }