11.11. Các kênh L2CAP¶
GATT là mô hình key/value. Các thao tác nó cung cấp (đọc, ghi, notify, indicate) chỉ truyền một giá trị ngắn mỗi lần, và payload lớn nhất mà chúng có thể mang là kích thước MTU đã thương lượng -- tốt nhất là vài trăm byte. Điều đó hoạt động tốt cho các giá trị đọc từ cảm biến, thanh ghi lệnh và cờ trạng thái. Nhưng nó không hiệu quả với kilobyte hay megabyte: việc chia một vùng màu (blob) dài thành hàng trăm lần ghi nhỏ tốn nhiều vòng quay mà radio có thể truyền nhanh hơn nhiều.
Đối với các luồng dữ liệu lớn -- một khung hình mà camera truyền tới điện thoại, ảnh cập nhật qua mạng (OTA), hay một lô xuất dữ liệu đo lường -- BLE cung cấp một đường dẫn thay thế: Giao thức Điều khiển Liên kết Logic và Thích ứng (L2CAP). L2CAP nằm giữa lớp liên kết và GATT, cho phép ứng dụng yêu cầu một kênh hướng kết nối riêng trên cùng liên kết radio. Kênh này là một đường truyền byte được kiểm soát bằng credit-flow với MTU mỗi gói lớn hơn nhiều và không có khung GATT ở giữa.
11.11.1. Khi nào nên dùng L2CAP¶
Kênh L2CAP là công cụ phù hợp khi:
Lượng dữ liệu truyền vượt quá vài trăm byte.
Cả hai đầu đều biết sẽ sử dụng kênh L2CAP (kênh này không được công bố trong payload quảng cáo; client phải biết protocol/service multiplexer (PSM) của kênh từ nguồn ngoài).
Ứng dụng sẵn sàng từ bỏ những tiện ích của GATT: không có khả năng định địa chỉ theo UUID, không cho phép client khám phá qua ứng dụng chuẩn, không có thông báo.
Trường hợp phổ biến nhất trong các ứng dụng dùng aioble là truyền một vùng màu (blob) nhị phân giữa hai phần mềm đều biết về quy ước PSM -- một giao thức camera-to-phone tùy chỉnh, một cặp camera openmv giao tiếp với nhau, hay một đường cập nhật firmware nội bộ dưới dịch vụ GATT của một ngoại vi.
Với mọi trường hợp khác, hãy dùng GATT. Trạng thái ngắn, thanh ghi điều khiển, giá trị đọc từ cảm biến -- tất cả những thứ đó thuộc về một characteristic.
11.11.2. Thiết lập kênh¶
L2CAP chạy trên nền một aioble.DeviceConnection đang tồn tại, vì vậy luồng khám phá/quảng cáo/kết nối phía GAP hoàn toàn giống như với GATT. Khi cả hai bên đã có kết nối, một bên lắng nghe trên PSM, bên kia kết nối tới PSM đó.
PSM chỉ là một số nguyên nhỏ. Bluetooth SIG dành phần dưới của dải số cho việc sử dụng đã chuẩn hóa (0x0001-0x007F); với các kênh dành riêng cho ứng dụng, hãy dùng số trong dải động (0x0080-0x00FF cho PSM cố định, từ 0x0040 trở lên thường miễn phí cho mục đích tùy chỉnh). Cả hai bên phải đồng ý giá trị trước.
MTU trên kênh L2CAP là SDU (Service Data Unit) đơn lớn nhất mà mỗi bên sẽ truyền trong một lần send() -- không phải MTU liên kết BLE. Aioble tự động phân mảnh các payload lớn hơn. BLE host của camera giới hạn MTU L2CAP ở 1017 byte; 512 là giá trị mặc định hợp lý, để lại dư địa cho cả hai bên mà không tốn quá nhiều RAM.
Phía người nghe (ví dụ: camera đóng vai trò peripheral):
async def serve_l2cap(connection, image_bytes):
channel = await connection.l2cap_accept(psm=0x80, mtu=512)
async with channel:
# image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
# or a compressed JPEG buffer. send() fragments into MTU-sized
# chunks automatically and awaits flow-control credits between.
await channel.send(image_bytes)
await channel.flush()
Phía người kết nối (ví dụ: điện thoại hoặc central):
async def open_l2cap(connection, total_bytes):
channel = await connection.l2cap_connect(psm=0x80, mtu=512)
async with channel:
image_bytes = bytearray(total_bytes)
view = memoryview(image_bytes)
received = 0
while received < total_bytes:
n = await channel.recvinto(view[received:])
if n == 0:
break
received += n
return image_bytes
l2cap_accept() chặn cho đến khi peer kết nối (hoặc timeout_ms kích hoạt); l2cap_connect() chặn cho đến khi listener chấp nhận (hoặc thất bại). Cả hai đều trả về một aioble.L2CAPChannel -- bản thân nó là một async context manager đóng kênh khi thoát.
11.11.3. Gửi và nhận¶
Hai thao tác chính trên kênh là send() (ghi byte tới peer) và recvinto() (đọc vào bộ đệm được cấp phát trước). Cả hai đều là coroutine.
send()phân mảnh bộ đệm thành các đoạn có kích thước MTU và chờ credit kiểm soát luồng ở lớp liên kết giữa chúng. Một lần gửi dài chỉ là mộtawaittừ góc độ ứng dụng; bên trong nó có thể xếp nhiều gói vào hàng đợi và tạm dừng khi credit nhận của peer cạn kiệt.recvinto()điền vào bộ đệm được truyền vào với dữ liệu có sẵn (tối đa bằng MTU của kênh) và trả về số byte. Chờ nếu không có dữ liệu.available()trả vềTruemột cách đồng bộ nếu có dữ liệu đã được đệm sẵn sàng -- hữu ích để polling mà không cần tạm dừng.flush()chờ cho đến khi mọi lần gửi đang chờ xử lý đã được truyền hoàn toàn tới controller.
Các kênh L2CAP có tính chất giống luồng theo nghĩa các byte đến theo thứ tự và không bị mất, nhưng ranh giới của một lần send được giữ nguyên -- mỗi SDU xuất ra từ một lần recvinto duy nhất. Điều này khác với TCP, nơi ranh giới của một lần send() có thể trải dài qua nhiều lần gọi recv().
11.11.4. Xử lý ngắt kết nối¶
Kênh biến mất trong ba trường hợp: một trong hai bên gọi disconnect(), kết nối GAP cơ bản bị ngắt, hoặc có lệnh ngắt kết nối ở cấp L2CAP. Các thao tác đang hoạt động sẽ ném ra aioble.L2CAPDisconnectedError. Giống như phía GATT, điều này hiển thị dưới dạng một exception trong coroutine đang chờ, và khối async with channel thoát ra một cách sạch sẽ.
Nếu một kênh trở nên không thể truy cập do ngắt kết nối ở cấp GAP, ứng dụng quay lại quảng cáo hoặc quét theo cách tương tự như khi ngắt kết nối GATT.
11.11.5. Chi phí bộ nhớ¶
MTU lớn hơn và hàng đợi dài hơn sử dụng nhiều RAM hơn ở cả hai bên. MTU 512 byte cộng với bộ đệm nhận per-channel khoảng 1 KB mỗi kênh -- không miễn phí trên một camera nhỏ nếu có nhiều kênh mở cùng lúc. Hãy giữ một kênh mỗi kết nối và chọn MTU phù hợp với kích thước thông điệp mong đợi; mặc định một L2CAPChannel mỗi DeviceConnection là đủ cho hầu hết các ứng dụng.
L2CAP là van an toàn của BLE. GATT là thứ hầu hết mọi ứng dụng đều dùng trước tiên, và phần còn lại của các ví dụ central/peripheral trong phần này đều dùng GATT. API theo kiểu kênh là câu trả lời khi một ứng dụng đã vượt qua mô hình key/value.