Merge pull request #6013 from tannewt/esp_ble_gatt_client
Add S3 GATT client support
This commit is contained in:
commit
fe6e03f504
|
@ -2427,6 +2427,16 @@ msgstr ""
|
|||
msgid "Unhandled ESP TLS error %d %d %x %d"
|
||||
msgstr ""
|
||||
|
||||
#: ports/espressif/common-hal/_bleio/__init__.c
|
||||
#, c-format
|
||||
msgid "Unknown BLE error at %s:%d: %d"
|
||||
msgstr ""
|
||||
|
||||
#: ports/espressif/common-hal/_bleio/__init__.c
|
||||
#, c-format
|
||||
msgid "Unknown BLE error: %d"
|
||||
msgstr ""
|
||||
|
||||
#: shared-bindings/wifi/Radio.c
|
||||
#, c-format
|
||||
msgid "Unknown failure %d"
|
||||
|
@ -2498,12 +2508,14 @@ msgstr ""
|
|||
msgid "Update Failed"
|
||||
msgstr ""
|
||||
|
||||
#: ports/espressif/common-hal/_bleio/Characteristic.c
|
||||
#: ports/espressif/common-hal/_bleio/Descriptor.c
|
||||
#: ports/nrf/common-hal/_bleio/Characteristic.c
|
||||
#: ports/nrf/common-hal/_bleio/Descriptor.c
|
||||
msgid "Value length != required fixed length"
|
||||
msgstr ""
|
||||
|
||||
#: ports/espressif/common-hal/_bleio/Characteristic.c
|
||||
#: ports/espressif/common-hal/_bleio/Descriptor.c
|
||||
#: ports/nrf/common-hal/_bleio/Characteristic.c
|
||||
#: ports/nrf/common-hal/_bleio/Descriptor.c
|
||||
|
@ -3788,6 +3800,7 @@ msgstr ""
|
|||
msgid "non-Device in %q"
|
||||
msgstr ""
|
||||
|
||||
#: ports/espressif/common-hal/_bleio/Connection.c
|
||||
#: ports/nrf/common-hal/_bleio/Connection.c
|
||||
msgid "non-UUID found in service_uuids_whitelist"
|
||||
msgstr ""
|
||||
|
|
|
@ -268,6 +268,10 @@ ifneq ($(CIRCUITPY_USB),0)
|
|||
SRC_C += lib/tinyusb/src/portable/espressif/esp32sx/dcd_esp32sx.c
|
||||
endif
|
||||
|
||||
ifneq ($(CIRCUITPY_BLEIO),0)
|
||||
SRC_C += common-hal/_bleio/ble_events.c
|
||||
endif
|
||||
|
||||
SRC_COMMON_HAL_EXPANDED = \
|
||||
$(addprefix shared-bindings/, $(SRC_COMMON_HAL)) \
|
||||
$(addprefix shared-bindings/, $(SRC_BINDINGS_ENUMS)) \
|
||||
|
|
|
@ -256,6 +256,17 @@ STATIC void _convert_address(const bleio_address_obj_t *address, ble_addr_t *nim
|
|||
memcpy(nimble_address->val, (uint8_t *)address_buf_info.buf, NUM_BLEIO_ADDRESS_BYTES);
|
||||
}
|
||||
|
||||
STATIC int _mtu_reply(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
uint16_t mtu, void *arg) {
|
||||
bleio_connection_internal_t *connection = (bleio_connection_internal_t *)arg;
|
||||
if (conn_handle != connection->conn_handle || error->status != 0) {
|
||||
return 0;
|
||||
}
|
||||
connection->mtu = mtu;
|
||||
return 0;
|
||||
}
|
||||
|
||||
STATIC void _new_connection(uint16_t conn_handle) {
|
||||
// Set the tx_power for the connection higher than the advertisement.
|
||||
esp_ble_tx_power_set(conn_handle, ESP_PWR_LVL_N0);
|
||||
|
@ -275,12 +286,96 @@ STATIC void _new_connection(uint16_t conn_handle) {
|
|||
connection->pair_status = PAIR_NOT_PAIRED;
|
||||
connection->mtu = 0;
|
||||
|
||||
ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection);
|
||||
|
||||
// Change the callback for the connection.
|
||||
ble_gap_set_event_cb(conn_handle, bleio_connection_event_cb, connection);
|
||||
}
|
||||
|
||||
static int _connect_event(struct ble_gap_event *event, void *self_in) {
|
||||
bleio_adapter_obj_t *self = (bleio_adapter_obj_t *)self_in;
|
||||
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "Connect event: %d\n", event->type);
|
||||
#endif
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
if (event->connect.status == 0) {
|
||||
_new_connection(event->connect.conn_handle);
|
||||
// Set connections objs back to NULL since we have a new
|
||||
// connection and need a new tuple.
|
||||
self->connection_objs = NULL;
|
||||
xTaskNotify(cp_task, event->connect.conn_handle, eSetValueWithOverwrite);
|
||||
} else {
|
||||
xTaskNotify(cp_task, -event->connect.status, eSetValueWithOverwrite);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
// For debugging.
|
||||
mp_printf(&mp_plat_print, "Unhandled connect event: %d\n", event->type);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
mp_obj_t common_hal_bleio_adapter_connect(bleio_adapter_obj_t *self, bleio_address_obj_t *address, mp_float_t timeout) {
|
||||
mp_raise_NotImplementedError(NULL);
|
||||
// Stop any active scan.
|
||||
if (self->scan_results != NULL) {
|
||||
common_hal_bleio_adapter_stop_scan(self);
|
||||
}
|
||||
|
||||
struct ble_gap_conn_params conn_params = {
|
||||
.scan_itvl = MSEC_TO_UNITS(100, UNIT_0_625_MS),
|
||||
.scan_window = MSEC_TO_UNITS(100, UNIT_0_625_MS),
|
||||
.itvl_min = MSEC_TO_UNITS(15, UNIT_1_25_MS),
|
||||
.itvl_max = MSEC_TO_UNITS(300, UNIT_1_25_MS),
|
||||
.latency = 0,
|
||||
.supervision_timeout = MSEC_TO_UNITS(4000, UNIT_10_MS),
|
||||
.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN,
|
||||
.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN
|
||||
};
|
||||
|
||||
uint8_t own_addr_type;
|
||||
// TODO: Use a resolvable address if the peer has our key.
|
||||
CHECK_NIMBLE_ERROR(ble_hs_id_infer_auto(false, &own_addr_type));
|
||||
|
||||
ble_addr_t addr;
|
||||
_convert_address(address, &addr);
|
||||
|
||||
cp_task = xTaskGetCurrentTaskHandle();
|
||||
// Make sure we don't have a pending notification from a previous time. This
|
||||
// can happen if a previous wait timed out before the notification was given.
|
||||
xTaskNotifyStateClear(cp_task);
|
||||
CHECK_NIMBLE_ERROR(
|
||||
ble_gap_connect(own_addr_type, &addr,
|
||||
SEC_TO_UNITS(timeout, UNIT_1_MS) + 0.5f,
|
||||
&conn_params,
|
||||
_connect_event, self));
|
||||
|
||||
int error_code;
|
||||
CHECK_NOTIFY(xTaskNotifyWait(0, 0, (uint32_t *)&error_code, 200));
|
||||
// Negative values are error codes, connection handle otherwise.
|
||||
if (error_code < 0) {
|
||||
CHECK_BLE_ERROR(-error_code);
|
||||
}
|
||||
uint16_t conn_handle = error_code;
|
||||
|
||||
// TODO: If we have keys, then try and encrypt the connection.
|
||||
|
||||
// TODO: Negotiate for better PHY and data lengths since we are the central. These are
|
||||
// nice-to-haves so ignore any errors.
|
||||
|
||||
// Make the connection object and return it.
|
||||
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
|
||||
bleio_connection_internal_t *connection = &bleio_connections[i];
|
||||
if (connection->conn_handle == conn_handle) {
|
||||
connection->is_central = true;
|
||||
return bleio_connection_new_from_internal(connection);
|
||||
}
|
||||
}
|
||||
|
||||
mp_raise_bleio_BluetoothError(translate("Failed to connect: internal error"));
|
||||
|
||||
|
|
|
@ -42,40 +42,22 @@ void common_hal_bleio_characteristic_construct(bleio_characteristic_obj_t *self,
|
|||
bleio_attribute_security_mode_t read_perm, bleio_attribute_security_mode_t write_perm,
|
||||
mp_int_t max_length, bool fixed_length, mp_buffer_info_t *initial_value_bufinfo,
|
||||
const char *user_description) {
|
||||
mp_raise_NotImplementedError(NULL);
|
||||
self->service = service;
|
||||
self->uuid = uuid;
|
||||
self->handle = BLEIO_HANDLE_INVALID;
|
||||
self->cccd_handle = BLEIO_HANDLE_INVALID;
|
||||
self->sccd_handle = BLEIO_HANDLE_INVALID;
|
||||
self->props = props;
|
||||
self->read_perm = read_perm;
|
||||
self->write_perm = write_perm;
|
||||
self->initial_value_len = 0;
|
||||
self->initial_value = NULL;
|
||||
if (initial_value_bufinfo != NULL) {
|
||||
// Copy the initial value if it's on the heap. Otherwise it's internal and we may not be able
|
||||
// to allocate.
|
||||
self->initial_value_len = initial_value_bufinfo->len;
|
||||
common_hal_bleio_characteristic_set_value(self, initial_value_bufinfo);
|
||||
|
||||
if (gc_alloc_possible()) {
|
||||
if (gc_nbytes(initial_value_bufinfo->buf) > 0) {
|
||||
uint8_t *initial_value = m_malloc(self->initial_value_len, false);
|
||||
memcpy(initial_value, initial_value_bufinfo->buf, self->initial_value_len);
|
||||
self->initial_value = initial_value;
|
||||
} else {
|
||||
self->initial_value = initial_value_bufinfo->buf;
|
||||
}
|
||||
self->descriptor_list = mp_obj_new_list(0, NULL);
|
||||
} else {
|
||||
self->initial_value = initial_value_bufinfo->buf;
|
||||
self->descriptor_list = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// const mp_int_t max_length_max = fixed_length ? BLE_GATTS_FIX_ATTR_LEN_MAX : BLE_GATTS_VAR_ATTR_LEN_MAX;
|
||||
// if (max_length < 0 || max_length > max_length_max) {
|
||||
// mp_raise_ValueError_varg(translate("max_length must be 0-%d when fixed_length is %s"),
|
||||
// max_length_max, fixed_length ? "True" : "False");
|
||||
// }
|
||||
// TODO: Implement this.
|
||||
self->max_length = max_length;
|
||||
self->fixed_length = fixed_length;
|
||||
|
||||
|
@ -97,8 +79,60 @@ bleio_service_obj_t *common_hal_bleio_characteristic_get_service(bleio_character
|
|||
return self->service;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
TaskHandle_t task;
|
||||
uint8_t *buf;
|
||||
uint16_t len;
|
||||
} _read_info_t;
|
||||
|
||||
STATIC int _read_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr,
|
||||
void *arg) {
|
||||
_read_info_t *read_info = (_read_info_t *)arg;
|
||||
switch (error->status) {
|
||||
case 0: {
|
||||
int len = MIN(read_info->len, OS_MBUF_PKTLEN(attr->om));
|
||||
os_mbuf_copydata(attr->om, attr->offset, len, read_info->buf);
|
||||
read_info->len = len;
|
||||
}
|
||||
MP_FALLTHROUGH;
|
||||
|
||||
default:
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
// For debugging.
|
||||
mp_printf(&mp_plat_print, "Read status: %d\n", error->status);
|
||||
#endif
|
||||
xTaskNotify(read_info->task, error->status, eSetValueWithOverwrite);
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t common_hal_bleio_characteristic_get_value(bleio_characteristic_obj_t *self, uint8_t *buf, size_t len) {
|
||||
// TODO: Implement this.
|
||||
// Do GATT operations only if this characteristic has been added to a registered service.
|
||||
if (self->handle == BLEIO_HANDLE_INVALID) {
|
||||
return 0;
|
||||
}
|
||||
uint16_t conn_handle = bleio_connection_get_conn_handle(self->service->connection);
|
||||
if (common_hal_bleio_service_get_is_remote(self->service)) {
|
||||
_read_info_t read_info = {
|
||||
.task = xTaskGetCurrentTaskHandle(),
|
||||
.buf = buf,
|
||||
.len = len
|
||||
};
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_read(conn_handle, self->handle, _read_cb, &read_info));
|
||||
int error_code;
|
||||
xTaskNotifyWait(0, 0, (uint32_t *)&error_code, 200);
|
||||
CHECK_BLE_ERROR(error_code);
|
||||
return read_info.len;
|
||||
} else {
|
||||
len = MIN(self->current_value_len, len);
|
||||
memcpy(buf, self->current_value, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -106,8 +140,62 @@ size_t common_hal_bleio_characteristic_get_max_length(bleio_characteristic_obj_t
|
|||
return self->max_length;
|
||||
}
|
||||
|
||||
STATIC int _write_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr,
|
||||
void *arg) {
|
||||
TaskHandle_t task = (TaskHandle_t)arg;
|
||||
xTaskNotify(task, error->status, eSetValueWithOverwrite);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void common_hal_bleio_characteristic_set_value(bleio_characteristic_obj_t *self, mp_buffer_info_t *bufinfo) {
|
||||
// TODO: Implement this.
|
||||
if (common_hal_bleio_service_get_is_remote(self->service)) {
|
||||
uint16_t conn_handle = bleio_connection_get_conn_handle(self->service->connection);
|
||||
if ((self->props & CHAR_PROP_WRITE_NO_RESPONSE) != 0) {
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_write_no_rsp_flat(conn_handle, self->handle, bufinfo->buf, bufinfo->len));
|
||||
} else {
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_write_flat(conn_handle, self->handle, bufinfo->buf, bufinfo->len, _write_cb, xTaskGetCurrentTaskHandle()));
|
||||
int error_code;
|
||||
xTaskNotifyWait(0, 0, (uint32_t *)&error_code, 200);
|
||||
CHECK_BLE_ERROR(error_code);
|
||||
}
|
||||
} else {
|
||||
// Validate data length for local characteristics only.
|
||||
// TODO: Test this once we can get servers going.
|
||||
if (self->fixed_length && bufinfo->len != self->max_length) {
|
||||
mp_raise_ValueError(translate("Value length != required fixed length"));
|
||||
}
|
||||
if (bufinfo->len > self->max_length) {
|
||||
mp_raise_ValueError(translate("Value length > max_length"));
|
||||
}
|
||||
|
||||
if (bufinfo == NULL) {
|
||||
self->current_value_len = 0;
|
||||
ble_gatts_chr_updated(self->handle);
|
||||
return;
|
||||
}
|
||||
|
||||
self->current_value_len = bufinfo->len;
|
||||
// If we've already allocated an internal buffer or the provided buffer
|
||||
// is on the heap, then copy into the internal buffer.
|
||||
if (self->current_value_alloc > 0 || gc_nbytes(bufinfo->buf) > 0) {
|
||||
if (self->current_value_alloc < bufinfo->len) {
|
||||
self->current_value = m_realloc(self->current_value, bufinfo->len);
|
||||
// Get the number of bytes from the heap because it may be more
|
||||
// than the len due to gc block size.
|
||||
self->current_value_alloc = gc_nbytes(self->current_value);
|
||||
}
|
||||
memcpy(self->current_value, bufinfo->buf, bufinfo->len);
|
||||
} else {
|
||||
// Otherwise, use the provided buffer to delay any heap allocation.
|
||||
self->current_value = bufinfo->buf;
|
||||
self->current_value_alloc = 0;
|
||||
}
|
||||
|
||||
ble_gatts_chr_updated(self->handle);
|
||||
}
|
||||
}
|
||||
|
||||
bleio_uuid_obj_t *common_hal_bleio_characteristic_get_uuid(bleio_characteristic_obj_t *self) {
|
||||
|
@ -118,10 +206,32 @@ bleio_characteristic_properties_t common_hal_bleio_characteristic_get_properties
|
|||
return self->props;
|
||||
}
|
||||
|
||||
void common_hal_bleio_characteristic_add_descriptor(bleio_characteristic_obj_t *self, bleio_descriptor_obj_t *descriptor) {
|
||||
void common_hal_bleio_characteristic_add_descriptor(bleio_characteristic_obj_t *self,
|
||||
bleio_descriptor_obj_t *descriptor) {
|
||||
// TODO: Implement this.
|
||||
|
||||
mp_obj_list_append(MP_OBJ_FROM_PTR(self->descriptor_list),
|
||||
MP_OBJ_FROM_PTR(descriptor));
|
||||
}
|
||||
|
||||
void common_hal_bleio_characteristic_set_cccd(bleio_characteristic_obj_t *self, bool notify, bool indicate) {
|
||||
// TODO: Implement this.
|
||||
if (self->cccd_handle == BLEIO_HANDLE_INVALID) {
|
||||
mp_raise_bleio_BluetoothError(translate("No CCCD for this Characteristic"));
|
||||
}
|
||||
|
||||
if (!common_hal_bleio_service_get_is_remote(self->service)) {
|
||||
mp_raise_bleio_RoleError(translate("Can't set CCCD on local Characteristic"));
|
||||
}
|
||||
|
||||
const uint16_t conn_handle = bleio_connection_get_conn_handle(self->service->connection);
|
||||
common_hal_bleio_check_connected(conn_handle);
|
||||
|
||||
uint16_t cccd_value =
|
||||
(notify ? 1 << 0 : 0) |
|
||||
(indicate ? 1 << 1: 0);
|
||||
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_write_flat(conn_handle, self->cccd_handle, &cccd_value, 2, _write_cb, xTaskGetCurrentTaskHandle()));
|
||||
int error_code;
|
||||
xTaskNotifyWait(0, 0, (uint32_t *)&error_code, 200);
|
||||
CHECK_BLE_ERROR(error_code);
|
||||
}
|
||||
|
|
|
@ -39,9 +39,13 @@ typedef struct _bleio_characteristic_obj {
|
|||
// Will be MP_OBJ_NULL before being assigned to a Service.
|
||||
bleio_service_obj_t *service;
|
||||
bleio_uuid_obj_t *uuid;
|
||||
const uint8_t *initial_value;
|
||||
uint16_t initial_value_len;
|
||||
uint8_t *current_value;
|
||||
uint16_t current_value_len;
|
||||
// Our internal allocation length. If > 0, then current_value is managed by
|
||||
// this characteristic.
|
||||
uint16_t current_value_alloc;
|
||||
uint16_t max_length;
|
||||
uint16_t def_handle;
|
||||
uint16_t handle;
|
||||
bleio_characteristic_properties_t props;
|
||||
bleio_attribute_security_mode_t read_perm;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
#include <stdio.h>
|
||||
|
||||
#include "shared/runtime/interrupt_char.h"
|
||||
#include "py/ringbuf.h"
|
||||
#include "py/runtime.h"
|
||||
#include "py/stream.h"
|
||||
|
||||
|
@ -37,14 +38,38 @@
|
|||
#include "common-hal/_bleio/CharacteristicBuffer.h"
|
||||
#include "shared-bindings/_bleio/CharacteristicBuffer.h"
|
||||
|
||||
STATIC int characteristic_buffer_on_ble_evt(struct ble_gap_event *event, void *param) {
|
||||
bleio_characteristic_buffer_obj_t *self = (bleio_characteristic_buffer_obj_t *)param;
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_NOTIFY_RX: {
|
||||
// A remote service wrote to this characteristic.
|
||||
|
||||
// Must be a notification, and event handle must match the handle for my characteristic.
|
||||
if (event->notify_rx.indication == 0 &&
|
||||
event->notify_rx.attr_handle == self->characteristic->handle) {
|
||||
const struct os_mbuf *m = event->notify_rx.om;
|
||||
while (m != NULL) {
|
||||
ringbuf_put_n(&self->ringbuf, m->om_data, m->om_len);
|
||||
m = SLIST_NEXT(m, om_next);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "Unhandled gap event %d\n", event->type);
|
||||
#endif
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _common_hal_bleio_characteristic_buffer_construct(bleio_characteristic_buffer_obj_t *self,
|
||||
bleio_characteristic_obj_t *characteristic,
|
||||
mp_float_t timeout,
|
||||
uint8_t *buffer, size_t buffer_size,
|
||||
void *static_handler_entry) {
|
||||
|
||||
mp_raise_NotImplementedError(NULL);
|
||||
|
||||
self->characteristic = characteristic;
|
||||
self->timeout_ms = timeout * 1000;
|
||||
|
||||
|
@ -53,6 +78,11 @@ void _common_hal_bleio_characteristic_buffer_construct(bleio_characteristic_buff
|
|||
self->ringbuf.iget = 0;
|
||||
self->ringbuf.iput = 0;
|
||||
|
||||
if (static_handler_entry != NULL) {
|
||||
ble_event_add_handler_entry((ble_event_handler_entry_t *)static_handler_entry, characteristic_buffer_on_ble_evt, self);
|
||||
} else {
|
||||
ble_event_add_handler(characteristic_buffer_on_ble_evt, self);
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes that timeout and buffer_size have been validated before call.
|
||||
|
@ -65,17 +95,32 @@ void common_hal_bleio_characteristic_buffer_construct(bleio_characteristic_buffe
|
|||
}
|
||||
|
||||
uint32_t common_hal_bleio_characteristic_buffer_read(bleio_characteristic_buffer_obj_t *self, uint8_t *data, size_t len, int *errcode) {
|
||||
// TODO: Implement this.
|
||||
uint64_t start_ticks = supervisor_ticks_ms64();
|
||||
|
||||
// Wait for all bytes received or timeout
|
||||
while ((ringbuf_num_filled(&self->ringbuf) < len) && (supervisor_ticks_ms64() - start_ticks < self->timeout_ms)) {
|
||||
RUN_BACKGROUND_TASKS;
|
||||
// Allow user to break out of a timeout with a KeyboardInterrupt.
|
||||
if (mp_hal_is_interrupted()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t num_bytes_read = ringbuf_get_n(&self->ringbuf, data, len);
|
||||
|
||||
return num_bytes_read;
|
||||
}
|
||||
|
||||
// NOTE: The nRF port has protection around these operations because the ringbuf
|
||||
// is filled from an interrupt. On ESP the ringbuf is filled from the BLE host
|
||||
// task that won't interrupt us.
|
||||
|
||||
uint32_t common_hal_bleio_characteristic_buffer_rx_characters_available(bleio_characteristic_buffer_obj_t *self) {
|
||||
// TODO: Implement this.
|
||||
return 0;
|
||||
return ringbuf_num_filled(&self->ringbuf);
|
||||
}
|
||||
|
||||
void common_hal_bleio_characteristic_buffer_clear_rx_buffer(bleio_characteristic_buffer_obj_t *self) {
|
||||
// TODO: Implement this.
|
||||
ringbuf_clear(&self->ringbuf);
|
||||
}
|
||||
|
||||
bool common_hal_bleio_characteristic_buffer_deinited(bleio_characteristic_buffer_obj_t *self) {
|
||||
|
@ -83,7 +128,10 @@ bool common_hal_bleio_characteristic_buffer_deinited(bleio_characteristic_buffer
|
|||
}
|
||||
|
||||
void common_hal_bleio_characteristic_buffer_deinit(bleio_characteristic_buffer_obj_t *self) {
|
||||
// TODO: Implement this.
|
||||
if (!common_hal_bleio_characteristic_buffer_deinited(self)) {
|
||||
ble_event_remove_handler(characteristic_buffer_on_ble_evt, self);
|
||||
self->characteristic = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool common_hal_bleio_characteristic_buffer_connected(bleio_characteristic_buffer_obj_t *self) {
|
||||
|
|
|
@ -48,6 +48,9 @@
|
|||
|
||||
#include "host/ble_att.h"
|
||||
|
||||
// Give 20 seconds for discovery
|
||||
#define DISCOVERY_TIMEOUT_MS 20000
|
||||
|
||||
int bleio_connection_event_cb(struct ble_gap_event *event, void *connection_in) {
|
||||
bleio_connection_internal_t *connection = (bleio_connection_internal_t *)connection_in;
|
||||
|
||||
|
@ -69,9 +72,35 @@ int bleio_connection_event_cb(struct ble_gap_event *event, void *connection_in)
|
|||
}
|
||||
|
||||
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: {
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "TODO connection event: PHY update complete\n");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
case BLE_GAP_EVENT_CONN_UPDATE: {
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "TODO connection event: connection update\n");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case BLE_GAP_EVENT_L2CAP_UPDATE_REQ: {
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "TODO connection event: l2cap update request\n");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
// These events are actually att specific so forward to all registered
|
||||
// handlers for them. The handlers themselves decide whether an event
|
||||
// is interesting to them.
|
||||
case BLE_GAP_EVENT_NOTIFY_RX:
|
||||
MP_FALLTHROUGH;
|
||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||
MP_FALLTHROUGH;
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
return ble_event_run_handlers(event);
|
||||
|
||||
default:
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "Unhandled connection event: %d\n", event->type);
|
||||
|
@ -96,7 +125,7 @@ bool common_hal_bleio_connection_get_connected(bleio_connection_obj_t *self) {
|
|||
}
|
||||
|
||||
void common_hal_bleio_connection_disconnect(bleio_connection_internal_t *self) {
|
||||
// TODO: Implement this.
|
||||
ble_gap_terminate(self->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
}
|
||||
|
||||
void common_hal_bleio_connection_pair(bleio_connection_internal_t *self, bool bond) {
|
||||
|
@ -121,7 +150,265 @@ void common_hal_bleio_connection_set_connection_interval(bleio_connection_intern
|
|||
// TODO: Implement this.
|
||||
}
|
||||
|
||||
STATIC volatile int _last_discovery_status;
|
||||
static TaskHandle_t discovery_task = NULL;
|
||||
|
||||
STATIC int _discovered_service_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_svc *svc,
|
||||
void *arg) {
|
||||
bleio_connection_internal_t *self = (bleio_connection_internal_t *)arg;
|
||||
|
||||
if (error->status != BLE_ERR_SUCCESS) {
|
||||
// Keep the first error in case it's due to memory.
|
||||
if (_last_discovery_status == BLE_ERR_SUCCESS) {
|
||||
_last_discovery_status = error->status;
|
||||
xTaskNotifyGive(discovery_task);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If any of these memory allocations fail, we set _last_discovery_status
|
||||
// and let the process continue.
|
||||
if (_last_discovery_status != BLE_ERR_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
bleio_service_obj_t *service = m_new_obj(bleio_service_obj_t);
|
||||
if (service == NULL) {
|
||||
_last_discovery_status = BLE_ERR_MEM_CAPACITY;
|
||||
return 0;
|
||||
}
|
||||
service->base.type = &bleio_service_type;
|
||||
|
||||
// Initialize several fields at once.
|
||||
bleio_service_from_connection(service, bleio_connection_new_from_internal(self));
|
||||
|
||||
service->is_remote = true;
|
||||
service->start_handle = svc->start_handle;
|
||||
service->end_handle = svc->end_handle;
|
||||
service->handle = svc->start_handle;
|
||||
|
||||
bleio_uuid_obj_t *uuid = m_new_obj(bleio_uuid_obj_t);
|
||||
if (uuid == NULL) {
|
||||
_last_discovery_status = BLE_ERR_MEM_CAPACITY;
|
||||
return 0;
|
||||
}
|
||||
uuid->base.type = &bleio_uuid_type;
|
||||
uuid->nimble_ble_uuid = svc->uuid;
|
||||
service->uuid = uuid;
|
||||
|
||||
mp_obj_list_append(MP_OBJ_FROM_PTR(self->remote_service_list),
|
||||
MP_OBJ_FROM_PTR(service));
|
||||
return 0;
|
||||
}
|
||||
|
||||
STATIC int _discovered_characteristic_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_chr *chr,
|
||||
void *arg) {
|
||||
bleio_service_obj_t *service = (bleio_service_obj_t *)arg;
|
||||
|
||||
if (error->status != BLE_ERR_SUCCESS) {
|
||||
// Keep the first error in case it's due to memory.
|
||||
if (_last_discovery_status == BLE_ERR_SUCCESS) {
|
||||
_last_discovery_status = error->status;
|
||||
xTaskNotifyGive(discovery_task);
|
||||
}
|
||||
}
|
||||
// If any of these memory allocations fail, we set _last_discovery_status
|
||||
// and let the process continue.
|
||||
if (_last_discovery_status != BLE_ERR_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bleio_characteristic_obj_t *characteristic = m_new_obj(bleio_characteristic_obj_t);
|
||||
if (characteristic == NULL) {
|
||||
_last_discovery_status = BLE_ERR_MEM_CAPACITY;
|
||||
return 0;
|
||||
}
|
||||
characteristic->base.type = &bleio_characteristic_type;
|
||||
|
||||
// Known characteristic UUID.
|
||||
bleio_uuid_obj_t *uuid = m_new_obj(bleio_uuid_obj_t);
|
||||
if (uuid == NULL) {
|
||||
_last_discovery_status = BLE_ERR_MEM_CAPACITY;
|
||||
return 0;
|
||||
}
|
||||
uuid->base.type = &bleio_uuid_type;
|
||||
uuid->nimble_ble_uuid = chr->uuid;
|
||||
|
||||
bleio_characteristic_properties_t props =
|
||||
((chr->properties & BLE_GATT_CHR_PROP_BROADCAST) != 0 ? CHAR_PROP_BROADCAST : 0) |
|
||||
((chr->properties & BLE_GATT_CHR_PROP_INDICATE) != 0 ? CHAR_PROP_INDICATE : 0) |
|
||||
((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) != 0 ? CHAR_PROP_NOTIFY : 0) |
|
||||
((chr->properties & BLE_GATT_CHR_PROP_READ) != 0 ? CHAR_PROP_READ : 0) |
|
||||
((chr->properties & BLE_GATT_CHR_PROP_WRITE) != 0 ? CHAR_PROP_WRITE : 0) |
|
||||
((chr->properties & BLE_GATT_CHR_PROP_WRITE_NO_RSP) != 0 ? CHAR_PROP_WRITE_NO_RESPONSE : 0);
|
||||
|
||||
// Call common_hal_bleio_characteristic_construct() to initalize some fields and set up evt handler.
|
||||
common_hal_bleio_characteristic_construct(
|
||||
characteristic, service, chr->val_handle, uuid,
|
||||
props, SECURITY_MODE_OPEN, SECURITY_MODE_OPEN,
|
||||
0, false, // max_length, fixed_length: values don't matter for gattc
|
||||
mp_const_empty_bytes,
|
||||
NULL);
|
||||
// Set def_handle directly since it is only used in discovery.
|
||||
characteristic->def_handle = chr->def_handle;
|
||||
|
||||
mp_obj_list_append(MP_OBJ_FROM_PTR(service->characteristic_list),
|
||||
MP_OBJ_FROM_PTR(characteristic));
|
||||
return 0;
|
||||
}
|
||||
|
||||
STATIC int _discovered_descriptor_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
uint16_t chr_val_handle,
|
||||
const struct ble_gatt_dsc *dsc,
|
||||
void *arg) {
|
||||
bleio_characteristic_obj_t *characteristic = (bleio_characteristic_obj_t *)arg;
|
||||
|
||||
if (error->status != BLE_ERR_SUCCESS) {
|
||||
// Keep the first error in case it's due to memory.
|
||||
if (_last_discovery_status == BLE_ERR_SUCCESS) {
|
||||
_last_discovery_status = error->status;
|
||||
}
|
||||
xTaskNotifyGive(discovery_task);
|
||||
}
|
||||
// If any of these memory allocations fail, we set _last_discovery_status
|
||||
// and let the process continue.
|
||||
if (_last_discovery_status != BLE_ERR_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remember handles for certain well-known descriptors.
|
||||
switch (dsc->uuid.u16.value) {
|
||||
case 0x2902:
|
||||
characteristic->cccd_handle = dsc->handle;
|
||||
break;
|
||||
|
||||
case 0x2903:
|
||||
characteristic->sccd_handle = dsc->handle;
|
||||
break;
|
||||
|
||||
case 0x2901:
|
||||
characteristic->user_desc_handle = dsc->handle;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
bleio_descriptor_obj_t *descriptor = m_new_obj(bleio_descriptor_obj_t);
|
||||
if (descriptor == NULL) {
|
||||
_last_discovery_status = BLE_ERR_MEM_CAPACITY;
|
||||
return 0;
|
||||
}
|
||||
descriptor->base.type = &bleio_descriptor_type;
|
||||
|
||||
bleio_uuid_obj_t *uuid = m_new_obj(bleio_uuid_obj_t);
|
||||
if (uuid == NULL) {
|
||||
_last_discovery_status = BLE_ERR_MEM_CAPACITY;
|
||||
return 0;
|
||||
}
|
||||
uuid->base.type = &bleio_uuid_type;
|
||||
uuid->nimble_ble_uuid = dsc->uuid;
|
||||
|
||||
common_hal_bleio_descriptor_construct(
|
||||
descriptor, characteristic, uuid,
|
||||
SECURITY_MODE_OPEN, SECURITY_MODE_OPEN,
|
||||
0, false, mp_const_empty_bytes);
|
||||
descriptor->handle = dsc->handle;
|
||||
|
||||
mp_obj_list_append(MP_OBJ_FROM_PTR(characteristic->descriptor_list),
|
||||
MP_OBJ_FROM_PTR(descriptor));
|
||||
return 0;
|
||||
}
|
||||
|
||||
STATIC void discover_remote_services(bleio_connection_internal_t *self, mp_obj_t service_uuids_whitelist) {
|
||||
// Start over with an empty list.
|
||||
self->remote_service_list = mp_obj_new_list(0, NULL);
|
||||
|
||||
discovery_task = xTaskGetCurrentTaskHandle();
|
||||
if (service_uuids_whitelist == mp_const_none) {
|
||||
_last_discovery_status = BLE_ERR_SUCCESS;
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_disc_all_svcs(self->conn_handle, _discovered_service_cb, self));
|
||||
|
||||
// Wait for sync.
|
||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS));
|
||||
if (_last_discovery_status != BLE_HS_EDONE) {
|
||||
CHECK_BLE_ERROR(_last_discovery_status);
|
||||
}
|
||||
} else {
|
||||
mp_obj_iter_buf_t iter_buf;
|
||||
mp_obj_t iterable = mp_getiter(service_uuids_whitelist, &iter_buf);
|
||||
mp_obj_t uuid_obj;
|
||||
while ((uuid_obj = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
|
||||
if (!mp_obj_is_type(uuid_obj, &bleio_uuid_type)) {
|
||||
mp_raise_TypeError(translate("non-UUID found in service_uuids_whitelist"));
|
||||
}
|
||||
bleio_uuid_obj_t *uuid = MP_OBJ_TO_PTR(uuid_obj);
|
||||
|
||||
_last_discovery_status = BLE_ERR_SUCCESS;
|
||||
// Make sure we start with a clean notification state
|
||||
ulTaskNotifyValueClear(discovery_task, 0xffffffff);
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_disc_svc_by_uuid(self->conn_handle, &uuid->nimble_ble_uuid.u,
|
||||
_discovered_service_cb, self));
|
||||
// Wait for sync.
|
||||
CHECK_NOTIFY(ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS)));
|
||||
if (_last_discovery_status != BLE_HS_EDONE) {
|
||||
CHECK_BLE_ERROR(_last_discovery_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < self->remote_service_list->len; i++) {
|
||||
bleio_service_obj_t *service = MP_OBJ_TO_PTR(self->remote_service_list->items[i]);
|
||||
|
||||
_last_discovery_status = BLE_ERR_SUCCESS;
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_disc_all_chrs(self->conn_handle,
|
||||
service->start_handle,
|
||||
service->end_handle,
|
||||
_discovered_characteristic_cb,
|
||||
service));
|
||||
|
||||
// Wait for sync.
|
||||
CHECK_NOTIFY(ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS)));
|
||||
if (_last_discovery_status != BLE_HS_EDONE) {
|
||||
CHECK_BLE_ERROR(_last_discovery_status);
|
||||
}
|
||||
|
||||
// Got characteristics for this service. Now discover descriptors for each characteristic.
|
||||
size_t char_list_len = service->characteristic_list->len;
|
||||
for (size_t char_idx = 0; char_idx < char_list_len; ++char_idx) {
|
||||
bleio_characteristic_obj_t *characteristic =
|
||||
MP_OBJ_TO_PTR(service->characteristic_list->items[char_idx]);
|
||||
// Determine the handle range for the given characteristic's descriptors.
|
||||
// The end of the range is dictated by the next characteristic or the end
|
||||
// handle of the service.
|
||||
const bool last_characteristic = char_idx == char_list_len - 1;
|
||||
bleio_characteristic_obj_t *next_characteristic = last_characteristic
|
||||
? NULL
|
||||
: MP_OBJ_TO_PTR(service->characteristic_list->items[char_idx + 1]);
|
||||
|
||||
uint16_t end_handle = next_characteristic == NULL
|
||||
? service->end_handle
|
||||
: next_characteristic->def_handle - 1;
|
||||
|
||||
_last_discovery_status = BLE_ERR_SUCCESS;
|
||||
CHECK_NIMBLE_ERROR(ble_gattc_disc_all_dscs(self->conn_handle, characteristic->handle,
|
||||
end_handle,
|
||||
_discovered_descriptor_cb, characteristic));
|
||||
// Wait for sync.
|
||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS));
|
||||
if (_last_discovery_status != BLE_HS_EDONE) {
|
||||
CHECK_BLE_ERROR(_last_discovery_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mp_obj_tuple_t *common_hal_bleio_connection_discover_remote_services(bleio_connection_obj_t *self, mp_obj_t service_uuids_whitelist) {
|
||||
discover_remote_services(self->connection, service_uuids_whitelist);
|
||||
bleio_connection_ensure_connected(self);
|
||||
// Convert to a tuple and then clear the list so the callee will take ownership.
|
||||
mp_obj_tuple_t *services_tuple =
|
||||
|
@ -129,7 +416,6 @@ mp_obj_tuple_t *common_hal_bleio_connection_discover_remote_services(bleio_conne
|
|||
self->connection->remote_service_list->items);
|
||||
mp_obj_list_clear(MP_OBJ_FROM_PTR(self->connection->remote_service_list));
|
||||
|
||||
// TODO: Implement this.
|
||||
return services_tuple;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
#include "host/ble_att.h"
|
||||
|
||||
void common_hal_bleio_descriptor_construct(bleio_descriptor_obj_t *self, bleio_characteristic_obj_t *characteristic, bleio_uuid_obj_t *uuid, bleio_attribute_security_mode_t read_perm, bleio_attribute_security_mode_t write_perm, mp_int_t max_length, bool fixed_length, mp_buffer_info_t *initial_value_bufinfo) {
|
||||
mp_raise_NotImplementedError(NULL);
|
||||
self->characteristic = characteristic;
|
||||
self->uuid = uuid;
|
||||
self->handle = BLEIO_HANDLE_INVALID;
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
#include "common-hal/_bleio/UUID.h"
|
||||
|
||||
#include "host/ble_gatt.h"
|
||||
|
||||
// Forward declare characteristic because it includes a Descriptor.
|
||||
struct _bleio_characteristic_obj;
|
||||
|
||||
|
@ -45,6 +47,7 @@ typedef struct _bleio_descriptor_obj {
|
|||
uint16_t max_length;
|
||||
bool fixed_length;
|
||||
uint16_t handle;
|
||||
struct ble_gatt_dsc_def def;
|
||||
bleio_attribute_security_mode_t read_perm;
|
||||
bleio_attribute_security_mode_t write_perm;
|
||||
} bleio_descriptor_obj_t;
|
||||
|
|
|
@ -40,13 +40,113 @@
|
|||
|
||||
#include "host/ble_att.h"
|
||||
|
||||
STATIC void write_to_ringbuf(bleio_packet_buffer_obj_t *self, const struct os_mbuf *mbuf) {
|
||||
size_t len = OS_MBUF_PKTLEN(mbuf);
|
||||
if (len + sizeof(uint16_t) > ringbuf_capacity(&self->ringbuf)) {
|
||||
// This shouldn't happen but can if our buffer size was much smaller than
|
||||
// the writes the client actually makes.
|
||||
return;
|
||||
}
|
||||
// Make room for the new value by dropping the oldest packets first.
|
||||
while (ringbuf_capacity(&self->ringbuf) - ringbuf_num_filled(&self->ringbuf) < len + sizeof(uint16_t)) {
|
||||
uint16_t packet_length;
|
||||
ringbuf_get_n(&self->ringbuf, (uint8_t *)&packet_length, sizeof(uint16_t));
|
||||
for (uint16_t i = 0; i < packet_length; i++) {
|
||||
ringbuf_get(&self->ringbuf);
|
||||
}
|
||||
// set an overflow flag?
|
||||
}
|
||||
ringbuf_put_n(&self->ringbuf, (uint8_t *)&len, sizeof(uint16_t));
|
||||
while (mbuf != NULL) {
|
||||
ringbuf_put_n(&self->ringbuf, mbuf->om_data, mbuf->om_len);
|
||||
mbuf = SLIST_NEXT(mbuf, om_next);
|
||||
}
|
||||
}
|
||||
|
||||
STATIC int packet_buffer_on_ble_client_evt(struct ble_gap_event *event, void *param);
|
||||
STATIC int queue_next_write(bleio_packet_buffer_obj_t *self);
|
||||
|
||||
STATIC int _write_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr,
|
||||
void *arg) {
|
||||
if (error->status != 0) {
|
||||
mp_printf(&mp_plat_print, "write failed %d\n", error->status);
|
||||
}
|
||||
bleio_packet_buffer_obj_t *self = (bleio_packet_buffer_obj_t *)arg;
|
||||
queue_next_write(self);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
STATIC int queue_next_write(bleio_packet_buffer_obj_t *self) {
|
||||
// Queue up the next outgoing buffer. We use two, one that has been passed to the SD for
|
||||
// transmission (when packet_queued is true) and the other is `pending` and can still be
|
||||
// modified. By primarily appending to the `pending` buffer we can reduce the protocol overhead
|
||||
// of the lower level link and ATT layers.
|
||||
self->packet_queued = false;
|
||||
if (self->pending_size > 0) {
|
||||
uint16_t conn_handle = self->conn_handle;
|
||||
int err_code = NIMBLE_OK;
|
||||
if (self->client) {
|
||||
if (self->write_type == CHAR_PROP_WRITE_NO_RESPONSE) {
|
||||
err_code = ble_gattc_write_no_rsp_flat(conn_handle,
|
||||
self->characteristic->handle,
|
||||
self->outgoing[self->pending_index],
|
||||
self->pending_size);
|
||||
// We don't set packet_queued because we NimBLE will buffer our
|
||||
// outgoing packets.
|
||||
} else {
|
||||
err_code = ble_gattc_write_flat(conn_handle,
|
||||
self->characteristic->handle,
|
||||
self->outgoing[self->pending_index],
|
||||
self->pending_size,
|
||||
_write_cb, self);
|
||||
self->pending_index = (self->pending_index + 1) % 2;
|
||||
self->packet_queued = true;
|
||||
}
|
||||
self->pending_size = 0;
|
||||
} else {
|
||||
// TODO: Notify because we're the server.
|
||||
}
|
||||
if (err_code != NIMBLE_OK) {
|
||||
// On error, simply skip updating the pending buffers so that the next HVC or WRITE
|
||||
// complete event triggers another attempt.
|
||||
return err_code;
|
||||
}
|
||||
}
|
||||
return NIMBLE_OK;
|
||||
}
|
||||
|
||||
STATIC int packet_buffer_on_ble_client_evt(struct ble_gap_event *event, void *param) {
|
||||
bleio_packet_buffer_obj_t *self = (bleio_packet_buffer_obj_t *)param;
|
||||
if (event->type == BLE_GAP_EVENT_DISCONNECT && self->conn_handle == event->disconnect.conn.conn_handle) {
|
||||
self->conn_handle = BLEIO_HANDLE_INVALID;
|
||||
}
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_NOTIFY_RX: {
|
||||
if (event->notify_rx.conn_handle != self->conn_handle) {
|
||||
return false;
|
||||
}
|
||||
// Must be a notification, and event handle must match the handle for my characteristic.
|
||||
if (event->notify_rx.attr_handle == self->characteristic->handle) {
|
||||
write_to_ringbuf(self, event->notify_rx.om);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _common_hal_bleio_packet_buffer_construct(
|
||||
bleio_packet_buffer_obj_t *self, bleio_characteristic_obj_t *characteristic,
|
||||
uint32_t *incoming_buffer, size_t incoming_buffer_size,
|
||||
uint32_t *outgoing_buffer1, uint32_t *outgoing_buffer2, size_t max_packet_size,
|
||||
void *static_handler_entry) {
|
||||
|
||||
mp_raise_NotImplementedError(NULL);
|
||||
self->characteristic = characteristic;
|
||||
self->client = self->characteristic->service->is_remote;
|
||||
self->max_packet_size = max_packet_size;
|
||||
|
@ -76,6 +176,29 @@ void _common_hal_bleio_packet_buffer_construct(
|
|||
self->outgoing[0] = outgoing_buffer1;
|
||||
self->outgoing[1] = outgoing_buffer2;
|
||||
|
||||
if (self->client) {
|
||||
if (static_handler_entry != NULL) {
|
||||
ble_event_add_handler_entry((ble_event_handler_entry_t *)static_handler_entry, packet_buffer_on_ble_client_evt, self);
|
||||
} else {
|
||||
ble_event_add_handler(packet_buffer_on_ble_client_evt, self);
|
||||
}
|
||||
if (incoming) {
|
||||
// Prefer notify if both are available.
|
||||
if (incoming & CHAR_PROP_NOTIFY) {
|
||||
common_hal_bleio_characteristic_set_cccd(self->characteristic, true, false);
|
||||
} else {
|
||||
common_hal_bleio_characteristic_set_cccd(self->characteristic, false, true);
|
||||
}
|
||||
}
|
||||
if (outgoing) {
|
||||
self->write_type = CHAR_PROP_WRITE;
|
||||
if (outgoing & CHAR_PROP_WRITE_NO_RESPONSE) {
|
||||
self->write_type = CHAR_PROP_WRITE_NO_RESPONSE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: Setup for server.
|
||||
}
|
||||
}
|
||||
|
||||
void common_hal_bleio_packet_buffer_construct(
|
||||
|
@ -104,8 +227,13 @@ void common_hal_bleio_packet_buffer_construct(
|
|||
uint32_t *outgoing2 = NULL;
|
||||
if (outgoing) {
|
||||
outgoing1 = m_malloc(max_packet_size, false);
|
||||
// Only allocate the second buffer if we are doing writes with responses.
|
||||
// Without responses, we just write as quickly as we can.
|
||||
if (outgoing == CHAR_PROP_WRITE) {
|
||||
outgoing2 = m_malloc(max_packet_size, false);
|
||||
}
|
||||
|
||||
}
|
||||
_common_hal_bleio_packet_buffer_construct(self, characteristic,
|
||||
incoming_buffer, incoming_buffer_size,
|
||||
outgoing1, outgoing2, max_packet_size,
|
||||
|
@ -117,9 +245,25 @@ mp_int_t common_hal_bleio_packet_buffer_readinto(bleio_packet_buffer_obj_t *self
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Copy received data. Lock out write interrupt handler while copying.
|
||||
// TODO: Implement this.
|
||||
return 0;
|
||||
// Get packet length, which is in first two bytes of packet.
|
||||
uint16_t packet_length;
|
||||
ringbuf_get_n(&self->ringbuf, (uint8_t *)&packet_length, sizeof(uint16_t));
|
||||
|
||||
mp_int_t ret;
|
||||
if (packet_length > len) {
|
||||
// Packet is longer than requested. Return negative of overrun value.
|
||||
ret = len - packet_length;
|
||||
// Discard the packet if it's too large. Don't fill data.
|
||||
while (packet_length--) {
|
||||
(void)ringbuf_get(&self->ringbuf);
|
||||
}
|
||||
} else {
|
||||
// Read as much as possible, but might be shorter than len.
|
||||
ringbuf_get_n(&self->ringbuf, data, packet_length);
|
||||
ret = packet_length;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
mp_int_t common_hal_bleio_packet_buffer_write(bleio_packet_buffer_obj_t *self, const uint8_t *data, size_t len, uint8_t *header, size_t header_len) {
|
||||
|
@ -172,10 +316,9 @@ mp_int_t common_hal_bleio_packet_buffer_write(bleio_packet_buffer_obj_t *self, c
|
|||
self->pending_size += len;
|
||||
num_bytes_written += len;
|
||||
|
||||
// TODO: Implement this.
|
||||
|
||||
// If no writes are queued then sneak in this data.
|
||||
if (!self->packet_queued) {
|
||||
CHECK_NIMBLE_ERROR(queue_next_write(self));
|
||||
}
|
||||
return num_bytes_written;
|
||||
}
|
||||
|
@ -270,6 +413,6 @@ bool common_hal_bleio_packet_buffer_deinited(bleio_packet_buffer_obj_t *self) {
|
|||
|
||||
void common_hal_bleio_packet_buffer_deinit(bleio_packet_buffer_obj_t *self) {
|
||||
if (!common_hal_bleio_packet_buffer_deinited(self)) {
|
||||
ble_event_remove_handler(packet_buffer_on_ble_client_evt, self);
|
||||
}
|
||||
// TODO: Implement this.
|
||||
}
|
||||
|
|
|
@ -41,12 +41,6 @@ uint32_t _common_hal_bleio_service_construct(bleio_service_obj_t *self, bleio_uu
|
|||
self->is_remote = false;
|
||||
self->connection = NULL;
|
||||
self->is_secondary = is_secondary;
|
||||
|
||||
// uint8_t service_type = BLE_GATT_SVC_TYPE_PRIMARY;
|
||||
// if (is_secondary) {
|
||||
// service_type = BLE_GATT_SVC_TYPE_SECONDARY;
|
||||
// }
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -57,7 +51,7 @@ void common_hal_bleio_service_construct(bleio_service_obj_t *self, bleio_uuid_ob
|
|||
}
|
||||
|
||||
void bleio_service_from_connection(bleio_service_obj_t *self, mp_obj_t connection) {
|
||||
self->handle = 0xFFFF;
|
||||
self->handle = BLEIO_HANDLE_INVALID;
|
||||
self->uuid = NULL;
|
||||
self->characteristic_list = mp_obj_new_list(0, NULL);
|
||||
self->is_remote = true;
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
#include "common-hal/_bleio/__init__.h"
|
||||
// #include "common-hal/_bleio/bonding.h"
|
||||
#include "common-hal/_bleio/ble_events.h"
|
||||
|
||||
// Turn off BLE on a reset or reload.
|
||||
void bleio_reset() {
|
||||
|
@ -50,6 +51,7 @@ void bleio_reset() {
|
|||
}
|
||||
|
||||
supervisor_stop_bluetooth();
|
||||
ble_event_reset();
|
||||
bleio_adapter_reset(&common_hal_bleio_adapter_obj);
|
||||
common_hal_bleio_adapter_set_enabled(&common_hal_bleio_adapter_obj, false);
|
||||
supervisor_start_bluetooth();
|
||||
|
@ -97,3 +99,36 @@ void check_nimble_error(int rc, const char *file, size_t line) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void check_ble_error(int error_code, const char *file, size_t line) {
|
||||
if (error_code == BLE_ERR_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
switch (error_code) {
|
||||
default:
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
if (file) {
|
||||
mp_raise_bleio_BluetoothError(translate("Unknown BLE error at %s:%d: %d"), file, line, error_code);
|
||||
}
|
||||
#else
|
||||
(void)file;
|
||||
(void)line;
|
||||
mp_raise_bleio_BluetoothError(translate("Unknown BLE error: %d"), error_code);
|
||||
#endif
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void check_notify(BaseType_t result) {
|
||||
if (result == pdTRUE) {
|
||||
return;
|
||||
}
|
||||
mp_raise_msg(&mp_type_TimeoutError, NULL);
|
||||
}
|
||||
|
||||
void common_hal_bleio_check_connected(uint16_t conn_handle) {
|
||||
if (conn_handle == BLEIO_HANDLE_INVALID) {
|
||||
mp_raise_ConnectionError(translate("Not connected"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
#ifndef MICROPY_INCLUDED_ESPRESSIF_COMMON_HAL_BLEIO_INIT_H
|
||||
#define MICROPY_INCLUDED_ESPRESSIF_COMMON_HAL_BLEIO_INIT_H
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
|
||||
void bleio_background(void);
|
||||
|
||||
// typedef struct {
|
||||
|
@ -43,6 +45,10 @@ void bleio_background(void);
|
|||
|
||||
void check_nimble_error(int rc, const char *file, size_t line);
|
||||
#define CHECK_NIMBLE_ERROR(rc) check_nimble_error(rc, __FILE__, __LINE__)
|
||||
void check_ble_error(int error_code, const char *file, size_t line);
|
||||
#define CHECK_BLE_ERROR(error_code) check_ble_error(error_code, __FILE__, __LINE__)
|
||||
void check_notify(BaseType_t result);
|
||||
#define CHECK_NOTIFY(result) check_notify(result)
|
||||
|
||||
#define MSEC_TO_UNITS(TIME, RESOLUTION) (((TIME) * 1000) / (RESOLUTION))
|
||||
#define SEC_TO_UNITS(TIME, RESOLUTION) (((TIME) * 1000000) / (RESOLUTION))
|
||||
|
@ -51,6 +57,7 @@ void check_nimble_error(int rc, const char *file, size_t line);
|
|||
#define ADV_INTERVAL_UNIT_FLOAT_SECS (0.000625)
|
||||
// Microseconds is the base unit. The macros above know that.
|
||||
#define UNIT_0_625_MS (625)
|
||||
#define UNIT_1_MS (1000)
|
||||
#define UNIT_1_25_MS (1250)
|
||||
#define UNIT_10_MS (10000)
|
||||
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2019 Dan Halbert for Adafruit Industries
|
||||
* Copyright (c) 2018 Artur Pacholec
|
||||
* Copyright (c) 2016 Glenn Ruben Bakke
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "common-hal/_bleio/ble_events.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "py/misc.h"
|
||||
#include "py/mpstate.h"
|
||||
#include "py/runtime.h"
|
||||
|
||||
#if CIRCUITPY_SERIAL_BLE && CIRCUITPY_VERBOSE_BLE
|
||||
#include "supervisor/shared/bluetooth/serial.h"
|
||||
#endif
|
||||
|
||||
void ble_event_reset(void) {
|
||||
// Linked list items will be gc'd.
|
||||
MP_STATE_VM(ble_event_handler_entries) = NULL;
|
||||
}
|
||||
|
||||
void ble_event_add_handler_entry(ble_event_handler_entry_t *entry,
|
||||
ble_gap_event_fn *func, void *param) {
|
||||
ble_event_handler_entry_t *it = MP_STATE_VM(ble_event_handler_entries);
|
||||
while (it != NULL) {
|
||||
// If event handler and its corresponding param are already on the list, don't add again.
|
||||
if ((it->func == func) && (it->param == param)) {
|
||||
return;
|
||||
}
|
||||
it = it->next;
|
||||
}
|
||||
entry->next = MP_STATE_VM(ble_event_handler_entries);
|
||||
entry->param = param;
|
||||
entry->func = func;
|
||||
|
||||
MP_STATE_VM(ble_event_handler_entries) = entry;
|
||||
}
|
||||
|
||||
void ble_event_add_handler(ble_gap_event_fn *func, void *param) {
|
||||
ble_event_handler_entry_t *it = MP_STATE_VM(ble_event_handler_entries);
|
||||
while (it != NULL) {
|
||||
// If event handler and its corresponding param are already on the list, don't add again.
|
||||
if ((it->func == func) && (it->param == param)) {
|
||||
return;
|
||||
}
|
||||
it = it->next;
|
||||
}
|
||||
|
||||
// Add a new handler to the front of the list
|
||||
ble_event_handler_entry_t *handler = m_new_ll(ble_event_handler_entry_t, 1);
|
||||
ble_event_add_handler_entry(handler, func, param);
|
||||
}
|
||||
|
||||
void ble_event_remove_handler(ble_gap_event_fn *func, void *param) {
|
||||
ble_event_handler_entry_t *it = MP_STATE_VM(ble_event_handler_entries);
|
||||
ble_event_handler_entry_t **prev = &MP_STATE_VM(ble_event_handler_entries);
|
||||
while (it != NULL) {
|
||||
if ((it->func == func) && (it->param == param)) {
|
||||
// Splice out the matching handler.
|
||||
*prev = it->next;
|
||||
// Clear next of the removed node so it's clearly not in a list.
|
||||
it->next = NULL;
|
||||
return;
|
||||
}
|
||||
prev = &(it->next);
|
||||
it = it->next;
|
||||
}
|
||||
}
|
||||
|
||||
int ble_event_run_handlers(struct ble_gap_event *event) {
|
||||
#if CIRCUITPY_SERIAL_BLE && CIRCUITPY_VERBOSE_BLE
|
||||
ble_serial_disable();
|
||||
#endif
|
||||
|
||||
#if CIRCUITPY_VERBOSE_BLE
|
||||
mp_printf(&mp_plat_print, "BLE GAP event: 0x%04x\n", event->type);
|
||||
#endif
|
||||
|
||||
ble_event_handler_entry_t *it = MP_STATE_VM(ble_event_handler_entries);
|
||||
bool done = false;
|
||||
while (it != NULL) {
|
||||
// Capture next before calling the function in case it removes itself from the list.
|
||||
ble_event_handler_entry_t *next = it->next;
|
||||
done = it->func(event, it->param) || done;
|
||||
it = next;
|
||||
}
|
||||
#if CIRCUITPY_SERIAL_BLE && CIRCUITPY_VERBOSE_BLE
|
||||
ble_serial_enable();
|
||||
#endif
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2019 Dan Halbert for Adafruit Industries
|
||||
* Copyright (c) 2018 Artur Pacholec
|
||||
* Copyright (c) 2016 Glenn Ruben Bakke
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef MICROPY_INCLUDED_ESPRESSIF_COMMON_HAL__BLEIO_BLE_EVENTS_H
|
||||
#define MICROPY_INCLUDED_ESPRESSIF_COMMON_HAL__BLEIO_BLE_EVENTS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
|
||||
typedef struct ble_event_handler_entry {
|
||||
struct ble_event_handler_entry *next;
|
||||
void *param;
|
||||
ble_gap_event_fn *func;
|
||||
} ble_event_handler_entry_t;
|
||||
|
||||
void ble_event_reset(void);
|
||||
void ble_event_add_handler(ble_gap_event_fn *func, void *param);
|
||||
void ble_event_remove_handler(ble_gap_event_fn *func, void *param);
|
||||
|
||||
// Allow for user provided entries to prevent allocations outside the VM.
|
||||
void ble_event_add_handler_entry(ble_event_handler_entry_t *entry, ble_gap_event_fn *func, void *param);
|
||||
|
||||
int ble_event_run_handlers(struct ble_gap_event *event);
|
||||
|
||||
#endif // MICROPY_INCLUDED_ESPRESSIF_COMMON_HAL__BLEIO_BLE_EVENTS_H
|
|
@ -35,8 +35,19 @@
|
|||
|
||||
#include "py/circuitpy_mpconfig.h"
|
||||
|
||||
#if CIRCUITPY_BLEIO
|
||||
#include "common-hal/_bleio/ble_events.h"
|
||||
#endif
|
||||
|
||||
#if CIRCUITPY_BLEIO
|
||||
#define MICROPY_PORT_ROOT_POINTERS \
|
||||
CIRCUITPY_COMMON_ROOT_POINTERS \
|
||||
ble_event_handler_entry_t *ble_event_handler_entries;
|
||||
#else
|
||||
#define MICROPY_PORT_ROOT_POINTERS \
|
||||
CIRCUITPY_COMMON_ROOT_POINTERS
|
||||
#endif
|
||||
|
||||
#define MICROPY_NLR_SETJMP (1)
|
||||
#define CIRCUITPY_DEFAULT_STACK_SIZE 0x6000
|
||||
|
||||
|
@ -61,4 +72,5 @@
|
|||
#ifndef CIRCUITPY_I2C_ALLOW_INTERNAL_PULL_UP
|
||||
#define CIRCUITPY_I2C_ALLOW_INTERNAL_PULL_UP (0)
|
||||
#endif
|
||||
|
||||
#endif // MICROPY_INCLUDED_ESPRESSIF_MPCONFIGPORT_H
|
||||
|
|
|
@ -158,6 +158,7 @@ bool common_hal_bleio_characteristic_buffer_deinited(bleio_characteristic_buffer
|
|||
void common_hal_bleio_characteristic_buffer_deinit(bleio_characteristic_buffer_obj_t *self) {
|
||||
if (!common_hal_bleio_characteristic_buffer_deinited(self)) {
|
||||
ble_drv_remove_event_handler(characteristic_buffer_on_ble_evt, self);
|
||||
self->characteristic = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "shared-module/vectorio/__init__.h"
|
||||
#include "shared-bindings/vectorio/VectorShape.h"
|
||||
|
||||
#include "py/misc.h"
|
||||
#include "py/runtime.h"
|
||||
#include "shared-bindings/time/__init__.h"
|
||||
#include "shared-bindings/displayio/ColorConverter.h"
|
||||
|
@ -61,17 +62,6 @@
|
|||
(u32 & 0x2 ? '1' : '0'), \
|
||||
(u32 & 0x1 ? '1' : '0')
|
||||
|
||||
|
||||
inline __attribute__((always_inline))
|
||||
static int32_t max(int32_t a, int32_t b) {
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
inline __attribute__((always_inline))
|
||||
static uint32_t min(uint32_t a, uint32_t b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
static void short_bound_check(mp_int_t i, qstr name) {
|
||||
if (i < SHRT_MIN || i > SHRT_MAX) {
|
||||
mp_raise_ValueError_varg(translate("%q must be between %d and %d"), name, SHRT_MIN, SHRT_MAX);
|
||||
|
@ -456,7 +446,7 @@ bool vectorio_vector_shape_fill_area(vectorio_vector_shape_t *self, const _displ
|
|||
mp_obj_get_type_str(self->ishape.shape),
|
||||
(overlap.x2 - overlap.x1) * (overlap.y2 - overlap.y1),
|
||||
(double)((end - start) / 1000000.0),
|
||||
(double)(max(1, pixels * (1000000000.0 / (end - start)))),
|
||||
(double)(MAX(1, pixels * (1000000000.0 / (end - start)))),
|
||||
(double)(pixel_time / 1000.0),
|
||||
(double)(pixel_time / 1000.0 / pixels)
|
||||
);
|
||||
|
@ -514,7 +504,7 @@ displayio_area_t *vectorio_vector_shape_get_refresh_areas(vectorio_vector_shape_
|
|||
union_size, dirty_size, current_size, overlap_size, (int32_t)union_size - dirty_size - current_size + overlap_size
|
||||
);
|
||||
|
||||
if ((int32_t)union_size - dirty_size - current_size + overlap_size <= min(dirty_size, current_size)) {
|
||||
if ((int32_t)union_size - dirty_size - current_size + overlap_size <= MIN(dirty_size, current_size)) {
|
||||
// The excluded / non-overlapping area from the disjoint dirty and current areas is smaller
|
||||
// than the smallest area we need to draw. Redrawing the overlapping area would cost more
|
||||
// than just drawing the union disjoint area once.
|
||||
|
|
Loading…
Reference in New Issue