LilyGo T-Deck

T-Deck

The LilyGo T-Deck is an ESP32-S3 development board with a touchscreen display, keyboard, trackball, audio output, and expansion headers.

The espp::TDeck component provides a singleton hardware abstraction for initializing the touch, display, keyboard, trackball, audio, and micro-SD card subsystems. The LCD now uses the shared espp::Spi / SpiPanelIo transport path while SDSPI-mounted microSD access stays on the same SPI host.

API Reference

Header File

Classes

class TDeck : public espp::BaseComponent

The TDeck class provides an interface to the LilyGo T-Deck ESP32-S3 development board.

The class provides access to the following features:

  • Touchpad

  • Display

  • Keyboard

  • Audio

  • Interrupts

  • I2C

  • microSD (uSD) card

  • LoRa radio (SX1262)

  • GPS (T-Deck Plus only)

For more information, see https://github.com/Xinyuan-LilyGO/T-Deck/tree/master and https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/UnitTest/utilities.h

The class is a singleton and can be accessed using the get() method.

Example

  espp::TDeck &tdeck = espp::TDeck::get();
  tdeck.set_log_level(espp::Logger::Verbosity::INFO);

  // initialize the uSD card
  using SdCardConfig = espp::TDeck::SdCardConfig;
  SdCardConfig sdcard_config{};
  if (!tdeck.initialize_sdcard(sdcard_config)) {
    logger.warn("Failed to initialize uSD card, there may not be a uSD card inserted!");
  }
  // initialize the sound at the example's fixed rate (see AUDIO_SAMPLE_RATE_HZ)
  // so the speaker runs at that rate from the start - no runtime rate
  // reconfiguration, which is where a speaker-vs-recording pitch mismatch
  // could creep in
  if (!tdeck.initialize_sound(AUDIO_SAMPLE_RATE_HZ)) {
    logger.error("Failed to initialize sound!");
    return;
  }
  // initialize the LCD
  if (!tdeck.initialize_lcd()) {
    logger.error("Failed to initialize LCD!");
    return;
  }
  // set the pixel buffer to be 50 lines high
  static constexpr size_t pixel_buffer_size = tdeck.lcd_width() * 50;
  // initialize the LVGL display for the T-Deck
  if (!tdeck.initialize_display(pixel_buffer_size)) {
    logger.error("Failed to initialize display!");
    return;
  }

  // create the GUI: builds the UI (label, buttons, circle layer) and starts
  // the task which updates LVGL. All of its public methods are thread-safe,
  // so the keyboard and touch callbacks below can call them directly.
  static Gui gui({.log_level = espp::Logger::Verbosity::INFO});
  gui.set_label_text(
      fmt::format("Touch the screen to draw!\nPress the delete key or the {} button to clear "
                  "circles.\nPress the space key or the {} button to rotate the display.\n"
                  "The Audio tab (or the 'r' / 'p' keys) records and plays back audio; "
                  "'n' / '$' / 'm' adjust / mute the speaker volume.\n"
                  "On the LoRa tab, type a message and press Send (or Enter) to transmit "
                  "it over the SX1262 radio.",
                  LV_SYMBOL_TRASH, LV_SYMBOL_REFRESH));

  // initialize the Keyboard after the Gui exists so key presses can act on it
  // immediately
  auto keypress_callback = [&](uint8_t key) {
    logger.info("Key pressed: {}", key);
    // When the LoRa tab is active, the keyboard composes a message in its text
    // box: Enter sends, backspace deletes, and printable keys are appended.
    // (This takes over the keys from the Draw / Audio shortcuts while typing.)
    if (gui.lora_page_active()) {
      if (key == '\r' || key == '\n') {
        gui.send_lora_message();
      } else if (key == 8) {
        gui.lora_input_add_char('\b');
      } else if (key >= 0x20 && key < 0x7f) {
        gui.lora_input_add_char(static_cast<char>(key));
      }
      return;
    }
    if (key == 8) {
      // delete key will clear the circles
      logger.info("Clearing circles");
      gui.clear_circles();
    } else if (key == ' ') {
      // space key will rotate the display
      logger.info("Rotating display");
      gui.next_rotation();
    } else if (key == 'm') {
      // 'm' key will toggle audio mute
      logger.info("Toggling mute");
      tdeck.mute(!tdeck.is_muted());
      logger.info("Muted: {}", tdeck.is_muted());
    } else if (key == 'n') {
      // 'n' key will decrease audio volume (left of 'm' key)
      logger.info("Decreasing volume");
      tdeck.volume(tdeck.volume() - 10.0f);
      logger.info("Volume: {}", tdeck.volume());
      gui.refresh_audio_label();
    } else if (key == '$') {
      // '$' key will increase audio volume (right of 'm' key)
      logger.info("Increasing volume");
      tdeck.volume(tdeck.volume() + 10.0f);
      logger.info("Volume: {}", tdeck.volume());
      gui.refresh_audio_label();
    } else if (key == 'r') {
      // 'r' key toggles recording from the microphones
      if (toggle_record) {
        toggle_record();
      }
    } else if (key == 'p') {
      // 'p' key toggles playback of the recording
      if (toggle_play) {
        toggle_play();
      }
    }
  };
  bool start_task = true;
  if (!tdeck.initialize_keyboard(start_task, keypress_callback)) {
    logger.error("Failed to initialize Keyboard!");
    return;
  }

  // initialize the trackball. This example uses the microphone, and per
  // LilyGO's T-Deck documentation GPIO0 (the trackball's center / click
  // button, shared with BOOT) is not available while the microphone is
  // enabled - leaving it configured produces spurious interrupts that jitter
  // the audio capture (a robotic / staticy recording). So initialize the
  // trackball with the center button disabled; the four directional pins
  // still work.
  auto trackball_callback = [&](const auto &trackball) {
    logger.debug("Trackball: {}", trackball);
  };
  if (!tdeck.initialize_trackball(trackball_callback, 10, /*enable_center_button=*/false)) {
    logger.error("Failed to initialize trackball!");
    return;
  }

  // initialize the touchpad; each touch draws a circle (and plays a click
  // sound)
  auto touch_callback = [&](const auto &touch) {
    // NOTE: since we're directly using the touchpad data, and not using the
    // TouchpadInput + LVGL, we'll need to ensure the touchpad data is
    // converted into proper screen coordinates instead of simply using the
    // raw values.
    static auto previous_touchpad_data = tdeck.touchpad_convert(touch);
    auto touchpad_data = tdeck.touchpad_convert(touch);
    if (touchpad_data != previous_touchpad_data) {
      logger.info("Touch: {}", touchpad_data);
      previous_touchpad_data = touchpad_data;
      // if there is a touch point on the Draw tab, draw a circle and play a
      // click sound (touches on the other tabs go to their widgets)
      if (touchpad_data.num_touch_points > 0 && gui.draw_page_active()) {
        play_click(tdeck);
        gui.draw_circle(touchpad_data.x, touchpad_data.y, 10);
      }
    }
  };
  // NOTE: this example raises the BSP interrupt-task stack size via
  // sdkconfig.defaults (CONFIG_TDECK_INTERRUPT_STACK_SIZE=8192); the touch
  // controller is read from that task and its error-logging path needs
  // more than the 4 KB BSP default. See the example README.
  if (!tdeck.initialize_touch(touch_callback)) {
    logger.error("Failed to initialize touchpad!");
    return;
  }

  // load the audio file (wav file bundled in memory)
  size_t wav_size = 0;
  size_t wav_sample_rate = 0;
  if (!load_audio(wav_size, wav_sample_rate)) {
    logger.error("Failed to load audio file!");
    return;
  }
  logger.info("Loaded {} bytes of audio ({} Hz)", wav_size, wav_sample_rate);

  // Run the whole example at 16 kHz (see AUDIO_SAMPLE_RATE_HZ). The speaker
  // was already initialized at that rate; the bundled click was decoded at
  // its native rate, so resample it to 16 kHz once here so it plays at the
  // right pitch through the 16 kHz speaker.
  resample_click(static_cast<uint32_t>(wav_sample_rate), AUDIO_SAMPLE_RATE_HZ);

  // unmute the audio and set the volume to 20%
  tdeck.mute(false);
  tdeck.volume(20.0f);

  // set the display brightness to be 75%
  tdeck.brightness(75.0f);

  // Initialize the microphones (the ES7210 is on its own I2S bus, so pick
  // the speaker's sample rate to make the recording directly playable) and
  // buffer the recorded stereo frames; the recording auto-stops when the
  // buffer is full and the main loop notices and updates the GUI
  auto mic_callback = [](const uint8_t *data, size_t num_bytes) {
    if (!recording) {
      return;
    }
    // drop the warm-up (settling) bytes at the very start of the recording
    size_t warm = warmup_bytes_remaining;
    if (warm > 0) {
      if (num_bytes <= warm) {
        warmup_bytes_remaining = warm - num_bytes;
        return;
      }
      data += warm;
      num_bytes -= warm;
      warmup_bytes_remaining = 0;
    }
    int64_t now = esp_timer_get_time();
    // stamp the true start of retained audio on the first kept sample so the
    // measured capture rate excludes the warm-up period
    if (recording_start_us == 0) {
      recording_start_us = now;
    }
    size_t offset = recording_len;
    size_t to_copy = std::min(num_bytes, recording_capacity - offset);
    if (to_copy > 0) {
      memcpy(recording_buffer + offset, data, to_copy);
      recording_last_us = now;
      recording_len = offset + to_copy;
    }
    if (recording_len >= recording_capacity) {
      recording = false;
    }
  };
  // record at the ES7210's comfortable 16 kHz (see MIC_SAMPLE_RATE_HZ), which
  // is independent of the speaker's rate (the ES7210 is on its own I2S bus)
  bool have_mic = tdeck.initialize_microphone(mic_callback, MIC_SAMPLE_RATE_HZ);
  if (have_mic) {
    // The T-Deck's electret mics are genuinely low-sensitivity (LilyGO's own
    // firmware reads only ~200 counts for loud speech next to the mic), so the
    // analog stage is driven fairly hard here (~30 dB). Earlier this railed the
    // ADC, but that was the mic's DC offset being amplified with the ES7210
    // high-pass filter disabled; the driver now enables the HPF, so the signal
    // swings symmetrically around zero and this gain no longer saturates. The
    // software auto-gain applied on stop (see the RMS normalization below)
    // makes up whatever level remains; the mic +/- buttons adjust from here.
    tdeck.microphone_volume(70.0f);
    // allocate the recording buffer (16-bit interleaved stereo): prefer
    // PSRAM, fall back to a couple of seconds in internal RAM
    size_t bytes_per_second = tdeck.microphone_sample_rate() * 2 * sizeof(int16_t);
    recording_capacity = MAX_RECORDING_SECONDS * bytes_per_second;
    recording_buffer = static_cast<uint8_t *>(
        heap_caps_malloc(recording_capacity, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
    if (recording_buffer == nullptr) {
      recording_capacity = FALLBACK_RECORDING_SECONDS * bytes_per_second;
      recording_buffer =
          static_cast<uint8_t *>(heap_caps_malloc(recording_capacity, MALLOC_CAP_8BIT));
    }
    if (recording_buffer == nullptr) {
      logger.warn("Could not allocate a recording buffer; recording disabled");
      gui.set_audio_status("No recording buffer");
      recording_capacity = 0;
    } else {
      logger.info("Recording buffer: {} KB ({} s at {} Hz stereo)", recording_capacity / 1024,
                  recording_capacity / bytes_per_second, tdeck.microphone_sample_rate());
    }
  } else {
    logger.warn("Could not initialize the microphone!");
    gui.set_audio_status("Mic unavailable (see log)");
  }

  // The record button / 'r' key toggles recording; the play button / 'p' key
  // toggles playback of the recording (streamed to the speaker by the main
  // loop)
  toggle_record = [&]() {
    if (!have_mic || recording_capacity == 0) {
      logger.warn("Recording unavailable (no microphone / no buffer)");
      gui.set_audio_status("Mic unavailable (see log)");
      return;
    }
    if (recording) {
      recording = false; // the main loop notices and logs the summary
    } else {
      playing = false;
      gui.set_play_active(false);
      recording_len = 0;
      // discard the first ~250 ms of settling garbage; recording_start_us is
      // stamped by the mic callback on the first retained sample
      warmup_bytes_remaining = (tdeck.microphone_sample_rate() / 4) * 2 * sizeof(int16_t);
      recording_start_us = 0;
      recording_last_us = 0;
      recording = true;
      gui.set_record_active(true);
      gui.set_audio_status("Recording...");
    }
  };
  toggle_play = [&]() {
    if (playing) {
      playing = false;
      gui.set_play_active(false);
      gui.set_audio_status("Playback stopped");
    } else if (recording_len > 0) {
      recording = false;
      playing = true;
      gui.set_play_active(true);
      gui.set_audio_status("Playing...");
    } else {
      logger.info("Nothing recorded yet; press the record button (or 'r') first");
      gui.set_audio_status("Nothing recorded yet");
    }
  };
  gui.set_record_callback(toggle_record);
  gui.set_play_callback(toggle_play);

  // Initialize the LoRa radio (SX1262) and wire it to the LoRa tab. This is a
  // simple raw-LoRa demo: type a message in the LoRa tab's text box and press
  // Send (or Enter) to transmit it; received messages appear in the log. A
  // private sync word (0x12) is used so this link does not pick up Meshtastic
  // traffic (which uses 0x2B - see the meshtastic component/example for actual
  // Meshtastic interop). The radio shares the SPI bus with the display and uSD
  // card, and its DIO1 interrupt is serviced by the BSP, so received packets
  // arrive on the callback below.
  espp::Sx126x::RadioConfig lora_config{};
  lora_config.sync_word = 0x12; // private link, not Meshtastic
  std::shared_ptr<espp::Sx126x> radio;
  bool have_lora = tdeck.initialize_lora(lora_config);
  if (have_lora) {
    radio = tdeck.lora();
    // deliver received packets to the LoRa tab (runs in the BSP interrupt task)
    radio->set_receive_callback([&](const espp::Sx126x::RxPacket &packet) {
      gui.add_lora_message(fmt::format("RX {:.0f}dBm/{:.1f}dB: {}", packet.status.rssi,
                                       packet.status.snr, printable(packet.data)));
    });
    std::error_code ec;
    if (radio->start_receive(ec)) {
      gui.set_lora_status(fmt::format("Listening @ {:.3f} MHz, SF11/BW250 (private)",
                                      radio->radio_config().frequency_hz / 1e6f));
      // the Send button / Enter hand the message text here; the main loop
      // performs the (blocking) transmit
      gui.set_lora_send_callback([](const std::string &text) {
        {
          std::lock_guard<std::mutex> lock(lora_tx_mutex);
          lora_tx_message = text;
        }
        lora_send_requested = true;
      });
    } else {
      logger.error("Failed to start LoRa receive: {}", ec.message());
      gui.set_lora_status(fmt::format("LoRa RX failed: {}", ec.message()));
      gui.set_lora_send_enabled(false);
      have_lora = false;
    }
  } else {
    logger.warn("Could not initialize the LoRa radio!");
    gui.set_lora_status("LoRa unavailable (see log)");
    gui.set_lora_send_enabled(false);
  }

  // Main loop: stream any active playback to the speaker and notice when a
  // recording stops (either button press or the buffer filling up)
  size_t play_offset = 0;
  bool was_recording = false;
  while (true) {
    // service a LoRa send requested from the GUI. transmit() blocks for the
    // packet's time-on-air (a few hundred ms at SF11) then returns the radio
    // to receive, so doing it here keeps the LVGL task responsive.
    if (have_lora && lora_send_requested.exchange(false)) {
      std::string message;
      {
        std::lock_guard<std::mutex> lock(lora_tx_mutex);
        message = lora_tx_message;
      }
      std::span<const uint8_t> payload{reinterpret_cast<const uint8_t *>(message.data()),
                                       message.size()};
      std::error_code ec;
      if (radio->transmit(payload, 3s, ec)) {
        gui.add_lora_message("TX: " + message);
        logger.info("LoRa sent: {}", message);
      } else {
        gui.add_lora_message(fmt::format("TX failed: {}", ec.message()));
        logger.error("LoRa transmit failed: {}", ec.message());
      }
    }
    // feed the active playback in chunks, advancing by however much the
    // speaker's stream buffer accepted
    if (playing) {
      size_t len = recording_len;
      if (play_offset >= len) {
        playing = false;
        play_offset = 0;
        gui.set_play_active(false);
        gui.set_audio_status("Playback done");
        logger.info("Playback done");
      } else {
        // Top up the speaker's stream buffer until it is full (play_audio
        // returns less than requested once it can't accept more). A single
        // chunk per loop leaves the buffer able to drain between iterations,
        // which underruns and sounds like static / dropouts during a
        // continuous playback.
        while (play_offset < len) {
          size_t chunk = std::min<size_t>(len - play_offset, 16384);
          size_t queued = tdeck.play_audio(recording_buffer + play_offset, chunk);
          play_offset += queued;
          if (queued < chunk) {
            break; // stream buffer full for now
          }
        }
      }
    } else {
      play_offset = 0;
    }
    // notice when the recording stopped (button press or buffer full)
    bool now_recording = recording;
    if (was_recording && !now_recording) {
      gui.set_record_active(false);
      gui.set_audio_status(fmt::format("Recorded {:.1f}s ({} plays)",
                                       static_cast<float>(recording_len) /
                                           (tdeck.microphone_sample_rate() * 2 * sizeof(int16_t)),
                                       LV_SYMBOL_PLAY));
      // report the measured capture rate: stereo frames recorded over the
      // wall clock they took to arrive should match the nominal sample rate
      size_t num_frames = recording_len / (2 * sizeof(int16_t));
      float elapsed_s = static_cast<float>(recording_last_us - recording_start_us) / 1e6f;
      float effective_hz = elapsed_s > 0.0f ? num_frames / elapsed_s : 0.0f;
      // Post-process the recording for playback on the T-Deck's MONO speaker.
      // Use a single microphone (MIC1, the left slot - summing the two
      // spatially separated mics comb-filters the sound) and mirror it to both
      // stereo channels. The processing chain is: (1) de-glitch the electrical
      // impulse noise, (2) remove the DC offset, (3) apply an RMS-normalized
      // software makeup gain. The T-Deck mic is low-sensitivity (LilyGO's own
      // firmware sees only ~200 counts for loud speech), so the makeup gain -
      // not the analog stage - provides the loudness; RMS (rather than peak)
      // sets it so a residual glitch cannot collapse the gain, and clipping
      // catches any amplified outliers.
      auto *samples = reinterpret_cast<int16_t *>(recording_buffer);
      // De-glitch: the ES7210 capture on this board carries random, bursty
      // electrical impulse noise - roughly 0.3% of samples jump to |v| ~ 8000+
      // in runs of one to a few samples. (The de-spike frame gaps were measured
      // to be non-periodic, i.e. line noise coupling into the mic / I2S rather
      // than a framing or DMA-boundary artifact, so it cannot be removed by any
      // codec register and has to be concealed here.) Detect each glitch by its
      // large deviation from a local 7-point median (which tracks the real,
      // slew-limited waveform, so legitimate speech is never flagged) and then
      // linearly interpolate across each contiguous bad run from the good
      // samples on either side. Interpolating a whole run conceals short bursts
      // that a single-sample median replacement would leave behind. Done before
      // the RMS/gain measurement so the glitches cannot inflate the makeup gain.
      static constexpr int32_t kSpikeDelta = 2500;
      size_t despiked = 0;
      std::vector<uint8_t> bad(num_frames, 0);
      for (size_t i = 3; i + 3 < num_frames; i++) {
        int32_t w[7];
        for (int k = 0; k < 7; k++) {
          w[k] = samples[2 * (i - 3 + k)];
        }
        for (int a = 1; a < 7; a++) { // insertion sort the 7-sample window
          int32_t key = w[a];
          int b = a - 1;
          while (b >= 0 && w[b] > key) {
            w[b + 1] = w[b];
            b--;
          }
          w[b + 1] = key;
        }
        int32_t median = w[3];
        if (std::abs(static_cast<int32_t>(samples[2 * i]) - median) > kSpikeDelta) {
          bad[i] = 1;
        }
      }
      for (size_t i = 0; i < num_frames;) {
        if (!bad[i]) {
          i++;
          continue;
        }
        size_t j = i; // [i, j) is a contiguous run of bad samples
        while (j < num_frames && bad[j]) {
          j++;
        }
        int32_t left = (i > 0) ? samples[2 * (i - 1)] : 0;
        int32_t right = (j < num_frames) ? samples[2 * j] : left;
        int32_t span = static_cast<int32_t>(j - i) + 1;
        for (size_t k = i; k < j; k++) {
          int32_t t = static_cast<int32_t>(k - i) + 1;
          samples[2 * k] = static_cast<int16_t>(left + (right - left) * t / span);
        }
        despiked += j - i;
        i = j;
      }
      int16_t min_l = 32767, max_l = -32768;
      int64_t sum_l = 0, sum_sq = 0;
      for (size_t i = 0; i < num_frames; i++) {
        int16_t l = samples[2 * i]; // MIC1 (left)
        min_l = std::min(min_l, l);
        max_l = std::max(max_l, l);
        sum_l += l;
      }
      int32_t dc_l =
          num_frames ? static_cast<int32_t>(sum_l / static_cast<int64_t>(num_frames)) : 0;
      for (size_t i = 0; i < num_frames; i++) {
        int32_t v = samples[2 * i] - dc_l;
        sum_sq += static_cast<int64_t>(v) * v;
      }
      double rms = num_frames ? std::sqrt(static_cast<double>(sum_sq) / num_frames) : 0.0;
      // target RMS ~4000 (about -18 dBFS) leaves headroom; cap the gain so a
      // near-silent recording is not blown up into noise
      static constexpr double target_rms = 4000.0;
      double gain = rms > 1.0 ? std::clamp(target_rms / rms, 1.0, 64.0) : 1.0;
      for (size_t i = 0; i < num_frames; i++) {
        int32_t v = static_cast<int32_t>((samples[2 * i] - dc_l) * gain);
        int16_t mono = static_cast<int16_t>(std::clamp<int32_t>(v, -32768, 32767));
        samples[2 * i] = mono;
        samples[2 * i + 1] = mono; // mono: duplicate MIC1 to both channels
      }
      logger.info("Recorded {} frames in {:.2f} s (~{:.0f} Hz effective, {} Hz nominal)",
                  num_frames, elapsed_s, effective_hz, tdeck.microphone_sample_rate());
      logger.info("  MIC1: raw min={} max={} dc={} rms={:.0f}; de-spiked {} samples; "
                  "applied software gain {:.1f}x",
                  min_l, max_l, dc_l, rms, despiked, gain);
    }
    was_recording = now_recording;
    // tick faster while playing so the stream buffer is topped up well before
    // it can drain (avoids underrun static); idle more slowly otherwise
    std::this_thread::sleep_for(playing ? 10ms : 50ms);
  }

Note

The keyboard has a backlight, which you can control with the shortcut alt + b. The keyboard backlight is off by default.

Public Types

using button_callback_t = espp::Interrupt::event_callback_fn

Alias for the button callback function.

using Pixel = lv_color16_t

Alias for the pixel type used by the TDeck display.

using DisplayDriver = espp::St7789

Alias for the display driver used by the TDeck.

using keypress_callback_t = TKeyboard::key_cb_fn

Alias for the keypress callback for keyboard keypresses.

using TouchpadData = espp::TouchpadData

Alias for the touchpad data used by the TDeck touchpad.

using touch_callback_t = std::function<void(const TouchpadData&)>

Alias for the touch callback when touch events are received.

using PointerData = espp::PointerData

Alias for the pointer data used by the TDeck trackball.

using trackball_callback_t = std::function<void(const PointerData&)>

Alias for the callback used to inform the user code of new trackball data.

using microphone_callback_t = std::function<void(const uint8_t *data, size_t num_bytes)>

Alias for the microphone callback, called with recorded audio data.

Public Functions

inline I2c &internal_i2c()

Get a reference to the internal I2C bus

Note

The internal I2C bus is used for the touchscreen

Returns:

A reference to the internal I2C bus

inline espp::Interrupt &interrupts()

Get a reference to the interrupts

Returns:

A reference to the interrupts

void peripheral_power(bool on)

Enable or disable power to the peripherals

Parameters:

on – Whether to enable or disable power to the peripherals

bool peripheral_power() const

Get the state of the peripheral power

Returns:

true if power is enabled, false otherwise

bool initialize_sdcard(const SdCardConfig &config)

Initialize the uSD card

Parameters:

config – The configuration for the uSD card

Returns:

True if the uSD card was initialized properly.

inline sdmmc_card_t *sdcard() const

Get the uSD card

Note

The uSD card is only available if it was successfully initialized and the mount point is valid

Returns:

A pointer to the uSD card

bool initialize_lora(const Sx126x::RadioConfig &radio_config = {})

Initialize the LoRa radio (SX1262)

Note

The radio shares the SPI bus with the display and uSD card.

Note

The radio’s DIO1 interrupt is automatically serviced via the board’s interrupt handler, so packets are delivered through the callbacks registered on the returned driver (see espp::Sx126x::set_receive_callback and friends).

Note

The radio’s reset line (GPIO 17) is also routed to the T-Deck’s PDM digital-microphone clock / ES7210 interrupt line. The espp microphone path (the ES7210 array) uses a different set of pins, so the radio and microphone can be used together; only a PDM microphone (which would drive GPIO 17) would conflict.

Parameters:

radio_config – The radio (modem) configuration to apply

Returns:

True if the radio was initialized properly

inline std::shared_ptr<Sx126x> lora() const

Get the LoRa radio

Note

The radio is only available if initialize_lora() succeeded

Returns:

A shared pointer to the LoRa radio driver

bool initialize_gps(const Gps::fix_callback_fn &fix_cb = nullptr, uint32_t baud_rate = 9600)

Initialize the GPS (T-Deck Plus only)

Note

The GPS UART pins are routed to the Grove connector on the base T-Deck; on the T-Deck Plus they connect to the internal u-blox MIA-M10Q receiver.

Parameters:
  • fix_cb – Optional callback invoked on each fix update

  • baud_rate – The baud rate of the GPS UART (9600 by default)

Returns:

True if the GPS was initialized properly

inline std::shared_ptr<Gps> gps() const

Get the GPS

Note

The GPS is only available if initialize_gps() succeeded

Returns:

A shared pointer to the GPS driver

bool initialize_keyboard(bool start_task = true, const keypress_callback_t &key_cb = nullptr, std::chrono::milliseconds poll_interval = std::chrono::milliseconds(10))

Initialize the keyboard

See also

TKeyboard

See also

keyboard()

Note

The Keyboard has an interrupt pin connected from it (the esp32c3) to the main esp32s3. However, the default firmware on the keyboard (esp32c3) does not use this interrupt pin. Instead, the main esp32s3 must poll the keyboard to get key presses. This is done by the keyboard task. If you update the firmware on the keyboard to use the interrupt pin, you can set start_task to false and wire up the interrupt to the keyboard()->read_key() method.

Parameters:
  • start_task – Whether to start the keyboard task

  • key_cb – The key callback function, called when a key is pressed if not null and the keyboard task is started

  • poll_interval – The interval at which to poll the keyboard

Returns:

true if the keyboard was successfully initialized, false otherwise

std::shared_ptr<TKeyboard> keyboard() const

Get the keyboard

Note

The keyboard is only available if it was successfully initialized

Returns:

A shared pointer to the keyboard

bool initialize_trackball(const trackball_callback_t &trackball_cb = nullptr, int sensitivity = 10, bool enable_center_button = true)

Initialize the trackball

See also

trackball()

See also

trackball_data()

See also

trackball_read()

Parameters:
  • trackball_cb – The trackball callback function, called when the trackball is moved if not null

  • sensitivity – The sensitivity of the trackball. The higher the sensitivity, the faster the trackball will move

  • enable_center_button – Whether to configure the trackball’s center (click) button on GPIO0. Set this to false when the microphone is in use: per LilyGO’s documentation GPIO0 is not available while the microphone is enabled, and leaving the button interrupt configured on GPIO0 while recording produces a burst of spurious interrupts that jitters the real-time audio capture. The four directional quadrature pins are unaffected and still work.

Returns:

true if the trackball was successfully initialized, false otherwise

std::shared_ptr<PointerInput> trackball() const

Get the trackball

See also

trackball_data()

See also

trackball_read()

Note

The trackball is only available if it was successfully initialized

Note

This is the same as the pointer_input() method

Returns:

A shared pointer to the trackball

void set_trackball_sensitivity(int sensitivity)

Set the trackball sensitivity

Note

The sensitivity can be negative, which will invert the direction of the trackball

Parameters:

sensitivity – The sensitivity of the trackball. The higher the sensitivity, the faster the trackball will move

std::shared_ptr<PointerInput> pointer_input() const

Get the pointer input for the trackball

Returns:

A shared pointer to the pointer input for the trackball

PointerData trackball_data() const

Get the most recent trackball data

Returns:

The trackball data

void trackball_read(int &x, int &y, bool &left_pressed, bool &right_pressed)

Get the most recent trackball data

Note

This method is a convenience method for integrating with LVGL, the data it returns is identical to the data returned by the trackball_data() method

Parameters:
  • x – The x coordinate

  • y – The y coordinate

  • left_pressed – Whether the left button is pressed

  • right_pressed – Whether the right button is pressed

bool initialize_touch(const touch_callback_t &touch_cb = nullptr)

Initialize the touchpad

Note

This will configure the touchpad interrupt pin which will automatically call the touch callback function when the touchpad is touched

Note

This can be called even if you have not initialized the display or the LCD.

Parameters:

touch_cb – The touch callback function, called when the touchpad is touched if not null

Returns:

true if the touchpad was successfully initialized, false otherwise

std::shared_ptr<TouchpadInput> touchpad_input() const

Get the touchpad input

Returns:

A shared pointer to the touchpad input

TouchpadData touchpad_data() const

Get the most recent touchpad data

Returns:

The touchpad data

void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state)

Get the most recent touchpad data

See also

touchpad_data()

Note

This method is a convenience method for integrating with LVGL, the data it returns is identical to the data returned by the touchpad_data() method

Parameters:
  • num_touch_points – The number of touch points

  • x – The x coordinate

  • y – The y coordinate

  • btn_state – The button state (0 = button released, 1 = button pressed)

TouchpadData touchpad_convert(const TouchpadData &data) const

Convert touchpad data from raw reading to display coordinates

Note

Uses the touch_invert_x and touch_invert_y settings to determine if the x and y coordinates should be inverted

Parameters:

data – The touchpad data to convert

Returns:

The converted touchpad data

bool initialize_lcd()

Initialize the LCD (low level display driver)

Returns:

true if the LCD was successfully initialized, false otherwise

bool initialize_display(size_t pixel_buffer_size)

Initialize the display (lvgl display driver)

Note

This will also allocate two full frame buffers in the SPIRAM

Parameters:

pixel_buffer_size – The size of the pixel buffer

Returns:

true if the display was successfully initialized, false otherwise

size_t rotated_display_width() const

Get the display width in pixels, according to the current orientation

Returns:

The display width in pixels, according to the current orientation

size_t rotated_display_height() const

Get the display height in pixels, according to the current orientation

Returns:

The display height in pixels, according to the current orientation

std::shared_ptr<Display<Pixel>> display() const

Get a shared pointer to the display

Returns:

A shared pointer to the display

inline const std::shared_ptr<DisplayDriver> &display_driver() const

Get a shared pointer to the low-level display driver

Returns:

A shared pointer to the display driver

void brightness(float brightness)

Set the brightness of the backlight

Note

This function will only work after initialize_lcd() has been called

Parameters:

brightness – The brightness of the backlight as a percentage (0 - 100)

float brightness() const

Get the brightness of the backlight

Note

This function will only work after initialize_lcd() has been called

Returns:

The brightness of the backlight as a percentage (0 - 100)

Pixel *vram0() const

Get the VRAM 0 pointer (DMA memory used by LVGL)

Note

This is the memory used by LVGL for rendering

Note

This is null unless initialize_display() has been called

Returns:

The VRAM 0 pointer

Pixel *vram1() const

Get the VRAM 1 pointer (DMA memory used by LVGL)

Note

This is the memory used by LVGL for rendering

Note

This is null unless initialize_display() has been called

Returns:

The VRAM 1 pointer

uint8_t *frame_buffer0() const

Get the frame buffer 0 pointer

Note

This memory is designed to be used by the application developer and is provided as a convenience. It is not used by the display driver.

Note

This is null unless initialize_display() has been called

Returns:

The frame buffer 0 pointer

uint8_t *frame_buffer1() const

Get the frame buffer 1 pointer

Note

This memory is designed to be used by the application developer and is provided as a convenience. It is not used by the display driver.

Note

This is null unless initialize_display() has been called

Returns:

The frame buffer 1 pointer

void write_command(uint8_t command, std::span<const uint8_t> parameters, uint32_t user_data)

Write command and optional parameters to the LCD

Note

This method is designed to be used by the display driver

Note

This method queues the data to be written to the LCD, only blocking if there is an ongoing SPI transaction

Parameters:
  • command – The command to write

  • parameters – The command parameters to write

  • user_data – User data to pass to the spi transaction callback

void write_lcd_frame(const uint16_t x, const uint16_t y, const uint16_t width, const uint16_t height, uint8_t *data)

Write a frame to the LCD

Note

This method queues the data to be written to the LCD, only blocking if there is an ongoing SPI transaction

Parameters:
  • x – The x coordinate

  • y – The y coordinate

  • width – The width of the frame, in pixels

  • height – The height of the frame, in pixels

  • data – The data to write

void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data)

Write lines to the LCD

Note

This method queues the panel transfer asynchronously and may return before the write has completed.

Parameters:
  • xs – The x start coordinate

  • ys – The y start coordinate

  • xe – The x end coordinate

  • ye – The y end coordinate

  • data – The data to write

  • user_data – User data to pass to the spi transaction callback

bool initialize_sound (uint32_t default_audio_rate=48000, const espp::Task::BaseConfig &task_config={ .name="audio",.stack_size_bytes=4096,.priority=19,.core_id=1})

Initialize the sound subsystem

Parameters:
  • default_audio_rate – The default audio rate

  • task_config – The task configuration for the audio task

Returns:

true if the sound subsystem was successfully initialized, false otherwise

uint32_t audio_sample_rate() const

Get the audio sample rate

Returns:

The audio sample rate, in Hz

void audio_sample_rate(uint32_t sample_rate)

Set the audio sample rate

Parameters:

sample_rate – The audio sample rate, in Hz

size_t audio_buffer_size() const

Get the audio buffer size

Returns:

The audio buffer size, in bytes

void mute(bool mute)

Mute or unmute the audio

Parameters:

mute – true to mute the audio, false to unmute the audio

bool is_muted() const

Check if the audio is muted

Returns:

true if the audio is muted, false otherwise

void volume(float volume)

Set the volume

Parameters:

volume – The volume in percent (0 - 100)

float volume() const

Get the volume

Returns:

The volume in percent (0 - 100)

size_t play_audio(const std::vector<uint8_t> &data)

Play audio

Note

This function is non-blocking and queues the data for the audio task to play; to stream data larger than the internal buffer, call it repeatedly, advancing by the returned number of bytes

Note

Must be called from task context, not from an ISR.

Parameters:

data – The audio data to play (16-bit signed interleaved stereo)

Returns:

The number of bytes actually queued (may be less than the data size if the internal stream buffer is full)

size_t play_audio(const uint8_t *data, uint32_t num_bytes)

Play audio

Note

This function is non-blocking and queues the data for the audio task to play; to stream data larger than the internal buffer, call it repeatedly, advancing by the returned number of bytes

Note

Must be called from task context, not from an ISR.

Parameters:
  • data – The audio data to play (16-bit signed interleaved stereo)

  • num_bytes – The number of bytes to play

Returns:

The number of bytes actually queued (may be less than num_bytes if the internal stream buffer is full)

bool initialize_microphone (const microphone_callback_t &callback, uint32_t sample_rate=16000, const espp::Task::BaseConfig &task_config={.name="microphone",.stack_size_bytes=4096,.priority=10,.core_id=1})

Initialize the microphones (the dual microphone array through the ES7210 ADC, on its own I2S bus) and start delivering audio data to the provided callback

Note

The callback runs in the microphone task’s context, so the task’s stack must be large enough for whatever the callback does with the audio data

Parameters:
  • callback – The callback to call with recorded audio data: 16-bit signed interleaved stereo. The two populated ES7210 microphones are delivered as MIC1 (TDM slot 0) on the left channel and MIC3 (TDM slot 2) on the right, at sample_rate

  • sample_rate – The sample rate for the microphones, in Hz. The ES7210 is on a separate I2S bus from the speaker amplifier, so this is independent of the speaker’s sample rate.

  • task_config – The configuration for the microphone task

Returns:

true if the microphone was successfully initialized, false otherwise

uint32_t microphone_sample_rate() const

Get the microphone sample rate

Returns:

The microphone sample rate, in Hz

void microphone_volume(float volume)

Set the microphone volume

Parameters:

volume – The volume as a percentage (0 - 100), mapped onto the ES7210 analog microphone gain range (0 dB - +37.5 dB)

float microphone_volume() const

Get the microphone volume

Returns:

The microphone volume as a percentage (0 - 100)

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns:

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters:

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters:

rate_limit – The rate limit to use for the logger

Public Static Functions

static inline TDeck &get()

Access the singleton instance of the TDeck class.

Returns:

Reference to the singleton instance of the TDeck class

static inline constexpr auto peripheral_power_pin()

Get the GPIO pin for the peripheral power

Note

This pin is used to enable/disable power to the peripherals, such as the keyboard, screen, etc.

Returns:

The GPIO pin for the peripheral power

static inline constexpr auto lora_cs_gpio()

Get the GPIO pin for the LoRa radio chip select

Returns:

The GPIO pin for the LoRa radio chip select

static inline constexpr auto lora_dio1_gpio()

Get the GPIO pin for the LoRa radio DIO1 (interrupt) line

Returns:

The GPIO pin for the LoRa radio DIO1 line

static inline constexpr auto lora_busy_gpio()

Get the GPIO pin for the LoRa radio BUSY line

Returns:

The GPIO pin for the LoRa radio BUSY line

static inline constexpr auto lora_reset_gpio()

Get the GPIO pin for the LoRa radio reset line

Returns:

The GPIO pin for the LoRa radio reset line

static inline constexpr auto gps_tx_gpio()

Get the GPIO pin for the GPS UART TX (ESP32 -> GPS)

Returns:

The GPIO pin for the GPS UART TX

static inline constexpr auto gps_rx_gpio()

Get the GPIO pin for the GPS UART RX (GPS -> ESP32)

Returns:

The GPIO pin for the GPS UART RX

static inline constexpr auto keyboard_interrupt()

Get the GPIO pin for the keyboard interrupt

Note

This pin is used to detect when a key is pressed on the keyboard and is connected to the main esp32s3, however the default firmware on the keyboard does not use this pin

Returns:

The GPIO pin for the keyboard interrupt

static inline constexpr auto trackball_up_gpio()

Get the GPIO pin for the trackball up button

Returns:

The GPIO pin for the trackball up button

static inline constexpr auto trackball_down_gpio()

Get the GPIO pin for the trackball down button

Returns:

The GPIO pin for the trackball down button

static inline constexpr auto trackball_left_gpio()

Get the GPIO pin for the trackball left button

Returns:

The GPIO pin for the trackball left button

static inline constexpr auto trackball_right_gpio()

Get the GPIO pin for the trackball right button

Returns:

The GPIO pin for the trackball right button

static inline constexpr auto trackball_btn_gpio()

Get the GPIO pin for the trackball button

Returns:

The GPIO pin for the trackball button

static inline constexpr size_t lcd_width()

Get the width of the LCD in pixels

Returns:

The width of the LCD in pixels

static inline constexpr size_t lcd_height()

Get the height of the LCD in pixels

Returns:

The height of the LCD in pixels

static inline constexpr auto get_lcd_dc_gpio()

Get the GPIO pin for the LCD data/command signal

Returns:

The GPIO pin for the LCD data/command signal

static inline constexpr size_t display_width()

Get the display width in pixels

Returns:

The display width in pixels

static inline constexpr size_t display_height()

Get the display height in pixels

Returns:

The display height in pixels

static inline constexpr auto get_mute_pin()

Get the GPIO pin for the mute button (top of the box)

Returns:

The GPIO pin for the mute button

Public Static Attributes

static constexpr size_t SPI_MAX_TRANSFER_BYTES = SPI_LL_DMA_MAX_BIT_LEN / 8

Maximum number of bytes that can be transferred in a single SPI transaction to the Display. 32k on the ESP32-S3.

static constexpr char mount_point[] = "/sdcard"

Mount point for the uSD card on the TDeck.

struct SdCardConfig

Configuration for the uSD card.

Public Members

bool format_if_mount_failed = false

Format the uSD card if mount failed.

int max_files = 5

The maximum number of files to open at once.

size_t allocation_unit_size = 2 * 1024

The allocation unit size in bytes.