Dawn Framework 1.0
Universal data acquisition framework for embedded systems
prph_ots.cxx
1// dawn/src/proto/nimble/prph_ots.cxx
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5
6#include "dawn/proto/nimble/prph_ots.hxx"
7
8#include <cstdlib>
9#include <cstring>
10#include <new>
11
12#include "dawn/io/common.hxx"
13#include "host/ble_hs.h"
14#include "host/ble_hs_mbuf.h"
15#include "os/os_mbuf.h"
16
17using namespace dawn;
18
19// OTS Feature characteristic (BT OTS spec Table 3.7).
20// OACP Features: Read=bit4, Write=bit5, Truncation=bit7, Abort=bit9.
21// Truncation is advertised because our seekable backing IOs (CIOFile et
22// al.) inherently truncate the object on offset==0 writes, which matches
23// the spec's "truncate" mode (mode bit 1 / 0x02 in OACP Write per BT OTS
24// spec Table 3.11). Non-truncating writes (mode == 0) are also accepted
25// for clients that don't request truncation.
26
27static constexpr uint32_t OACP_FEATURE_BITS = (1u << 4) | (1u << 5) | (1u << 7) | (1u << 9);
28
29// OACP Write mode byte: only bits 0 (reserved=0) and 1 (Truncate=0x02)
30// are defined by the spec. We accept both 0 and 0x02; any other value is
31// either reserved or unsupported.
32
33static constexpr uint8_t OACP_WRITE_MODE_TRUNCATE = 0x02;
34
35// OLCP Features: Go To=bit0. First/Last/Prev/Next are mandatory and not
36// advertised in the bitmap.
37
38static constexpr uint32_t OLCP_FEATURE_BITS = (1u << 0);
39
40// Pack a little-endian uint32 value into a 4-byte buffer.
41
42static inline void packU32LE(uint8_t *out, uint32_t v)
43{
44 out[0] = static_cast<uint8_t>(v & 0xff);
45 out[1] = static_cast<uint8_t>((v >> 8) & 0xff);
46 out[2] = static_cast<uint8_t>((v >> 16) & 0xff);
47 out[3] = static_cast<uint8_t>((v >> 24) & 0xff);
48}
49
50static inline uint32_t unpackU32LE(const uint8_t *in)
51{
52 return (static_cast<uint32_t>(in[0])) | (static_cast<uint32_t>(in[1]) << 8) |
53 (static_cast<uint32_t>(in[2]) << 16) | (static_cast<uint32_t>(in[3]) << 24);
54}
55
56static inline void packU48LE(uint8_t *out, uint64_t v)
57{
58 int i;
59
60 for (i = 0; i < 6; i++)
61 {
62 out[i] = static_cast<uint8_t>((v >> (8 * i)) & 0xff);
63 }
64}
65
66static inline uint64_t unpackU48LE(const uint8_t *in)
67{
68 uint64_t v = 0;
69 int i;
70
71 for (i = 0; i < 6; i++)
72 {
73 v |= static_cast<uint64_t>(in[i]) << (8 * i);
74 }
75
76 return v;
77}
78
79// We allocate the 16-bit UUIDs on the heap so the chr def can hold a stable
80// pointer. NimBLE's ble_uuid_any_t is the canonical "any uuid" structure.
81
82static ble_uuid_any_t *makeUuid16(uint16_t value)
83{
84 ble_uuid_any_t *u = new (std::nothrow) ble_uuid_any_t{};
85 if (u == nullptr)
86 {
87 return nullptr;
88 }
89
90 u->u.type = BLE_UUID_TYPE_16;
91 u->u16.value = value;
92 return u;
93}
94
95static void freeUuid(const ble_uuid_t *u)
96{
97 if (u != nullptr)
98 {
99 delete reinterpret_cast<const ble_uuid_any_t *>(u);
100 }
101}
102
103CProtoNimblePrphOts::CProtoNimblePrphOts(const SObjectCfg::SObjectCfgItem *desc_,
105 : IProtoNimblePrphService(desc_, cb_)
106 , created(false)
107{
108 id = -1;
109 chrs = nullptr;
110 hOacp = 0;
111 hOlcp = 0;
112 xferBuf = nullptr;
113
114 std::memset(&svc, 0, sizeof(svc));
115}
116
117CProtoNimblePrphOts::~CProtoNimblePrphOts()
118{
119 deinit();
120}
121
122void CProtoNimblePrphOts::allocObject(const SProtoNimblePrphIOBindOtsObjid &entry)
123{
124 SOtsObjectMeta meta;
125 uint8_t kind;
126
127 kind = cfgGetType(entry.cfg);
128 if (kind != PRPH_OTS_TYPE_FILE && kind != PRPH_OTS_TYPE_DESCRIPTOR && kind != PRPH_OTS_TYPE_CAPS)
129 {
130 DAWNERR("OTS: invalid object kind %u\n", kind);
131 return;
132 }
133
134 std::memcpy(meta.name, entry.name, OBJ_NAME_MAX);
135 switch (kind)
136 {
137 case PRPH_OTS_TYPE_DESCRIPTOR:
138 meta.typeUuid = OBJ_TYPE_DESCRIPTOR;
139 break;
140 case PRPH_OTS_TYPE_CAPS:
141 meta.typeUuid = OBJ_TYPE_CAPS;
142 break;
143 case PRPH_OTS_TYPE_FILE:
144 default:
145 meta.typeUuid = OBJ_TYPE_UNSPECIFIED;
146 break;
147 }
148
149 meta.access = cfgGetAccess(entry.cfg);
150 meta.kind = kind;
151 meta.iid = entry.objid.v;
152
153 vmeta.push_back(meta);
154
155 cb->regObject(entry.objid.v);
156 vio.push_back(entry.objid.v);
157
158 DAWNINFO("OTS: register object '%.*s' kind=%u access=%u id=0x%" PRIx32 "\n",
160 entry.name,
161 kind,
162 meta.access,
163 entry.objid.v);
164}
165
166void CProtoNimblePrphOts::configureDesc(const SObjectCfg::SObjectCfgItem *item)
167{
168 size_t k;
169
170 for (k = 0; k < item->cfgid.s.size;)
171 {
172 const SProtoNimblePrphIOBindOts *blob;
173 uint32_t count;
174 uint32_t i;
175
176 blob = reinterpret_cast<const SProtoNimblePrphIOBindOts *>(&item->data[k]);
177 count = blob->cfg0;
178
179 for (i = 0; i < count; i++)
180 {
181 allocObject(blob->obj[i]);
182 }
183
184 k += (sizeof(SProtoNimblePrphIOBindOts) + count * sizeof(SProtoNimblePrphIOBindOtsObjid)) / 4;
185 }
186}
187
188int CProtoNimblePrphOts::setChrDef(size_t idx,
189 uint16_t uuid16,
190 ble_gatt_access_fn *cb_,
191 uint16_t flags,
192 uint16_t *valHandle)
193{
194 ble_uuid_any_t *u;
195
196 u = makeUuid16(uuid16);
197 if (u == nullptr)
198 {
199 return -ENOMEM;
200 }
201
202 chrs[idx].uuid = &u->u;
203 chrs[idx].access_cb = cb_;
204 chrs[idx].arg = this;
205 chrs[idx].flags = flags;
206 chrs[idx].val_handle = valHandle;
207
208 return OK;
209}
210
211// Number of mandatory OTS characteristics (Feature, Name, Type, Size, ID,
212// Props, OACP, OLCP). Shared between allocOTS() and deleteOTS().
213
214static constexpr size_t kNumChrs = 8;
215
216int CProtoNimblePrphOts::allocOTS()
217{
218 // OACP and OLCP need val_handle pointers we can use later for
219 // indications; the metadata characteristics are read-only and don't.
220
221 constexpr uint16_t kRd = BLE_GATT_CHR_F_READ;
222 constexpr uint16_t kInd = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE;
223
224 const struct
225 {
226 uint16_t uuid;
227 ble_gatt_access_fn *cb;
228 uint16_t flags;
229 uint16_t *handle;
230 } table[kNumChrs] = {
231 {UUID16_FEATURE, featureCb, kRd, nullptr},
232 {UUID16_OBJ_NAME, nameCb, kRd, nullptr},
233 {UUID16_OBJ_TYPE, typeCb, kRd, nullptr},
234 {UUID16_OBJ_SIZE, sizeCb, kRd, nullptr},
235 {UUID16_OBJ_ID, idCb, kRd, nullptr},
236 {UUID16_OBJ_PROPS, propsCb, kRd, nullptr},
237 {UUID16_OACP, oacpCb, kInd, &hOacp},
238 {UUID16_OLCP, olcpCb, kInd, &hOlcp},
239 };
240
241 ble_uuid_any_t *svcUuid;
242
243 chrs = new (std::nothrow) ble_gatt_chr_def[kNumChrs + 1]();
244 if (chrs == nullptr)
245 {
246 DAWNERR("OTS: chr alloc failed\n");
247 return -ENOMEM;
248 }
249
250 for (size_t i = 0; i < kNumChrs; i++)
251 {
252 if (setChrDef(i, table[i].uuid, table[i].cb, table[i].flags, table[i].handle) < 0)
253 {
254 return -ENOMEM;
255 }
256 }
257
258 // Service definition
259
260 svcUuid = makeUuid16(UUID16_OTS);
261 if (svcUuid == nullptr)
262 {
263 return -ENOMEM;
264 }
265
266 svc.type = BLE_GATT_SVC_TYPE_PRIMARY;
267 svc.uuid = &svcUuid->u;
268 svc.includes = nullptr;
269 svc.characteristics = chrs;
270
271 // Pre-allocate the L2CAP TX/RX scratch buffer at init time. The L2CAP
272 // RX/TX paths must NOT allocate per-packet (no runtime allocation
273 // outside init).
274
275 xferBuf = new (std::nothrow) io_ddata_t(1, L2CAP_MTU_OTS, 1, SObjectId::DTYPE_UINT8);
276 if (xferBuf == nullptr || !xferBuf->isAllocated())
277 {
278 DAWNERR("OTS: xfer buffer alloc failed\n");
279 delete xferBuf;
280 xferBuf = nullptr;
281 return -ENOMEM;
282 }
283
284 return OK;
285}
286
287void CProtoNimblePrphOts::deleteOTS()
288{
289 if (chrs != nullptr)
290 {
291 for (size_t i = 0; i < kNumChrs; i++)
292 {
293 freeUuid(chrs[i].uuid);
294 }
295
296 delete[] chrs;
297 chrs = nullptr;
298 }
299
300 if (svc.uuid != nullptr)
301 {
302 freeUuid(svc.uuid);
303 svc.uuid = nullptr;
304 }
305
306 if (xferBuf != nullptr)
307 {
308 delete xferBuf;
309 xferBuf = nullptr;
310 }
311}
312
313int CProtoNimblePrphOts::createOTS()
314{
315 // After NimBLE has finished registering the service, hOacp and hOlcp hold
316 // the assigned attr handles (NimBLE wrote into the val_handle pointers we
317 // gave it). Validate that every bound IO is seekable -- non-seekable IOs
318 // cannot back an OTS object (inverse of the AIOS rule).
319
320 for (SObjectId::ObjectId objid : vio)
321 {
322 CIOCommon *io;
323
324 io = cb->getObject(objid);
325 if (io == nullptr)
326 {
327 DAWNERR("OTS: IO 0x%" PRIx32 " not bound\n", objid);
328 return -EIO;
329 }
330
331 if (!io->isSeekable())
332 {
333 DAWNERR("OTS: IO 0x%" PRIx32 " is not seekable\n", objid);
334 return -ENOTSUP;
335 }
336 }
337
338 return OK;
339}
340
342{
343 int ret;
344
345 configureDesc(desc);
346
347 ret = allocOTS();
348 if (ret != OK)
349 {
350 return ret;
351 }
352
353 id = cb->serviceRegister(&svc);
354 if (id < 0)
355 {
356 DAWNERR("OTS service registration failed\n");
357 return -EIO;
358 }
359
360 return OK;
361}
362
364{
365 deleteOTS();
366 vmeta.clear();
367 vconn.clear();
368 created = false;
369 return OK;
370}
371
373{
374 int ret;
375
376 if (!created)
377 {
378 ret = createOTS();
379 if (ret != OK)
380 {
381 return ret;
382 }
383
384 created = true;
385 }
386
387 // Open L2CAP CoC server. The dummy backend (used in tests) ignores this.
388
389#ifndef CONFIG_DAWN_PROTO_NIMBLE_DUMMY
390 ret =
391 ble_l2cap_create_server(L2CAP_PSM_OTS, L2CAP_MTU_OTS, CProtoNimblePrphOts::l2capEventCb, this);
392 if (ret != 0)
393 {
394 DAWNERR("OTS: ble_l2cap_create_server failed: %d\n", ret);
395 return -EIO;
396 }
397#endif
398
399 return cb->startService(id);
400}
401
403{
404 // Drain per-connection state; channel disconnects are pushed by NimBLE
405 // through l2capEventCb, but we clear here to be safe.
406
407 vconn.clear();
408
409 return cb->stopService(id);
410}
411
412bool CProtoNimblePrphOts::selected(uint16_t conn, SOtsConnState *&state, SOtsObjectMeta *&meta)
413{
414 std::map<uint16_t, SOtsConnState>::iterator it;
415
416 it = vconn.find(conn);
417 if (it == vconn.end())
418 {
419 return false;
420 }
421
422 state = &it->second;
423 if (state->cursor < 0 || static_cast<size_t>(state->cursor) >= vmeta.size())
424 {
425 return false;
426 }
427
428 meta = &vmeta[state->cursor];
429 return true;
430}
431
432// Append a flat byte buffer to a GATT read response mbuf. Returns 0 on
433// success or BLE_ATT_ERR_INSUFFICIENT_RES so the caller can `return` directly.
434
435static inline int gattAppend(struct os_mbuf *om, const void *buf, size_t len)
436{
437 return os_mbuf_append(om, buf, len) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
438}
439
440// Common prologue for every metadata-read callback: enforce READ op, cast
441// arg to ``self``, and resolve the currently-selected object for ``conn``.
442// On success returns 0 with @c self / @c meta filled; on failure returns the
443// BLE ATT error code the caller should propagate.
444
445int CProtoNimblePrphOts::metaPrologue(uint16_t conn,
446 ble_gatt_access_ctxt *ctxt,
447 void *arg,
448 CProtoNimblePrphOts **self,
449 SOtsObjectMeta **meta,
450 int16_t *cursorOut)
451{
452 SOtsConnState *st;
453
454 if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR)
455 {
456 return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
457 }
458
459 *self = static_cast<CProtoNimblePrphOts *>(arg);
460 if (!(*self)->selected(conn, st, *meta))
461 {
462 return BLE_ATT_ERR_UNLIKELY;
463 }
464
465 if (cursorOut != nullptr)
466 {
467 *cursorOut = st->cursor;
468 }
469
470 return 0;
471}
472
473int CProtoNimblePrphOts::featureCb(uint16_t /*conn*/,
474 uint16_t /*attr*/,
475 struct ble_gatt_access_ctxt *ctxt,
476 void * /*arg*/)
477{
478 uint8_t buf[8];
479
480 if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR)
481 {
482 return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
483 }
484
485 packU32LE(&buf[0], OACP_FEATURE_BITS);
486 packU32LE(&buf[4], OLCP_FEATURE_BITS);
487
488 return gattAppend(ctxt->om, buf, sizeof(buf));
489}
490
491int CProtoNimblePrphOts::nameCb(uint16_t conn,
492 uint16_t /*attr*/,
493 struct ble_gatt_access_ctxt *ctxt,
494 void *arg)
495{
497 SOtsConnState *st;
498 SOtsObjectMeta *meta;
499
500 // Empty Name when nothing is selected yet -- nameCb is the only metadata
501 // characteristic that returns success in that state (helps naive clients
502 // that don't drive OLCP first).
503
504 if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR)
505 {
506 return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
507 }
508
509 self = static_cast<CProtoNimblePrphOts *>(arg);
510 if (!self->selected(conn, st, meta))
511 {
512 return 0;
513 }
514
515 return gattAppend(ctxt->om, meta->name, strnlen(meta->name, OBJ_NAME_MAX));
516}
517
518int CProtoNimblePrphOts::typeCb(uint16_t conn,
519 uint16_t /*attr*/,
520 struct ble_gatt_access_ctxt *ctxt,
521 void *arg)
522{
524 SOtsObjectMeta *meta;
525 uint8_t buf[2];
526 int err;
527
528 err = metaPrologue(conn, ctxt, arg, &self, &meta, nullptr);
529 if (err != 0)
530 {
531 return err;
532 }
533
534 buf[0] = static_cast<uint8_t>(meta->typeUuid & 0xff);
535 buf[1] = static_cast<uint8_t>((meta->typeUuid >> 8) & 0xff);
536 return gattAppend(ctxt->om, buf, sizeof(buf));
537}
538
539int CProtoNimblePrphOts::sizeCb(uint16_t conn,
540 uint16_t /*attr*/,
541 struct ble_gatt_access_ctxt *ctxt,
542 void *arg)
543{
545 SOtsObjectMeta *meta;
546 CIOCommon *io;
547 uint32_t cur;
548 uint8_t buf[8];
549 int err;
550
551 err = metaPrologue(conn, ctxt, arg, &self, &meta, nullptr);
552 if (err != 0)
553 {
554 return err;
555 }
556
557 io = self->cb->getObject(meta->iid);
558 cur = (io != nullptr) ? static_cast<uint32_t>(io->getDataSize()) : 0;
559
560 // The Object Size characteristic carries (current, allocated). The
561 // backing IOs (CIOFile, CIODescriptor, CIOCapabilities) do not expose a
562 // separate "allocated" capacity distinct from the current data size, so
563 // both fields are reported as `cur`. Clients should treat current ==
564 // max writable size and not attempt to grow the object beyond it.
565
566 packU32LE(&buf[0], cur);
567 packU32LE(&buf[4], cur);
568 return gattAppend(ctxt->om, buf, sizeof(buf));
569}
570
571int CProtoNimblePrphOts::idCb(uint16_t conn,
572 uint16_t /*attr*/,
573 struct ble_gatt_access_ctxt *ctxt,
574 void *arg)
575{
577 SOtsObjectMeta *meta;
578 uint8_t buf[6];
579 int16_t cursor = 0;
580 int err;
581
582 err = metaPrologue(conn, ctxt, arg, &self, &meta, &cursor);
583 if (err != 0)
584 {
585 return err;
586 }
587
588 // Map array index 0..N-1 to object IDs 256..256+N-1 (spec reserves
589 // 0..255 for "Directory Listing" object plus future use).
590
591 packU48LE(buf, 256ull + static_cast<uint64_t>(cursor));
592 return gattAppend(ctxt->om, buf, sizeof(buf));
593}
594
595int CProtoNimblePrphOts::propsCb(uint16_t conn,
596 uint16_t /*attr*/,
597 struct ble_gatt_access_ctxt *ctxt,
598 void *arg)
599{
601 SOtsObjectMeta *meta;
602 uint8_t buf[4];
603 uint32_t props = 0;
604 int err;
605
606 err = metaPrologue(conn, ctxt, arg, &self, &meta, nullptr);
607 if (err != 0)
608 {
609 return err;
610 }
611
612 if (meta->access == PRPH_OTS_ACCESS_READ || meta->access == PRPH_OTS_ACCESS_RW)
613 {
614 props |= OBJ_PROP_READ;
615 }
616
617 if (meta->access == PRPH_OTS_ACCESS_WRITE || meta->access == PRPH_OTS_ACCESS_RW)
618 {
619 props |= OBJ_PROP_WRITE | OBJ_PROP_TRUNC;
620 }
621
622 packU32LE(buf, props);
623 return gattAppend(ctxt->om, buf, sizeof(buf));
624}
625
626void CProtoNimblePrphOts::sendCpResponse(uint16_t conn,
627 uint16_t handle,
628 uint8_t respOp,
629 uint8_t reqOp,
630 uint8_t result)
631{
632#ifndef CONFIG_DAWN_PROTO_NIMBLE_DUMMY
633 uint8_t buf[3] = {respOp, reqOp, result};
634 struct os_mbuf *om;
635 int rc;
636
637 om = ble_hs_mbuf_from_flat(buf, sizeof(buf));
638 if (om == nullptr)
639 {
640 DAWNERR("OTS: response mbuf alloc failed\n");
641 return;
642 }
643
644 rc = ble_gatts_indicate_custom(conn, handle, om);
645 if (rc != 0)
646 {
647 DAWNERR("OTS: indicate failed: %d\n", rc);
648 }
649#else
650 (void)conn;
651 (void)handle;
652 (void)respOp;
653 (void)reqOp;
654 (void)result;
655#endif
656}
657
658inline void CProtoNimblePrphOts::sendOacpResponse(uint16_t conn, uint8_t reqOp, uint8_t result)
659{
660 sendCpResponse(conn, hOacp, OACP_OP_RESPONSE, reqOp, result);
661}
662
663inline void CProtoNimblePrphOts::sendOlcpResponse(uint16_t conn, uint8_t reqOp, uint8_t result)
664{
665 sendCpResponse(conn, hOlcp, OLCP_OP_RESPONSE, reqOp, result);
666}
667
668int CProtoNimblePrphOts::oacpCb(uint16_t conn,
669 uint16_t /*attr*/,
670 struct ble_gatt_access_ctxt *ctxt,
671 void *arg)
672{
674 uint8_t op;
675 uint8_t pkt[16];
676 uint16_t copied = 0;
677 int rc;
678
679 if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR)
680 {
681 return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
682 }
683
684 rc = ble_hs_mbuf_to_flat(ctxt->om, pkt, sizeof(pkt), &copied);
685 if (rc != 0 || copied < 1)
686 {
687 return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
688 }
689
690 self = static_cast<CProtoNimblePrphOts *>(arg);
691 op = pkt[0];
692
693 // Make sure connection has state.
694
695 self->vconn.try_emplace(conn);
696
697 switch (op)
698 {
699 case OACP_OP_READ:
700 {
701 uint32_t off;
702 uint32_t len;
703
704 if (copied < 9)
705 {
706 self->sendOacpResponse(conn, op, OACP_RES_INVALID_PARAM);
707 return 0;
708 }
709
710 off = unpackU32LE(&pkt[1]);
711 len = unpackU32LE(&pkt[5]);
712 self->handleOacpRead(conn, off, len);
713 return 0;
714 }
715
716 case OACP_OP_WRITE:
717 {
718 uint32_t off;
719 uint32_t len;
720 uint8_t mode;
721
722 if (copied < 10)
723 {
724 self->sendOacpResponse(conn, op, OACP_RES_INVALID_PARAM);
725 return 0;
726 }
727
728 off = unpackU32LE(&pkt[1]);
729 len = unpackU32LE(&pkt[5]);
730 mode = pkt[9];
731 self->handleOacpWrite(conn, off, len, mode);
732 return 0;
733 }
734
735 case OACP_OP_ABORT:
736 self->handleOacpAbort(conn);
737 return 0;
738
739 default:
740 self->sendOacpResponse(conn, op, OACP_RES_OPCODE_NS);
741 return 0;
742 }
743}
744
745int CProtoNimblePrphOts::olcpCb(uint16_t conn,
746 uint16_t /*attr*/,
747 struct ble_gatt_access_ctxt *ctxt,
748 void *arg)
749{
751 uint8_t op;
752 uint8_t pkt[16];
753 uint16_t copied = 0;
754 int rc;
755
756 if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR)
757 {
758 return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
759 }
760
761 rc = ble_hs_mbuf_to_flat(ctxt->om, pkt, sizeof(pkt), &copied);
762 if (rc != 0 || copied < 1)
763 {
764 return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
765 }
766
767 self = static_cast<CProtoNimblePrphOts *>(arg);
768 op = pkt[0];
769
770 self->vconn.try_emplace(conn);
771
772 switch (op)
773 {
774 case OLCP_OP_FIRST:
775 return self->handleOlcpFirst(conn);
776 case OLCP_OP_LAST:
777 return self->handleOlcpLast(conn);
778 case OLCP_OP_PREVIOUS:
779 return self->handleOlcpPrev(conn);
780 case OLCP_OP_NEXT:
781 return self->handleOlcpNext(conn);
782 case OLCP_OP_GOTO:
783 {
784 uint64_t oid;
785
786 if (copied < 7)
787 {
788 self->sendOlcpResponse(conn, op, OLCP_RES_INVALID_PARAM);
789 return 0;
790 }
791
792 oid = unpackU48LE(&pkt[1]);
793 return self->handleOlcpGoto(conn, oid);
794 }
795
796 default:
797 self->sendOlcpResponse(conn, op, OLCP_RES_OPCODE_NS);
798 return 0;
799 }
800}
801
802// Validate the OACP read/write preconditions: an object is selected, the
803// requested access matches the object's properties, and the backing IO is
804// resolvable and seekable. On success returns OACP_RES_SUCCESS with @c st
805// and @c io filled; on failure returns the OACP result code the caller
806// should reply with.
807
808uint8_t CProtoNimblePrphOts::oacpValidate(uint16_t conn,
809 bool wantWrite,
810 SOtsConnState *&st,
811 CIOCommon *&io)
812{
813 SOtsObjectMeta *meta;
814
815 if (!selected(conn, st, meta))
816 {
817 return OACP_RES_INVALID_OBJ;
818 }
819
820 if (wantWrite)
821 {
822 if (meta->access != PRPH_OTS_ACCESS_WRITE && meta->access != PRPH_OTS_ACCESS_RW)
823 {
824 return OACP_RES_PROC_NOT_PERM;
825 }
826 }
827 else
828 {
829 if (meta->access != PRPH_OTS_ACCESS_READ && meta->access != PRPH_OTS_ACCESS_RW)
830 {
831 return OACP_RES_PROC_NOT_PERM;
832 }
833 }
834
835 io = cb->getObject(meta->iid);
836 if (io == nullptr || !io->isSeekable())
837 {
838 return OACP_RES_OPER_FAILED;
839 }
840
841 return OACP_RES_SUCCESS;
842}
843
844int CProtoNimblePrphOts::handleOacpRead(uint16_t conn, uint32_t off, uint32_t len)
845{
846 SOtsConnState *st;
847 CIOCommon *io;
848 uint8_t res;
849
850 res = oacpValidate(conn, false, st, io);
851 if (res == OACP_RES_SUCCESS && off + len > io->getDataSize())
852 {
853 res = OACP_RES_INVALID_PARAM;
854 }
855
856 if (res != OACP_RES_SUCCESS)
857 {
858 sendOacpResponse(conn, OACP_OP_READ, res);
859 return 0;
860 }
861
862 st->mode = MODE_READING;
863 st->offset = off;
864 st->remaining = len;
865
866 sendOacpResponse(conn, OACP_OP_READ, OACP_RES_SUCCESS);
867 pumpRead(conn);
868 return 0;
869}
870
871int CProtoNimblePrphOts::handleOacpWrite(uint16_t conn, uint32_t off, uint32_t len, uint8_t mode)
872{
873 SOtsConnState *st;
874 CIOCommon *io;
875 uint8_t res;
876
877 // OACP Write mode parameter validation. We support mode == 0 (plain
878 // write) and mode == 0x02 (truncate). The truncate flag matches our
879 // backing IOs' natural behavior on offset-0 writes, so both modes
880 // produce the same on-disk result; rejecting other values keeps the
881 // contract honest and surfaces typos / future spec extensions.
882
883 if (mode != 0 && mode != OACP_WRITE_MODE_TRUNCATE)
884 {
885 sendOacpResponse(conn, OACP_OP_WRITE, OACP_RES_INVALID_PARAM);
886 return 0;
887 }
888
889 res = oacpValidate(conn, true, st, io);
890 if (res != OACP_RES_SUCCESS)
891 {
892 sendOacpResponse(conn, OACP_OP_WRITE, res);
893 return 0;
894 }
895
896 st->mode = MODE_WRITING;
897 st->offset = off;
898 st->remaining = len;
899
900 sendOacpResponse(conn, OACP_OP_WRITE, OACP_RES_SUCCESS);
901 return 0;
902}
903
904int CProtoNimblePrphOts::handleOacpAbort(uint16_t conn)
905{
906 std::map<uint16_t, SOtsConnState>::iterator it;
907
908 it = vconn.find(conn);
909 if (it != vconn.end())
910 {
911 it->second.mode = MODE_IDLE;
912 it->second.remaining = 0;
913 }
914
915 sendOacpResponse(conn, OACP_OP_ABORT, OACP_RES_SUCCESS);
916 return 0;
917}
918
919// Set the per-connection cursor to an absolute index, replying with
920// OLCP_RES_NO_OBJECT when the object list is empty. Used by First/Last.
921
922int CProtoNimblePrphOts::olcpSeekAbs(uint16_t conn, uint8_t op, size_t idx)
923{
924 if (vmeta.empty())
925 {
926 sendOlcpResponse(conn, op, OLCP_RES_NO_OBJECT);
927 return 0;
928 }
929
930 vconn[conn].cursor = static_cast<int16_t>(idx);
931 sendOlcpResponse(conn, op, OLCP_RES_SUCCESS);
932 return 0;
933}
934
935// Step the per-connection cursor by ``delta`` (+/-1), replying with
936// OLCP_RES_OOR when the result would fall outside [0, vmeta.size()).
937// Used by Prev/Next.
938
939int CProtoNimblePrphOts::olcpStep(uint16_t conn, uint8_t op, int delta)
940{
941 int16_t &c = vconn[conn].cursor;
942 int next = static_cast<int>(c) + delta;
943
944 if (next < 0 || static_cast<size_t>(next) >= vmeta.size())
945 {
946 sendOlcpResponse(conn, op, OLCP_RES_OOR);
947 return 0;
948 }
949
950 c = static_cast<int16_t>(next);
951 sendOlcpResponse(conn, op, OLCP_RES_SUCCESS);
952 return 0;
953}
954
955int CProtoNimblePrphOts::handleOlcpFirst(uint16_t conn)
956{
957 return olcpSeekAbs(conn, OLCP_OP_FIRST, 0);
958}
959
960int CProtoNimblePrphOts::handleOlcpLast(uint16_t conn)
961{
962 return olcpSeekAbs(conn, OLCP_OP_LAST, vmeta.empty() ? 0 : vmeta.size() - 1);
963}
964
965int CProtoNimblePrphOts::handleOlcpPrev(uint16_t conn)
966{
967 return olcpStep(conn, OLCP_OP_PREVIOUS, -1);
968}
969
970int CProtoNimblePrphOts::handleOlcpNext(uint16_t conn)
971{
972 return olcpStep(conn, OLCP_OP_NEXT, +1);
973}
974
975int CProtoNimblePrphOts::handleOlcpGoto(uint16_t conn, uint64_t obj_id)
976{
977 if (obj_id < 256)
978 {
979 sendOlcpResponse(conn, OLCP_OP_GOTO, OLCP_RES_OBJECT_ID_NF);
980 return 0;
981 }
982
983 uint64_t idx = obj_id - 256;
984 if (idx >= vmeta.size())
985 {
986 sendOlcpResponse(conn, OLCP_OP_GOTO, OLCP_RES_OBJECT_ID_NF);
987 return 0;
988 }
989
990 vconn[conn].cursor = static_cast<int16_t>(idx);
991 sendOlcpResponse(conn, OLCP_OP_GOTO, OLCP_RES_SUCCESS);
992 return 0;
993}
994
995int CProtoNimblePrphOts::pumpRead(uint16_t conn)
996{
997#ifdef CONFIG_DAWN_PROTO_NIMBLE_DUMMY
998 (void)conn;
999 return 0;
1000#else
1001 SOtsConnState *st;
1002 SOtsObjectMeta *meta;
1003 CIOCommon *io;
1004
1005 if (!selected(conn, st, meta))
1006 {
1007 return -EINVAL;
1008 }
1009
1010 if (st->mode != MODE_READING || st->chan == nullptr)
1011 {
1012 return 0;
1013 }
1014
1015 io = cb->getObject(meta->iid);
1016 if (io == nullptr)
1017 {
1018 return -EINVAL;
1019 }
1020
1021 if (xferBuf == nullptr)
1022 {
1023 DAWNERR("OTS: xferBuf not initialised\n");
1024 st->mode = MODE_IDLE;
1025 return -EINVAL;
1026 }
1027
1028 while (st->remaining > 0)
1029 {
1030 struct os_mbuf *sdu;
1031 size_t chunk;
1032 int rc;
1033
1034 chunk = std::min<size_t>(st->remaining, L2CAP_MTU_OTS);
1035
1036 // The pre-allocated xferBuf is fixed at L2CAP_MTU_OTS bytes; trim
1037 // its logical size (T*N) to the per-chunk amount so the seekable
1038 // IO read fills exactly `chunk` bytes.
1039
1040 xferBuf->N = chunk;
1041
1042 rc = io->getData(*xferBuf, 1, st->offset);
1043 if (rc < 0)
1044 {
1045 st->mode = MODE_IDLE;
1046 return rc;
1047 }
1048
1049 sdu = ble_hs_mbuf_from_flat(xferBuf->getDataPtr(), chunk);
1050 if (sdu == nullptr)
1051 {
1052 return -ENOMEM;
1053 }
1054
1055 rc = ble_l2cap_send(st->chan, sdu);
1056 if (rc == BLE_HS_ESTALLED)
1057 {
1058 // Wait for TX_UNSTALLED -- flow control will resume the pump.
1059 // Account this chunk as already in flight (NimBLE owns the mbuf).
1060
1061 st->offset += chunk;
1062 st->remaining -= chunk;
1063 return 0;
1064 }
1065
1066 if (rc != 0)
1067 {
1068 DAWNERR("OTS: l2cap_send failed: %d\n", rc);
1069 st->mode = MODE_IDLE;
1070 return -EIO;
1071 }
1072
1073 st->offset += chunk;
1074 st->remaining -= chunk;
1075 }
1076
1077 st->mode = MODE_IDLE;
1078 return 0;
1079#endif
1080}
1081
1082#ifndef CONFIG_DAWN_PROTO_NIMBLE_DUMMY
1083void CProtoNimblePrphOts::onL2capRx(struct ble_l2cap_event *event)
1084{
1085 std::map<uint16_t, SOtsConnState>::iterator it;
1086 struct os_mbuf *sdu;
1087 struct os_mbuf *next;
1088 SOtsObjectMeta *meta;
1089 CIOCommon *io;
1090 uint16_t conn;
1091 uint16_t pktLen;
1092 uint16_t copied;
1093 bool drop;
1094 int rc;
1095
1096 conn = event->receive.conn_handle;
1097 sdu = event->receive.sdu_rx;
1098 pktLen = OS_MBUF_PKTLEN(sdu);
1099 it = vconn.find(conn);
1100 drop = false;
1101
1102 // Validate prerequisites: connection known, in WRITING mode, packet
1103 // size sane, scratch buffer available, cursor valid. Anything else is
1104 // a protocol violation -- log and tear down the channel rather than
1105 // silently dropping bytes.
1106
1107 if (it == vconn.end() || it->second.mode != MODE_WRITING || pktLen == 0 ||
1108 pktLen > L2CAP_MTU_OTS || xferBuf == nullptr || it->second.cursor < 0 ||
1109 static_cast<size_t>(it->second.cursor) >= vmeta.size())
1110 {
1111 DAWNERR("OTS: unexpected L2CAP RX (conn=%u len=%u mode=%u)\n",
1112 conn,
1113 pktLen,
1114 (it != vconn.end()) ? it->second.mode : 0xff);
1115 drop = true;
1116 }
1117 else if (pktLen > it->second.remaining)
1118 {
1119 // Client sent more data than declared in the OACP Write request.
1120
1121 DAWNERR("OTS: write overflow (conn=%u declared=%" PRIu32 " got=%u)\n",
1122 conn,
1123 it->second.remaining,
1124 pktLen);
1125 it->second.mode = MODE_IDLE;
1126 drop = true;
1127 }
1128 else
1129 {
1130 meta = &vmeta[it->second.cursor];
1131 io = cb->getObject(meta->iid);
1132 if (io == nullptr)
1133 {
1134 DAWNERR("OTS: backing IO 0x%" PRIx32 " gone mid-write\n", meta->iid);
1135 it->second.mode = MODE_IDLE;
1136 drop = true;
1137 }
1138 else
1139 {
1140 copied = 0;
1141 ble_hs_mbuf_to_flat(sdu, xferBuf->getDataPtr(), pktLen, &copied);
1142
1143 // The pre-allocated xferBuf is fixed at L2CAP_MTU_OTS bytes;
1144 // trim its logical size so setDataAtImpl writes exactly the
1145 // bytes received in this packet.
1146
1147 xferBuf->N = copied;
1148 rc = io->setData(*xferBuf, it->second.offset);
1149 if (rc < 0)
1150 {
1151 DAWNERR("OTS: setData failed off=%" PRIu32 " rc=%d\n", it->second.offset, rc);
1152 it->second.mode = MODE_IDLE;
1153 drop = true;
1154 }
1155 else
1156 {
1157 it->second.offset += copied;
1158 it->second.remaining =
1159 (it->second.remaining > copied) ? it->second.remaining - copied : 0;
1160 if (it->second.remaining == 0)
1161 {
1162 it->second.mode = MODE_IDLE;
1163 }
1164 }
1165 }
1166 }
1167
1168 os_mbuf_free_chain(sdu);
1169
1170 if (drop)
1171 {
1172 // Tear down the L2CAP channel so the client sees the failure.
1173 // NimBLE will deliver COC_DISCONNECTED, which clears chan in vconn.
1174
1175 if (event->receive.chan != nullptr)
1176 {
1177 ble_l2cap_disconnect(event->receive.chan);
1178 }
1179
1180 return;
1181 }
1182
1183 // Re-arm the L2CAP receive buffer for the next inbound SDU.
1184
1185 next = ble_hs_mbuf_att_pkt();
1186 if (next != nullptr)
1187 {
1188 ble_l2cap_recv_ready(event->receive.chan, next);
1189 }
1190}
1191#endif
1192
1193int CProtoNimblePrphOts::l2capEventCb(struct ble_l2cap_event *event, void *arg)
1194{
1195#ifdef CONFIG_DAWN_PROTO_NIMBLE_DUMMY
1196 (void)event;
1197 (void)arg;
1198 return 0;
1199#else
1200 CProtoNimblePrphOts *self;
1201 uint16_t conn;
1202
1203 self = static_cast<CProtoNimblePrphOts *>(arg);
1204
1205 switch (event->type)
1206 {
1207 case BLE_L2CAP_EVENT_COC_ACCEPT:
1208 {
1209 // Accept the channel: NimBLE expects us to provide an SDU receive
1210 // buffer. We stash an empty mbuf for now.
1211 //
1212 // Single-client policy: the per-instance scratch buffer xferBuf
1213 // is shared across the service, so concurrent OTS sessions are
1214 // unsafe. If any vconn entry already has an active CoC channel,
1215 // refuse this accept with BLE_HS_EBUSY -- the central will see
1216 // the L2CAP connect fail and can retry once the first client
1217 // disconnects.
1218
1219 struct os_mbuf *sdu;
1220
1221 for (auto &kv : self->vconn)
1222 {
1223 if (kv.second.chan != nullptr)
1224 {
1225 DAWNINFO("OTS: refusing second L2CAP CoC (busy)\n");
1226 return BLE_HS_EBUSY;
1227 }
1228 }
1229
1230 sdu = ble_hs_mbuf_att_pkt();
1231 if (sdu == nullptr)
1232 {
1233 return BLE_HS_ENOMEM;
1234 }
1235
1236 ble_l2cap_recv_ready(event->accept.chan, sdu);
1237 return 0;
1238 }
1239
1240 case BLE_L2CAP_EVENT_COC_CONNECTED:
1241 {
1242 conn = event->connect.conn_handle;
1243 self->vconn.try_emplace(conn);
1244 self->vconn[conn].chan = event->connect.chan;
1245
1246 // If an OACP Read is already pending (the client may issue it
1247 // before opening the channel, but our server can also accept
1248 // the spec-recommended order channel-first then OACP), kick
1249 // off the TX pump now that we have a channel.
1250
1251 if (self->vconn[conn].mode == MODE_READING)
1252 {
1253 self->pumpRead(conn);
1254 }
1255
1256 return 0;
1257 }
1258
1259 case BLE_L2CAP_EVENT_COC_DISCONNECTED:
1260 {
1261 std::map<uint16_t, SOtsConnState>::iterator it;
1262
1263 conn = event->disconnect.conn_handle;
1264 it = self->vconn.find(conn);
1265 if (it != self->vconn.end())
1266 {
1267 it->second.chan = nullptr;
1268 it->second.mode = MODE_IDLE;
1269 }
1270 return 0;
1271 }
1272
1273 case BLE_L2CAP_EVENT_COC_TX_UNSTALLED:
1274 self->pumpRead(event->tx_unstalled.conn_handle);
1275 return 0;
1276
1277 case BLE_L2CAP_EVENT_COC_DATA_RECEIVED:
1278 self->onL2capRx(event);
1279 return 0;
1280
1281 default:
1282 return 0;
1283 }
1284#endif
1285}
Base class for all I/O objects.
Definition common.hxx:27
int setData(IODataCmn &data, size_t offset=0)
Set data for I/O (public interface with stats tracking).
Definition common.hxx:392
int getData(IODataCmn &data, size_t len, size_t offset=0)
Get data from I/O (public interface with stats tracking).
Definition common.hxx:353
virtual bool isSeekable() const
Check if IO supports partial (seekable) access.
Definition common.hxx:502
virtual size_t getDataSize() const =0
Get data size in bytes.
Object Transfer Service (OTS) for BLE Peripheral.
Definition prph_ots.hxx:42
static uint16_t UUID16_OBJ_PROPS
Object Properties characteristic.
Definition prph_ots.hxx:70
int start()
Start service.
Definition prph_ots.cxx:372
@ OACP_RES_OPCODE_NS
Opcode Not Supported.
Definition prph_ots.hxx:143
@ OLCP_RES_OOR
Out Of Bounds.
Definition prph_ots.hxx:176
static uint16_t UUID16_OTS
OTS Service UUID (Bluetooth SIG).
Definition prph_ots.hxx:46
int stop()
Stop service.
Definition prph_ots.cxx:402
static uint16_t OBJ_TYPE_DESCRIPTOR
Object Type for a Dawn descriptor blob (Dawn-private UUID16).
Definition prph_ots.hxx:92
static uint8_t OBJ_NAME_MAX
OTS object name length (bytes, fixed).
Definition prph_ots.hxx:208
static uint16_t L2CAP_PSM_OTS
L2CAP PSM for OTS bulk data (Bluetooth SIG-assigned).
Definition prph_ots.hxx:105
static uint16_t UUID16_OBJ_NAME
Object Name characteristic.
Definition prph_ots.hxx:54
static uint16_t UUID16_OBJ_SIZE
Object Size characteristic (current + allocated, 8 bytes).
Definition prph_ots.hxx:62
int init()
Initialize service.
Definition prph_ots.cxx:341
static uint16_t UUID16_OLCP
Object List Control Point characteristic.
Definition prph_ots.hxx:78
static uint16_t L2CAP_MTU_OTS
L2CAP CoC MTU for OTS data channel.
Definition prph_ots.hxx:109
static uint16_t OBJ_TYPE_UNSPECIFIED
Object Type for an "Unspecified" file (Bluetooth SIG).
Definition prph_ots.hxx:82
static uint16_t UUID16_OBJ_ID
Object ID characteristic (48-bit).
Definition prph_ots.hxx:66
static uint16_t UUID16_OACP
Object Action Control Point characteristic.
Definition prph_ots.hxx:74
int deinit()
Deinitialize service.
Definition prph_ots.cxx:363
static uint16_t UUID16_FEATURE
OTS Feature characteristic.
Definition prph_ots.hxx:50
static uint16_t UUID16_OBJ_TYPE
Object Type characteristic.
Definition prph_ots.hxx:58
static uint16_t OBJ_TYPE_CAPS
Object Type for a Dawn capabilities blob (Dawn-private UUID16).
Definition prph_ots.hxx:101
Interface for BLE peripheral services with GATT characteristics.
Definition iprph.hxx:24
virtual int startService(int id)=0
Start a specific service.
virtual CIOCommon * getObject(SObjectId::ObjectId id)=0
Get protocol object by ID.
virtual int serviceRegister(struct ble_gatt_svc_def *svc)=0
Register a GATT service with the peripheral.
virtual void regObject(SObjectId::ObjectId id)=0
Register an I/O object for this service.
virtual int stopService(int id)=0
Stop a specific service.
Base interface for GATT services exposed by BLE peripheral.
Definition iprph.hxx:467
std::vector< SObjectId::ObjectId > vio
Vector of I/O objects exposed by this service.
Definition iprph.hxx:557
const SObjectCfg::SObjectCfgItem * desc
Configuration descriptor for this service.
Definition iprph.hxx:574
IProtoNimblePrphCb * cb
Callback interface to peripheral.
Definition iprph.hxx:566
Out-of-tree user-extension hooks for Dawn.
Definition bindable.hxx:13
Single configuration item within object.
ObjectCfgData_t data[]
Configuration data array (flexible, size from cfgid.s.size).
UObjectCfgId cfgid
Configuration ID header (type, class, id, size, rw, dtype).
@ DTYPE_UINT8
Unsigned 8-bit integer (0 to 255).
Definition objectid.hxx:80
uint32_t ObjectId
ObjectID type - single 32-bit value.
Definition objectid.hxx:44
Heap-allocated dynamic I/O data buffer.
Definition ddata.hxx:21
void * getDataPtr(size_t batch=0)
Get pointer to data only (skips timestamp if present).
Definition ddata.hxx:180
uint32_t size
Configuration data size in 32-bit words (bits 5-14, max 1023).
struct dawn::SObjectCfg::UObjectCfgId::@10 s
Bit-field structure for named member access.