/**************************************************************************** * wireless/bluetooth/bt_conn.c * * Copyright (c) 2016, Intel Corporation * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * ****************************************************************************/ /**************************************************************************** * Included Files ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include "bt_atomic.h" #include "bt_queue.h" #include "bt_hcicore.h" #include "bt_conn.h" #include "bt_l2cap.h" #include "bt_keys.h" #include "bt_smp.h" /**************************************************************************** * Private Types ****************************************************************************/ struct bt_conn_handoff_s { sem_t sync_sem; FAR struct bt_conn_s *conn; }; /**************************************************************************** * Private Data ****************************************************************************/ static struct bt_conn_s g_conns[CONFIG_BLUETOOTH_MAX_CONN]; static struct bt_conn_handoff_s g_conn_handoff = { SEM_INITIALIZER(1), NULL }; /**************************************************************************** * Private Functions ****************************************************************************/ static const char *state2str(enum bt_conn_state_e state) { switch (state) { case BT_CONN_DISCONNECTED: return "disconnected"; case BT_CONN_CONNECT_SCAN: return "connect-scan"; case BT_CONN_CONNECT: return "connect"; case BT_CONN_CONNECTED: return "connected"; case BT_CONN_DISCONNECT: return "disconnect"; default: return "(unknown)"; } } static void bt_conn_reset_rx_state(FAR struct bt_conn_s *conn) { if (!conn->rx_len) { return; } bt_buf_release(conn->rx); conn->rx = NULL; conn->rx_len = 0; } static int conn_tx_kthread(int argc, FAR char *argv[]) { FAR struct bt_conn_s *conn; FAR struct bt_buf_s *buf; struct mq_attr attr; int ret; /* Get the connection instance */ conn = g_conn_handoff.conn; DEBUGASSERT(conn != NULL); nxsem_post(&g_conn_handoff.sync_sem); wlinfo("Started for handle %u\n", conn->handle); while (conn->state == BT_CONN_CONNECTED) { /* Wait until the controller can accept ACL packets */ wlinfo("calling nxsem_wait_uninterruptible()\n"); ret = nxsem_wait_uninterruptible(&g_btdev.le_pkts_sem); if (ret < 0) { wlerr("nxsem_wait_uninterruptible() failed: %d\n", ret); break; } /* Check for disconnection */ if (conn->state != BT_CONN_CONNECTED) { nxsem_post(&g_btdev.le_pkts_sem); break; } /* Get next ACL packet for connection */ ret = bt_queue_receive(&conn->tx_queue, &buf); DEBUGASSERT(ret >= 0 && buf != NULL); UNUSED(ret); if (conn->state != BT_CONN_CONNECTED) { nxsem_post(&g_btdev.le_pkts_sem); bt_buf_release(buf); break; } wlinfo("passing buf %p len %u to driver\n", buf, buf->len); bt_send(g_btdev.btdev, buf); bt_buf_release(buf); } wlinfo("handle %u disconnected - cleaning up\n", conn->handle); /* Give back any allocated buffers */ do { buf = NULL; /* Make sure the thread is not blocked forever on an empty queue. * SIOCBTCONNECT will fail if preceding SIOCBTDISCONNECT does not * result in a successful termination of this thread. */ ret = file_mq_getattr(&conn->tx_queue, &attr); if (ret != OK) { break; } if (attr.mq_curmsgs == 0) { break; } ret = bt_queue_receive(&conn->tx_queue, &buf); if (ret >= 0) { DEBUGASSERT(buf != NULL); bt_buf_release(buf); } } while (ret >= OK); bt_conn_reset_rx_state(conn); wlinfo("handle %u exiting\n", conn->handle); /* Release reference taken when thread was created */ bt_conn_release(conn); return EXIT_SUCCESS; } static int bt_hci_disconnect(FAR struct bt_conn_s *conn, uint8_t reason) { FAR struct bt_buf_s *buf; FAR struct bt_hci_cp_disconnect_s *disconn; int err; buf = bt_hci_cmd_create(BT_HCI_OP_DISCONNECT, sizeof(*disconn)); if (!buf) { return -ENOBUFS; } disconn = bt_buf_extend(buf, sizeof(*disconn)); disconn->handle = BT_HOST2LE16(conn->handle); disconn->reason = reason; err = bt_hci_cmd_send(BT_HCI_OP_DISCONNECT, buf); if (err) { return err; } bt_conn_set_state(conn, BT_CONN_DISCONNECT); return 0; } static int bt_hci_connect_le_cancel(FAR struct bt_conn_s *conn) { int err; err = bt_hci_cmd_send(BT_HCI_OP_LE_CREATE_CONN_CANCEL, NULL); if (err) { return err; } return 0; } /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** * Name: bt_conn_receive * * Description: * Receive packets from the HCI core on a registered connection. * * Input Parameters: * conn - The registered connection * buf - The buffer structure containing the packet * flags - Packet boundary flags * * Returned Value: * None * ****************************************************************************/ void bt_conn_receive(FAR struct bt_conn_s *conn, FAR struct bt_buf_s *buf, uint8_t flags) { FAR struct bt_l2cap_hdr_s *hdr; uint16_t len; wlinfo("handle %u len %u flags %02x\n", conn->handle, buf->len, flags); /* Check packet boundary flags */ switch (flags) { case BT_HCI_ACL_NEW: /* First packet */ hdr = (void *)buf->data; len = BT_LE162HOST(hdr->len); wlinfo("First, len %u final %u\n", buf->len, len); if (conn->rx_len) { wlerr("ERROR: Unexpected first L2CAP frame\n"); bt_conn_reset_rx_state(conn); } conn->rx_len = (sizeof(*hdr) + len) - buf->len; wlinfo("rx_len %u\n", conn->rx_len); if (conn->rx_len) { conn->rx = buf; return; } break; case BT_HCI_ACL_CONTINUATION: /* Continuation */ if (!conn->rx_len) { wlerr("ERROR: Unexpected L2CAP continuation\n"); bt_conn_reset_rx_state(conn); bt_buf_release(buf); return; } if (buf->len > conn->rx_len) { wlerr("ERROR: L2CAP data overflow\n"); bt_conn_reset_rx_state(conn); bt_buf_release(buf); return; } wlinfo("Cont, len %u rx_len %u\n", buf->len, conn->rx_len); if (buf->len > bt_buf_tailroom(conn->rx)) { wlerr("ERROR: Not enough buffer space for L2CAP data\n"); bt_conn_reset_rx_state(conn); bt_buf_release(buf); return; } memcpy(bt_buf_extend(conn->rx, buf->len), buf->data, buf->len); conn->rx_len -= buf->len; bt_buf_release(buf); if (conn->rx_len) { return; } buf = conn->rx; conn->rx = NULL; conn->rx_len = 0; break; default: wlerr("ERROR: Unexpected ACL flags (0x%02x)\n", flags); bt_conn_reset_rx_state(conn); bt_buf_release(buf); return; } hdr = (void *)buf->data; len = BT_LE162HOST(hdr->len); if (sizeof(*hdr) + len != buf->len) { wlerr("ERROR: ACL len mismatch (%u != %u)\n", len, buf->len); bt_buf_release(buf); return; } wlinfo("Successfully parsed %u byte L2CAP packet\n", buf->len); bt_l2cap_receive(conn, buf); } /**************************************************************************** * Name: bt_conn_send * * Description: * Send data over a connection * * Input Parameters: * conn - The registered connection * buf - The buffer structure containing the packet to be sent * * Returned Value: * None * ****************************************************************************/ void bt_conn_send(FAR struct bt_conn_s *conn, FAR struct bt_buf_s *buf) { FAR struct bt_hci_acl_hdr_s *hdr; sq_queue_t fraglist; uint16_t len; uint16_t remaining = buf->len; FAR uint8_t *ptr; DEBUGASSERT(conn != NULL && buf != NULL); sq_init(&fraglist); wlinfo("conn handle %u buf len %u\n", conn->handle, buf->len); if (conn->state != BT_CONN_CONNECTED) { wlerr("ERROR: not connected!\n"); return; } len = remaining; if (len > g_btdev.le_mtu) { len = g_btdev.le_mtu; } hdr = bt_buf_provide(buf, sizeof(*hdr)); hdr->handle = BT_HOST2LE16(conn->handle); hdr->len = BT_HOST2LE16(len); buf->len -= remaining - len; ptr = bt_buf_tail(buf); /* Add the fragment to the end of the list */ sq_addlast((FAR sq_entry_t *)buf, &fraglist); remaining -= len; while (remaining) { buf = bt_l2cap_create_pdu(conn); len = remaining; if (len < g_btdev.le_mtu) { len = g_btdev.le_mtu; } /* Copy from original buffer */ memcpy(bt_buf_extend(buf, len), ptr, len); ptr += len; hdr = bt_buf_provide(buf, sizeof(*hdr)); hdr->handle = BT_HOST2LE16(conn->handle | (1 << 12)); hdr->len = BT_HOST2LE16(len); /* Add the fragment to the end of the list */ sq_addlast((FAR sq_entry_t *)buf, &fraglist); remaining -= len; } /* Then send each fragment in the correct order */ while ((buf = (FAR struct bt_buf_s *)sq_remfirst(&fraglist)) != NULL) { bt_queue_send(&conn->tx_queue, buf, BT_NORMAL_PRIO); } } /**************************************************************************** * Name: bt_conn_add * * Description: * Add a new connection * * Input Parameters: * peer - The address of the Bluetooth peer * role - Either BT_HCI_ROLE_MASTER or BT_HCI_ROLE_SLAVE * * Returned Value: * A reference to the new connection structure is returned on success. * ****************************************************************************/ FAR struct bt_conn_s *bt_conn_add(FAR const bt_addr_le_t *peer, uint8_t role) { FAR struct bt_conn_s *conn = NULL; int i; for (i = 0; i < CONFIG_BLUETOOTH_MAX_CONN; i++) { if (!bt_addr_le_cmp(&g_conns[i].dst, BT_ADDR_LE_ANY)) { conn = &g_conns[i]; break; } } if (!conn) { return NULL; } memset(conn, 0, sizeof(*conn)); bt_atomic_set(&conn->ref, 1); conn->role = role; bt_addr_le_copy(&conn->dst, peer); return conn; } /**************************************************************************** * Name: bt_conn_set_state * * Description: * Set connection object in certain state and perform actions related to * state change. * * Input Parameters: * conn - The connection whose state will be changed. * state - The new state of the connection. * * Returned Value: * None * ****************************************************************************/ void bt_conn_set_state(FAR struct bt_conn_s *conn, enum bt_conn_state_e state) { enum bt_conn_state_e old_state; wlinfo("%s -> %s\n", state2str(conn->state), state2str(state)); if (conn->state == state) { wlwarn("no transition\n"); return; } old_state = conn->state; conn->state = state; /* Take a reference for the first state transition after bt_conn_add() and * keep it until reaching DISCONNECTED again. */ if (old_state == BT_CONN_DISCONNECTED) { bt_conn_addref(conn); } switch (conn->state) { case BT_CONN_CONNECTED: { pid_t pid; int ret; ret = bt_queue_open(BT_CONN_TX, O_RDWR | O_CREAT, CONFIG_BLUETOOTH_TXCONN_NMSGS, &conn->tx_queue); DEBUGASSERT(ret >= 0); UNUSED(ret); /* Get exclusive access to the handoff structure. The count will * be zero when we complete this. */ ret = nxsem_wait_uninterruptible(&g_conn_handoff.sync_sem); if (ret >= 0) { /* Start the Tx connection kernel thread */ g_conn_handoff.conn = bt_conn_addref(conn); pid = kthread_create("BT Conn Tx", CONFIG_BLUETOOTH_TXCONN_PRIORITY, CONFIG_BLUETOOTH_TXCONN_STACKSIZE, conn_tx_kthread, NULL); DEBUGASSERT(pid > 0); UNUSED(pid); /* Take the semaphore again. This will force us to wait with * the sem_count at -1. It will be zero again when we * continue. */ ret = nxsem_wait_uninterruptible(&g_conn_handoff.sync_sem); nxsem_post(&g_conn_handoff.sync_sem); } } break; case BT_CONN_DISCONNECTED: /* Send dummy buffer to wake up and stop the Tx thread for states * where it was running. */ if (old_state == BT_CONN_CONNECTED || old_state == BT_CONN_DISCONNECT) { bt_queue_send(&conn->tx_queue, bt_buf_alloc(BT_DUMMY, NULL, 0), BT_NORMAL_PRIO); } /* Release the reference we took for the very first state * transition. */ bt_conn_release(conn); break; case BT_CONN_CONNECT_SCAN: case BT_CONN_CONNECT: case BT_CONN_DISCONNECT: break; default: wlwarn("no valid (%u) state was set\n", state); break; } } /**************************************************************************** * Name: bt_conn_lookup_handle * * Description: * Look up an existing connection * * Input Parameters: * handle - The handle to be used to perform the lookup * * Returned Value: * A reference to the connection state instance is returned on success. * NULL is returned if the connection is not found. On success, the * caller gets a new reference to the connection object which must be * released with bt_conn_release() once done using the connection. * ****************************************************************************/ FAR struct bt_conn_s *bt_conn_lookup_handle(uint16_t handle) { int i; for (i = 0; i < CONFIG_BLUETOOTH_MAX_CONN; i++) { /* We only care about connections with a valid handle */ if (g_conns[i].state != BT_CONN_CONNECTED && g_conns[i].state != BT_CONN_DISCONNECT) { continue; } if (g_conns[i].handle == handle) { return bt_conn_addref(&g_conns[i]); } } return NULL; } /**************************************************************************** * Name: bt_conn_lookup_addr_le * * Description: * Look up an existing connection based on the remote address. * * Input Parameters: * peer - Remote address. * * Returned Value: * A reference to the connection state instance is returned on success. * NULL is returned if the connection is not found. On success, the * caller gets a new reference to the connection object which must be * released with bt_conn_release() once done using the connection. * ****************************************************************************/ FAR struct bt_conn_s *bt_conn_lookup_addr_le(FAR const bt_addr_le_t * peer) { int i; for (i = 0; i < CONFIG_BLUETOOTH_MAX_CONN; i++) { if (!bt_addr_le_cmp(peer, &g_conns[i].dst)) { return bt_conn_addref(&g_conns[i]); } } return NULL; } /**************************************************************************** * Name: bt_conn_lookup_state * * Description: * Look up a connection state. For BT_ADDR_LE_ANY, returns the first * connection with the specific state * * Input Parameters: * peer - The peer address to match * state - The connection state to match * * Returned Value: * A reference to the connection state instance is returned on success. * NULL is returned if the connection is not found. On success, the * caller gets a new reference to the connection object which must be * released with bt_conn_release() once done using the connection. * ****************************************************************************/ FAR struct bt_conn_s *bt_conn_lookup_state(FAR const bt_addr_le_t * peer, enum bt_conn_state_e state) { int i; for (i = 0; i < CONFIG_BLUETOOTH_MAX_CONN; i++) { if (!bt_addr_le_cmp(&g_conns[i].dst, BT_ADDR_LE_ANY)) { continue; } if (bt_addr_le_cmp(peer, BT_ADDR_LE_ANY) && bt_addr_le_cmp(peer, &g_conns[i].dst)) { continue; } if (g_conns[i].state == state) { return bt_conn_addref(&g_conns[i]); } } return NULL; } /**************************************************************************** * Name: bt_conn_addref * * Description: * Increment the reference count of a connection object. * * Input Parameters: * conn - Connection object. * * Returned Value: * Connection object with incremented reference count. * ****************************************************************************/ FAR struct bt_conn_s *bt_conn_addref(FAR struct bt_conn_s *conn) { bt_atomic_incr(&conn->ref); wlinfo("handle %u ref %u\n", conn->handle, bt_atomic_get(&conn->ref)); return conn; } /**************************************************************************** * Name: bt_conn_release * * Description: * Decrement the reference count of a connection object. * * Input Parameters: * conn - Connection object. * * Returned Value: * None * ****************************************************************************/ void bt_conn_release(FAR struct bt_conn_s *conn) { bt_atomic_t old_ref; old_ref = bt_atomic_decr(&conn->ref); wlinfo("handle %u ref %u\n", conn->handle, bt_atomic_get(&conn->ref)); if (old_ref > 1) { return; } bt_addr_le_copy(&conn->dst, BT_ADDR_LE_ANY); } /**************************************************************************** * Name: bt_conn_get_dst * * Description: * Get destination (peer) address of a connection. * * Input Parameters: * conn - Connection object. * * Returned Value: * Destination address. * ****************************************************************************/ FAR const bt_addr_le_t *bt_conn_get_dst(FAR const struct bt_conn_s *conn) { return &conn->dst; } /**************************************************************************** * Name: bt_conn_security * * Description: * This function enable security (encryption) for a connection. If device * is already paired with sufficiently strong key encryption will be * enabled. If link is already encrypted with sufficiently strong key this * function does nothing. * * If device is not paired pairing will be initiated. If device is paired * and keys are too weak but input output capabilities allow for strong * enough keys pairing will be initiated. * * This function may return error if required level of security is not * possible to achieve due to local or remote device limitation (eg input * output capabilities). * * Input Parameters: * conn - Connection object. * sec - Requested security level. * * Returned Value: * 0 on success or negative error * ****************************************************************************/ int bt_conn_security(FAR struct bt_conn_s *conn, enum bt_security_e sec) { FAR struct bt_keys_s *keys; if (conn->state != BT_CONN_CONNECTED) { return -ENOTCONN; } /* Nothing to do */ if (sec == BT_SECURITY_LOW) { return 0; } /* For now we only support JustWorks */ if (sec > BT_SECURITY_MEDIUM) { return -EINVAL; } if (conn->encrypt) { return 0; } if (conn->role == BT_HCI_ROLE_SLAVE) { return bt_smp_send_security_req(conn); } keys = bt_keys_find(BT_KEYS_LTK, &conn->dst); if (keys) { return bt_conn_le_start_encryption(conn, keys->ltk.rand, keys->ltk.ediv, keys->ltk.val); } return bt_smp_send_pairing_req(conn); } /**************************************************************************** * Name:bt_conn_set_auto_conn * * Description: * This function enables/disables automatic connection initiation. * Every time the device looses the connection with peer, this connection * will be re-established if connectible advertisement from peer is * received. * * Input Parameters: * conn - Existing connection object. * auto_conn - boolean value. If true, auto connect is enabled, if false, * auto connect is disabled. * * Returned Value: * None * ****************************************************************************/ void bt_conn_set_auto_conn(FAR struct bt_conn_s *conn, bool auto_conn) { if (auto_conn) { bt_atomic_setbit(conn->flags, BT_CONN_AUTO_CONNECT); } else { bt_atomic_clrbit(conn->flags, BT_CONN_AUTO_CONNECT); } } /**************************************************************************** * Name: bt_conn_disconnect * * Description: * Disconnect an active connection with the specified reason code or cancel * pending outgoing connection. * * Input Parameters: * conn - Connection to disconnect. * reason - Reason code for the disconnection. * * Returned Value: * Zero on success or (negative) error code on failure. * ****************************************************************************/ int bt_conn_disconnect(FAR struct bt_conn_s *conn, uint8_t reason) { /* Disconnection is initiated by us, so auto connection shall be disabled. * Otherwise the passive scan would be enabled and we could send LE Create * Connection as soon as the remote starts advertising. */ bt_conn_set_auto_conn(conn, false); switch (conn->state) { case BT_CONN_CONNECT_SCAN: bt_conn_set_state(conn, BT_CONN_DISCONNECTED); bt_le_scan_update(); return 0; case BT_CONN_CONNECT: return bt_hci_connect_le_cancel(conn); case BT_CONN_CONNECTED: return bt_hci_disconnect(conn, reason); case BT_CONN_DISCONNECT: return 0; case BT_CONN_DISCONNECTED: default: return -ENOTCONN; } } /**************************************************************************** * Name: bt_conn_create_le * * Description: * Allows initiate new LE link to remote peer using its address. * Returns a new reference that the the caller is responsible for managing. * * Input Parameters: * peer - Remote address. * * Returned Value: * Valid connection object on success or NULL otherwise. * ****************************************************************************/ FAR struct bt_conn_s *bt_conn_create_le(FAR const bt_addr_le_t *peer) { FAR struct bt_conn_s *conn; /* First check if this connection exists and that it is in a proper * state. */ conn = bt_conn_lookup_addr_le(peer); if (conn != NULL) { switch (conn->state) { case BT_CONN_CONNECT_SCAN: case BT_CONN_CONNECT: case BT_CONN_CONNECTED: return conn; default: bt_conn_release(conn); return NULL; } } /* No.. the connection does not exist. Create it assuming MASTER role * and put it in the BT_CONNECT_SCAN state. */ conn = bt_conn_add(peer, BT_HCI_ROLE_MASTER); if (!conn) { return NULL; } bt_conn_set_state(conn, BT_CONN_CONNECT_SCAN); bt_le_scan_update(); return conn; } /**************************************************************************** * Name: bt_conn_le_start_encryption * * Description: * See the HCI start encryption command. * * NOTE: rand and ediv should be in BT order. * * Input Parameters: * conn - The connection to send the command on. * rand, ediv - Values to use for the encryption key * ltk - * * Returned Value: * Zero is returned on success; a negated errno value is returned on any * failure. * ****************************************************************************/ int bt_conn_le_start_encryption(FAR struct bt_conn_s *conn, uint64_t rand, uint16_t ediv, FAR const uint8_t *ltk) { FAR struct bt_hci_cp_le_start_encryption_s *cp; FAR struct bt_buf_s *buf; buf = bt_hci_cmd_create(BT_HCI_OP_LE_START_ENCRYPTION, sizeof(*cp)); if (!buf) { return -ENOBUFS; } cp = bt_buf_extend(buf, sizeof(*cp)); cp->handle = BT_HOST2LE16(conn->handle); cp->rand = rand; cp->ediv = ediv; memcpy(cp->ltk, ltk, sizeof(cp->ltk)); return bt_hci_cmd_send_sync(BT_HCI_OP_LE_START_ENCRYPTION, buf, NULL); } int bt_conn_le_conn_update(FAR struct bt_conn_s *conn, uint16_t min, uint16_t max, uint16_t latency, uint16_t timeout) { FAR struct hci_cp_le_conn_update_s *conn_update; FAR struct bt_buf_s *buf; buf = bt_hci_cmd_create(BT_HCI_OP_LE_CONN_UPDATE, sizeof(*conn_update)); if (!buf) { return -ENOBUFS; } conn_update = bt_buf_extend(buf, sizeof(*conn_update)); memset(conn_update, 0, sizeof(*conn_update)); conn_update->handle = BT_HOST2LE16(conn->handle); conn_update->conn_interval_min = BT_HOST2LE16(min); conn_update->conn_interval_max = BT_HOST2LE16(max); conn_update->conn_latency = BT_HOST2LE16(latency); conn_update->supervision_timeout = BT_HOST2LE16(timeout); return bt_hci_cmd_send(BT_HCI_OP_LE_CONN_UPDATE, buf); }