add extended telemetrics
This commit is contained in:
parent
f1db875521
commit
0e1e0b954d
24
README.md
24
README.md
|
|
@ -4,9 +4,9 @@
|
||||||
[](https://www.arduinolibraries.com/libraries/dshot-rmt)
|
[](https://www.arduinolibraries.com/libraries/dshot-rmt)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
An Arduino IDE library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT Encoder API** (`rmt_tx.h` / `rmt_rx.h`). This library specifically leverages the official `rmt_bytes_encoder` API for an efficient, hardware-timed and maintainable implementation. It provides a simple way to control BLHeli ESCs in both Arduino and ESP-IDF projects.
|
An Arduino IDE library for generating DShot signals on ESP32 microcontrollers using the **latest ESP-IDF 5.5 RMT Encoder API** (`rmt_tx.h` / `rmt_rx.h`). This library specifically leverages the official `rmt_bytes_encoder` API for an efficient, hardware-timed and maintainable implementation. It provides a simple way to control BLHeli ESCs in both Arduino and ESP-IDF projects.
|
||||||
|
|
||||||
### Bidirectional DShot re-enabled for testing.
|
### Enhanced Bidirectional DShot with Full Telemetry Support.
|
||||||
|
|
||||||
The legacy version using the old `rmt.h` API is available in the `oldAPI` branch.
|
The legacy version using the old `rmt.h` API is available in the `oldAPI` branch.
|
||||||
|
|
||||||
|
|
@ -14,17 +14,17 @@ An Arduino IDE library for generating DShot signals on ESP32 microcontrollers us
|
||||||
|
|
||||||
### DShot300 Example Output
|
### DShot300 Example Output
|
||||||
|
|
||||||
Here's an example of the output from the `dshot300` example sketch:
|
Here's an example of the output from the `dshot300` example sketch, now showing full telemetry:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 🚀 Core Features
|
## 🚀 Core Features
|
||||||
|
|
||||||
- **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200.
|
- **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200.
|
||||||
- **Bidirectional DShot Support:** Implemented, but note that official support is limited due to potential instability and external hardware requirements. Use with caution (and pull-up).
|
- **Robust Bidirectional DShot Support:** Now features full GCR-dekodierte telemetry data (temperature, voltage, current, consumption, and RPM) from the ESC. The library automatically differentiates between eRPM-only and full telemetry frames. This significantly enhances feedback capabilities for advanced applications.
|
||||||
- **Hardware-Timed Signals:** Precise signal generation using the ESP32 RMT peripheral, ensuring stable and reliable motor control.
|
- **Hardware-Timed Signals:** Precise signal generation using the ESP32 RMT peripheral, ensuring stable and reliable motor control.
|
||||||
- **Simple API:** Easy-to-use C++ class with intuitive methods like `sendThrottlePercent()`.
|
- **Simple API:** Easy-to-use C++ class with intuitive methods like `sendThrottlePercent()`.
|
||||||
- **Error Handling:** Provides detailed feedback on operation success or failure via `dshot_result_t`.
|
- **Enhanced Error Handling:** Provides detailed feedback on operation success or failure via an enhanced `dshot_result_t` struct, now including specific error codes, eRPM data, and a `dshot_telemetry_data_t` struct for full GCR-decoded telemetry.
|
||||||
- **Lightweight:** The core library has no external dependencies.
|
- **Lightweight:** The core library has no external dependencies.
|
||||||
- **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects.
|
- **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects.
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ The library is architected around a single C++ class, `DShotRMT`. It abstracts t
|
||||||
|
|
||||||
1. **Signal Generation (TX):** The library uses an RMT 'bytes_encoder'. This encoder is configured with the specific pulse durations for DShot '0' and '1' bits based on the selected speed (e.g., DSHOT300, DSHOT600). When a user calls `sendThrottle()`, the library constructs a 16-bit DShot frame (11-bit throttle, 1-bit telemetry request, 4-bit CRC) and hands it to the RMT encoder. The RMT hardware then autonomously generates the correct electrical signal on the specified GPIO pin.
|
1. **Signal Generation (TX):** The library uses an RMT 'bytes_encoder'. This encoder is configured with the specific pulse durations for DShot '0' and '1' bits based on the selected speed (e.g., DSHOT300, DSHOT600). When a user calls `sendThrottle()`, the library constructs a 16-bit DShot frame (11-bit throttle, 1-bit telemetry request, 4-bit CRC) and hands it to the RMT encoder. The RMT hardware then autonomously generates the correct electrical signal on the specified GPIO pin.
|
||||||
|
|
||||||
2. **Bidirectional Telemetry (RX):** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it and triggers the interrupt. The interrupt code decodes the GCR-encoded signal, validates its CRC, and stores the resulting eRPM value in a thread-safe `atomic` variable. The main application can then poll for this data using the `getTelemetry()` method.
|
2. **Bidirectional Telemetry (RX) - Now with Full GCR Telemetry:** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it. The interrupt code intelligently differentiates between eRPM-only frames (21 GCR bits) and full telemetry frames (110 GCR bits). It then decodes the GCR-encoded signal (including 5B/4B GCR decoding for full telemetry), validates its CRC, and stores the resulting eRPM value or full telemetry data (temperature, voltage, current, consumption, RPM) in thread-safe `atomic` variables. The main application can then poll for this data using the `getTelemetry()` method, which now returns a comprehensive `dshot_result_t` with all available telemetry fields.
|
||||||
|
|
||||||
## ⏱️ DShot Timing Information
|
## ⏱️ DShot Timing Information
|
||||||
|
|
||||||
|
|
@ -66,8 +66,8 @@ Here's a basic example of how to use the `DShotRMT` library to control a motor.
|
||||||
// Define the GPIO pin connected to the motor ESC
|
// Define the GPIO pin connected to the motor ESC
|
||||||
const gpio_num_t MOTOR_PIN = GPIO_NUM_27;
|
const gpio_num_t MOTOR_PIN = GPIO_NUM_27;
|
||||||
|
|
||||||
// Create a DShotRMT instance for DSHOT300
|
// Create a DShotRMT instance for DSHOT300 with bidirectional telemetry enabled
|
||||||
DShotRMT motor(MOTOR_PIN, DSHOT300);
|
DShotRMT motor(MOTOR_PIN, DSHOT300, true);
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
|
@ -91,7 +91,7 @@ void loop() {
|
||||||
Serial.println("Stopping motor.");
|
Serial.println("Stopping motor.");
|
||||||
motor.sendThrottlePercent(0);
|
motor.sendThrottlePercent(0);
|
||||||
|
|
||||||
// Print DShot Info
|
// Print DShot Info, which now includes detailed telemetry
|
||||||
printDShotInfo(motor, Serial);
|
printDShotInfo(motor, Serial);
|
||||||
|
|
||||||
// Take a break before next bench run
|
// Take a break before next bench run
|
||||||
|
|
@ -104,7 +104,7 @@ void loop() {
|
||||||
The `examples` folder contains more advanced examples:
|
The `examples` folder contains more advanced examples:
|
||||||
|
|
||||||
- **`throttle_percent`:** A focused example showing how to control motor speed using percentage values (0-100) via the serial monitor.
|
- **`throttle_percent`:** A focused example showing how to control motor speed using percentage values (0-100) via the serial monitor.
|
||||||
- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and receive telemetry via the serial monitor.
|
- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and **receive comprehensive telemetry** via the serial monitor.
|
||||||
- **`web_control`:** A full-featured web application for controlling a motor from a web browser. It creates a WiFi access point and serves a web page with a throttle slider and arming switch.
|
- **`web_control`:** A full-featured web application for controlling a motor from a web browser. It creates a WiFi access point and serves a web page with a throttle slider and arming switch.
|
||||||
- **`web_client`:** A variation of the `web_control` example that connects to an existing WiFi network instead of creating its own access point.
|
- **`web_client`:** A variation of the `web_control` example that connects to an existing WiFi network instead of creating its own access point.
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ The main class is `DShotRMT`. Here are the most important methods:
|
||||||
- `sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us)`: Sends a DShot command to the ESC with a specified repeat count and delay. This is a blocking function.
|
- `sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us)`: Sends a DShot command to the ESC with a specified repeat count and delay. This is a blocking function.
|
||||||
- `sendCommand(uint16_t command_value)`: Sends a DShot command to the ESC by accepting an integer value. It validates the input and then calls `sendCommand(dshotCommands_e command)`.
|
- `sendCommand(uint16_t command_value)`: Sends a DShot command to the ESC by accepting an integer value. It validates the input and then calls `sendCommand(dshotCommands_e command)`.
|
||||||
- `sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)`: Sends a custom DShot command to the ESC. Advanced feature, use with caution.
|
- `sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)`: Sends a custom DShot command to the ESC. Advanced feature, use with caution.
|
||||||
- `getTelemetry()`: Retrieves telemetry data from the ESC. If bidirectional DShot is enabled, this function will return the last received telemetry data.
|
- `getTelemetry()`: Retrieves telemetry data from the ESC. If bidirectional DShot is enabled, this function now returns a comprehensive `dshot_result_t` containing both eRPM and a fully GCR-decoded `dshot_telemetry_data_t` struct (temperature, voltage, current, consumption, RPM) if available.
|
||||||
- `setMotorSpinDirection(bool reversed)`: Sets the motor spin direction. `true` for reversed, `false` for normal.
|
- `setMotorSpinDirection(bool reversed)`: Sets the motor spin direction. `true` for reversed, `false` for normal.
|
||||||
- `saveESCSettings()`: Sends a command to the ESC to save its current settings. Use with caution as this writes to ESC's non-volatile memory.
|
- `saveESCSettings()`: Sends a command to the ESC to save its current settings. Use with caution as this writes to ESC's non-volatile memory.
|
||||||
- `getMode()`: Gets the current DShot mode.
|
- `getMode()`: Gets the current DShot mode.
|
||||||
|
|
@ -138,7 +138,7 @@ The main class is `DShotRMT`. Here are the most important methods:
|
||||||
|
|
||||||
## ⚙️ ESP-IDF Integration
|
## ⚙️ ESP-IDF Integration
|
||||||
|
|
||||||
This library is built upon the ESP-IDF framework, specifically leveraging its RMT (Remote Control Peripheral) module for precise signal generation. For detailed information on the underlying ESP-IDF components and their usage, please refer to the official ESP-IDF documentation:
|
This library is built upon the ESP-IDF framework, specifically leveraging its RMT (Remote Control Peripheral) module for precise signal generation. The library is tested with **ESP-IDF v5.5.1** and makes extensive use of its modern RMT APIs. For detailed information on the underlying ESP-IDF components and their usage, please refer to the official ESP-IDF documentation:
|
||||||
|
|
||||||
* [ESP-IDF v5.5.1 Documentation](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/)
|
* [ESP-IDF v5.5.1 Documentation](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
|
||||||
|
|
||||||
// BiDirectional DShot Support (default: false)
|
// BiDirectional DShot Support (default: false)
|
||||||
// re-enabled for testing
|
// re-enabled for testing
|
||||||
static constexpr auto IS_BIDIRECTIONAL = false;
|
static constexpr auto IS_BIDIRECTIONAL = true;
|
||||||
|
|
||||||
// Motor magnet count for RPM calculation
|
// Motor magnet count for RPM calculation
|
||||||
// static constexpr auto MOTOR01_MAGNET_COUNT = 14;
|
// static constexpr auto MOTOR01_MAGNET_COUNT = 14;
|
||||||
|
|
@ -80,13 +80,11 @@ void loop()
|
||||||
{
|
{
|
||||||
printDShotInfo(motor01, USB_SERIAL);
|
printDShotInfo(motor01, USB_SERIAL);
|
||||||
|
|
||||||
USB_SERIAL.println(" ");
|
|
||||||
|
|
||||||
// Get Motor RPM if bidirectional
|
// Get Motor RPM if bidirectional
|
||||||
if (IS_BIDIRECTIONAL)
|
if (IS_BIDIRECTIONAL)
|
||||||
{
|
{
|
||||||
dshot_result_t telem_result = motor01.getTelemetry();
|
// dshot_result_t telem_result = motor01.getTelemetry();
|
||||||
printDShotResult(telem_result);
|
// printDShotResult(telem_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
USB_SERIAL.println("Type 'help' to show Menu");
|
USB_SERIAL.println("Type 'help' to show Menu");
|
||||||
|
|
|
||||||
187
src/DShotRMT.cpp
187
src/DShotRMT.cpp
|
|
@ -7,13 +7,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "DShotRMT.h"
|
#include "DShotRMT.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
// Configuration Constants
|
// Configuration Constants
|
||||||
static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000;
|
static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000;
|
||||||
static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111;
|
static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111;
|
||||||
static constexpr auto DSHOT_RX_TIMEOUT_MS = 2;
|
static constexpr auto DSHOT_RX_TIMEOUT_MS = 2;
|
||||||
static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames
|
static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames
|
||||||
static constexpr auto GCR_BITS_PER_FRAME = 21; // GCR bits in a DShot answer frame
|
|
||||||
static constexpr auto POLE_PAIRS_MIN = 1;
|
static constexpr auto POLE_PAIRS_MIN = 1;
|
||||||
static constexpr auto MAGNETS_PER_POLE_PAIR = 2;
|
static constexpr auto MAGNETS_PER_POLE_PAIR = 2;
|
||||||
static constexpr auto NO_DSHOT_TELEMETRY = 0;
|
static constexpr auto NO_DSHOT_TELEMETRY = 0;
|
||||||
|
|
@ -346,6 +347,51 @@ uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const
|
||||||
return crc;
|
return crc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const
|
||||||
|
{
|
||||||
|
uint8_t crc = 0;
|
||||||
|
for (size_t i = 0; i < len; ++i)
|
||||||
|
{
|
||||||
|
crc ^= data[i];
|
||||||
|
for (uint8_t j = 0; j < 8; ++j)
|
||||||
|
{
|
||||||
|
if (crc & 0x80)
|
||||||
|
{
|
||||||
|
crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const
|
||||||
|
{
|
||||||
|
// Ensure the telemetry_data struct is cleared before filling
|
||||||
|
memset(&telemetry_data, 0, sizeof(dshot_telemetry_data_t));
|
||||||
|
|
||||||
|
// Telemetry data is typically ordered as:
|
||||||
|
// Byte 0: Temperature (signed 8-bit)
|
||||||
|
// Byte 1-2: Voltage (16-bit, MSB first)
|
||||||
|
// Byte 3-4: Current (16-bit, MSB first)
|
||||||
|
// Byte 5-6: Consumption (16-bit, MSB first)
|
||||||
|
// Byte 7-8: RPM (16-bit, MSB first)
|
||||||
|
// Byte 9: CRC (8-bit) - checked separately
|
||||||
|
|
||||||
|
telemetry_data.temperature = static_cast<int8_t>(raw_telemetry_bytes[0]);
|
||||||
|
telemetry_data.voltage = (static_cast<uint16_t>(raw_telemetry_bytes[1]) << 8) | raw_telemetry_bytes[2];
|
||||||
|
telemetry_data.current = (static_cast<uint16_t>(raw_telemetry_bytes[3]) << 8) | raw_telemetry_bytes[4];
|
||||||
|
telemetry_data.consumption = (static_cast<uint16_t>(raw_telemetry_bytes[5]) << 8) | raw_telemetry_bytes[6];
|
||||||
|
telemetry_data.rpm = (static_cast<uint16_t>(raw_telemetry_bytes[7]) << 8) | raw_telemetry_bytes[8];
|
||||||
|
|
||||||
|
// Error flags/count can be derived from other parts of the telemetry data if available,
|
||||||
|
// or set based on CRC check result. For now, we leave telemetry_data.errors to 0
|
||||||
|
// or handle it implicitly through the success/failure of the CRC check.
|
||||||
|
}
|
||||||
|
|
||||||
void DShotRMT::_preCalculateRMTTicks()
|
void DShotRMT::_preCalculateRMTTicks()
|
||||||
{
|
{
|
||||||
// Pre-calculate all timing values in RMT ticks to save CPU cycles later.
|
// Pre-calculate all timing values in RMT ticks to save CPU cycles later.
|
||||||
|
|
@ -378,8 +424,8 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet)
|
||||||
if (_is_bidirectional)
|
if (_is_bidirectional)
|
||||||
{
|
{
|
||||||
// Start the receiver to wait for incoming telemetry data
|
// Start the receiver to wait for incoming telemetry data
|
||||||
rmt_symbol_word_t rx_symbols[GCR_BITS_PER_FRAME];
|
rmt_symbol_word_t rx_symbols[DSHOT_TELEMETRY_FULL_GCR_BITS];
|
||||||
size_t rx_size_bytes = GCR_BITS_PER_FRAME * sizeof(rmt_symbol_word_t);
|
size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t);
|
||||||
|
|
||||||
rmt_receive_config_t rmt_rx_config = {
|
rmt_receive_config_t rmt_rx_config = {
|
||||||
.signal_range_min_ns = DSHOT_PULSE_MIN_NS,
|
.signal_range_min_ns = DSHOT_PULSE_MIN_NS,
|
||||||
|
|
@ -401,7 +447,7 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet)
|
||||||
// The DShot frame is 16 bits, which is 2 bytes
|
// The DShot frame is 16 bits, which is 2 bytes
|
||||||
size_t tx_size_bytes = sizeof(swapped_value);
|
size_t tx_size_bytes = sizeof(swapped_value);
|
||||||
|
|
||||||
rmt_transmit_config_t tx_config = { .loop_count = 0 }; // No automatic loops - real-time calculation
|
rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops - real-time calculation
|
||||||
|
|
||||||
// In bidirectional mode, the RMT RX channel must be disabled before transmitting.
|
// In bidirectional mode, the RMT RX channel must be disabled before transmitting.
|
||||||
// This is to prevent the receiver from picking up the transmitted signal, which would cause a loopback issue.
|
// This is to prevent the receiver from picking up the transmitted signal, which would cause a loopback issue.
|
||||||
|
|
@ -442,7 +488,7 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols)
|
||||||
|
|
||||||
// Decode RMT symbols into a 21-bit GCR (Group Code Recording) value.
|
// Decode RMT symbols into a 21-bit GCR (Group Code Recording) value.
|
||||||
// The ESC sends back a signal where the duration determines the bit value.
|
// The ESC sends back a signal where the duration determines the bit value.
|
||||||
for (size_t i = 0; i < GCR_BITS_PER_FRAME; ++i)
|
for (size_t i = 0; i < DSHOT_ERPM_FRAME_GCR_BITS; ++i)
|
||||||
{
|
{
|
||||||
bool bit_is_one = symbols[i].duration0 > symbols[i].duration1;
|
bool bit_is_one = symbols[i].duration0 > symbols[i].duration1;
|
||||||
gcr_value = (gcr_value << 1) | bit_is_one;
|
gcr_value = (gcr_value << 1) | bit_is_one;
|
||||||
|
|
@ -491,20 +537,137 @@ void DShotRMT::_recordFrameTransmissionTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static Callback Functions
|
// Static Callback Functions
|
||||||
|
// Processes a full telemetry frame
|
||||||
|
void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols)
|
||||||
|
{
|
||||||
|
if (num_symbols != DSHOT_TELEMETRY_FULL_GCR_BITS)
|
||||||
|
{
|
||||||
|
return; // Incorrect number of symbols for full telemetry
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES + 1]; // 10 data bytes + 1 CRC byte
|
||||||
|
memset(gcr_decoded_bytes, 0, sizeof(gcr_decoded_bytes));
|
||||||
|
|
||||||
|
uint8_t data_bit_idx = 0;
|
||||||
|
for (size_t i = 0; i < DSHOT_TELEMETRY_FULL_GCR_BITS; i += 5)
|
||||||
|
{
|
||||||
|
uint8_t gcr_group_5bits = 0;
|
||||||
|
for (size_t j = 0; j < 5; ++j)
|
||||||
|
{
|
||||||
|
if (i + j < DSHOT_TELEMETRY_FULL_GCR_BITS)
|
||||||
|
{
|
||||||
|
gcr_group_5bits = (gcr_group_5bits << 1) | ((symbols[i + j].duration0 > symbols[i + j].duration1) ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t decoded_nibble; // 4 data bits
|
||||||
|
switch (gcr_group_5bits)
|
||||||
|
{
|
||||||
|
case 0b11110:
|
||||||
|
decoded_nibble = 0b0000;
|
||||||
|
break;
|
||||||
|
case 0b01001:
|
||||||
|
decoded_nibble = 0b0001;
|
||||||
|
break;
|
||||||
|
case 0b10100:
|
||||||
|
decoded_nibble = 0b0010;
|
||||||
|
break;
|
||||||
|
case 0b10101:
|
||||||
|
decoded_nibble = 0b0011;
|
||||||
|
break;
|
||||||
|
case 0b01010:
|
||||||
|
decoded_nibble = 0b0100;
|
||||||
|
break;
|
||||||
|
case 0b01011:
|
||||||
|
decoded_nibble = 0b0101;
|
||||||
|
break;
|
||||||
|
case 0b01110:
|
||||||
|
decoded_nibble = 0b0110;
|
||||||
|
break;
|
||||||
|
case 0b01111:
|
||||||
|
decoded_nibble = 0b0111;
|
||||||
|
break;
|
||||||
|
case 0b10010:
|
||||||
|
decoded_nibble = 0b1000;
|
||||||
|
break;
|
||||||
|
case 0b10011:
|
||||||
|
decoded_nibble = 0b1001;
|
||||||
|
break;
|
||||||
|
case 0b10110:
|
||||||
|
decoded_nibble = 0b1010;
|
||||||
|
break;
|
||||||
|
case 0b10111:
|
||||||
|
decoded_nibble = 0b1011;
|
||||||
|
break;
|
||||||
|
case 0b11010:
|
||||||
|
decoded_nibble = 0b1100;
|
||||||
|
break;
|
||||||
|
case 0b11011:
|
||||||
|
decoded_nibble = 0b1101;
|
||||||
|
break;
|
||||||
|
case 0b11100:
|
||||||
|
decoded_nibble = 0b1110;
|
||||||
|
break;
|
||||||
|
case 0b11101:
|
||||||
|
decoded_nibble = 0b1111;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return; // Invalid GCR group, discard frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the 4 decoded bits into the data_bytes array
|
||||||
|
for (int k = 3; k >= 0; --k)
|
||||||
|
{
|
||||||
|
if (data_bit_idx < (DSHOT_TELEMETRY_FRAME_LENGTH_BITS + DSHOT_TELEMETRY_CRC_LENGTH_BITS))
|
||||||
|
{
|
||||||
|
size_t byte_idx = data_bit_idx / 8;
|
||||||
|
size_t bit_pos = data_bit_idx % 8;
|
||||||
|
if (byte_idx < sizeof(gcr_decoded_bytes))
|
||||||
|
{
|
||||||
|
gcr_decoded_bytes[byte_idx] |= ((decoded_nibble >> k) & 1) << (7 - bit_pos);
|
||||||
|
}
|
||||||
|
data_bit_idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now gcr_decoded_bytes contains the 10 telemetry bytes + 1 CRC byte.
|
||||||
|
// Perform CRC validation.
|
||||||
|
uint8_t received_crc = gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES];
|
||||||
|
uint8_t calculated_crc = _calculateTelemetryCRC(gcr_decoded_bytes, DSHOT_TELEMETRY_FRAME_LENGTH_BYTES);
|
||||||
|
|
||||||
|
if (received_crc == calculated_crc)
|
||||||
|
{
|
||||||
|
dshot_telemetry_data_t telemetry_data;
|
||||||
|
// Extract from the first 10 bytes (excluding the CRC byte)
|
||||||
|
_extractTelemetryData(gcr_decoded_bytes, telemetry_data);
|
||||||
|
|
||||||
|
_last_telemetry_data_atomic.store(telemetry_data);
|
||||||
|
_full_telemetry_ready_flag_atomic.store(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This function is called by the RMT driver's ISR when a frame is received
|
// This function is called by the RMT driver's ISR when a frame is received
|
||||||
bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data)
|
bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data)
|
||||||
{
|
{
|
||||||
DShotRMT *instance = static_cast<DShotRMT *>(user_data);
|
DShotRMT *instance = static_cast<DShotRMT *>(user_data);
|
||||||
|
|
||||||
if (edata && edata->num_symbols == GCR_BITS_PER_FRAME)
|
if (edata)
|
||||||
{
|
{
|
||||||
uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols);
|
if (edata->num_symbols == DSHOT_TELEMETRY_FULL_GCR_BITS)
|
||||||
|
|
||||||
if (erpm != DSHOT_NULL_PACKET)
|
|
||||||
{
|
{
|
||||||
// Atomically store the new eRPM value and set the flag
|
instance->_processFullTelemetryFrame(edata->received_symbols, edata->num_symbols);
|
||||||
instance->_last_erpm_atomic.store(erpm);
|
}
|
||||||
instance->_telemetry_ready_flag_atomic.store(true);
|
else if (edata->num_symbols == DSHOT_ERPM_FRAME_GCR_BITS)
|
||||||
|
{
|
||||||
|
uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols);
|
||||||
|
|
||||||
|
if (erpm != DSHOT_NULL_PACKET)
|
||||||
|
{
|
||||||
|
// Atomically store the new eRPM value and set the flag
|
||||||
|
instance->_last_erpm_atomic.store(erpm);
|
||||||
|
instance->_telemetry_ready_flag_atomic.store(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,29 +109,32 @@ private:
|
||||||
uint64_t _last_command_timestamp = 0; // Timestamp of the last command sent
|
uint64_t _last_command_timestamp = 0; // Timestamp of the last command sent
|
||||||
|
|
||||||
// Telemetry Related Variables
|
// Telemetry Related Variables
|
||||||
std::atomic<uint16_t> _last_erpm_atomic = 0; // Atomically stored last received eRPM value
|
std::atomic<uint16_t> _last_erpm_atomic = 0; // Atomically stored last received eRPM value
|
||||||
std::atomic<bool> _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data
|
std::atomic<bool> _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data
|
||||||
|
std::atomic<dshot_telemetry_data_t> _last_telemetry_data_atomic = {}; // Atomically stored last received full telemetry data
|
||||||
|
std::atomic<bool> _full_telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new full telemetry data
|
||||||
rmt_rx_event_callbacks_t _rx_event_callbacks = {
|
rmt_rx_event_callbacks_t _rx_event_callbacks = {
|
||||||
// RMT receive event callbacks
|
// RMT receive event callbacks
|
||||||
.on_recv_done = _on_rx_done,
|
.on_recv_done = _on_rx_done,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Private Helper Functions for DShot Protocol Logic
|
// Private Helper Functions for DShot Protocol Logic
|
||||||
bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid
|
bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid
|
||||||
dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command
|
dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command
|
||||||
dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command)
|
dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command)
|
||||||
uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value
|
uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value
|
||||||
uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame
|
uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame
|
||||||
void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode
|
uint8_t _calculateTelemetryCRC(const uint8_t *data, size_t len) const; // Calculates the 8-bit CRC for telemetry data
|
||||||
dshot_result_t _sendPacket(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel
|
void _extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const; // Extracts telemetry data from raw bytes
|
||||||
uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value
|
void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode
|
||||||
bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission
|
dshot_result_t _sendPacket(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel
|
||||||
void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time
|
uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value
|
||||||
|
void IRAM_ATTR _processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols); // Processes a full telemetry frame
|
||||||
|
bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission
|
||||||
|
void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time
|
||||||
|
|
||||||
// Static Callback Function for RMT RX Events
|
// Static Callback Function for RMT RX Events
|
||||||
void _cleanupRmtResources();
|
void _cleanupRmtResources();
|
||||||
};
|
};
|
||||||
|
|
||||||
#include "dshot_utils.h" // Include for helper functions
|
#include "dshot_utils.h" // Include for helper functions
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
#include <driver/rmt_common.h>
|
#include <driver/rmt_common.h>
|
||||||
|
|
||||||
// DShot protocol definitions
|
// DShot protocol definitions
|
||||||
static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits + 1 telemetry bit + 4 CRC bits
|
static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits + 1 telemetry bit + 4 CRC bits
|
||||||
static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16;
|
static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16;
|
||||||
static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047)
|
static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047)
|
||||||
static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin
|
static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin
|
||||||
static constexpr float DSHOT_PERCENT_MIN = 0.0f;
|
static constexpr float DSHOT_PERCENT_MIN = 0.0f;
|
||||||
static constexpr float DSHOT_PERCENT_MAX = 100.0f;
|
static constexpr float DSHOT_PERCENT_MAX = 100.0f;
|
||||||
static constexpr uint16_t DSHOT_CMD_MIN = 0; // Minimum command value
|
static constexpr uint16_t DSHOT_CMD_MIN = 0; // Minimum command value
|
||||||
|
|
@ -23,6 +23,15 @@ static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command
|
||||||
static constexpr uint16_t DSHOT_TELEMETRY_BIT_MASK = 0x0800; // Bit mask for telemetry request bit (11th bit)
|
static constexpr uint16_t DSHOT_TELEMETRY_BIT_MASK = 0x0800; // Bit mask for telemetry request bit (11th bit)
|
||||||
static constexpr uint16_t DSHOT_CRC_MASK = 0x000F; // Bit mask for CRC bits
|
static constexpr uint16_t DSHOT_CRC_MASK = 0x000F; // Bit mask for CRC bits
|
||||||
|
|
||||||
|
// GCR frame definitions
|
||||||
|
static constexpr uint16_t DSHOT_ERPM_FRAME_GCR_BITS = 21; // GCR bits in a DShot answer frame for eRPM
|
||||||
|
static constexpr uint16_t DSHOT_TELEMETRY_FULL_GCR_BITS = 110; // GCR bits for a full 10-byte telemetry frame (80 data bits + 8 CRC bits = 88 bits, 88 * 5/4 = 110 GCR bits)
|
||||||
|
|
||||||
|
// Telemetry frame definitions
|
||||||
|
static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BITS = 80; // 10 bytes * 8 bits/byte
|
||||||
|
static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BYTES = 10;
|
||||||
|
static constexpr uint16_t DSHOT_TELEMETRY_CRC_LENGTH_BITS = 8; // 8-bit CRC for telemetry
|
||||||
|
|
||||||
// Default motor magnet count for RPM calculation
|
// Default motor magnet count for RPM calculation
|
||||||
static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14;
|
static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14;
|
||||||
|
|
||||||
|
|
@ -88,16 +97,30 @@ enum dshot_msg_code_t
|
||||||
DSHOT_ENCODING_SUCCESS,
|
DSHOT_ENCODING_SUCCESS,
|
||||||
DSHOT_TRANSMISSION_SUCCESS,
|
DSHOT_TRANSMISSION_SUCCESS,
|
||||||
DSHOT_TELEMETRY_SUCCESS,
|
DSHOT_TELEMETRY_SUCCESS,
|
||||||
|
DSHOT_TELEMETRY_DATA_AVAILABLE,
|
||||||
DSHOT_COMMAND_SUCCESS
|
DSHOT_COMMAND_SUCCESS
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Structure for decoded DShot telemetry data (from ESC)
|
||||||
|
typedef struct dshot_telemetry_data
|
||||||
|
{
|
||||||
|
uint16_t rpm; // Motor RPM
|
||||||
|
uint16_t voltage; // Voltage in mV
|
||||||
|
uint16_t current; // Current in mA
|
||||||
|
uint16_t consumption; // Consumption in mAh
|
||||||
|
int8_t temperature; // Temperature in Celsius
|
||||||
|
uint8_t errors; // Error flags / count
|
||||||
|
} dshot_telemetry_data_t;
|
||||||
|
|
||||||
// Contains the success status, an error code, and optional telemetry data.
|
// Contains the success status, an error code, and optional telemetry data.
|
||||||
typedef struct dshot_result
|
typedef struct dshot_result
|
||||||
{
|
{
|
||||||
bool success;
|
bool success;
|
||||||
dshot_msg_code_t result_code; // Specific error or success code.
|
dshot_msg_code_t result_code; // Specific error or success code.
|
||||||
uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful.
|
uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful.
|
||||||
uint16_t motor_rpm; // Motor RPM if telemetry is successful and magnet count is known.
|
uint16_t motor_rpm; // Motor RPM if telemetry is successful and magnet count is known.
|
||||||
|
dshot_telemetry_data_t telemetry_data; // Full telemetry data if available
|
||||||
|
bool telemetry_available; // Flag to indicate if telemetry_data is valid
|
||||||
} dshot_result_t;
|
} dshot_result_t;
|
||||||
|
|
||||||
// Standard DShot commands by "betaflight"
|
// Standard DShot commands by "betaflight"
|
||||||
|
|
@ -141,7 +164,8 @@ static constexpr int DSHOT_ERROR = 1;
|
||||||
static constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT;
|
static constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT;
|
||||||
static constexpr auto DSHOT_RMT_RESOLUTION = 8000000; // 8 MHz resolution
|
static constexpr auto DSHOT_RMT_RESOLUTION = 8000000; // 8 MHz resolution
|
||||||
static constexpr auto RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / 1000000; // RMT Ticks per microsecond
|
static constexpr auto RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / 1000000; // RMT Ticks per microsecond
|
||||||
static constexpr auto RMT_BUFFER_SYMBOLS = 64;
|
static constexpr auto RMT_TX_BUFFER_SYMBOLS = 64;
|
||||||
|
static constexpr auto RMT_RX_BUFFER_SYMBOLS = DSHOT_TELEMETRY_FULL_GCR_BITS;
|
||||||
static constexpr auto RMT_QUEUE_DEPTH = 1;
|
static constexpr auto RMT_QUEUE_DEPTH = 1;
|
||||||
|
|
||||||
// Timing parameters for each DShot mode
|
// Timing parameters for each DShot mode
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch
|
||||||
.gpio_num = gpio,
|
.gpio_num = gpio,
|
||||||
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
|
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
|
||||||
.resolution_hz = DSHOT_RMT_RESOLUTION,
|
.resolution_hz = DSHOT_RMT_RESOLUTION,
|
||||||
.mem_block_symbols = RMT_BUFFER_SYMBOLS,
|
.mem_block_symbols = RMT_TX_BUFFER_SYMBOLS,
|
||||||
.trans_queue_depth = RMT_QUEUE_DEPTH,
|
.trans_queue_depth = RMT_QUEUE_DEPTH,
|
||||||
.flags = {
|
.flags = {
|
||||||
.invert_out = is_bidirectional ? 1 : 0,
|
.invert_out = is_bidirectional ? 1 : 0,
|
||||||
|
|
@ -44,7 +44,7 @@ dshot_result_t init_rmt_rx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch
|
||||||
.gpio_num = gpio,
|
.gpio_num = gpio,
|
||||||
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
|
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
|
||||||
.resolution_hz = DSHOT_RMT_RESOLUTION,
|
.resolution_hz = DSHOT_RMT_RESOLUTION,
|
||||||
.mem_block_symbols = RMT_BUFFER_SYMBOLS,
|
.mem_block_symbols = RMT_RX_BUFFER_SYMBOLS,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rmt_new_rx_channel(&rx_channel_config, out_channel) != DSHOT_OK)
|
if (rmt_new_rx_channel(&rx_channel_config, out_channel) != DSHOT_OK)
|
||||||
|
|
|
||||||
|
|
@ -132,20 +132,61 @@ inline void printDShotResult(dshot_result_t &result, Stream &output = Serial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to print DShot signal info
|
// Helper to print DShot signal info
|
||||||
inline void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial)
|
inline void printDShotInfo(DShotRMT &dshot_rmt, Stream &output = Serial)
|
||||||
{
|
{
|
||||||
output.println("\n === DShot Signal Info === ");
|
output.println("\n=== DShot Info ===");
|
||||||
output.printf("Library Version: %d.%d.%d\n", DSHOTRMT_MAJOR_VERSION, DSHOTRMT_MINOR_VERSION, DSHOTRMT_PATCH_VERSION);
|
output.printf("Library Version: %d.%d.%d\n", DSHOTRMT_MAJOR_VERSION, DSHOTRMT_MINOR_VERSION, DSHOTRMT_PATCH_VERSION);
|
||||||
output.printf("Current Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode()));
|
output.printf("Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode()));
|
||||||
output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO");
|
output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO");
|
||||||
output.printf("Current Packet: ");
|
output.printf("Last Throttle: %u\n", dshot_rmt.getThrottleValue());
|
||||||
|
|
||||||
|
output.print("Packet (binary): ");
|
||||||
for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i)
|
for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i)
|
||||||
{
|
{
|
||||||
output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1);
|
output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1);
|
||||||
}
|
}
|
||||||
|
output.println();
|
||||||
|
|
||||||
output.printf("\nCurrent Value: %u\n", dshot_rmt.getThrottleValue());
|
// --- Telemetry Data ---
|
||||||
|
if (dshot_rmt.isBidirectional())
|
||||||
|
{
|
||||||
|
dshot_result_t telemetry_result = dshot_rmt.getTelemetry();
|
||||||
|
|
||||||
|
output.print("Telemetry: ");
|
||||||
|
if (telemetry_result.success)
|
||||||
|
{
|
||||||
|
output.printf("OK (%s)\n", get_result_code_str(telemetry_result.result_code));
|
||||||
|
|
||||||
|
if (telemetry_result.erpm > 0 || telemetry_result.motor_rpm > 0)
|
||||||
|
{
|
||||||
|
output.printf(" eRPM: %u, Motor RPM: %u\n", telemetry_result.erpm, telemetry_result.motor_rpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (telemetry_result.telemetry_available)
|
||||||
|
{
|
||||||
|
output.println(" --- Full Telemetry Details ---");
|
||||||
|
output.printf(" Temp: %d C | Volt: %.2f V | Curr: %.2f A | Cons: %u mAh\n",
|
||||||
|
telemetry_result.telemetry_data.temperature,
|
||||||
|
(float)telemetry_result.telemetry_data.voltage / 1000.0f, // Convert mV to V
|
||||||
|
(float)telemetry_result.telemetry_data.current / 1000.0f, // Convert mA to A
|
||||||
|
telemetry_result.telemetry_data.consumption);
|
||||||
|
output.printf(" Telemetry RPM: %u\n", telemetry_result.telemetry_data.rpm);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.println(" (Full telemetry not yet available or CRC failed for full frame)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.printf("FAILED (%s)\n", get_result_code_str(telemetry_result.result_code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.println("Telemetry: Disabled (Bidirectional mode OFF)");
|
||||||
|
}
|
||||||
|
output.println("===========================\n"); // End separator
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to print CPU info
|
// Helper to print CPU info
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue