Интернет полон примеров как соединяться с 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).
Так же приветствуется проверка библиотеки на других устройствах и выявленные баги.