C++ windows соединение с Bluetooth HC-06 по сокетам

Интернет полон примеров как соединяться с bluetooth из Android, но когда я попытался связаться с ардуинкой через bluetooth-модуль HT-06 с ноутбука под управлением Windows, то сколько не искал - не смог найти внятного работающего примера который бы просто… работал. Есть примеры под Linux (к Windows не подходят), есть примеры для ESP32 (не работают для HT-06, о чём ниже), поэтому мне пришлось потратить довольно приличное время пытаясь выпрямить примеры и добиться работоспособности под Windows.
И оформил это всё в класс BTSerialClient которым можно взять и пользоваться сразу.
Пример использования:

#include "btserialclient.h"
#include <iostream>
#include <algorithm>

std::string trim( std::string const &src )
{
	auto begin_ns = std::find_if_not( src.begin(), src.end(), ::isspace );
	auto end_ns = src.end();
	if ( begin_ns != src.end() ) 
	{
		end_ns = std::find_if_not( src.rbegin(), src.rend(), ::isspace ).base();
	}
	return std::string(begin_ns, end_ns);
}

int main()
{
	BTSerialClient::Infos devices = BTSerialClient::scanDevices();
	for ( auto &device : devices )
	{
		std::wcout << L"'" << device.name << L"' auth: " << device.authenticated << L" addr: " << std::hex << device.address << std::dec << L"\n";
	};
	BTSerialClient btSerial;
	int connectError = btSerial.connect( devices, L"alx-hc-06" );
	if ( connectError != 0 )
	{
		std::wcout << L"Connect error: " << connectError << L" wsaError: " << btSerial.getWSAError() << L"\n";
		return 1;
	};
	std::wcout.flush();
	std::string cmd;
	while ( true )
	{
		std::cout << "Enter command: ";
		std::getline( std::cin, cmd );
		cmd = trim( cmd );
		if ( cmd == "" )
		{
		}
		else if ( cmd == "exit" )
		{
			break;
		}
		else
		{
			if ( !btSerial.write( cmd ) )
			{
				std::cout << "Write error: " << btSerial.getWSAError() << "\n";
				break;
			}
		};
		if ( btSerial.available() )
		{
			std::cout << "Got message: " << btSerial.readString() << "\n";
		};
	};
	return 0;
};

Консольное приложение (испорченное этим фактом по сути в том, что может принимать данные только когда пользователь завершит вводить очередную строку, но это не проблема библиотеки).
Причём заметьте, что в начале при поиске устройств и выходу на нужное устройство по имени используются длинные строки wchar_t (литералы префиксируются L), ибо именно в своём аналоге юникода и работает WinAPI, но когда уже общаемся с устройством - переключаемся на char-строки и соответствующие классы.
Надо линковать с библиотеками ws2_32, bthprops и uuid.
Причём последняя необязательна в MSVC как я понял из интернета, но я делал под mingw64 и результирующая строка для компиляции следующая:
g++ btserialclient.cpp main.cpp -lws2_32 -lbthprops -luuid -o btscan.exe

Перед использованием надо подключить bluetooth устройство в менеджере устройств Windows - и тут обратите внимание, что HT-06 виден в двух экземплярах - один из них это LE (Low energy) и он не подойдёт - нужен тот экземпляр который будет просить ввести пароль (по дефолту 1234)! У меня он в списке появляется первым.
После этого уже всё начинает работать и можно прототипировать общение с ардуинкой через bluetooth прямо с ноутбука.

Библиотека состоит из двух файлов:
btserialclient.h

#ifndef BT_SERIAL_CLIENT_H
#define BT_SERIAL_CLIENT_H

#include <Winsock2.h>
#include <Ws2bth.h>
#include <BluetoothAPIs.h>

#include <vector>
#include <string>

class BTSerialClient
{
private:
	static int wsaInitError;
	static int instanceCount;

	SOCKET btSocket = INVALID_SOCKET;
	int wsaError = 0;

	void wsaInit();
	void wsaDone();
public:
	// Information about discovered bluetooth device.
	// ------------------------------------------------------------------
	// Информация об обнаруженном устройстве bluetooth.
	struct Info
	{
		std::wstring name;
		bool authenticated;
		BTH_ADDR address;
	};
	typedef std::vector<Info> Infos;

	// Constructors/destructors just get care of WSA initialization/deinialization.
	// ------------------------------------------------------------------
	// Конструкторы/деструкторы просто обеспечивают инициализацию/деинициализацию WSA.
	BTSerialClient()
	{
		if ( instanceCount == 0 )
			wsaInit();
		instanceCount++;
	};
	~BTSerialClient()
	{
		disconnect();
		instanceCount--;
		if ( instanceCount == 0 )
			wsaDone();
	}

	// Scans for available devices and returns std::vector<Info>.
	// Note: this static method can be used before creation of first instance of class.
	// ------------------------------------------------------------------
	// Сканирует доступные устройства и возвращает std::vector<Info>.
	// Важно: этот статический метод может быть использован до создания первого экземпляра класса.
	static Infos scanDevices();
	
	// First instance of BTSerialClient initializes WSA.
	// If getWSAInitError() returns non-zero then this process is failed.
	// ------------------------------------------------------------------
	// Первый экземпляр BTSerialClient инициализирует подсистему WSA.
	// Если getWASInitError() возвращает не ноль, то значит инициализация не удалась.
	int getWSAInitError() { return wsaInitError; };

	// If there was socket error getWSAError() returns WSA error code.
	// getWSAError() returns 0 if there is no socket error.
	// ------------------------------------------------------------------
	// При возникновении ошибок сокета getWSAError() возвращает код ошибки WSA.
	// getWSAError() возвращает 0 если нет ошибки сокета.
	int getWSAError() { return wsaError; };

	// Connects socket to address. Returns true on success.
	// Returns false in case of socket error (use getWSAError() to get error code).
	// ------------------------------------------------------------------
	// Соединяет сокет по адресу. Возвращает true при успехе.
	// Возвращает false при ошибке сокета (используйте getWSAError() для кода ошибки).
	bool connect( BTH_ADDR addr );

	// Connects socket to device with specified name from devices list.
	// Returns:
	//	0 - success
	//	1 - socket error (use getWSAError() to get error code)
	//	2 - device not found or not authenticated
	// ------------------------------------------------------------------
	// Соединяет сокет с устройством с указанным именем из списка устройств.
	// Возвращает:
	//	0 - успех
	//	1 - ошибка сокета (используйте getWSAError() для кода ошибки)
	//	2 - устройство не найдено или не аутентифицировано
	int connect( const Infos &devices, const std::wstring &name );

	// Scans for devices and connects socket to device with specified name.
	// Returns:
	//	0 - success
	//	1 - socket error (use getWSAError() to get error code)
	//	2 - device not found or not authenticated
	// ------------------------------------------------------------------
	// Выполняет поиск устройств и соединяет сокет с устройством с указанным именем.
	// Возвращает:
	//	0 - успех
	//	1 - ошибка сокета (используйте getWSAError() для кода ошибки)
	//	2 - устройство не найдено или не аутентифицировано
	int connect( const std::wstring &name );

	// Writes count bytes from buffer to socket. Returns true on success.
	// Returns false in case of socket error (use getWSAError() to get error code).
	// ------------------------------------------------------------------
	// Записывает count байт из буфера в сокет. Возвращает true при успехе.
	// Возвращает false при ошибке сокета (используйте getWSAError() для кода ошибки).
	bool write( const char *bytes, size_t count );

	// Writes std::vector<char> to socket. Returns true on success.
	// Returns false in case of socket error (use getWSAError() to get error code).
	// ------------------------------------------------------------------
	// Записывает std::vector<char> в сокет. Возвращает true при успехе.
	// Возвращает false при ошибке сокета (используйте getWSAError() для кода ошибки).
	bool write( const std::vector< char > &data )
	{
		return write( &data[ 0 ], data.size() );
	};

	// Writes std::string to socket. Returns true on success.
	// Returns false in case of socket error (use getWSAError() to get error code).
	// ------------------------------------------------------------------
	// Записывает std::string в сокет. Возвращает true при успехе.
	// Возвращает false при ошибке сокета (используйте getWSAError() для кода ошибки).
	bool write( const std::string &data )
	{
		return write( data.c_str(), data.size() );
	};

	// Returns true if there is data to receive from socket.
	// ------------------------------------------------------------------
	// Возвращает true если есть данные для получения из сокета.
	bool available();

	// Reads data from socket to buffer up to count bytes.
	// Returns:
	//	>0 - number of bytes read
	//	 0 - there is no data to receive
	//	-1 - socket error (use getWSAError() to get error code)
	// Note: this method doesn't return WSAEWOULDBLOCK as an error, but as result of 0!
	// ------------------------------------------------------------------
	// Читает данные из сокета в буфер не более count байт.
	// Возвращает:
	//	>0 - число прочитанных байт
	//	 0 - нет данных для получения
	//	-1 - ошибка сокета (используйте getWSAError() для кода ошибки)
	// Важно: этот метод не возвращает WSAEWOULDBLOCK как ошибку, но как результат 0!
	int readBuffer( char *buffer, int count );

	// Reads std::string from socket. 
	// if there is not data to read or socket error returns empty string.
	// Use wsaGetError() to distinguish these cases if needed.
	// ------------------------------------------------------------------
	// Считывает std::string из сокета. 
	// Если нет данных или произошла ошибка сокета - возвращает пустую строку..
	// Используйте wsaGetError() чтобы различить эти случаи, если необходимо.
	std::string readString();

	// Disconnects socket. Always returns true.
	// ------------------------------------------------------------------
	// Отсоединяет сокет. Всегда возвращает true.
	bool disconnect();
};

#endif

btserialclient.cpp

#include "btserialclient.h"

/*static*/ int BTSerialClient::instanceCount = 0;
/*static*/ int BTSerialClient::wsaInitError = 0;

void BTSerialClient::wsaInit()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	wVersionRequested = MAKEWORD( 2, 2 );
	wsaInitError = WSAStartup( wVersionRequested, &wsaData );
};

void BTSerialClient::wsaDone()
{
	if ( wsaInitError == 0 )
	{
		WSACleanup();
	};
};

/*static*/ BTSerialClient::Infos BTSerialClient::scanDevices()
{
	Infos result;
	
	BLUETOOTH_DEVICE_SEARCH_PARAMS btDeviceSearchParameters =
	{
		sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS),
		1, // authenticated devices                
		0, // remembered devices
		0, // unknown devices
		0, // connected devices
		1, // issue inquery
		2, // timeout multipler (1.28 seconds times).
		NULL // radio handler
	};
	BLUETOOTH_DEVICE_INFO btDeviceInfo = { sizeof(BLUETOOTH_DEVICE_INFO), 0 };
	HBLUETOOTH_DEVICE_FIND btDevice = NULL;
	btDevice = BluetoothFindFirstDevice(&btDeviceSearchParameters, &btDeviceInfo);
	if (btDevice)
	{
		Info info;
		do
		{
			info.name = btDeviceInfo.szName;
			info.authenticated = btDeviceInfo.fAuthenticated;
			info.address = btDeviceInfo.Address.ullLong;
			result.push_back(info);
		} while (BluetoothFindNextDevice(btDevice, &btDeviceInfo));
		BluetoothFindDeviceClose(btDevice);
	}

	return result;
};

bool BTSerialClient::connect( BTH_ADDR addr )
{
	SOCKADDR_BTH btSocketAddress;
	btSocket = socket( AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM );
	if ( btSocket == INVALID_SOCKET )
	{
		wsaError = WSAGetLastError();
		return false;
	};
   	memset( &btSocketAddress, 0, sizeof( btSocketAddress ) );
	btSocketAddress.addressFamily = AF_BTH;
	//btSocketAddress.serviceClassId = RFCOMM_PROTOCOL_UUID; // Some
	btSocketAddress.serviceClassId = SerialPortServiceClass_UUID;
	btSocketAddress.port = 0;
	btSocketAddress.btAddr = addr;
	if ( ::connect( btSocket, (SOCKADDR*) &btSocketAddress, sizeof( btSocketAddress ) ) != 0 )
	{
		wsaError = WSAGetLastError();
		return false;
	}
	unsigned long nonBlockingMode = 1;
	if ( ioctlsocket( btSocket, FIONBIO, (unsigned long*) &nonBlockingMode ) != 0 )
	{
		wsaError = WSAGetLastError();
		disconnect();
		return false;
	};
	return true;
};

int BTSerialClient::connect( const Infos &devices, const std::wstring &name )
{
	for ( auto &device : devices )
	{
		if ( (device.name == name) && (device.authenticated) )
		{
			if ( connect( device.address ) )
				return 0;
			else
				return 1;	// connect error
		};
	};
	return 2;	// not found	
};

int BTSerialClient::connect( const std::wstring &name )
{
	Infos devices = scanDevices();
	return connect( devices, name );
};

bool BTSerialClient::write( const char *bytes, size_t count )
{
	wsaError = 0;
	int sendResult = send( btSocket, (const char *) bytes, count, 0 );
	if ( sendResult == SOCKET_ERROR )
	{
		wsaError = WSAGetLastError();
		return false;
	}
	return true;
};

bool BTSerialClient::available()
{
	char buffer;
	int res = recv( btSocket, &buffer, 1, MSG_PEEK );
        return res > 0;
};

int BTSerialClient::readBuffer( char *buffer, int count )
{
	wsaError = 0;
	int res = recv( btSocket, (char *) buffer, count, 0 );
	if ( res == INVALID_SOCKET )
	{
		int error = WSAGetLastError();
		if ( error == WSAEWOULDBLOCK )
			return 0;	// no data available
		wsaError = error;
		return -1;
		
	}
	else if ( res == 0 )
	{
		wsaError = WSAENOTCONN; // socket is closed
		return -1;
	}
	return res;
};

std::string BTSerialClient::readString()
{
	std::string result;
	const int bufSize = 1024;
	char buffer[ bufSize ];
	int res = readBuffer( buffer, bufSize );
	if ( res > 0 )
		result.append( buffer, res );
	return result;
};

bool BTSerialClient::disconnect()
{
	if ( btSocket != INVALID_SOCKET )
	{
		shutdown( btSocket, SD_BOTH );
		closesocket( btSocket );
		btSocket = INVALID_SOCKET;
	};
	return true;
};

Лицензия - полный copyleft, с кодом можно делать что угодно без каких либо как гарантий так и обязательств, но упоминание автора приветствуется (ну хотя бы как aa_dav с arduino.ru).
Так же приветствуется проверка библиотеки на других устройствах и выявленные баги.

1 лайк

P.S.
Еще замечу, что в принципе возможно общение с Bluetooth-устройством посредством эмуляции COM-порта, что Windows как правило предоставляет из коробки, но это фактически регресс и неудобство - вместо получения списка устройств и выходу на нужное по имени начинаются ненужные приседания с необходимостью определять какой виртуальный ком-порт назначен устройству.
Здесь же мы просто перечисляем устройства, находим нужное по имени и общаемся напрямую без эволюционных хвостов COM-портов через настоящие сокеты по протоколу RFCOMM.
И вот тут есть интересное замечание - есть код в сети автор которого утверждает, что успешно работает с ESP32 когда использует в качестве ГУИДа протокола RFCOMM_PROTOCOL_UUID. У меня в коде сейчас строка с таким классом закомментирована, т.к. для HT-06 это не работало.
Вместо этого используется ГУИД SerialPortServiceClass_UUID - и вот он срабатывает как полагается. Именно этот же ГУИД используется в многочисленных примерах на Android так что я уверен, что код будет работать и с ESP32 тоже, т.к. андроид я успешно к этому микрику тоже подключал.
Но похоже на то, что ESP32 может работать и с первым упомянутым ГУИДом, возможно реализуя протокол RFCOMM более полным образом.
Так или иначе должно работать и с ESP32 и с HT-05. Буду рад если кто-нибудь кому библиотека пригодится подтвердит это.

все это уже немодно, щас главный тренд в блутус коммуникациях - использование глобальной таблицы атрибутов (GATT) с использованием дескрипторов из документации IEEE, обмен данными короткими порциями, стейтлесс запросы и ответы. Наверное, изза постоянно снижения мощности при обмене.

Спасибо.

Мне лично это нужно чтобы протокол общения с девайсом напрограммировать и отладить на ноутбуке прежде чем перецеплять управляющую логику на смартофон.
Гораздо удобнее на ноуте программировать и сразу видеть результат, чем на смартфон заливать постоянно пробы и ошибки.

aa_dav, грамотный ты парень. Вот только нафига ты ’ ;’ ставишь после закрывающих фигурных скобок? Ну просто коробит.(

следы яваскрипта вижу я в этом :sunglasses:

Странный вы народ – следопыты. Вили то, чего никто другой не видит.

Вы не викинг, случаем. Это ведь у них …

Мне любопытнее, почему на “правило пяти” плюёт, но спрашивать не стал. Да и … в Ардуино не принято об этом заморачиваться (ну, кроме штатных библиотек, тип String – там всё нормально).

то не я, то яваскрипт странный. там можно объявить функцию как локальную переменную, прямо в коде, и переопределить ее тоже можно, ну а в конце операции почти везде в нормальных ЯП ставится точка с запятой:

var s = function(a,b) {
return a + b;
};

Ну это специфика для плюсов, а мне тут больше синтаксис бросается в глаза. Типа, ну как может такой грамотный чел, так безграмотно писать?

А пробелы ВНУТРИ скобок не покоробили?
:slight_smile:
А отсутствие _ перед членами класса не покоробило?
Ну это от неопытности.
Плюсы многогранны, программирование как море - плюнешь, а оно брызгами в лицо.

Тут есть нюанс. В некоторых языках точка с запятой – разделитель операций, а в других – часть синтаксиса операции. В большинство случаев эта тонкость никак не проявляется, но иногда … например, перед else в С и в Паскале.

А эта обсессивность RAII мне известна - это псевдоправило из плюсов на самом деле мракобесие когда дело доходит до реальной практики.
К плюсам всегда надо относится с изрядной долей скепсиса - к RAII в том числе.
То, что диктат RAII спорен и плох давно уже известно и сомнению не подлежит.
Подружить концепцию с исключениями так и не удалось никому в этом мире.
Не утверждаю правда что в этом куске кода сделал идеально, но просто потому что идеала нет.
Но RAII неосуществим - это надо бы уже всем понять.

Там где исключение не выбросить, а wasError() решает - там нет места RAII и всяким плюсовым идеалам.

Причём тут RAII? Каким она боком к “правилу пяти”?

Это правило не про RAII, а, например, про то, что Вашего клиента нельзя, например, создать в функции и вернуть из неё. Причём ладно бы было просто нельзя, это ещё пол беды, беда в том, что технически (синтаксически) можно! И если программист это сделает, компилятор пропустит, только работать будет через задницу. Если бы Вы более внимательно и профессионально относились к разработке, Вы бы либо сделали бы так, что создавать и возвращать можно, либо уж “нельзя так нельзя” (вызывает ошибку компиляции). Но для этого надо было соблюдать “правило пяти”.

Дополнение:
Вы будете смеяться, но я сейчас заглянул в статью про RAII в википедии (надеясь увидеть там привязку к правилу пяти, т.к. я решительно не понимаю откуда Вы её взяли) и знаете, что увидел? Никакой привязки там нет, зато в приведённом там примере класса file это правило тоже не соблюдено и у них те же проблемы, что и у Вас - экземпляр класса нельзя вернуть из функции (хотя, синтаксически можно).

Ну потому что все проблемы типа “кто и когда освободит ресурсы” вызваны RAII - правило из плюсов что это должен сделать консруктор, а деструктор освободить.
В плюсах конструкторы и деструкторы - это те места которые ДОЛЖНЫ в соответсвии с идиомой заниматься этими вещами, но конструкторы не могут возвращать кода ошибки по архитектуре откуда начинают появляться приседания многочисленные с в том числе конструкторами копирования.
Это на самом деле кодинг вокруг кодинга - когда конструктор обязан бросать исключение, но чуть позже выясняется, что из-за глубоко сидящей внутри кривизны деструктор этого делать по правильному не может.
Короче просто лучше не связываться с этим и писать как проще.

Но согласен с тем, что надо было запретить конструкторы копирования - тут да, плюсы ставят подножки в таких вещах и чтобы нельзя было накосячить возвращая объект из функции надо было = delete проставить. Согласен, что предотвратило бы неправильно использование.

Не понял для чего Вы мне это написали, а, главное, не понял какое это имеет отношение к тому, что экземпляр Вашего BTSerialClient (как и экземпляр file из статьи в Википедии) нельзя вернуть из функции (вернее, вернуть-то как раз можно, но при этом программа сломается).

Вам RAII помешала сделать, чтобы не ломалась или уж запретить синтаксически? Или какая-то другая религия?

Впрочем, можете не отвечать, я понимаю, что вопрос не для любительского форума.

Ну я по сути уже ответил, что согласен, но вообще плюсы это конечно уникальный продукт в том как сильно он повлиял на общественное сознательное в отношении к ООП и повлиял на мир программирования. Про это отдельную тему во флейме надо создавать если по честному, не хочу тут всю тему испортить этим.
Но так то да - класс владеет ресурсом и он некопируемый.

Вот и прекрасно, что Вы согласны, что правило пяти необходимо соблюдать.

Ксстати запретить (или реализовать) нужно было не только конструктор копирования, но и конструктор перемещения и оба оператора присваивания, т.е. все пять :slight_smile: