capacitor-community/bluetooth-le

connection timout on write function regardless of timout option supplied.

kairos0ne opened this issue · 4 comments

Describe the bug

I am running Moddable on a ESP32-S3 TFT

/*
 * Copyright (c) 2016-2019  Moddable Tech, Inc.
 *
 *   This file is part of the Moddable SDK.
 * 
 *   This work is licensed under the
 *       Creative Commons Attribution 4.0 International License.
 *   To view a copy of this license, visit
 *       <http://creativecommons.org/licenses/by/4.0>.
 *   or send a letter to Creative Commons, PO Box 1866,
 *   Mountain View, CA 94042, USA.
 *
 */
 /*
 	Nordic UART Service Server 
	The Nordic UART Service is a simple GATT-based service with TX and RX characteristics.
	Strings written to the UART Service server are echoed back to the client as characteristic notifications.

	https://devzone.nordicsemi.com/f/nordic-q-a/10567/what-is-nus-nordic-uart-service
 */

import BLEServer from "bleserver"; // import the bleserver module
import {uuid} from "btutils"; // import the uuid module";

// Import the tricolor led module
import Digital from "pins/digital";
import Timer from "timer";
import NeoPixel from 'neopixel';
import Pins from 'pins/digital'; // import the pins module


const SERVICE_UUID = uuid`6E400001-B5A3-F393-E0A9-E50E24DCCA9E`; // create a uuid for the service

// create a uuid for the characteristic
const UART_SERVER = "UART Server"; // create a name for the service

// pin for the neopixel
const neopixelPin = 10;

let neopixel = new NeoPixel({length: 12,  pin: neopixelPin, order: "GRB"}); 

const red = neopixel.makeRGB(255, 0, 0);
const yellow = neopixel.makeRGB(255, 255, 0);
const orange = neopixel.makeRGB(255, 165, 0);
const purple = neopixel.makeRGB(128, 0, 128);
const pink = neopixel.makeRGB(255, 192, 203);
const cyan = neopixel.makeRGB(0, 255, 255);
const magenta = neopixel.makeRGB(255, 0, 255);
const green = neopixel.makeRGB(0, 255, 0);
const blue = neopixel.makeRGB(0, 0, 255);
const white = neopixel.makeRGB(255, 255, 255);
const black = neopixel.makeRGB(0, 0, 0);

function setAll(color) {
	for (let i = 0; i <= neopixel.length; i++) {
		neopixel.setPixel(i, color);
	}
	neopixel.update({ length: neopixel.length, start: 0, end: neopixel.length, repeat: 0, delay: 0, offset: 0, count: neopixel.length });
}

function setPixel(pixel, color) {
	neopixel.setPixel(pixel, color);
	neopixel.update();
}

function clearAll() {
	setAll(black);
}

function setAllColor(color) {
	setAll(color);
}


class UARTServer extends BLEServer {
	onReady() {
		this.deviceName = "VEGAONE";
		this.onDisconnected();
	}
	onConnected() {
		this.stopAdvertising();
	}
	onDisconnected() {
		delete this.tx;
		this.startAdvertising({
			advertisingData: {flags: 6, completeName: this.deviceName, completeUUID128List: [SERVICE_UUID]}
		});
	}
	onCharacteristicNotifyEnabled(characteristic) {
		this.tx = characteristic;
	}
	onCharacteristicNotifyDisabled(characteristic) {
		delete this.tx;
	}
	onPasskeyConfirm(connection, passkey) {
		trace("Confirm connection: " + connection + " Passkey: " + passkey + "\n");
	}
	onAuthenticationComplete(connection, result) {
		trace("Authentication complete connection: " + connection + " result: " + result + "\n");
	}
	onCharacteristicRead(characteristic) {
		trace("Read characteristic: " + characteristic + "\n");
	}

	onCharacteristicWritten(characteristic, value) {
		const timeoutMs = 5000; // Set the timeout value in milliseconds (e.g., 5000 for 5 seconds)
		let timeoutId;

		const handleTimeout = () => {
			// Handle the timeout event here
			// You can perform any necessary actions when the write operation times out
			// For example, you could log an error message or retry the operation
			trace("Timeout\n");

			// Example actions for a timeout:

			// 1. Log an error message
			trace("Error: Write operation timed out\n");
			// 2. Retry the operation

			// 2. Retry the operation
			// Retry the write operation here
		    if (this.tx) {
				this.notifyValue(this.tx, value);
			}

			// 3. Clear the timeout and handle the write operation
			// Clear the timeout and handle the write operation
			clearTimeoutAndHandleWrite();
			
		};

   		const clearTimeoutAndHandleWrite = () => {
        Timer.clear(timeoutId); // Clear the timeout if the write operation completes within the timeout duration

        // Perform your existing logic for handling the write operation here
        // This code block includes the existing if statements and actions based on the value
		trace.left(value, UART_SERVER);
		if (this.tx) {
			this.notifyValue(this.tx, value);
			trace("value: " + value);
		}

        // Example actions for a value of 2:
		if (value == 1) {
			clearAll();

			// Set a timeout for the write operation
			timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 2) {
			trace("two: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(red);

			// Set a timeout for the write operation
			timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 3) {
			trace("three: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(green);

				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 4) {
			trace("four: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(blue);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 5) {
			trace("five: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(yellow);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 6) {
			trace("six: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(orange);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 7) {
			trace("seven: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(purple);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 8) {
			trace("eight: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(pink);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 9) {
			trace("nine: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(cyan);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}

		if (value == 10) {
			trace("ten: " + value);
			// this.notifyValue(this.tx, value);
			setAllColor(magenta);
				// Set a timeout for the write operation
				timeoutId = Timer.set(() => handleTimeout(), timeoutMs);
		}
    };

    // Set the timeout and handle the write operation
    timeoutId = Timer.set(handleTimeout, timeoutMs);
    clearTimeoutAndHandleWrite();

	// Note: If you are using multiple characteristics, you can use a different timeout value for each characteristic
	// For example, you could set a longer timeout for a characteristic that requires more time to process the data
	
	}
}
let server = new UARTServer;

and Im running capacitor in vue with the following code in my component:

<template>
  <div class="connection">
    <v-btn v-if="!device" block class="mb-5" @click="scanDevices()">
      <v-spacer></v-spacer>
      <v-icon>mdi-bluetooth</v-icon> Connect to device
      <v-spacer></v-spacer>
    </v-btn>
    <h3 class="mb-5">Connected devices</h3>
    <v-card class="mx-auto elevation-5 ma-2" v-if="device">
      <v-list-item three-line class="mt-5">
        <v-list-item-media>
          <div class="d-flex">
            <div class="flex-fill">
              <v-list-item-title @click="dialog = !dialog" class="headline mb-1">{{device.localName}}</v-list-item-title>
              <v-list-item-subtitle @click="dialog = !dialog">{{device.device.deviceId}}</v-list-item-subtitle>
            </div>
            <div>
              <v-icon size="50" class="mr-5 mb-5" large>mdi-connection</v-icon>
            </div>
          </div>
        </v-list-item-media>
      </v-list-item>

      <v-divider></v-divider>

      <v-card-actions>
        <v-btn @click="disconnect(device)" text> <v-icon class="mr-5">mdi-lan-connect</v-icon> Disconnect</v-btn>
      </v-card-actions>
    </v-card>
    <div v-else class="text-center">
      No data available
    </div>

    <h3 v-if="devices != null || devices != undefined" class="mb-5">Available devices</h3>
    <div v-if="devices">

      <v-card  class="mx-auto elevation-5 ma-2" v-for="device in devices" :key="device.id">
        <v-list-item three-line class="mt-5">
          <v-list-item-media>
            <div class="d-flex">
              <div class="flex-fill">
                <v-list-item-title class="headline mb-1">{{device.localName}}</v-list-item-title>
                <v-list-item-subtitle>{{device.device.deviceId}}</v-list-item-subtitle>
              </div>
              <div>
                <v-icon size="50" class="mr-5 mb-5" large>mdi-connection</v-icon>
              </div>
            </div>
          </v-list-item-media>
        </v-list-item>

        <v-divider></v-divider>

        <v-card-actions>
          <v-btn @click="join(device)" text> <v-icon class="mr-5">mdi-lan-connect</v-icon> Connect</v-btn>
        </v-card-actions>
      </v-card>
    </div>

    <!-- Create a dialog for when the user clicks the connected microcontroller -->
    <v-dialog v-model="dialog" max-width="500px" class="my-10">
      <v-card>
        <v-card-title>
          <span class="headline">Device information</span>
        </v-card-title>

        <v-card-text>
          <v-container>
            <v-row>
              <v-col cols="12" sm="6" md="4">
                <v-text-field v-model="bleDeviceId" label="Device ID" readonly></v-text-field>
              </v-col>
              <v-col cols="12" sm="6" md="4">
                <v-text-field v-model="bleDeviceName" label="Device name" readonly></v-text-field>
              </v-col>
              <v-col cols="12" sm="6" md="4">
                <v-text-field v-model="bleDeviceServices" label="Services" readonly></v-text-field>
              </v-col>
            </v-row>
          </v-container>
        </v-card-text>

        <v-form>
          <v-text-field
            v-model="writeData"
            label="UART Service"
            type="number"
            hint="Enter a number"
            persistent-hint
            clearable
          />
          <v-btn @click.once="write(writeData)" block>
            Send data
          </v-btn>
        </v-form>

        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="blue darken-1" text @click="dialog = false">Close</v-btn>
        </v-card-actions>

      </v-card>
    
    </v-dialog>
  </div>
</template>

<script>
  // Implement capacitor plugin ble 
  // scan for devices

  import { BleClient } from '@capacitor-community/bluetooth-le';

  // import { Capacitor } from '@capacitor/core';
  // import { Plugins } from '@capacitor/core';
  // const { BluetoothLe } = Plugins;

  const tx_characteristic = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E';
  const rx_characteristic = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E';


  

  export default {
    name: 'ConnectionComponent',
    components: {
      // HelloWorld
    },
    data: () => ({
      writeData: null,
      dialog: false,
      device: null,
      devices: [],
      bleClient: null,
      bleServer: null,
      bleDevice: null,
      bleDeviceId: null,
      bleDeviceName: null,
      bleDeviceServices: null,
      bleDeviceCharacteristic: null,
    }),
    computed: {
      canvasWidth() {
        return (this.$vuetify.display.width - 40)
      }
    },
    methods: {
      async scanDevices() {
        console.log('scan devices');

        // Set UART service UUID
        const UART_SERVICE = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E'; 
        this.devices = [];
        await BleClient.requestLEScan(
        {
          services: [UART_SERVICE],
          name: "VEGAONE" // Specify the desired device name
        },
        (result) => {
          console.log('received new scan result', result);
          // this.devices.push(result);
          this.devices = [...this.devices, result];
        },
        (error) => {
          console.log('error while scanning', error);
        },
      );
    },
    async join(device) {

      console.log('join device', device);
      this.bleDeviceId = device.device.deviceId;
      this.bleDeviceName = device.localName;
      console.log('ble device to connect', device);
      this.device = device;
      // Connect to the device
      await BleClient.connect(device.device.deviceId);
      console.log('connected to device', device);

      // Get the services
      const services = await BleClient.getServices(this.bleDeviceId);
      console.log('get services', services);

      this.bleDeviceServices = services;

      // Get the characteristics

      // const characteristics = await BleClient.getCharacteristics(this.bleDeviceId);

      // Show the dialog
      console.log('show dialog');
      this.dialog = true;
      // Remove the device from the list of available devices
      this.devices = this.devices.filter((d) => d.device.deviceId !== device.device.deviceId);
    },
    async disconnect(device) {
      console.log('disconnect');
      await BleClient.disconnect( device.device.deviceId );
      // clear data 
      this.bleDeviceId = null;
      this.bleDeviceName = null;
      this.bleDeviceServices = null;
      this.bleDevice = null;
      this.device = null;
    },
    
    async write(input) {
      console.log('send');

      // Function to convert string to Uint8Array using TextEncoder
      function stringToUint8Array(string) {
        const encoder = new TextEncoder();
        return encoder.encode(string);
      }

       // Input is a string
          const data = stringToUint8Array(input);

          // set options 
          const options = {
            timeout: 5000,
          };

      try {
        await BleClient.write (this.bleDeviceId, this.bleDeviceServices[0].uuid, rx_characteristic, data, options);
        this.writeData = null;
        console.log('data sent');
      } catch (error) {
        console.error('Error sending data:', error);
      }
    },

    async read() {
      console.log('read');
      await BleClient.read(this.bleDeviceId, this.bleDeviceCharacteristic);
    },
    async subscribe() {
      console.log('subscribe');
      await BleClient.subscribe(this.bleDeviceId, this.bleDeviceCharacteristic, (result) => {
        console.log('received new subscribe result', result);
        this.bleDeviceCharacteristicValue = result.value;
      });
    },
    async unsubscribe() {
      console.log('unsubscribe');
      await BleClient.unsubscribe(this.bleDeviceId, this.bleDeviceCharacteristic);
    },
    async readDescriptor() {
      console.log('read descriptor');
      await BleClient.readDescriptor(this.bleDeviceId, this.bleDeviceCharacteristic, this.bleDeviceCharacteristicDescriptor);
    },
    async writeDescriptor() {
      console.log('write descriptor');
      const data = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
      await BleClient.writeDescriptor(this.bleDeviceId, this.bleDeviceCharacteristic, this.bleDeviceCharacteristicDescriptor, data);
    },
    async readRssi() {
      console.log('read rssi');
      await BleClient.readRssi(this.bleDeviceId);
    },
    async requestConnectionPriority() {
      console.log('request connection priority');
      await BleClient.requestConnectionPriority(this.bleDeviceId, 'high');
    },
    async requestMtu() {
      console.log('request mtu');
      await BleClient.requestMtu(this.bleDeviceId, 512);
    },
    async refreshCache() {
      console.log('refresh cache');
      await BleClient.refreshCache(this.bleDeviceId);
    },
  },
  created() {
    console.log('created');
    BleClient.initialize();
  },
  mounted() {
    console.log('mounted');
    // initialize ble client
    if (this.bleDeviceId) {
      BleClient.getConnectedDevices({services: [this.bleDeviceId]}, (result) => {
        console.log('get connected devices', result);
        this.device = result;
      },
      (error) => {
      console.log('error while getting connected devices', error);
      },
    );
    } else {
      console.log('no ble device id');
    }
  },
  // beforeUnmount() {
  //   console.log('before destroy');
  //   // disconnect from device
  //   BleClient.disconnect( this.bleDeviceId );
  //   // clear data 
  //   this.bleDeviceId = null;
  //   this.bleDeviceName = null;
  //   this.bleDeviceServices = null;
  //   this.bleDevice = null;
  // },
  watch: {
    bleDeviceId: function (val) {
      console.log('ble device id changed', val);
    }
  }
};
</script>

Im getting disconnected on write while and Im not sure why it seems fine when i use the bluefruit app on the uart service.

The errors reported are:

[log] - show dialog
⚡️  To Native ->  BluetoothLe write 16540818
⚡️  [log] - send
⚡️  BluetoothLe - Resolve onDisconnected|D0746ABE-530A-9085-FAE6-B2740050BCA1 Disconnected.
⚡️  BluetoothLe - The connection has timed out unexpectedly.
⚡️  BluetoothLe - Reject write|6E400001-B5A3-F393-E0A9-E50E24DCCA9E|6E400002-B5A3-F393-E0A9-E50E24DCCA9E Write timeout.
ERROR MESSAGE:  {"message":"Write timeout.","errorMessage":"Write timeout."}
⚡️  [error] - {"message":"Write timeout.","errorMessage":"Write timeout."}
⚡️  [error] - Error sending data: {"errorMessage":"Write timeout."}

Expected behavior
A clear and concise description of what you expected to happen.

I don't expect the timeout and the timeout seems to happen immediately regardless of timeout option supplied

Screenshots
If applicable, add screenshots to help explain your problem.

Plugin version:

  • @capacitor-community/bluetooth-le:latest

Desktop (please complete the following information):

  • OS: IOS

Smartphone (please complete the following information):
iPhone 11
latest OS

pwespi commented

Do you know if the same happens on Android?

If you call getServices, do you see the service and characteristic and does it have write property true?

I know that the blufruit app (ios)writes to uart perfectly fine and no disconnection so there nothing wrong with moddable code used if thats useful, I have not yet tested android as i have none but will get hold of one and test, np

I clicked that button accidentally

This can be closed as it was a hardware issue - works fine on a another esp32-s3 I have.