CodexPad Arduino Lib 3.1.2
Loading...
Searching...
No Matches
codex_pad.cpp
1#include "codex_pad.h"
2
3#include <Arduino.h>
4
5#include <memory>
6
7#include "WString.h"
8
9namespace {
10constexpr uint16_t kGapServiceUuid{0x1800};
11constexpr uint16_t kGapDeviceNameUuid{0x2A00};
12
13constexpr uint16_t kInputsServiceUuid{0xFFA0};
14constexpr uint16_t kInputsCharacteristicUuid{0xFFA1};
15
16constexpr uint16_t kBatteryServiceUuid{0x180F};
17constexpr uint16_t kBatteryLevelCharacteristicUuid{0x2A19};
18
19constexpr uint16_t kDeviceInfoServiceUuid{0x180A};
20constexpr uint16_t kModelNumberCharacteristicUuid{0x2A24};
21constexpr uint16_t kSerialNumberCharacteristicUuid{0x2A25};
22constexpr uint16_t kFirmwareRevisionCharacteristicUuid{0x2A26};
23constexpr uint16_t kManufacturerNameCharacteristicUuid{0x2A29};
24
25constexpr uint16_t kConnectionParamMinInterval{6};
26constexpr uint16_t kConnectionParamMaxInterval{8};
27constexpr uint16_t kConnectionParamLatency{5};
28constexpr uint16_t kConnectionParamSupervisionTimeout{100};
29
30static_assert(kConnectionParamSupervisionTimeout * 10.0 >
31 (1 + kConnectionParamLatency) * kConnectionParamMaxInterval * 2 * 1.25);
32} // namespace
33
34CodexPad::CodexPad() noexcept {}
35
36CodexPad::~CodexPad() noexcept { Reset(); }
37
38void CodexPad::Init() noexcept {
39 if (!NimBLEDevice::isInitialized()) {
40 NimBLEDevice::init("CodexPadClient");
41 }
42}
43
44bool CodexPad::Connect(const std::string& bluetooth_device_address, const uint32_t timeout_ms) noexcept {
45 // check mac address is valid
46 if (bluetooth_device_address.length() != 17 || bluetooth_device_address[2] != ':' || bluetooth_device_address[5] != ':' ||
47 bluetooth_device_address[8] != ':' || bluetooth_device_address[11] != ':' || bluetooth_device_address[14] != ':') {
48 abort();
49 return false;
50 }
51
52 return Connect(NimBLEAddress(bluetooth_device_address, 0), false, timeout_ms);
53}
54
55bool CodexPad::ScanAndConnect(const gamepad::input::Button buttons) noexcept {
56 auto scanner = NimBLEDevice::getScan();
57 scanner->setActiveScan(true); // active scan uses more power, but get results faster
58 scanner->setInterval(1000);
59 scanner->setWindow(999); // less or equal setInterval value
60
61 if (!scanner->start(1000)) {
62 scanner->stop();
63 scanner->clearResults();
64 // printf("Scan failed\n");
65 return false;
66 }
67
68 while (scanner->isScanning()) {
69 delay(100);
70 }
71
72 // printf("Scan done, device count: %d\n", scanner->getResults().getCount());
73
74 int8_t rssi = INT8_MIN;
75 NimBLEAddress address;
76
77#pragma pack(push, 1)
78 struct ManufacturerSpecificData {
79 uint16_t company_id = 0xFFFF;
80 uint8_t header[8] = {'C', 'o', 'd', 'e', 'x', 'P', 'a', 'd'};
81 uint8_t version_major = 0;
82 uint8_t version_minor = 0;
83 uint8_t version_patch = 0;
84 uint32_t button_state = 0;
85 uint8_t button_states_duration_seconds = 0;
86 };
87#pragma pack(pop)
88
89 for (const auto device : scanner->getResults()) {
90 if (device->haveName() && String(device->getName().c_str()).startsWith("CodexPad-") && device->haveManufacturerData()) {
91 // printf("Name: %s\n", device->getName().c_str());
92 const auto manufacturer_data = device->getManufacturerData();
93 if (manufacturer_data.length() >= sizeof(ManufacturerSpecificData)) {
94 const auto data = reinterpret_cast<const ManufacturerSpecificData*>(manufacturer_data.c_str());
95 if (data->company_id == 0xFFFF // company id
96 && memcmp(data->header, "CodexPad", 8) == 0 // header
97 && data->button_state == static_cast<uint32_t>(buttons) // button mask
98 && device->getRSSI() > rssi // rssi
99 && (data->version_major < 2 || data->button_states_duration_seconds >= 1) // button states duration
100 ) {
101 rssi = device->getRSSI();
102 address = device->getAddress();
103 // printf("Found device, rssi: %d, address: %s\n", rssi, address.toString().c_str());
104 }
105 }
106 }
107 }
108
109 scanner->clearResults();
110
111 return address.isNull() ? false : Connect(address, 2000);
112}
113
114const gamepad::input::Tracker& CodexPad::Update() noexcept {
115 if (ble_client_ == nullptr) {
116 return input_tracker_;
117 }
118
119 if (!ble_client_->isConnected()) {
120 Reset();
121 return input_tracker_;
122 }
123
124 input_tracker_.Tick();
125
126 do {
127 std::lock_guard<std::mutex> l(mutex_);
128 if (inputs_queue_.empty()) {
129 break;
130 }
131
132 input_tracker_.Update(inputs_queue_.front());
133 inputs_queue_.pop();
134 } while (false);
135
136 return input_tracker_;
137}
138
139bool CodexPad::is_connected() const noexcept { return ble_client_ != nullptr && ble_client_->isConnected(); }
140
142 if (ble_client_ == nullptr) {
143 return false;
144 }
145
146 if (!ble_client_->isConnected()) {
147 return false;
148 }
149
150 auto remote_service = ble_client_->getService(uint16_t{0x1804});
151 if (remote_service == nullptr) {
152 return false;
153 }
154
155 auto remote_characteristic = remote_service->getCharacteristic(uint16_t{0x2A07});
156 if (remote_characteristic == nullptr) {
157 return false;
158 }
159
160 return remote_characteristic->writeValue(static_cast<uint8_t>(tx_power));
161}
162
163bool CodexPad::Connect(const NimBLEAddress& address, bool async_connect, const uint32_t timeout_ms) {
164 Reset();
165 assert(ble_client_ == nullptr);
166 ble_client_ = NimBLEDevice::createClient(address);
167 ble_client_->setConnectTimeout(timeout_ms);
168 ble_client_->setConnectionParams(kConnectionParamMinInterval, kConnectionParamMaxInterval, kConnectionParamLatency,
169 kConnectionParamSupervisionTimeout);
170 ble_client_->setClientCallbacks(this, false);
171 auto ret = ble_client_->connect(true, async_connect, true);
172
173 if (!ret || !ble_client_->isConnected()) {
174 goto FAILED;
175 }
176
177 remote_device_name_ = ble_client_->getValue(kGapServiceUuid, kGapDeviceNameUuid);
178 remote_model_number_ = ble_client_->getValue(kDeviceInfoServiceUuid, kModelNumberCharacteristicUuid);
179 {
180 auto firmware_revision = ble_client_->getValue(kDeviceInfoServiceUuid, kFirmwareRevisionCharacteristicUuid);
181 if (firmware_revision.length() == remote_firmware_version_.size()) {
182 memcpy(remote_firmware_version_.data(), firmware_revision.data(), firmware_revision.length());
183 }
184 }
185
186 {
187 auto remote_service = ble_client_->getService(kInputsServiceUuid);
188 if (remote_service == nullptr) {
189 goto FAILED;
190 }
191
192 auto remote_characteristic = remote_service->getCharacteristic(kInputsCharacteristicUuid);
193 if (remote_characteristic == nullptr) {
194 goto FAILED;
195 }
196
197 if (!remote_characteristic->canNotify()) {
198 goto FAILED;
199 }
200
201 if (!remote_characteristic->subscribe(
202 true, std::bind(&CodexPad::OnNotify, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
203 std::placeholders::_4))) {
204 goto FAILED;
205 }
206 }
207
208 return ret;
209
210FAILED:
211 Reset();
212 return false;
213}
214
215void CodexPad::OnNotify(const NimBLERemoteCharacteristic* remote_characteristic, const uint8_t* data, const size_t length,
216 const bool is_notify) {
217 if (remote_characteristic != nullptr && remote_characteristic->getUUID().equals(kInputsCharacteristicUuid)) {
218 if (length != sizeof(gamepad::input::State)) {
219 printf("WARNING: length != sizeof(Inputs)\n");
220 return;
221 }
222
223 std::lock_guard<std::mutex> l(mutex_);
224 if (inputs_queue_.size() > kInputsQueueMax) {
225 inputs_queue_.pop();
226 }
227 inputs_queue_.emplace(gamepad::input::State::FromBytes(data));
228 }
229}
230
231void CodexPad::Reset() {
232 if (ble_client_ != nullptr) {
233 ble_client_->cancelConnect();
234 ble_client_->disconnect();
235 NimBLEDevice::deleteClient(ble_client_);
236 ble_client_ = nullptr;
237 }
238
239 remote_device_name_.clear();
240 remote_model_number_.clear();
241 remote_firmware_version_.fill(0);
242 input_tracker_.Reset();
243 std::lock_guard<std::mutex> l(mutex_);
244 inputs_queue_ = {};
245}
~CodexPad() noexcept
Destructor.
Definition codex_pad.cpp:36
CodexPad() noexcept
Constructor.
Definition codex_pad.cpp:34
bool Connect(const std::string &bluetooth_device_address, uint32_t timeout_ms=5000) noexcept
Connect.
Definition codex_pad.cpp:44
bool is_connected() const noexcept
Is connected.
const gamepad::input::Tracker & Update() noexcept
Update, need to be called in Loop.
void Init() noexcept
Initialize.
Definition codex_pad.cpp:38
bool set_remote_tx_power(TxPower power) noexcept
Set transmission power, only effective when connected, immediately effective for current connection,...
bool ScanAndConnect(gamepad::input::Button buttons) noexcept
Scans for nearby CodexPad devices and automatically connects to a device whose button state matches t...
Definition codex_pad.cpp:55