Skip to content

Advanced Topics

devilhyt edited this page May 31, 2025 · 2 revisions

Note

Unless otherwise specified, the guides use a SPIKE Hub with Pybricks and an ESP32-C3 SuperMini as an example.

Debug Mode

Debug mode helps developers monitor and troubleshoot the device during development. It outputs categorized debug messages, including general information, device state tracking, decoded host messages, watchdog timer activity, and error messages.

Setup

To enable Debug mode, define the following macros before including the library header:

#define LUMP_DEBUG_SERIAL Serial
#define LUMP_DEBUG_SPEED  115200
#include <LumpDeviceBuilder.h>
  • LUMP_DEBUG_SERIAL: Serial interface for debug output (typically the same one as the USB port).

  • LUMP_DEBUG_SPEED: Speed for the debug serial interface (default: 115200).

Message Categories

  • Info: General information.
  • State: Device state tracking.
  • RX: Decoded host messages.
  • WDT: Watchdog timer activity.
  • Error: Error messages.

Sample Output

With Debug mode enabled, you should see the output similar to the following in the serial monitor:

[State] Init WDT
[WDT] WDT enabled
[State] Reset
[WDT] Feeds
[Info] Starting handshake...
[State] Init AutoID
[State] Waiting for AutoID
[RX] 52 0 C2 1 0 6E | speed: 115200
[Info] LPF2 host detected
[WDT] Feeds
[Info] AutoID complete
[Info] Host type: LPF2
[Info] Speed: 115200
[State] Init UART
[State] Waiting for UART init...
[Info] UART init complete
[Info] Sends ACK to LPF2 host
[State] Sending type
[State] Sending modes
[State] Sending speed
[State] Sending version
[INFO] Sends mode 1
[State] Sending name
[State] Sending value spans
[State] Sending symbol
[State] Sending mapping
[State] Sending format
[WDT] Feeds
[State] Inter-mode pause
[INFO] Sends mode 0
[State] Sending name
[State] Sending value spans
[State] Sending symbol
[State] Sending mapping
[State] Sending format
[WDT] Feeds
[State] Sending ACK
[State] Waiting for ACK reply...
[RX] 4 | ACK
[Info] Handshake success
[State] Switching UART speed
[INFO] Communication speed: 115200
[State] Init Mode: 0
[RX] 43 0 BC | select mode: 0
[State] Init Mode: 0
[RX] 2 | NACK
[WDT] Feeds
[RX] 2 | NACK
[WDT] Feeds
...

Event-Driven Data Transmission

In the Quickstart guide, data is sent to the host every 5 milliseconds (200 Hz). A more efficient approach is Event-Driven Data Transmission, where data is sent only when:

  • A NACK is received from the host.
  • The data values have changed.

This approach reduces unnecessary data transmission and improves overall performance.

Guide

The following guide demonstrates how to modify the code from the Quickstart guide to implement event-driven data transmission. Only the LumpDeviceState::Communicating branch of the switch statement within the runDeviceModes() function requires modification.

Step 1: Add variables to track value changes

Add variables to store previous values and a value changed flag.

case LumpDeviceState::Communicating: {
  static uint16_t value0     = 0;
  static uint16_t prevValue0 = 0;     // Added
  static uint8_t value1      = 0;
  static uint8_t prevValue1  = 0;     // Added
  static bool valueChanged   = false; // Added
/* ... remaining code ... */

Step 2: Limit the data sending rate

Set the period variable to 1 millisecond to keep the data sending rate below 1000 Hz. This helps prevent UART overrun errors.

  static uint32_t period     = 1; // Modified
  static uint32_t prevMillis = 0;
  uint32_t currentMillis     = millis();
/* ... remaining code ... */

Step 3: Check for value changes

Update the values and check for changes.

  // Update the values and check for changes.
  switch (mode) {
    case 0:
      value0 = analogRead(ANALOG_PIN);
      if (value0 != prevValue0) {
        valueChanged = true;
        prevValue0   = value0;
      }
      break;

    case 1:
      value1 = digitalRead(DIGITAL_PIN);
      if (value1 != prevValue1) {
        valueChanged = true;
        prevValue1   = value1;
      }
      break;

    default:
      break;
  }
/* ... remaining code ... */

Step 4: Send data only when necessary

Send data only when:

  • A NACK is received from the host.
  • The data values have changed.

After sending data, reset the valueChanged flag.

  // Send data to the host.
  if (device.hasNack() || (valueChanged && currentMillis - prevMillis > period)) {
    switch (mode) {
      case 0:
        device.send(value0);
        break;

      case 1:
        device.send(value1);
        break;

      default:
        break;
    }

    valueChanged = false; // Reset the valueChanged flag.
    prevMillis = currentMillis;
  }
  break;
}

The full code is here.

Watchdog Timer

The watchdog timer helps maintain device stability by automatically triggering a reset if the device freezes or crashes. The library provides a straightforward interface for integrating the watchdog timer. Just register the callback functions for watchdog timer initialization, feeding, and deinitialization. The library handles the rest.

Setup

Step 1: Define the watchdog timer callback functions

Define the callback functions for watchdog timer initialization, feeding, and deinitialization. Set the timeout to LUMP_NACK_TIMEOUT (1500 milliseconds):

#include <esp_task_wdt.h>

/**
 * Initializes the watchdog timer.
 */
void initWdt() {
#if ESP_IDF_VERSION_MAJOR > 4
  esp_task_wdt_config_t config = {
      .timeout_ms     = LUMP_NACK_TIMEOUT,
      .idle_core_mask = 0,
      .trigger_panic  = true,
  };
  esp_task_wdt_init(&config);
  esp_task_wdt_add(NULL);
#else
  esp_task_wdt_init(LUMP_NACK_TIMEOUT / 1000, true);
  esp_task_wdt_add(NULL);
#endif
}

/**
 * Feeds the watchdog timer.
 */
void feedWdt() {
  esp_task_wdt_reset();
}

/**
 * Deinitializes the watchdog timer.
 */
void deinitWdt() {
  esp_task_wdt_delete(NULL);
  esp_task_wdt_deinit();
}

Step 2: Register the callback functions

Call setWdtCallback() method in the setup() function to register the callback functions.

void setup() {
  device.begin();
  device.setWdtCallback(initWdt, feedWdt, deinitWdt);
}

Step 3: Test the watchdog timer

To verify that the watchdog timer is working correctly, you can intentionally cause a 3 second delay in the loop() function.

void loop() {
  device.run();
  runDeviceModes();

  delay(3000); // Intentionally cause a delay to trigger the watchdog timer.
}

Warning

Don't forget to remove the delay after testing.

Enable Constant Power on SPIKE Hub Pin 2

The library provides an option to enable constant power on SPIKE Hub pin 2. When enabled, the pin outputs the battery voltage. This feature is particularly useful for powering external peripherals connected to the device, such as servo motors or camera modules.

Setup

To enable constant power, set the power parameter to true in the LumpMode constructor. Note that the mode name must not exceed 5 characters.

Caution

When the power parameter is set to true in any mode, constant power on SPIKE Hub pin 2 is enabled across all modes.

The following example, based on the Quickstart code, demonstrates how to enable constant power by modifying the first mode:

LumpMode modes[]{
    {"Anlg", DATA16, 1, 4, 0, "raw", {0, 4095}, {0, 100}, {0, 4095}, LUMP_INFO_MAPPING_NONE, LUMP_INFO_MAPPING_NONE, true},
    {"Digital", DATA8, 1, 1, 0, "raw", {0, 1}, {0, 100}, {0, 1}}
};

To verify constant power is enabled, measure the voltage on SPIKE Hub pin 2 with a voltmeter. The measured voltage should correspond the battery voltage.

                      ┌─────────────┐
  ┌─────────────┐     │  Voltmeter  │     ┌─────────────┐
  │  Dev Board  │     ├─────────────┤     │  Host Port  │
  ├─────────────┤     │           + ├───┐ ├─────────────┤
  │             │     │           - ├─┐ │ │  1. M1      │      LPF2 Socket Pinout
  │             │     └─────────────┘ │ └─┤  2. M2      │     ┌─────────────────┐
  │     GND     ├─────────────────────┴───┤  3. GND     │     │   6 5 4 3 2 1   │
  │     VIN     ├─────────────────────────┤  4. VCC     │     │ ┌─┴─┴─┴─┴─┴─┴─┐ │
  │     RX      ├─────────────────────────┤  5. TX      │     └─┘             └─┘
  │     TX      ├─────────────────────────┤  6. RX      │
  └─────────────┘                         └─────────────┘

Receive Data from the Host

In the Quickstart guide, we have already learned how to send data from the device to the host. Now, let’s see how to receive data from the host (send data from the host to the device). This is very useful in cases such as setting the angle of a servo motor or adjusting the parameters of a camera module.

Guide

This guide demonstrates how to implement an echo mode that receives data from the host and sends it back. The host will send 2 data values to the device: a positive number and its negative counterpart (range: -1023 to 1023).

The example code follows the same structure as the Quickstart code, with modifications only the LumpMode modes[] array and the runDeviceModes() function.

Step 1: Define the Echo mode

Define the Echo mode as follows:

#define NUM_DATA 2

LumpMode modes[]{
  {"Echo", DATA16, NUM_DATA, 4, 0, "", {-1023, 1023}, {0, 100}, {-1023, 1023}, LUMP_INFO_MAPPING_NONE, LUMP_INFO_MAPPING_ABS}
};

There are several important parameters to note:

  • dataType: Set to DATA16, which satisfies the minimum requirement.
  • numData: Set to NUM_DATA to specify that the mode sends and receives 2 data values.
  • mapOut: Set to any value (e.g., LUMP_INFO_MAPPING_ABS) other than LUMP_INFO_MAPPING_NONE to make the mode writable.

Step 2: Receive data from the host and send it back

Add 2 int16_t variables, positive and negative, to store the received values.

Check whether new data has been received from the host using hasDataMsg(). If data is available, read it using readDataMsg<U>(), which returns a pointer to an array.

Finally, send it back to the host using send().

Note

The send() function is an overloaded method that accepts either a single value or an array.

void runDeviceModes() {
  // Get the device state and mode.
  auto state = device.state();
  auto mode  = device.mode();

  // Check the device state and perform actions accordingly.
  switch (state) {
    case LumpDeviceState::InitMode:
      break;

    case LumpDeviceState::Communicating: {
      static int16_t positive    = 0;
      static int16_t negative    = 0;
      static uint32_t period     = 5; // 200Hz
      static uint32_t prevMillis = 0;
      uint32_t currentMillis     = millis();

      // Read data from the host.
      switch (mode) {
        case 0: {
          if (device.hasDataMsg(mode)) {
            int16_t *data = device.readDataMsg<int16_t>(mode);
            positive      = data[0];
            negative      = data[1];
          }
          break;
        }

        default:
          break;
      }

      // Send data to the host.
      if (device.hasNack() || (currentMillis - prevMillis > period)) {
        switch (mode) {
          case 0: {
            int16_t data[] = {positive, negative};
            device.send(data, NUM_DATA);
            break;
          }

          default:
            break;
        }

        prevMillis = currentMillis;
      }
      break;
    }

    default:
      break;
  }
}

The full code is here.

Step 3: Test the device

Connect the dev board to SPIKE Hub Port A. Copy and paste the following code into the Pybricks IDE, then run it to verify device functionality:

from pybricks.iodevices import PUPDevice
from pybricks.parameters import Port
from pybricks.tools import wait

msg = 0

device = PUPDevice(Port.A)
print(device.info())

while True:
    sendMsgs = [msg, -msg]
    device.write(0, sendMsgs)
    print(f"send: {sendMsgs}")

    receivedMsgs = device.read(0)
    print(f"received: {receivedMsgs}")

    msg += 1
    if msg > 1023:
        msg = 0

    wait(500)

You should see the following output in the console:

{'id': 68, 'modes': (('Echo', 2, 1),)}
send: [0, 0]
received: (0, 0)
send: [1, -1]
received: (1, -1)
send: [2, -2]
received: (2, -2)
send: [3, -3]
received: (3, -3)