nativescript-community / ble

Connect to and interact with Bluetooth LE peripherals.
https://nativescript-community.github.io/ble/
MIT License
194 stars 78 forks source link

Service Not Found Error when Writing Commands to ESP32 in NativeScript BLE App #280

Open luigi7up opened 1 month ago

luigi7up commented 1 month ago

I’m currently building a NativeScript app that uses BLE to connect to an ESP32 microcontroller. The microcontroller initializes a BLE server with a name, a service UUID, and characteristic UUIDs, as shown in the code snippet below. My NativeScript app successfully connects to the BLE service, but when I attempt to write commands to the characteristic, I consistently receive a service_not_found error.

#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <ESP32Servo.h>
#include <string>

#define LOCKER_MAIN_SERVICE_UUID "3e64e8c5-6b16-4b5d-b244-5bfa29c64aa6"
#define CHARACTERISTIC_SERVO_COMMANDS_UUID "c18c2e14-0b84-4b1e-8c9e-2195c62ee5e2"

Servo myServo;
int servoPin = 5;  // Pin for servo signal

BLECharacteristic *pCharacteristic;
bool deviceConnected = false;

class LockerServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("Device connected");
  }

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("Device disconnected");
    // Restart advertising
    pServer->startAdvertising();
    Serial.println("Advertising restarted");
  }
};

class ServoCharacteristicsCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) {
    // Use Arduino's String class to get the value
    String rxValue = pCharacteristic->getValue().c_str(); // Get the value as a String

    if (rxValue.length() > 0) {
      // Convert the received string to an integer (assuming single-digit commands)
      int command = rxValue[0] - '0';  // Convert ASCII character to integer

      Serial.print("Received: ");
      Serial.println(command);  // Print the received command

      // Move the servo based on the command
      moveServo(command);
    }
  }
  void moveServo(int command) {
    //moving the servo code...
  }
};

void setup() {
  Serial.begin(115200);

  // Attach the servo to the pin
  myServo.attach(servoPin);  
  myServo.write(90); // Center the servo
  delay(1000); // Wait for 2 seconds for the servo to stabilize

  // BLE setup
  BLEDevice::init("MyESP32Locker");
  Serial.println("Initializing MyESP32Locker");

  // Create BLE Server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new LockerServerCallbacks());

  // Create BLE service
  BLEService *pService = pServer->createService(LOCKER_MAIN_SERVICE_UUID);

  // Create a BLE characteristic
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_SERVO_COMMANDS_UUID,
                      BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE
                    );

  pCharacteristic->setCallbacks(new ServoCharacteristicsCallbacks());

  // Start the service
  pService->start();
  Serial.println("BLE service started!");

  // Start advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();

  // Enable scan response
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // Functions that help with iPhone connections
  pAdvertising->setMinPreferred(0x12);

  Serial.println("Starting BLE advertising...");
  // BLEDevice::startAdvertising();

  // pAdvertising->start(); 
  pServer->startAdvertising();

  Serial.println("BLE advertising started, Waiting for a client connection to notify...");

}

void loop() {
  // Do nothing here

}```

Native script code:

```javascript

const Bluetooth = require("@nativescript-community/ble").Bluetooth;
const { fromObject } = require("@nativescript/core");
const createViewModel = require("@/view-models/loading-view-model").createViewModel;
const Permissions = require("nativescript-permissions"); // Correctly import the permissions module

// UUIDs for your ESP32 BLE service and characteristic (replace with actual ones)

const LOCKER_MAIN_SERVICE_UUID                          = "3e64e8c5-6b16-4b5d-b244-5bfa29c64aa6"; // Change this to a unique UUID
const CHARACTERISTIC_SERVO_COMMANDS_UUID   = "c18c2e14-0b84-4b1e-8c9e-2195c62ee5e2"; // Change this to a uniq
// Create and export the ViewModel
const viewModel = createViewModel();
const bluetooth = new Bluetooth();
let connectedPeripheral;

// Function to check and request Bluetooth permissions
function doRequestPermissions() {
    return Permissions.requestPermissions([
        android.Manifest.permission.BLUETOOTH,
        android.Manifest.permission.BLUETOOTH_ADMIN,
        android.Manifest.permission.BLUETOOTH_CONNECT,
        android.Manifest.permission.ACCESS_FINE_LOCATION,
        android.Manifest.permission.ACCESS_COARSE_LOCATION,
    ]);
}

async function onNavigatingTo(args) {
    const page = args.object;
    page.bindingContext = viewModel; // Set the binding context to the view model

    // TODO Check if Bluetooth on the device is enabled?!

    // Request permissions
    try {
        await doRequestPermissions();
        console.log("All permissions granted");

        // Start scanning for devices after permissions are granted
        scanAndConnect();
    } catch (error) {
        console.error("Error while requesting permissions: ", error);
    }
};

async function scanAndConnect() {

    try {
        viewModel.isConnecting = true;
        viewModel.connectionStatus = `Scanning for Locker device with service UUID ${LOCKER_MAIN_SERVICE_UUID}`;
        console.log(`Scanning for Locker device with service UUID ${LOCKER_MAIN_SERVICE_UUID}`);
        // Scan for devices with the matching SERVICE_UUID
        await bluetooth.startScanning({
            serviceUUIDs: [LOCKER_MAIN_SERVICE_UUID],
            seconds: 20,
            onDiscovered: (peripheral) => {
                if (peripheral.name && peripheral.name.startsWith("MyESP32Locker")) {
                    console.log("Discovered Locker device");
                    viewModel.connectionStatus = "Discovered Locker. Now Connecting...";
                    console.log("Peripheral object attributes:")
                    console.dir(peripheral)
                    connectedPeripheral = peripheral;
                    bluetooth.stopScanning();
                    connectToLocker(peripheral);
                }
            },
            onError:(error)=> {
                console.error("Error during BLE scan: ", error);
                viewModel.connectionStatus = "Could not find the device. Try again.";
                viewModel.isConnecting = false;
                bluetooth.stopScanning();

            }
        });
    } catch (error) {
        console.error("Error during BLE scan: ", error);
        viewModel.connectionStatus = "Error scanning for devices.";
        viewModel.isConnecting = false;
    }
}

// Function to connect to the Locker device
async function connectToLocker(peripheral) {
    try {
        console.log("Connecting to peripheral")
        viewModel.connectionStatus = "Connecting to Locker...";

        await bluetooth.connect({
            UUID: peripheral.UUID,
            onConnected: (peripheral) => {
                console.log("Connected successfuly to "+ peripheral.name)

                console.log("Periperhal connected with name: " + peripheral.name);

                // // the peripheral object now has a list of available services:
                // peripheral.services.forEach(function(service) {
                //     console.log("service found: " + JSON.stringify(service));
                // });< 

                viewModel.connectionStatus = "Connected to " + peripheral.name;
                viewModel.isConnecting = false;
            },
            onDisconnected: () => {
                console.log("Disconnected successfuly to "+ peripheral.name)
                viewModel.connectionStatus = "Disconnected from " + peripheral.name;

            }
        });
    } catch (error) {
        console.error("Error connecting to peripheral: ", error);
        viewModel.connectionStatus = "Connection failed.";
        viewModel.isConnecting = false;
    }
}

// Functions to send commands to the ESP32
async function sendCommand(command) {
    try {
        if (connectedPeripheral) {

            console.log("Sending command:")
            console.log("connectedPeripheral.UUID "+connectedPeripheral.UUID)
            console.log("LOCKER_MAIN_SERVICE_UUID "+LOCKER_MAIN_SERVICE_UUID)
            console.log("CHARACTERISTIC_SERVO_COMMANDS_UUID "+CHARACTERISTIC_SERVO_COMMANDS_UUID)
            console.log("[command] "+command)
            console.log("[command as Uint8Array] "+new Uint8Array([command]) ) // Use Uint8Array directly

            await bluetooth.write({
                peripheralUUID: connectedPeripheral.UUID,  // Use peripheralUUID instead of UUID
                serviceUUID: LOCKER_MAIN_SERVICE_UUID,
                characteristicUUID: CHARACTERISTIC_SERVO_COMMANDS_UUID,
                value: [0]
            });
            console.log(`Command ${command} sent to Locker.`);
        } else {
            console.warn("No device connected.");
        }
    } catch (error) {
        console.error("Error sending command: ", error);
    }
}

// Export functions for button taps
exports.onNavigatingTo = onNavigatingTo;

exports.onScanAndConnectTap = ()=> scanAndConnect();
exports.onOpenButtonTap = () => sendCommand(0); // Send 0 to open
exports.onCloseButtonTap = () => sendCommand(180); // Send 180 to close

Error I receive:

Sending command:
Error sending command: [BluetoothError]:service_not_found,arguments: {"peripheralUUID":"08:B6:1F:28:62:86","serviceUUID":"180e","characteristicUUID":"c18c2e14-0b84-4b1e-8c9e-2195c62ee5e2","value":[0]}

Additional Notes:

nrfconnect

I would greatly appreciate any help or suggestions on how to resolve this issue. Thank you in advance!

farfromrefug commented 1 month ago

@luigi7up you need to discover services first. This is not done automatically (or can using a connect parameter). Without this services cant be used

luigi7up commented 1 month ago

@farfromrefug hi, thank you for your reply... I¡m sorry for not providing the whole code where you can see that I'm calling the bluetooth.startScanning({}) method

I'll update my question above

farfromrefug commented 1 month ago

@luigi7up i am.not talking about scanning. I am talking about discovering services and characteristics. It is mandatory. Look at the typings for how to do it either manually or through the connect method

luigi7up commented 1 month ago

@farfromrefug Hi! Thanks a lot for your help. It wasn’t immediately clear to me that even though I knew my SERVICE UUID, I couldn’t send commands to it without first discovering it during the connect(). Following your guidance, I was able to solve the issue by adding theautoDiscoverAll: true parameter to the bluetooth.connect({}) call.

One thing I found a bit confusing is the lack of mention of service discovery in the README. If it’s necessary to discover services before interacting with them, shouldn’t the documentation include this process?

I’d be happy to contribute by opening a PR to improve this, but I want to fully understand it before doing so.

await bluetooth.connect({
            UUID: peripheral.UUID,
            autoDiscoverAll: true, // this is the line I added
            onConnected: (peripheral) => {
                peripheral.services.forEach(function (service) {
                    console.log("service found: " + JSON.stringify(service));
                });

            },
            onDisconnected: () => {
                //console.log("Disconnected successfuly to "+ peripheral.name);            
                //...
            }
        });

Now, the peripheral object holds services object and now you can use bluetooth.write({})

await bluetooth.write({
                peripheralUUID: connectedPeripheral.UUID,  // Use peripheralUUID instead of UUID
                serviceUUID: MY_SERVICE_UUID,
                characteristicUUID: CHARACTERISTIC_SERVO_COMMANDS_UUID,
                value: [command]
            });
farfromrefug commented 1 month ago

@luigi7up you are right not everything is in the README. Please open A PR if you want to update it. This is the file to modify https://github.com/nativescript-community/ble/blob/master/packages/ble/blueprint.md. Keep in mind that i want users to actually read platform native docs. BLE is very specific to platforms and i dont think you can really start working with it using the N plugin if you dont know how it works on the native side. Thanks!

luigi7up commented 1 month ago

Hey! Sure. Would you mind pointing me in the direction of the platform native docs you had in mind. I assume you mean, for example, the docs for Android/BLE https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview and the same for iOS ?

luigi7up commented 1 month ago

Hey! Sure, I’ll definitely take a look at the file and work on a PR to update the README. I agree that having a solid understanding of the platform-native BLE docs is important. Just to clarify, are you referring to docs like Android BLE and the iOS equivalents? If so, I can see how they’re essential for learning the core concepts, but the code differs quite a bit from working with this plugin.

I think adding some references or context in the README would still be valuable to bridge that gap for users who might not have much experience with native BLE development. Thanks for the guidance!

farfromrefug commented 1 month ago

@luigi7up maybe this one is better https://developer.android.com/develop/connectivity/bluetooth/ble/connect-gatt-server But yes those are the docs i am referring too