Xinyuan-LilyGO / T-Echo

MIT License
156 stars 22 forks source link

Bluetooth connect to HID keyboard example fails #50

Open goedzo opened 1 month ago

goedzo commented 1 month ago

I am trying to connect a bluetooth HID keyboard to my t-echo, however the example code https://github.com/Xinyuan-LilyGO/T-Echo/tree/main/examples/ble_hid_central does not work.

Even the comment in the code itself says this: display->print("Scan ble keyboard"); //crashes here

So how can I connect my bluetooth keyboard to my t-echo device?

goedzo commented 1 month ago

This is the code I use to find my keyboard, but I never see any keyboard device:

#include <bluefruit.h>

#define CONNECTION_TIMEOUT_MS 5000  // 5 seconds timeout for connection attempts
#define heartbeat_TIMEOUT_MS 5000  //

BLEClientDis disClient; // GATT client for Device Information Service (DIS)

uint32_t lastConnectionAttempt = 0;  // Time of the last connection attempt
uint32_t heartbeat = 0;  // Time of the last connection attempt
bool isConnecting = false;           // Flag to track if we are attempting to connect
uint16_t connHandle = BLE_CONN_HANDLE_INVALID;  // Connection handle to track the current connection

// List to store MAC addresses of devices that have been tried (as strings)
const int MAX_TRIED_DEVICES = 50;  // Allow more devices to be tried
String triedDevices[MAX_TRIED_DEVICES];
int triedDeviceCount = 0;

String currentDeviceAddr;  // Store current device address during connection attempt

// Helper function to convert a MAC address to a string
String addressToString(uint8_t* addr) {
  char str[18];  // MAC address as string "XX:XX:XX:XX:XX:XX"
  snprintf(str, sizeof(str), "%02X:%02X:%02X:%02X:%02X:%02X",
           addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
  return String(str);
}

bool isDeviceInTriedList(String addr) {
  for (int i = 0; i < triedDeviceCount; i++) {
    if (triedDevices[i] == addr) {
      return true;  // Device already tried
    }
  }
  return false;
}

void addDeviceToTriedList(String addr) {
  if (triedDeviceCount < MAX_TRIED_DEVICES) {
    triedDevices[triedDeviceCount] = addr;  // Add the address as a string
    triedDeviceCount++;
  }
}

// Callback function when a connection is established
void connect_callback(uint16_t conn_handle) {
  BLEConnection* conn = Bluefruit.Connection(conn_handle);

  isConnecting = false;
  connHandle = conn_handle;  // Store the connection handle

  // Discover GATT Services (including Device Information Service)
  if (disClient.discover(conn_handle)) {
    char buffer[32] = {0};  // Buffer to store characteristic values

    // Query the manufacturer name
    if (disClient.getManufacturer(buffer, sizeof(buffer))) {
      Serial.print("Manufacturer: ");
      Serial.println(buffer);
    }

    // Query the model number
    if (disClient.getModel(buffer, sizeof(buffer))) {
      Serial.print("Model: ");
      Serial.println(buffer);
    }

    // Query the serial number
    if (disClient.getSerial(buffer, sizeof(buffer))) {
      Serial.print("Serial Number: ");
      Serial.println(buffer);
    }

    // Query the firmware revision
    if (disClient.getFirmwareRev(buffer, sizeof(buffer))) {
      Serial.print("Firmware Version: ");
      Serial.println(buffer);
    }

    // Query the hardware revision
    if (disClient.getHardwareRev(buffer, sizeof(buffer))) {
      Serial.print("Hardware Version: ");
      Serial.println(buffer);
    }

    // Query the software revision (if available)
    if (disClient.getSoftwareRev(buffer, sizeof(buffer))) {
      Serial.print("Software Version: ");
      Serial.println(buffer);
    }

  } else {
    Serial.println("Device Information Service NOT found");
  }

  // Disconnect after retrieving the information
  conn->disconnect();
}

// Callback function when a connection is dropped or disconnected
void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
  connHandle = BLE_CONN_HANDLE_INVALID;  // Reset the connection handle
  isConnecting = false;
  // Scanning will automatically resume due to `restartOnDisconnect`
}

void parseAppleManufacturerData(uint8_t* data, int len) {
  if (len >= 25) {  // iBeacon packets are at least 25 bytes long
    uint16_t companyId = (data[1] << 8) | data[0];
    if (companyId == 0x004C) {  // Apple Inc.
      uint8_t beaconType = data[2];
      if (beaconType == 0x02 && data[3] == 0x15) {  // iBeacon type
        Serial.print("iBeacon UUID: ");
        for (int i = 4; i < 20; i++) {
          Serial.printf("%02X", data[i]);
          if (i == 7 || i == 9 || i == 11 || i == 13) Serial.print("-");
        }
        Serial.println();
        int major = (data[20] << 8) | data[21];
        int minor = (data[22] << 8) | data[23];
        int8_t txPower = data[24];

        Serial.printf("Major: %d, Minor: %d, Tx Power: %d dBm\n", major, minor, txPower);
      }
    }
  }
}

void scan_callback(ble_gap_evt_adv_report_t* report) {
  // Stop scanning before connecting
  Bluefruit.Scanner.stop();

  // Convert the device address to a string
  currentDeviceAddr = addressToString(report->peer_addr.addr);

  // Check if the device is in the tried list before proceeding
  if (isDeviceInTriedList(currentDeviceAddr)) {
    Bluefruit.Scanner.start(0);  // Resume scanning if the device has already been tried
    return;  // Exit if the device is in the tried list
  }

  // Add the device to the tried list
  addDeviceToTriedList(currentDeviceAddr);

  // Print new device found
  Serial.printf("New Device: %s\n", currentDeviceAddr.c_str());

  // Compact Advertising Report Type Details
  Serial.printf("Connectable: %d, Scannable: %d, Directed: %d, Scan Resp: %d, Ext PDU: %d, Data Status: %d\n",
                report->type.connectable, report->type.scannable, report->type.directed,
                report->type.scan_response, report->type.extended_pdu, report->type.status);

  // Skip non-connectable devices
  if (report->type.connectable == 0) {
    Serial.println("Skipping non-connectable device.");
    Bluefruit.Scanner.start(0);  // Resume scanning
    return;
  }

  // Check for Appearance (if available)
  uint8_t buffer[2] = {0};
  if (Bluefruit.Scanner.parseReportByType(report, BLE_GAP_AD_TYPE_APPEARANCE, buffer, sizeof(buffer))) {
    uint16_t appearance = buffer[1] << 8 | buffer[0];  // Combine the two bytes
    Serial.printf("Appearance: 0x%04X\n", appearance);
    printAppearance(appearance);  // Custom function to print human-readable appearance
  } else {
    Serial.println("Appearance: Not available");
  }

  // Check for Manufacturer Specific Data (if available)
  uint8_t manuData[32] = {0};
  int manuDataLen = Bluefruit.Scanner.parseReportByType(report, BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, manuData, sizeof(manuData));
  if (manuDataLen > 0) {
    Serial.print("Manufacturer Data: ");
    for (int i = 0; i < manuDataLen; i++) {
      Serial.printf("%02X ", manuData[i]);
    }
    Serial.println();

    // If Apple Manufacturer Data, try to parse it further (e.g., iBeacon data)
    if (manuData[0] == 0x4C && manuData[1] == 0x00) {
      parseAppleManufacturerData(manuData, manuDataLen);  // Custom function to parse Apple data
    }
  } else {
    Serial.println("Manufacturer Data: Not available");
  }

  // Check for TX Power Level (if available)
  uint8_t txPower[1] = {0};
  if (Bluefruit.Scanner.parseReportByType(report, BLE_GAP_AD_TYPE_TX_POWER_LEVEL, txPower, sizeof(txPower))) {
    Serial.printf("TX Power Level: %d dBm\n", (int8_t)txPower[0]);
  } else {
    Serial.println("TX Power Level: Not available");
  }

  // Check for common GATT services to identify the device type
  if (Bluefruit.Scanner.checkReportForUuid(report, 0x1812)) {
    Serial.println("Service: HID (Keyboard/Mouse/Gamepad)");
  } else if (Bluefruit.Scanner.checkReportForUuid(report, 0x180F)) {
    Serial.println("Service: Battery Service");
  } else if (Bluefruit.Scanner.checkReportForUuid(report, 0x180E)) {
    Serial.println("Service: Phone Alert Status");
  } else {
    Serial.println("Service: Unknown or Not Advertised");
  }

  // Try to connect if no name is advertised
  uint8_t nameBuffer[32] = {0};
  if (!Bluefruit.Scanner.parseReportByType(report, BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME, nameBuffer, sizeof(nameBuffer)) &&
      !Bluefruit.Scanner.parseReportByType(report, BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME, nameBuffer, sizeof(nameBuffer))) {

    // No name advertised, attempting to connect
    if (!isConnecting) {
      lastConnectionAttempt = millis();
      isConnecting = true;

      // Try to connect to the device
      Bluefruit.Central.connect(report);
    }
  } else {
    Serial.printf("Advertised Name: %s\n", nameBuffer);
  }

  Bluefruit.Scanner.start(0);  // Resume scanning
}

// Helper function to print human-readable appearance
void printAppearance(uint16_t appearance) {
  switch (appearance) {
    case 961:
      Serial.println("Device Type: Keyboard");
      break;
    case 962:
      Serial.println("Device Type: Mouse");
      break;
    case 963:
      Serial.println("Device Type: Gamepad");
      break;
    case 1344:
      Serial.println("Device Type: Phone");
      break;
    case 832:
      Serial.println("Device Type: Watch");
      break;
    default:
      Serial.println("Device Type: Unknown");
      break;
  }
}

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);

  // Initialize Bluefruit with max connections (Peripheral = 0, Central = 1)
  Bluefruit.begin(0, 1);

  // Set name for this BLE Central device
  Bluefruit.setName("Bluefruit52 Central");

  // Set connection and disconnection callbacks
  Bluefruit.Central.setConnectCallback(connect_callback);
  Bluefruit.Central.setDisconnectCallback(disconnect_callback);

  // Automatically restart scanning on disconnection
  Bluefruit.Scanner.restartOnDisconnect(true);  // Automatically restart scanning after disconnect

  // Initialize the Device Information Service client
  disClient.begin();

  // Start scanning for devices
  Bluefruit.Scanner.setRxCallback(scan_callback);
  Bluefruit.Scanner.setInterval(160, 80);       // Set scan interval and window
  Bluefruit.Scanner.useActiveScan(true);        // Request scan response data
  Bluefruit.Scanner.start(0);                   // 0 = scan forever
}

void loop() {
  if(millis() - heartbeat > heartbeat_TIMEOUT_MS ){
      Serial.printf(".");
      heartbeat=millis();
  }

  // Check for timeout on connection attempts
  if (isConnecting && (millis() - lastConnectionAttempt > CONNECTION_TIMEOUT_MS)) {
    // Forcefully stop the connection attempt by resetting BLE Central
    if (connHandle != BLE_CONN_HANDLE_INVALID) {
      BLEConnection* conn = Bluefruit.Connection(connHandle);
      if (conn) {
        conn->disconnect();  // Properly disconnect the active connection
      }
    }

    // Reset connection-related variables
    isConnecting = false;
    connHandle = BLE_CONN_HANDLE_INVALID;

    // Restart scanning
    Bluefruit.Scanner.stop();
    delay(100);  // Short delay to ensure the stop completes
    Bluefruit.Scanner.start(0);  // Restart scanning
  }
}