Direct-BT v3.3.0-1-gc2d430c
Direct-BT - Direct Bluetooth Programming.
BTGattCmd.java
Go to the documentation of this file.
1/**
2 * Author: Sven Gothel <sgothel@jausoft.com>
3 * Copyright (c) 2021 Gothel Software e.K.
4 * Copyright (c) 2021 ZAFENA AB
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining
7 * a copy of this software and associated documentation files (the
8 * "Software"), to deal in the Software without restriction, including
9 * without limitation the rights to use, copy, modify, merge, publish,
10 * distribute, sublicense, and/or sell copies of the Software, and to
11 * permit persons to whom the Software is furnished to do so, subject to
12 * the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be
15 * included in all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 */
25
26package org.direct_bt;
27
28import org.jau.io.PrintUtil;
29import org.jau.util.BasicTypes;
30
31/**
32 * Class maps a GATT command and optionally its asynchronous response
33 * to a synchronous atomic operation.
34 *
35 * The GATT command is issued by writing the associated GATT characteristic value
36 * via BTGattChar.writeValueNoResp() or BTGattChar.writeValue().
37 *
38 * Its optional asynchronous characteristic value notification or indication response
39 * is awaited and collected after command issuance.
40 *
41 * If a response jau.uuid_t is given, notification or indication will be enabled at first send() command
42 * and disabled at close() or destruction.
43 *
44 * @see BTGattChar.writeValueNoResp()
45 * @see BTGattChar.writeValue()
46 * @since 2.4.0
47 */
48public class BTGattCmd implements AutoCloseable
49{
50 public static interface DataCallback {
51 public void run(final BTGattChar charDecl, byte[] char_value, long timestampMS);
52 }
53
54 private static final boolean DEBUG = BTFactory.DEBUG;
55
56 /** Name, representing the command */
57 private final String name;
58 /** Command's BTGattService jau.uuid_t, may be null. */
59 private final String service_uuid;
60 /** Command's BTGattChar value jau.uuid_t to write command, never null. */
61 private final String cmd_uuid;
62 /** Command's optional BTGattChar value jau.uuid_t for the notification or indication response, may be null. */
63 private final String rsp_uuid;
64
65 private final Object mtxRspReceived = new Object();
66 private final BTDevice dev;
67 private volatile byte[] rsp_data;
68 private BTGattChar cmdCharRef;
69 private BTGattChar rspCharRef;
70 private int rspMinSize;
71 private DataCallback dataCallback;
72 private boolean setup_done;
73
74 private static class ResponseCharListener extends BTGattCharListener {
75 private final BTGattCmd source;
76
77 public ResponseCharListener(final BTGattCmd source_) {
78 super();
79 source = source_;
80 }
81
82 private void store(final byte[] value) {
83 if( null == source.rsp_data ) {
84 source.rsp_data = value;
85 } else {
86 final int new_size = source.rsp_data.length + value.length;
87 final byte[] new_buf = new byte[new_size];
88 System.arraycopy(source.rsp_data, 0, new_buf, 0, source.rsp_data.length);
89 System.arraycopy(value, 0, new_buf, source.rsp_data.length, value.length);
90 source.rsp_data = new_buf;
91 }
92 }
93
94 @Override
95 public void notificationReceived(final BTGattChar charDecl,
96 final byte[] value, final long timestamp) {
97 synchronized( source.mtxRspReceived ) {
98 if( DEBUG ) {
99 PrintUtil.fprintf_td(System.err, "BTGattCmd.notificationReceived: Resp %s, value[%s]\n",
100 charDecl.toString(), BasicTypes.bytesHexString(value, 0, value.length, true /* lsbFirst */));
101 }
102 store(value);
103 if( null != source.dataCallback ) {
104 source.dataCallback.run(charDecl, value, timestamp);
105 }
106 source.mtxRspReceived.notifyAll();
107 }
108 }
109
110 @Override
111 public void indicationReceived(final BTGattChar charDecl,
112 final byte[] value, final long timestamp,
113 final boolean confirmationSent) {
114 synchronized( source.mtxRspReceived ) {
115 if( DEBUG ) {
116 PrintUtil.fprintf_td(System.err, "BTGattCmd.indicationReceived: Resp %s, value[%s]\n",
117 charDecl.toString(), BasicTypes.bytesHexString(value, 0, value.length, true /* lsbFirst */));
118 }
119 store(value);
120 if( null != source.dataCallback ) {
121 source.dataCallback.run(charDecl, value, timestamp);
122 }
123 source.mtxRspReceived.notifyAll();
124 }
125 }
126 }
127 private final ResponseCharListener rspCharListener;
128 private boolean verbose;
129
130 private boolean isConnected() { return dev.getConnected(); }
131
132 private boolean isResolvedEq() {
133 return null != cmdCharRef;
134 }
135
136 private String rspCharStr() { return null != rspCharRef ? rspCharRef.toString() : "n/a"; }
137
138 private HCIStatusCode setup() {
139 if( setup_done ) {
140 return isResolvedEq() ? HCIStatusCode.SUCCESS : HCIStatusCode.NOT_SUPPORTED;
141 }
142 setup_done = true;
143 cmdCharRef = null != service_uuid ? dev.findGattChar(service_uuid, cmd_uuid)
144 : dev.findGattChar(cmd_uuid);
145 if( null == cmdCharRef ) {
146 if( verbose ) {
147 PrintUtil.fprintf_td(System.err, "Command not found: service %s, char %s\n", service_uuid, cmd_uuid);
148 }
149 return HCIStatusCode.NOT_SUPPORTED;
150 }
151
152 if( !cmdCharRef.getProperties().isSet(GattCharPropertySet.Type.WriteNoAck) &&
153 !cmdCharRef.getProperties().isSet(GattCharPropertySet.Type.WriteWithAck) ) {
154 if( verbose ) {
155 PrintUtil.fprintf_td(System.err, "Command has no write property: %s\n", cmdCharRef.toString());
156 }
157 cmdCharRef = null;
158 return HCIStatusCode.NOT_SUPPORTED;
159 }
160
161 if( null != rsp_uuid ) {
162 rspCharRef = null != service_uuid ? dev.findGattChar(service_uuid, rsp_uuid)
163 : dev.findGattChar(rsp_uuid);
164 if( null == rspCharRef ) {
165 if( verbose ) {
166 PrintUtil.fprintf_td(System.err, "Response not found: service %s, char %s\n", service_uuid, rsp_uuid);
167 }
168 cmdCharRef = null;
169 return HCIStatusCode.NOT_SUPPORTED;
170 }
171 try {
172 final boolean cccdEnableResult[] = { false, false };
173 if( rspCharRef.addCharListener( rspCharListener, cccdEnableResult ) ) {
174 return HCIStatusCode.SUCCESS;
175 } else {
176 if( verbose ) {
177 PrintUtil.fprintf_td(System.err, "CCCD Notify/Indicate not supported on response %s\n", rspCharRef.toString());
178 }
179 cmdCharRef = null;
180 rspCharRef = null;
181 return HCIStatusCode.NOT_SUPPORTED;
182 }
183 } catch ( final Exception e ) {
184 PrintUtil.fprintf_td(System.err, "Exception caught for %s: %s\n", e.toString(), toString());
185 cmdCharRef = null;
186 rspCharRef = null;
187 return HCIStatusCode.TIMEOUT;
188 }
189 } else {
190 return HCIStatusCode.SUCCESS;
191 }
192 }
193
194 /**
195 * Close this command instance, usually called at destruction.
196 * <p>
197 * If a response jau.uuid_t has been given, notification or indication will be disabled.
198 * </p>
199 * {@inheritDoc}
200 */
201 @Override
202 public void close() {
203 close0();
204 }
205
206 /**
207 * Close this command instance, usually called at destruction.
208 *
209 * If a response jau.uuid_t has been given, notification or indication will be disabled.
210 */
211 public synchronized HCIStatusCode close0() {
212 final boolean wasResolved = isResolvedEq();
213 final BTGattChar rspCharRefCopy = rspCharRef;
214 cmdCharRef = null;
215 rspCharRef = null;
216 if( !setup_done ) {
217 return HCIStatusCode.SUCCESS;
218 }
219 setup_done = false;
220 if( !wasResolved ) {
221 return HCIStatusCode.SUCCESS;
222 }
223 if( !isConnected() ) {
225 }
226 if( null != rspCharRefCopy) {
227 try {
228 final boolean res1 = rspCharRefCopy.removeCharListener(rspCharListener);
229 final boolean res2 = rspCharRefCopy.disableIndicationNotification();
230 if( res1 && res2 ) {
231 return HCIStatusCode.SUCCESS;
232 } else {
233 return HCIStatusCode.FAILED;
234 }
235 } catch (final Exception e ) {
236 PrintUtil.fprintf_td(System.err, "Exception caught for %s: %s\n", e.toString(), toString());
237 return HCIStatusCode.TIMEOUT;
238 }
239 } else {
240 return HCIStatusCode.SUCCESS;
241 }
242 }
243
244 /**
245 * Constructor for commands with notification or indication response.
246 *
247 * @param dev_ the remote BTDevice
248 * @param name_ user given name, representing the command
249 * @param service_uuid_ command's BTGattService jau.uuid_t, may be null for using a less efficient BTGattChar lookup
250 * @param cmd_uuid_ command's BTGattChar value jau.uuid_t to write the command
251 * @param rsp_uuid_ command's BTGattChar value jau.uuid_t for the notification or indication response.
252 */
253 public BTGattCmd(final BTDevice dev_, final String name_,
254 final String service_uuid_, final String cmd_uuid_, final String rsp_uuid_ )
255 {
256 name = name_;
257 service_uuid = service_uuid_;
258 cmd_uuid = cmd_uuid_;
259 rsp_uuid = rsp_uuid_;
260 dev = dev_;
261 rsp_data = null;
262 cmdCharRef = null;
263 rspCharRef = null;
264 rspMinSize = 0;
265 dataCallback = null;
266 setup_done = false;
267 rspCharListener = new ResponseCharListener(this);
268 verbose = DEBUG;
269 }
270
271 /**
272 * Constructor for commands without response.
273 *
274 * @param dev_ the remote BTDevice
275 * @param name_ user given name, representing the command
276 * @param service_uuid_ command's BTGattService jau.uuid_t, may be null for using a less efficient BTGattChar lookup
277 * @param cmd_uuid_ command's BTGattChar value jau.uuid_t to write the command
278 */
279 public BTGattCmd(final BTDevice dev_, final String name_, final String service_uuid_, final String cmd_uuid_)
280 {
281 name = name_;
282 service_uuid = service_uuid_;
283 cmd_uuid = cmd_uuid_;
284 rsp_uuid = null;
285 dev = dev_;
286 rsp_data = null;
287 cmdCharRef = null;
288 rspCharRef = null;
289 rspMinSize = 0;
290 dataCallback = null;
291 setup_done = false;
292 rspCharListener = null;
293 verbose = DEBUG;
294 }
295
296 @Override
297 public void finalize() { close(); }
298
299 public void setResponseMinSize(final int v) { rspMinSize = v; }
300 public void setDataCallback(final DataCallback dcb) { dataCallback = dcb; }
301
302 /** Return name, representing the command */
303 public String getName() { return name; }
304
305 /** Return command's BTGattService jau::uuid_t, may be null. */
306 public String getServiceUUID() { return service_uuid; }
307
308 /** Return command's BTGattChar value jau::uuid_t to write command, never null. */
309 public String getCommandUUID() { return cmd_uuid; }
310
311 /** Return true if a notification or indication response has been set via constructor, otherwise false. */
312 public boolean hasResponseSet() { return null != rsp_uuid; }
313
314 /** Return command's optional BTGattChar value jau::uuid_t for the notification or indication response, may be null. */
315 public String getResponseUUID() { return rsp_uuid; }
316
317 /** Set verbosity for UUID resolution. */
318 public void setVerbose(final boolean v) { verbose = v; }
319
320 /**
321 * Returns the read-only response data object
322 * for configured commands with response notification or indication.
323 *
324 * jau.TROOctets.size() matches the size of last received command response or zero.
325 * @see #send(boolean, byte[], int)
326 */
327 public byte[] getResponse() { return rsp_data; }
328
329 private String rspDataToString() {
330 return null == rsp_data ? "null" : BasicTypes.bytesHexString(rsp_data, 0, rsp_data.length, true /* lsbFirst */);
331 }
332
333 /**
334 * Query whether all UUIDs of this commands have been resolved.
335 *
336 * In case no command has been issued via send() yet,
337 * the UUIDs will be resolved with this call.
338 *
339 * @return true if all UUIDs have been resolved, otherwise false
340 */
341 public synchronized boolean isResolved() {
342 if( !setup_done ) {
343 return HCIStatusCode.SUCCESS == setup();
344 } else {
345 return isResolvedEq();
346 }
347 }
348
349 /**
350 * Send the command to the remote BTDevice.
351 *
352 * If a notification or indication result jau.uuid_t has been set via constructor,
353 * it will be awaited and can be retrieved via {@link #getResponse()} after command returns.
354 *
355 * @param prefNoAck pass true to prefer command write without acknowledge, otherwise use with-ack if available
356 * @param cmd_data raw command octets
357 * @param timeoutMS timeout in milliseconds. Defaults to 10 seconds limited blocking for the response to become available, if any.
358 * @return
359 * @see #getResponse()
360 */
361 public synchronized HCIStatusCode send(final boolean prefNoAck, final byte[] cmd_data, final int timeoutMS) {
362 return sendImpl(prefNoAck, cmd_data, timeoutMS, true);
363 }
364
365 /**
366 * Send the command to the remote BTDevice, only.
367 *
368 * Regardless whether a notification or indication result jau::uuid_t has been set via constructor,
369 * this command will not wait for the response.
370 *
371 * @param prefNoAck pass true to prefer command write without acknowledge, otherwise use with-ack if available
372 * @param cmd_data raw command octets
373 * @return
374 * @see #getResponse()
375 */
376 public synchronized HCIStatusCode sendOnly(final boolean prefNoAck, final byte[] cmd_data) {
377 return sendImpl(prefNoAck, cmd_data, 0, false);
378 }
379
380 private synchronized HCIStatusCode sendImpl(final boolean prefNoAck, final byte[] cmd_data, final int timeoutMS, final boolean allowResponse) {
382
383 if( !isConnected() ) {
385 }
386 synchronized( mtxRspReceived ) {
387 res = setup();
388 if( HCIStatusCode.SUCCESS != res ) {
389 return res;
390 }
391 rsp_data = null;
392
393 if( DEBUG ) {
394 PrintUtil.fprintf_td(System.err, "BTGattCmd.sendBlocking: Start: Cmd %s, args[%s], Resp %s, result[%s]",
395 cmdCharRef.toString(), cmd_data.toString(),
396 rspCharStr(), rspDataToString());
397 }
398
399 final boolean hasWriteNoAck = cmdCharRef.getProperties().isSet(GattCharPropertySet.Type.WriteNoAck);
400 final boolean hasWriteWithAck = cmdCharRef.getProperties().isSet(GattCharPropertySet.Type.WriteWithAck);
401 // Prefer WriteNoAck, if hasWriteNoAck and ( prefNoAck -or- !hasWriteWithAck )
402 final boolean prefWriteNoAck = hasWriteNoAck && ( prefNoAck || !hasWriteWithAck );
403
404 if( prefWriteNoAck ) {
405 try {
406 if( !cmdCharRef.writeValue(cmd_data, false /* withResponse */) ) {
407 PrintUtil.fprintf_td(System.err, "Write (noAck) to command failed: Cmd %s, args[%s]\n",
408 cmdCharRef.toString(), BasicTypes.bytesHexString(cmd_data, 0, cmd_data.length, true /* lsbFirst */));
409 res = HCIStatusCode.FAILED;
410 }
411 } catch ( final Throwable t ) {
412 PrintUtil.fprintf_td(System.err, "Exception caught @ Write (noAck) to command failed: Cmd %s, args[%s]: %s\n",
413 cmdCharRef.toString(), BasicTypes.bytesHexString(cmd_data, 0, cmd_data.length, true /* lsbFirst */), t.toString());
414 res = HCIStatusCode.TIMEOUT;
415 }
416 } else if( hasWriteWithAck ) {
417 try {
418 if( !cmdCharRef.writeValue(cmd_data, true /* withResponse */) ) {
419 PrintUtil.fprintf_td(System.err, "Write (withAck) to command failed: Cmd %s, args[%s]\n",
420 cmdCharRef.toString(), BasicTypes.bytesHexString(cmd_data, 0, cmd_data.length, true /* lsbFirst */));
421 res = HCIStatusCode.TIMEOUT;
422 }
423 } catch ( final Throwable t ) {
424 PrintUtil.fprintf_td(System.err, "Exception caught @ Write (withAck) to command failed: Cmd %s, args[%s]: %s\n",
425 cmdCharRef.toString(), BasicTypes.bytesHexString(cmd_data, 0, cmd_data.length, true /* lsbFirst */), t.toString());
426 res = HCIStatusCode.TIMEOUT;
427 }
428 } else {
429 PrintUtil.fprintf_td(System.err, "Command has no write property: %s\n", cmdCharRef.toString());
430 res = HCIStatusCode.FAILED;
431 }
432
433 if( null != rspCharRef && allowResponse ) {
434 while( HCIStatusCode.SUCCESS == res &&
435 ( null == rsp_data || rspMinSize > rsp_data.length || ( 0 == rspMinSize && 0 == rsp_data.length ) )
436 )
437 {
438 if( 0 == timeoutMS ) {
439 try {
440 mtxRspReceived.wait();
441 } catch (final InterruptedException e) { }
442 } else {
443 try {
444 mtxRspReceived.wait(timeoutMS);
445 } catch (final Throwable t) {}
446 if( null == rsp_data ) {
447 PrintUtil.fprintf_td(System.err, "BTGattCmd.sendBlocking: Timeout: Cmd %s, args[%s]\n",
448 cmdCharRef.toString(), BasicTypes.bytesHexString(cmd_data, 0, cmd_data.length, true /* lsbFirst */));
449 res = HCIStatusCode.TIMEOUT;
450 }
451 }
452 }
453 }
454 } // mtxRspReceived
455 if( DEBUG && HCIStatusCode.SUCCESS == res ) {
456 PrintUtil.fprintf_td(System.err, "BTGattCmd.sendBlocking: OK: Cmd %s, args[%s], Resp %s, result[%s]\n",
457 cmdCharRef.toString(), BasicTypes.bytesHexString(cmd_data, 0, cmd_data.length, true /* lsbFirst */),
458 rspCharStr(), rspDataToString());
459 }
460 return res;
461 }
462
463 @Override
464 public String toString() {
465 return "BTGattCmd["+dev.getName()+":"+name+", service "+service_uuid+
466 ", char[cmd "+cmd_uuid+", rsp "+rsp_uuid+
467 ", set["+setup_done+", resolved "+isResolvedEq()+"]]]";
468 }
469}
One stop BTManager API entry point.
Definition: BTFactory.java:52
static final boolean DEBUG
Debug logging enabled or disabled.
Definition: BTFactory.java:147
BTGattChar event listener for notification and indication events.
Class maps a GATT command and optionally its asynchronous response to a synchronous atomic operation.
Definition: BTGattCmd.java:49
String getCommandUUID()
Return command's BTGattChar value jau::uuid_t to write command, never null.
Definition: BTGattCmd.java:309
String getServiceUUID()
Return command's BTGattService jau::uuid_t, may be null.
Definition: BTGattCmd.java:306
void setVerbose(final boolean v)
Set verbosity for UUID resolution.
Definition: BTGattCmd.java:318
synchronized HCIStatusCode send(final boolean prefNoAck, final byte[] cmd_data, final int timeoutMS)
Send the command to the remote BTDevice.
Definition: BTGattCmd.java:361
void close()
Close this command instance, usually called at destruction.
Definition: BTGattCmd.java:202
byte[] getResponse()
Returns the read-only response data object for configured commands with response notification or indi...
Definition: BTGattCmd.java:327
synchronized boolean isResolved()
Query whether all UUIDs of this commands have been resolved.
Definition: BTGattCmd.java:341
boolean hasResponseSet()
Return true if a notification or indication response has been set via constructor,...
Definition: BTGattCmd.java:312
String getName()
Return name, representing the command.
Definition: BTGattCmd.java:303
synchronized HCIStatusCode close0()
Close this command instance, usually called at destruction.
Definition: BTGattCmd.java:211
String getResponseUUID()
Return command's optional BTGattChar value jau::uuid_t for the notification or indication response,...
Definition: BTGattCmd.java:315
synchronized HCIStatusCode sendOnly(final boolean prefNoAck, final byte[] cmd_data)
Send the command to the remote BTDevice, only.
Definition: BTGattCmd.java:376
BTGattCmd(final BTDevice dev_, final String name_, final String service_uuid_, final String cmd_uuid_)
Constructor for commands without response.
Definition: BTGattCmd.java:279
BTGattCmd(final BTDevice dev_, final String name_, final String service_uuid_, final String cmd_uuid_, final String rsp_uuid_)
Constructor for commands with notification or indication response.
Definition: BTGattCmd.java:253
void setResponseMinSize(final int v)
Definition: BTGattCmd.java:299
void setDataCallback(final DataCallback dcb)
Definition: BTGattCmd.java:300
BT Core Spec v5.2: Vol 1, Part F Controller Error Codes: 1.3 List of Error Codes.
BTDevice represents one remote Bluetooth device.
Definition: BTDevice.java:47
String getName()
Returns the remote device name.
boolean getConnected()
Returns the connected state of the device.
BTGattChar findGattChar(String service_uuid, String char_uuid)
Find a BTGattChar by its service_uuid and char_uuid.
Representing a Gatt Characteristic object from the GATT client perspective.
Definition: BTGattChar.java:49
boolean writeValue(byte[] argValue, boolean withResponse)
Writes the value of this characteristic, using one of the following methods depending on withRespons...
boolean removeCharListener(final BTGattCharListener listener)
Remove the given associated BTGattCharListener from the listener list if present.
boolean addCharListener(final BTGattCharListener listener)
Add the given BTGattCharListener to the listener list if not already present.
boolean disableIndicationNotification()
BT Core Spec v5.2: Vol 3, Part G GATT: 3.3.3.3 Client Characteristic Configuration.
GattCharPropertySet getProperties()
Returns the properties of this characteristic.
void run(final BTGattChar charDecl, byte[] char_value, long timestampMS)