14. Interface and Application Programming - Week 14


14.1. Assignments

Interface and Application Programming

individual assignment

  • write an application that interfaces a user with an

    • input &/or output device that you made group assignment

  • compare as many tool options as possible

14.2. Group Assignment

Group Page

I tried out the tooling with Rust and Slint as a UI-Framework. For the communication with my dev-board I used the TinyUSB library from the esp-idf and set up my board as a CDC-ACM Serial device.

And for debugging I used the UNIX commands cat and echo and for convenience ‘minicom’ to easily read, write and reconnect to the USB Serial device.

14.2.1. General

There are many approaches for UI-Development but most of it resolves to using a library/framework (because implementing everything from scratch would be very time-consuming). And there are many of them.
The kinds of UI basically break down in 3 different groups:

  1. Web-Based

    • using a browser to render your content

  2. Native

    • using a native operating system application to render your content without a browser in-between

  3. CLI (command-line interface)/text-based

    • just using the command-line and text as a UI (they can also be beautiful e.g.: btop)

And also there are many different approaches the libraries/frameworks take to develop a UI, I’m going to explain only the approach my framework uses.
The Slint framework uses a DSL (Domain-Specific Language) just for creating the UI, that means you have a whole programming Language where the UI is created by ‘programming’ it. So the objects you create in there and the order you call them actually influences the UI and layout of elements in it. But the actual application logic is written in another language.
To communicate, the DSL then needs to be compiled (for the UI itself) and also transpiled (compiled into another language) to provide an interface the application logic can interact with.
This is done because:

  1. In bigger projects where many people work on it simultaneously, you want to split the work so that different people can work parallel without interfering other peoples work and a DSL is a pretty effective way doing this. Here you only initialy need to design an interface the 2 parts communicate with and then the rest can be developed parallel without interference. Also the back-end/frontend can be extended/fixed (almost) without breaking the other part.

  2. Compiling the DSL for different operating systems and visual styles is easier/more reliably then with most other frameworks because this can just be added by changing the DSL compiler. E.g. Slint can be compiled for Windows, Mac, Linux, Web and even Android and in the future also IOS.

../_images/slint_diagramm.jpg

System Diagramm of Slint (and also many DSL)
(Source)

For the application logic I used rust and used a simple Serial-Library to send and read data via USB. This USB device (in CDC-ACM mode) is then just a serial devices reading and sending characters over the interface that is then found in the operating system (e.g. in linux under /dev/ttyACM0) where you can read/write those characters.

14.2.2. Understanding the USB Protocol

Note

Generated with deepseek-r1:14b (self-hosted) with following prompt:

Can you explain the Protocol behind USB?

The Universal Serial Bus (USB) protocol is a fundamental technology that enables communication between devices and computers. Here’s a detailed breakdown of its key components and functions:

  1. History and Purpose:

    • Developed in the mid-1990s to replace older interfaces like parallel and serial ports.

    • Designed to be fast, versatile, and user-friendly, supporting a wide range of devices.

  2. Key Components:

    • Host: Typically the computer or laptop that acts as the central controller.

    • Device: The peripheral connected to the host, such as a printer or keyboard.

    • Bus: The physical medium (cable) through which data is transmitted between devices.

  3. Data Transfer Mechanisms:

    • Packets: Data is sent in packets, each containing information about its origin and destination.

    • Addresses: Assigned by the host to identify devices on the bus, facilitating communication.

  4. Transfer Types:

    • Control Transfers: Used for device configuration and management.

    • Bulk Transfers: For large data transfers, like files or images.

    • Interrupt Transfers: Real-time communication, essential for devices like keyboards and mice.

    • Isochronous Transfers: Guaranteed bandwidth for time-sensitive data, such as audio or video.

  5. Descriptors:

    • Endpoints: Channels for communication between the host and device, each with specific roles (e.g., input/output).

    • Configurations: Define how a device is set up and operates.

  6. Power Management:

    • Devices can draw power from the host, with mechanisms in place to prevent overloading.

    • Power requirements are negotiated during connection setup.

  7. Error Handling:

    • Protocols exist for detecting and recovering from data loss or corruption, including retransmission mechanisms.

  8. Physical Aspects:

    • Cables and connectors adhere to specific standards (e.g., USB-A, USB-C) ensuring compatibility and reliable communication.

  9. USB Versions:

    • USB 1.0/2.0/3.0: Offer different data transfer rates (e.g., USB 3.0 supports SuperSpeed).

    • Devices negotiate the highest supported speed during connection.

  10. Hubs and Root Hubs:

    • Hub: Allows multiple devices to connect to a single USB port.

    • Root Hub: Located on the host, manages connections to other hubs and devices.

14.2.3. Setup

14.2.3.1. Rust + Slint

For Rust I used my already used rust-setup (I basically downloaded rustup with my package-manager and the normal rustc compiler and rest of the default toolchain from rustup ).

To get started I used this template, which has the needed structure for slint and a simple counter to learn the concepts.

14.2.3.2. TinyUSB + ESP-IDF

14.3. Individual Assignment

I wanted to control the LEDs via USB and also display the touch value, when I press the touchpads on my device.

14.3.1. USB Device Setup

First I setup an USB Serial device with this example.

Here the TinyUSB Code:

/*
 * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */

#include "driver/gpio.h"
#include "driver/touch_sensor.h"
#include "driver/touch_sensor_common.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "led_strip.h"
#include "portmacro.h"
#include "sdkconfig.h"
#include "tinyusb.h"
#include "tusb_cdc_acm.h"
#include "tusb_console.h"
#include <stdint.h>
#include <stdio.h>

static const char *TAG = "example";
static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];

#define TOUCH_DEFAULT_LEVEL 50000
#define TOUCH_MAX_LEVEL 800000
#define NUM_OF_TOUCHPADS 2
#define NUM_OF_LEDS 6

static const touch_pad_t button[NUM_OF_TOUCHPADS] = {
    TOUCH_PAD_NUM1,
    TOUCH_PAD_NUM2,
};

#define LED_V0 1
#define LED_V1 2
#define LED_V2 3

#define LED_G0 6
#define LED_G1 5
#define LED_G2 4
static const char *TOUCH_TAG = "CoolKeyboard";

static led_strip_handle_t led_strip;

union Leds {
  struct {
    uint8_t id;
    uint8_t r;
    uint8_t g;
    uint8_t b;
  } value;
  uint32_t raw;
};

/**
 * @brief Application Queue
     */
static QueueHandle_t app_queue;
typedef struct {
  uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1]; // Data buffer
  size_t buf_len;                                 // Number of bytes received
  uint8_t itf; // Index of CDC device interface
} app_message_t;

int init_device() {

      // TOUCH PAD INIT
      touch_pad_init();
  for (int i = 0; i < NUM_OF_TOUCHPADS; i++) {
           touch_pad_config(button[i]);
  }
  touch_pad_set_measurement_interval(TOUCH_PAD_SLEEP_CYCLE_DEFAULT);
  touch_pad_set_charge_discharge_times(TOUCH_PAD_MEASURE_CYCLE_DEFAULT);
  touch_pad_set_voltage(TOUCH_PAD_HIGH_VOLTAGE_THRESHOLD,
                                                TOUCH_PAD_LOW_VOLTAGE_THRESHOLD,
                                                TOUCH_PAD_ATTEN_VOLTAGE_THRESHOLD);
  touch_pad_set_idle_channel_connect(TOUCH_PAD_IDLE_CH_CONNECT_DEFAULT);
  for (int i = 0; i < NUM_OF_TOUCHPADS; i++) {
           touch_pad_set_cnt_mode(button[i], TOUCH_PAD_SLOPE_DEFAULT,
                                  TOUCH_PAD_TIE_OPT_DEFAULT);
  }
  /* Denoise setting at TouchSensor 0. */
      touch_pad_denoise_t denoise = {
      /* The bits to be cancelled are determined according to the noise level.
       *        */
      .grade = TOUCH_PAD_DENOISE_BIT4,
      .cap_level = TOUCH_PAD_DENOISE_CAP_L4,
  };
      touch_pad_denoise_set_config(&denoise);
  touch_pad_denoise_enable();
  ESP_LOGI(TOUCH_TAG, "Denoise function init");

  /* Enable touch sensor clock. Work mode is "timer trigger". */
      touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
  touch_pad_fsm_start();
  // END TOUCHPAD

      // LED STRIP INIT
      led_strip_config_t strip_config = {
      .strip_gpio_num = 3,
      .max_leds = NUM_OF_LEDS, // at least one LED on board
  };

      led_strip_spi_config_t spi_config = {
      .spi_bus = SPI2_HOST,
      .flags.with_dma = true,
  };
      ESP_ERROR_CHECK(
          led_strip_new_spi_device(&strip_config, &spi_config, &led_strip));

  led_strip_clear(led_strip);
  // END LED STRIP

      return 1;
}

/**
 * @brief CDC device RX callback
 *
 * CDC device signals, that new data were received
 *
 * @param[in] itf   CDC device index
 * @param[in] event CDC event type
 */
void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
      /* initialization */
      size_t rx_size = 0;

      /* read */
      esp_err_t ret =
      tinyusb_cdcacm_read(itf, rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
      if (ret == ESP_OK) {

    app_message_t tx_msg = {
        .buf_len = rx_size,
        .itf = itf,
    };

    // copy from buffer to local object
    memcpy(tx_msg.buf, rx_buf, rx_size);
    // send local object to a queue used in another thread
    xQueueSend(app_queue, &tx_msg, 0);
  } else {
    ESP_LOGE(TAG, "Read Error");
  }
}

/**
 * @brief CDC device line change callback
 *
 * CDC device signals, that the DTR, RTS states changed, not needed
 *
 * @param[in] itf   CDC device index
 * @param[in] event CDC event type
 */
void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
      int dtr = event->line_state_changed_data.dtr;
      int rts = event->line_state_changed_data.rts;
      ESP_LOGI(TAG, "Line state changed on channel %d: DTR:%d, RTS:%d", itf, dtr,
                          rts);
}

void app_main(void) {
      // Create FreeRTOS queue to receive the messages send via serial
      app_queue = xQueueCreate(5, sizeof(app_message_t));
  assert(app_queue);
  app_message_t msg;

      ESP_LOGI(TAG, "USB initialization");
    // initialise the TinyUSB Library
  const tinyusb_config_t tusb_cfg = {
      .device_descriptor = NULL,
      .string_descriptor = NULL,
      .external_phy = false,
#if (TUD_OPT_HIGH_SPEED)
      .fs_configuration_descriptor = NULL,
      .hs_configuration_descriptor = NULL,
      .qualifier_descriptor = NULL,
#else
          .configuration_descriptor = NULL,
#endif // TUD_OPT_HIGH_SPEED
      };

        // install the usb driver, removing the default one used for flashing the XIAO
      ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));


    // initialise the CDC-ACM device that should be used on USB 
  tinyusb_config_cdcacm_t acm_cfg = {
      .usb_dev = TINYUSB_USBDEV_0,
      .cdc_port = TINYUSB_CDC_ACM_0,
      .rx_unread_buf_sz = 64,
      .callback_rx = NULL,
      .callback_rx =
          &tinyusb_cdc_rx_callback, // the first way to register a callback
      .callback_rx_wanted_char = NULL,
      .callback_line_state_changed = NULL,
      .callback_line_coding_changed = NULL};

      ESP_ERROR_CHECK(tusb_cdc_acm_init(&acm_cfg));

    // register a callback for the line change, not needed
      ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback(
                            TINYUSB_CDC_ACM_0, CDC_EVENT_LINE_STATE_CHANGED,
                            &tinyusb_cdc_line_state_changed_callback));

    // if more USB CDC-ACM devices exist on the host
#if (CONFIG_TINYUSB_CDC_COUNT > 1)
  acm_cfg.cdc_port = TINYUSB_CDC_ACM_1;
  ESP_ERROR_CHECK(tusb_cdc_acm_init(&acm_cfg));
#endif

      ESP_LOGI(TAG, "USB initialization DONE");
    // initialise led and touch
  init_device();
  union Touch {
    uint32_t touch[NUM_OF_TOUCHPADS];
    uint8_t raw[4 * NUM_OF_TOUCHPADS];
  };
      while (1) {
        // read the touch data
    union Touch touch_data = {.touch = {0, 0}};
    uint32_t sum = 0;
    for (int i = 0; i < NUM_OF_TOUCHPADS; i++) {
               ESP_ERROR_CHECK(touch_pad_read_raw_data(button[i], &touch_data.touch[i]));
      sum += touch_data.touch[i];
    }
        // write those bytes to the queue before sending them
    if (sum > NUM_OF_TOUCHPADS * TOUCH_DEFAULT_LEVEL) {
      int bytes =
          tinyusb_cdcacm_write_queue(0, touch_data.raw, sizeof(union Touch));
      ESP_LOGI(TAG, "%d bytes queued!", bytes);
            // the flush then sends the bytes to the host
      esp_err_t err = tinyusb_cdcacm_write_flush(0, 50);
      if (err != ESP_OK) {
        ESP_LOGE(TAG, "CDC ACM write flush error: %s", esp_err_to_name(err));
      }
    }
        // received send data from the host stored in the queue
    if (xQueueReceive(app_queue, &msg, 100 / portTICK_PERIOD_MS)) {
      if (msg.buf_len) {
        union Leds leds[NUM_OF_LEDS] = {};

        /* Print received data*/
        ESP_LOGI(TAG, "Data from channel %d:", msg.itf);
        ESP_LOG_BUFFER_HEXDUMP(TAG, msg.buf, msg.buf_len, ESP_LOG_INFO);

        // and then just set the pixel because only those bytes are send
        led_strip_set_pixel(led_strip, msg.buf[0], msg.buf[1], msg.buf[2],
                                                        msg.buf[3]);
        led_strip_refresh(led_strip);
      }
    }
  }
}

Here I had some problems first when sending because it waited at the QUEUE and I needed to change to blocking time: 100 / portTICK_PERIOD_MS which reduced the blocking time so it is interactive.

14.3.2. USB Rust Setup

As a first Approach I tried the library nusb And got the code working to find my device, but then I couldn’t find out, why I can’t send data via serial…

println!("Gathering USB Devices...");
let mut devices = list_devices().expect("Error Receiving Devices!");
for device in devices {
    println!("ID: {}:{}", device.vendor_id(), device.product_id());
}
let esp_devices = devices.find(|device| device.vendor_id() == 12346);
let keyboard: nusb::DeviceInfo = match esp_devices {
    Some(k) => {
        println!(
            "Found keyboard: \n product_string: {} \n ID: {}:{}",
            k.product_string().expect("No Product String avaibible"),
            k.vendor_id(),
            k.product_id()
        );
        k
    }
    None => {
        println!("No keyboard found...");
        std::process::exit(-1);
    }
};

let keyboard_dev = keyboard.open()?;
let kb_interface = keyboard_dev.detach_and_claim_interface(0)?;

loop {
    let buf = RequestBuffer::new(512);
    let received = block_on(kb_interface.bulk_in(0x80, buf)).into_result();
    dbg!(received);
}

… but I noticed, that this library is not used for CDC-ACM devices but for all other USB devices (or maybe there was also an option to use it but I couldn’t find it ._.).

Then I tried an explicit Serial Library: serialport which worked flawlessly (after learning, how to use ;] ).

To find (currently only the first) USB Device, I used:

use serialport::{available_ports, SerialPort, SerialPortType};

fn main() {

    // get currently available serial ports
    let devices = available_ports().expect("Unable to gather Serial Ports");
    let keyboard_dev = devices
        .iter()
        // using a Lambda-function I print out every found device and information from it
            .find(|&device| match device.port_type.clone() {
            SerialPortType::UsbPort(usb) => {
                println!("Found USB serial device '{}'", device.port_name);
                dbg!(usb);
                true
            }
            _ => false,
        }).expect("No USB Serial Device found!");

    // try to connect to the serial port
    let mut keyboard = serialport::new(&keyboard_dev.port_name, 115200).open()?;

}

Note

To find out the Baudrate (here 115200) I looked in the sdkconfig- file for ‘BAUD’ and found some parameter like: “CONFIG_CONSOLE_UART_BAUDRATE=115200”.

Then when its open, I could read the values and also write some back:

let id: i8: 0;
let id_prev: i8: 0;
loop {
    let mut buf: [u8; 4 * NUM_OF_KEYS] = [0, 0, 0, 0, 0, 0, 0, 0];
    // read from the serial device
    match keyboard.read(&mut buf) {
        Ok(l) => {
            println!("Read {l} bytes:");
            dbg!(buf);
            // convert them to uint32
            let touch_keys: [u32; NUM_OF_KEYS] = [
                u32::from_ne_bytes(buf[..4].try_into().expect("Could not convert to u32")),
                u32::from_ne_bytes(buf[4..8].try_into().expect("Could not convert to u32")),
            ];
            // print touch values
            println!("Actual touch values: {}; {}", touch_keys[0], touch_keys[1]);
        }
        Err(_) => {}
    };
    // used protocol with the id constantly changed for testing
    let led_on: [u8; 4] = [id, 0, 255, 255];
    let led_off: [u8; 4] = [id_prev, 0, 0, 0];
    // write to serial interface
    match keyboard.write(&led_on) {
        Ok(l) => println!("Wrote {l} bytes to turn ON {id}!"),
        Err(_) => {}
    };
    // like above, actually send it
    keyboard.flush();
    // turn of last led
    match keyboard.write(&led_off) {
        Ok(l) => println!("Wrote {l} bytes to turn OFF {id_prev}!"),
        Err(_) => {}
    };
    keyboard.flush();
    // just go through the leds in a cycle
    id_prev = id;
    id = (id + 1) % 6;
    sleep(time::Duration::from_millis(100));
}

This loop continuously reads the key presses and writes new values for the LEDs to switch around.

14.3.3. Rust Slint UI

A simple ui:

IMAGE

And ths DSL - Slint code

import { Button, VerticalBox, HorizontalBox, ComboBox, Slider } from "std-widgets.slint";


component LedsGrid inherits GridLayout {
    property <int> led_count: 6;
    property <int> board_count: 2;
    min-height: 100px;
    // callback function used to execute code in rust
    callback toggle_led(id: int, state: bool);
    // BEGIN Buttons for the leds
    Button {
        text: self.state ? "LED 0 ON" : "LED 0 OFF";
        row: 0;
        col: 0;
        property <int> id: 0;
        property <bool> state: false;
        // the 'clicked' function executed when button is pressed
        clicked => {
            self.state = !self.state;
            // call the toggle callback function to execute the rust code
            root.toggle_led(self.id, self.state);
        }
    }

    Button {
        text: self.state ? "LED 1 ON" : "LED 1 OFF";
        row: 0;
        col: 1;
        property <int> id: 1;
        property <bool> state: false;
        clicked => {
            self.state = !self.state;
            root.toggle_led(self.id, self.state);
        }
    }

    Button {
        text: self.state ? "LED 2 ON" : "LED 2 OFF";
        row: 1;
        col: 0;
        property <int> id: 2;
        property <bool> state: false;
        clicked => {
            self.state = !self.state;
            root.toggle_led(self.id, self.state);
        }
    }

    Button {
        text: self.state ? "LED 3 ON" : "LED 3 OFF";
        row: 1;
        col: 1;
        property <int> id: 3;
        property <bool> state: false;
        clicked => {
            self.state = !self.state;
            root.toggle_led(self.id, self.state);
        }
    }

    Button {
        text: self.state ? "LED 4 ON" : "LED 4 OFF";
        row: 2;
        col: 0;
        property <int> id: 4;
        property <bool> state: false;
        clicked => {
            self.state = !self.state;
            root.toggle_led(self.id, self.state);
        }
    }

    Button {
        text: self.state ? "LED 5 ON" : "LED 5 OFF";
        row: 2;
        col: 1;
        property <int> id: 5;
        property <bool> state: false;
        clicked => {
            self.state = !self.state;
            root.toggle_led(self.id, self.state);
        }
    }
    // END Buttons
}

component Piano inherits HorizontalBox {
    min-height: 50px;
    in property <bool> key0: false;
    in property <bool> key1: false;

    // BEGIN Rectangles to visualise key presses
    Rectangle {
        background: key0 ? green : black;
        border-color: white;
        border-width: 1px;
    }

    Rectangle {
        background: key1 ? green : black;
        border-color: white;
        border-width: 1px;
    }
    // END Rectangles to visualise key presses
}

export component AppWindow inherits Window {
    in-out property <int> counter: 42;
    in property <bool> key0: false;
    in property <bool> key1: false;
    min-height: 500px;
    
    // global callback to use in RUST connects to the callback in the led grid
    callback toggle_led <=> led_grid.toggle_led;
    VerticalBox {
        // just show the device (forgot to implement to show the actual used one xD)
        HorizontalBox {
            Text {
                text: "USB Device:";
            }

            ComboBox {
                model: ["/dev/ttyACM0"];
                width: 50%;
            }
        }

        Text {
            text: "LEDs:";
        }

        // add the led grid
        led_grid := LedsGrid { }

        Text {
            text: "Piano Keys";
        }

        // add the piano keys, connecting the input variables defined in the root to use in rust
        piano := Piano {
            key0 <=> key0;
            key1 <=> key1;
        }
    }
}

And here the integration with rust:

use core::time;
use std::{
    error::Error,
    rc::Rc,
    sync::{Arc, Mutex},
    thread::{self, sleep},
};

use serialport::{available_ports, SerialPort, SerialPortType};

// compile slint and get modules to work with them and be able to call the functions of the UI
slint::include_modules!();

use std::convert::TryFrom;

const NUM_OF_KEYS: usize = 2;

struct TouchValues {
    keys: [bool; NUM_OF_KEYS],
}

fn main() -> Result<(), Box<dyn Error>> {
    let mut t_values = Arc::new(Mutex::new(TouchValues {
        keys: [false, false],
    }));

fn start_ui(mut keyboard: Box<dyn SerialPort>) {
    // initialise the UI window
    let ui = AppWindow::new().expect("Couldn't create a window!");
        // register the callback to toggle the led
        ui.on_toggle_led(move |id, state| {
            println!("Toggle LED!");
            println!("ID: {id}");
            // check the button state (toggled or not)
            let buf = if state {
                [
                    id.try_into().expect("Could not convert i32 to u8 for id!"),
                    255,
                    255,
                    255,
                ]
                } else {
                [
                    id.try_into().expect("Could not convert i32 to u8 for id!"),
                    0,
                    0,
                    0,
                ]
                };
            // then write to the keyboard given as a mutable reference to the start_ui function
            match keyboard.write(&buf) {
                Ok(l) => println!("Wrote {l} bytes to turn ON {id}!"),
                Err(_) => println!("Error while writing to keyboard!"),
            };
            keyboard.flush();
        });

        // actually run the application ui
        ui.run().expect("Error running UI!");
    }

    // explained above
    let devices = available_ports().expect("Unable to gather Serial Ports");
    let keyboard_dev = devices
        .iter()
            .find(|&device| match device.port_type.clone() {
            SerialPortType::UsbPort(usb) => {
                println!("Found USB serial device '{}'", device.port_name);
                dbg!(usb);
                true
            }
            _ => false,
        })
                .expect("No USB Serial Device found!");

    let mut keyboard = serialport::new(&keyboard_dev.port_name, 115200).open()?;
    let mut kb2 = keyboard
        .try_clone()
            .expect("Couldn't clone SerialPort of keyboard!");
    loop {
        let mut buf: [u8; 4 * NUM_OF_KEYS] = [0, 0, 0, 0, 0, 0, 0, 0];
        match keyboard.read(&mut buf) {
            Ok(l) => {
                println!("Read {l} bytes:");
                dbg!(buf);
                let touch_keys: [u32; NUM_OF_KEYS] = [
                    u32::from_ne_bytes(buf[..4].try_into().expect("Could not convert to u32")),
                    u32::from_ne_bytes(buf[4..8].try_into().expect("Could not convert to u32")),
                ];
                println!("Actual touch values: {}; {}", touch_keys[0], touch_keys[1]);
                for i in 0..NUM_OF_KEYS {
                }
            }
            Err(_) => {}
        };
        sleep(time::Duration::from_millis(100));
    }

}

IMAGE

Unfortunately I didn’t manged to light up the Keys, only output on command line. I had some rust-language-problems X)